Quy đổi pixel sang mm bằng vật chuẩn và đánh giá sai số đo
Hiệu chuẩn tỉ lệ mm/pixel bằng tấm lưới chấm và căn mẫu, đo lặp lại để tính độ lặp lại, bias, tỉ số năng lực, và biết khi nào cần hiệu chuẩn đầy đủ.
Học xong bài này, bạn sẽ
- Hiệu chuẩn tỉ lệ bằng vật chuẩn
- Đánh giá độ lặp lại và sai số
- Biết khi nào quy đổi đơn giản là đủ
Nên học trước: Dự án chặng 7: đọc và xác minh số seri
Nội dung bài viết
Ở chặng 7, bài Đo cạnh subpixel, fitting đường thẳng và đường tròn đã cho bạn vị trí cạnh chính xác tới một phần nhỏ của pixel. Nhưng bản vẽ ghi mm, PLC cần mm, và câu hỏi của trưởng ca luôn là "máy đo có tin được không?". Chặng Calibration bắt đầu bằng cách đơn giản nhất và cũng được dùng nhiều nhất trong nhà máy: quy đổi pixel sang mm bằng một hệ số tỉ lệ, đo trên vật chuẩn. Bài này dạy cách làm đúng, cách chứng minh bằng số rằng hệ đo đủ tốt so với dung sai (độ lặp lại, sai lệch), và quan trọng nhất là biết khi nào cách đơn giản này không còn đủ để chuyển sang hiệu chuẩn đầy đủ ở các bài sau.
1. Quy đổi đơn giản là gì, và nó giả định điều gì?
kích thước (mm) = kích thước (pixel) × s s: hệ số tỉ lệ, đơn vị mm/pixel
Nếu tỉ lệ hai chiều khác nhau:
Δx (mm) = Δu (px) × sx Δy (mm) = Δv (px) × sy
Công thức chỉ có một dòng nhưng đứng trên bốn giả định. Ghi nhớ chúng, vì mỗi lần một giả định bị vi phạm là một nguồn sai số:
| Giả định | Bị vi phạm khi | Hậu quả |
|---|---|---|
| Camera nhìn vuông góc với mặt phẳng đo | Giá camera lệch, cố tình đặt nghiêng | Tỉ lệ thay đổi dần theo vị trí trong ảnh; sx ≠ sy |
| Ống kính không méo | Ống kính tiêu cự ngắn, góc rộng | Tỉ lệ ở mép ảnh khác ở tâm ảnh |
| Mọi đặc trưng nằm trên cùng một mặt phẳng, đúng mặt phẳng hiệu chuẩn | Vật lên xuống, vật dày, đo mặt trên nhưng hiệu chuẩn ở mặt đáy | Đổi tỉ lệ ΔZ/Z (bài Ống kính telecentric: khi nào cần?) |
| Vị trí cạnh tìm được trùng với cạnh thật | Đèn nền khuếch tán, ngưỡng cắt khác 50%, ba-via | Sai lệch biên cố định (bias), trái dấu cho kích thước ngoài và kích thước trong |
Vì sao lại có trường hợp sx ≠ sy? Camera area scan có pixel vuông nên sx = sy nếu camera vuông góc. Hai trường hợp hay gặp: camera nghiêng quanh một trục (chiều bị nghiêng "co" lại theo cos của góc nghiêng), và camera line scan, nơi sx do ống kính quyết định còn sy do tốc độ băng tải và encoder quyết định (bài tập 3). Hiệu chuẩn hai hệ số riêng không tốn thêm công sức, nên hãy luôn làm.
2. Chọn vật chuẩn
| Vật chuẩn | Đo được gì | Ghi chú |
|---|---|---|
| Tấm lưới chấm tròn (circle grid) hoặc bàn cờ trên kính / gốm | Hệ số tỉ lệ sx, sy từ khoảng cách tâm; phần dư cho biết méo/nghiêng | Có giấy chứng nhận bước lưới. Dùng được với đèn nền (chấm crôm trên kính) hoặc đèn chiếu mặt trên |
| Căn mẫu (gauge block) | Kích thước cạnh tới cạnh đã biết → sai lệch biên của cả hệ | Rất chính xác, rẻ, có sẵn trong phòng QC. Đặt đúng mặt phẳng đo |
| Thước kính vạch (stage micrometer) | Tỉ lệ theo một chiều | Tiện cho FOV nhỏ |
| Chốt chuẩn, vòng chuẩn (pin / ring gauge) | Bù biên cho đường kính ngoài và đường kính lỗ | Dùng khi đo đường kính là chính |
| Chi tiết mẫu đã đo bằng CMM (golden sample) | Kiểm tra toàn chuỗi trên chính sản phẩm | Dùng cho kiểm tra đầu ca (bài Kiểm tra hiệu chuẩn định kỳ) |
Quy tắc kinh nghiệm: sai số của vật chuẩn nên nhỏ hơn ít nhất khoảng 10 lần (tối thiểu khoảng 4 lần) sai số bạn muốn kiểm chứng. Thước kẹp in trên giấy A4 không phải vật chuẩn.
3. Dữ liệu mô phỏng
Để có số liệu kiểm chứng được, ta mô phỏng một camera nhìn xuống đèn nền. Script vẽ vật với độ chính xác dưới pixel (siêu lấy mẫu 8 × 8), làm mờ như ống kính thật, thêm nhiễu cảm biến. Ba "sự thật" được giấu trong mô phỏng mà phần mềm đo không biết, và bạn sẽ phải tìm ra:
- Tỉ lệ thật sx = 0,02500 mm/px, sy = 0,02510 mm/px (camera hơi nghiêng).
- Đèn nền khuếch tán làm biên vật tối bị "ăn" vào 4 µm mỗi phía.
- Chi tiết trên băng tải lên xuống ±0,1 mm ở khoảng cách 200 mm.
import os
import cv2
import numpy as np
SX, SY = 0.02500, 0.02510 # mm/pixel THẬT (phần mềm không biết): y lớn hơn 0,4% do camera hơi nghiêng
W, H = 640, 480
NEN, VAT = 220.0, 30.0 # mức xám nền sáng (backlight) và vật tối
AN_BIEN = 0.004 # đèn nền khuếch tán "ăn" vào biên vật tối 4 µm mỗi phía
K = 8 # siêu lấy mẫu 8×8 điểm mỗi pixel → biên chính xác dưới pixel
def ve(hinh, x0, y0, rng, ti_le=1.0):
"""hinh: danh sách ('tron', cx, cy, r) hoặc ('chu_nhat', x1, y1, x2, y2), đơn vị mm.
(x0, y0): tọa độ mm tại pixel (0, 0); ti_le: hệ số đổi tỉ lệ khi vật lên/xuống."""
sx, sy = SX * ti_le, SY * ti_le
phu = np.zeros((H, W), np.float32) # tỉ lệ diện tích pixel bị vật che
o = (np.arange(K) + 0.5) / K - 0.5 # vị trí mẫu con quanh tâm pixel
for h in hinh:
if h[0] == "tron":
_, cx, cy, r = h
r -= AN_BIEN
bx1, by1, bx2, by2 = cx - r, cy - r, cx + r, cy + r
else:
bx1, by1, bx2, by2 = h[1] + AN_BIEN, h[2] + AN_BIEN, h[3] - AN_BIEN, h[4] - AN_BIEN
u1, u2 = int((bx1 - x0) / sx) - 2, int((bx2 - x0) / sx) + 3
v1, v2 = int((by1 - y0) / sy) - 2, int((by2 - y0) / sy) + 3
uu = (np.arange(u1, u2)[:, None] + o[None, :]).ravel()
vv = (np.arange(v1, v2)[:, None] + o[None, :]).ravel()
X, Y = np.meshgrid(x0 + uu * sx, y0 + vv * sy)
if h[0] == "tron":
trong = (X - cx) ** 2 + (Y - cy) ** 2 <= r * r
else:
trong = (X >= bx1) & (X <= bx2) & (Y >= by1) & (Y <= by2)
cov = trong.reshape(v2 - v1, K, u2 - u1, K).mean(axis=(1, 3))
phu[v1:v2, u1:u2] = np.maximum(phu[v1:v2, u1:u2], cov)
anh = NEN - (NEN - VAT) * phu
anh = cv2.GaussianBlur(anh, (0, 0), 0.8) # ống kính không nét tuyệt đối
anh += rng.normal(0, 2.0, anh.shape) # nhiễu cảm biến
return np.clip(np.round(anh), 0, 255).astype(np.uint8)
rng = np.random.default_rng(7)
for d in ("tam_chuan", "can_mau", "chi_tiet"):
os.makedirs(d, exist_ok=True)
# 1) Tấm chuẩn lưới chấm tròn 5 × 4, bước 2,500 mm, chấm Ø1,250 mm; vị trí chấm sai cỡ 0,5 µm
sai = rng.normal(0, 0.0005, (20, 2))
luoi = [("tron", i * 2.5 + sai[5 * j + i, 0], j * 2.5 + sai[5 * j + i, 1], 0.625)
for j in range(4) for i in range(5)]
for k in range(10):
lx, ly = rng.uniform(-0.2, 0.2, 2)
cv2.imwrite(f"tam_chuan/tc_{k:02d}.png", ve(luoi, -3.0 + lx, -2.25 + ly, rng))
# 2) Căn mẫu (gauge block) rộng 10,000 mm, đặt đúng mặt phẳng đo
for k in range(10):
lx, ly = rng.uniform(-0.2, 0.2, 2)
cv2.imwrite(f"can_mau/cm_{k:02d}.png", ve([("chu_nhat", 0, 0, 10.0, 4.0)], -3.0 + lx, -4.0 + ly, rng))
# 3) Chi tiết THẬT 8,012 × 5,000 mm (bản vẽ 8,00 ± 0,05 và 5,00 ± 0,05), chụp 30 lần trên băng tải:
# vị trí lệch ±0,3 mm, chiều cao lên xuống ±0,1 mm ở Z = 200 mm
for k in range(30):
lx, ly = rng.uniform(-0.3, 0.3, 2)
dz = rng.uniform(-0.1, 0.1)
cv2.imwrite(f"chi_tiet/ct_{k:02d}.png",
ve([("chu_nhat", 0, 0, 8.012, 5.000)], -4.0 + lx, -3.5 + ly, rng, ti_le=(200 + dz) / 200))
print("Đã tạo 10 ảnh tấm chuẩn, 10 ảnh căn mẫu, 30 ảnh chi tiết")
Đã tạo 10 ảnh tấm chuẩn, 10 ảnh căn mẫu, 30 ảnh chi tiết
4. Hiệu chuẩn tỉ lệ bằng tấm lưới chấm
Tâm của một chấm tròn là đặc trưng rất ổn định: nó không phụ thuộc ngưỡng cắt hay việc biên bị ăn vào đều các phía. Vì vậy hệ số tỉ lệ nên lấy từ khoảng cách tâm, không lấy từ đường kính chấm. Tâm được tính bằng trọng tâm có trọng số theo độ tối (subpixel), sau đó fit một phép biến đổi affine từ tọa độ lưới (mm) sang tọa độ ảnh (pixel) bằng bình phương tối thiểu. Fit affine cho phép tấm chuẩn đặt hơi xoay, và cho ra hai hệ số sx, sy riêng.
import glob
import json
import cv2
import numpy as np
BUOC, COT, HANG = 2.5, 5, 4 # bước lưới (mm) và số chấm theo giấy chứng nhận tấm chuẩn
def tam_cac_cham(anh):
"""Tâm subpixel của các chấm tối: trọng tâm có trọng số theo độ tối trong vùng quanh mỗi chấm."""
_, nhi_phan = cv2.threshold(anh, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
cs, _ = cv2.findContours(nhi_phan, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
nen = float(np.median(anh))
vat = float(np.percentile(anh, 1))
tam = []
for c in cs:
if cv2.contourArea(c) < 50:
continue
x, y, w, h = cv2.boundingRect(c)
x, y = x - 4, y - 4
roi = anh[y:y + h + 8, x:x + w + 8].astype(np.float64)
trong_so = np.clip((nen - roi) / (nen - vat), 0, 1) # 0 ở nền, 1 trong chấm
yy, xx = np.mgrid[0:roi.shape[0], 0:roi.shape[1]]
tam.append((x + (trong_so * xx).sum() / trong_so.sum(), y + (trong_so * yy).sum() / trong_so.sum()))
tam = sorted(tam, key=lambda p: p[1]) # xếp theo hàng rồi theo cột
hang = [sorted(tam[i * COT:(i + 1) * COT]) for i in range(HANG)]
return np.array([p for h in hang for p in h])
def hieu_chuan(anh):
uv = tam_cac_cham(anh)
the_gioi = np.array([(i * BUOC, j * BUOC) for j in range(HANG) for i in range(COT)])
A = np.column_stack([the_gioi, np.ones(len(the_gioi))])
he_so, *_ = np.linalg.lstsq(A, uv, rcond=None) # u, v = a·X + b·Y + c
sx = 1 / np.hypot(he_so[0, 0], he_so[0, 1]) # mm/pixel theo x
sy = 1 / np.hypot(he_so[1, 0], he_so[1, 1]) # mm/pixel theo y
du = (uv - A @ he_so) * [sx, sy] # phần dư quy ra mm
return sx, sy, np.sqrt((du ** 2).sum(axis=1)).max()
files = sorted(glob.glob("tam_chuan/*.png"))
sx, sy, du_max = hieu_chuan(cv2.imread(files[0], cv2.IMREAD_GRAYSCALE))
print(f"Ảnh 1: sx = {1000 * sx:.4f} µm/px, sy = {1000 * sy:.4f} µm/px, phần dư lớn nhất {1000 * du_max:.2f} µm")
ket_qua = np.array([hieu_chuan(cv2.imread(f, cv2.IMREAD_GRAYSCALE))[:2] for f in files])
tb, ds = ket_qua.mean(axis=0), ket_qua.std(axis=0, ddof=1)
print(f"10 ảnh: sx = {1000 * tb[0]:.4f} ± {1000 * ds[0]:.4f} µm/px, sy = {1000 * tb[1]:.4f} ± {1000 * ds[1]:.4f} µm/px")
print(f"sy/sx = {tb[1] / tb[0]:.4f} → chênh {100 * (tb[1] / tb[0] - 1):.2f}%")
json.dump({"sx": tb[0], "sy": tb[1]}, open("ti_le.json", "w"))
Ảnh 1: sx = 24.9998 µm/px, sy = 25.0998 µm/px, phần dư lớn nhất 1.03 µm
10 ảnh: sx = 25.0002 ± 0.0002 µm/px, sy = 25.1000 ± 0.0001 µm/px
sy/sx = 1.0040 → chênh 0.40%
- Phần mềm tìm lại đúng sx = 25,000 µm/px và sy = 25,100 µm/px: chênh 0,40%. Dùng một hệ số chung cho cả hai chiều thì chiều cao 5 mm sẽ sai 20 µm, tức 20% dải dung sai ±0,05 mm (mục 5 kiểm chứng điều này).
- Phần dư lớn nhất khoảng 1 µm là sai số chế tạo của chính tấm chuẩn: lưới thẳng, không có dấu hiệu méo. Với camera thật, đây là con số đầu tiên cần nhìn: phần dư lớn và có quy luật (lớn dần ra mép ảnh) nghĩa là ống kính méo hoặc phối cảnh, quy đổi đơn giản sẽ không đủ (mục 7).
- Độ lệch chuẩn của hệ số qua 10 ảnh rất nhỏ (cỡ 0,0002 µm/px), vì 20 chấm, mỗi chấm hàng nghìn pixel, tự trung bình hóa nhiễu. Với chi tiết 8 mm (320 pixel), sai số do hệ số gây ra chỉ cỡ 320 × 0,0002 µm ≈ 0,06 µm, không đáng kể.
- Thực tế nên dùng tấm chuẩn phủ khoảng 70–80% trường nhìn và chụp ở vài vị trí, để phần dư phản ánh được cả vùng mép ảnh.
5. Sai lệch biên và đo chi tiết
Hệ số tỉ lệ lấy từ tâm chấm thì không thấy được sai lệch biên. Nhưng kích thước chi tiết là khoảng cách cạnh tới cạnh, nên mọi hiệu ứng làm cạnh dịch chuyển (đèn nền khuếch tán, ngưỡng, độ mờ không đối xứng) đều đi thẳng vào kết quả. Cách xử lý chuẩn: đo một căn mẫu có kích thước biết trước bằng chính thuật toán đo chi tiết, lấy chênh lệch làm giá trị bù.
Thuật toán đo cạnh: với mỗi hàng (hoặc cột) quét ngang vật, tìm nơi profile độ sáng cắt mức 50% giữa nền và vật, nội suy tuyến tính giữa hai pixel để có vị trí subpixel, rồi lấy trung bình nhiều đường quét ở nửa giữa vật (tránh góc).
import csv
import glob
import json
import cv2
import numpy as np
def canh_subpixel(prof, muc):
"""Vị trí subpixel nơi profile cắt mức `muc` lần đầu (cạnh trái/trên) và lần cuối (cạnh phải/dưới)."""
duoi = prof < muc
i = int(np.argmax(duoi)) # pixel tối đầu tiên
j = len(prof) - 1 - int(np.argmax(duoi[::-1])) # pixel tối cuối cùng
dau = i - 1 + (prof[i - 1] - muc) / (prof[i - 1] - prof[i])
cuoi = j + (prof[j] - muc) / (prof[j] - prof[j + 1])
return dau, cuoi
def do_vat(anh):
"""Bề rộng (theo x) và chiều cao (theo y) của vật tối, pixel, trung bình nhiều đường quét."""
a = anh.astype(np.float64)
_, nhi_phan = cv2.threshold(anh, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
x, y, w, h = cv2.boundingRect(cv2.findNonZero(nhi_phan))
muc = (np.median(a[nhi_phan == 0]) + np.median(a[nhi_phan > 0])) / 2 # mức 50% nền/vật
rong = [np.subtract(*canh_subpixel(a[r], muc)[::-1]) for r in range(y + h // 4, y + 3 * h // 4)]
cao = [np.subtract(*canh_subpixel(a[:, c], muc)[::-1]) for c in range(x + w // 4, x + 3 * w // 4)]
return np.mean(rong), np.mean(cao)
tl = json.load(open("ti_le.json"))
# Bước 1: đo căn mẫu 10,000 mm để tìm sai lệch biên (bias) của cả chuỗi đèn + ống kính + thuật toán
do_cm = [do_vat(cv2.imread(f, cv2.IMREAD_GRAYSCALE))[0] * tl["sx"] for f in sorted(glob.glob("can_mau/*.png"))]
bu = 10.000 - np.mean(do_cm)
print(f"Căn mẫu 10,000 mm đo được {np.mean(do_cm):.4f} mm → bù biên {bu:+.4f} mm cho kích thước ngoài")
tl["bu_bien"] = bu
json.dump(tl, open("ti_le.json", "w"))
# Bước 2: đo 30 ảnh chi tiết, ghi cả kết quả thô và kết quả đã bù
with open("ket_qua_lap.csv", "w", newline="") as f:
ghi = csv.writer(f)
ghi.writerow(["anh", "rong_tho", "cao_tho", "rong", "cao"])
for k, fn in enumerate(sorted(glob.glob("chi_tiet/*.png"))):
rong_px, cao_px = do_vat(cv2.imread(fn, cv2.IMREAD_GRAYSCALE))
rong, cao = rong_px * tl["sx"], cao_px * tl["sy"]
ghi.writerow([fn, rong, cao, rong + bu, cao + bu])
if k == 0:
print(f"Ảnh đầu: rộng {rong_px:.3f} px → {rong:.4f} mm, bù → {rong + bu:.4f} mm")
print(f" cao {cao_px:.3f} px → {cao:.4f} mm, bù → {cao + bu:.4f} mm"
f" (dùng nhầm sx: {cao_px * tl['sx'] + bu:.4f} mm)")
print("Đã ghi ket_qua_lap.csv")
Căn mẫu 10,000 mm đo được 9.9913 mm → bù biên +0.0087 mm cho kích thước ngoài
Ảnh đầu: rộng 320.196 px → 8.0049 mm, bù → 8.0136 mm
cao 198.900 px → 4.9924 mm, bù → 5.0011 mm (dùng nhầm sx: 4.9812 mm)
Đã ghi ket_qua_lap.csv
- Căn mẫu 10,000 mm đo ra 9,9913 mm: hệ đo "thấy" vật nhỏ hơn thật khoảng 8,7 µm (4 µm mỗi phía do đèn, phần còn lại do mờ và lấy mẫu). Đây là sai số hệ thống, lặp lại được nên bù được.
- Bù +0,0087 mm áp dụng cho kích thước ngoài (bề rộng vật). Với kích thước trong (đường kính lỗ), biên dịch theo chiều ngược lại nên dấu bù ngược lại: hãy xác định riêng bằng vòng chuẩn. Khoảng cách tâm tới tâm thì không cần bù.
- Dùng nhầm sx cho chiều cao: 4,9812 mm thay vì 5,0011 mm, sai gần 20 µm như đã dự đoán ở mục 4.
6. Độ lặp lại, sai lệch và năng lực đo
Một lần đo không nói lên gì. Phải đo cùng một chi tiết nhiều lần (ở đây 30 lần, mỗi lần chi tiết nằm ở vị trí và độ cao hơi khác, như trên băng tải thật) rồi tính:
| Đại lượng | Công thức | Ý nghĩa |
|---|---|---|
| Trung bình | x̄ = Σxᵢ / n | Hệ đo "cho" giá trị nào |
| Độ lệch chuẩn σ | √(Σ(xᵢ − x̄)² / (n − 1)) | Độ phân tán khi đo lặp lại |
| Độ lặp lại (repeatability) | ±3σ (dải 6σ) | Khoảng chứa gần như mọi kết quả đo lặp lại (khoảng 99,7% nếu phân bố chuẩn) |
| Sai lệch (bias) | x̄ − giá trị thật | Sai số hệ thống; giá trị thật lấy từ CMM hoặc vật chuẩn |
| Tỉ số năng lực (đơn giản hóa) | dải dung sai / [2 × (|bias| + 3σ)] | Dải dung sai lớn gấp bao nhiêu lần dải sai số của hệ đo |
Hướng dẫn thường dùng (quy tắc kinh nghiệm, không phải tiêu chuẩn bắt buộc): tỉ số ≥ 10:1 là tốt, có thể dùng thẳng cho quyết định OK/NG; từ 4:1 tới 10:1 chấp nhận được có điều kiện (cần dải bảo vệ, xem bài tập 1); dưới 4:1 hệ đo không đủ năng lực cho dung sai này. Doanh nghiệp có hệ thống chất lượng thường đánh giá chính thức bằng nghiên cứu Gage R&R; tỉ số ở đây là bước kiểm tra nhanh trước đó.
import csv
import numpy as np
THAT = {"rong": 8.012, "cao": 5.000} # giá trị thật (đo bằng máy CMM)
DUNG_SAI = 0.05 # bản vẽ: ±0,05 mm → dải dung sai 0,10 mm
dong = list(csv.DictReader(open("ket_qua_lap.csv")))
print("kích thước | TB (mm) | σ (µm) | ±3σ (µm) | bias (µm) | dải DS / 2(|bias|+3σ)")
for cot in ("rong_tho", "rong", "cao_tho", "cao"):
x = np.array([float(d[cot]) for d in dong])
tb, s = x.mean(), x.std(ddof=1)
bias = tb - THAT[cot.replace("_tho", "")]
ti_so = 2 * DUNG_SAI / (2 * (abs(bias) + 3 * s))
print(f"{cot:12s} | {tb:8.4f} | {1000 * s:7.2f} | {3000 * s:8.2f} | {1000 * bias:+9.2f} | {ti_so:6.1f}")
kích thước | TB (mm) | σ (µm) | ±3σ (µm) | bias (µm) | dải DS / 2(|bias|+3σ)
rong_tho | 8.0037 | 2.13 | 6.38 | -8.26 | 3.4
rong | 8.0124 | 2.13 | 6.38 | +0.42 | 7.4
cao_tho | 4.9919 | 1.38 | 4.15 | -8.06 | 4.1
cao | 5.0006 | 1.38 | 4.15 | +0.61 | 10.5
Đọc bảng như một kỹ sư đo lường:
- Trước khi bù, bề rộng có bias −8,3 µm và tỉ số chỉ 3,4: không đạt 4:1. Hệ quả: chi tiết thật 7,955 mm (đạt) bị đo thành khoảng 7,947 mm và bị loại oan (overkill); chi tiết thật 8,055 mm (không đạt) bị đo thành khoảng 8,047 mm và lọt qua (escape).
- Sau khi bù bằng căn mẫu, bias còn dưới 1 µm, tỉ số bề rộng 7,4 (chấp nhận được có điều kiện), chiều cao 10,5 (tốt).
- Vì sao σ của bề rộng (2,1 µm) lớn hơn chiều cao (1,4 µm)? Phần lớn σ đến từ việc chi tiết lên xuống ±0,1 mm: sai số tỉ lệ ΔZ/Z = 0,05% nhân với kích thước. Kích thước lớn hơn thì σ lớn hơn. Muốn tăng tỉ số bề rộng lên 10:1, cách hiệu quả nhất không phải thuật toán mà là đồ gá giữ chiều cao (hoặc ống kính telecentric).
7. Khi nào quy đổi đơn giản không còn đủ?
Đoạn mô phỏng dưới đây dùng mô hình camera đầy đủ của OpenCV (cv2.projectPoints): ống kính 25 mm, cảm biến 2448 × 2048, pixel 3,45 µm, cách vật 200 mm (FOV khoảng 68 × 57 mm). Hệ số tỉ lệ được hiệu chuẩn ở tâm ảnh. Sau đó đo một đoạn 10 mm ở tâm, giữa và gần góc ảnh, với bốn tình huống:
import math
import cv2
import numpy as np
f_px = 25 / 0.00345 # ống kính 25 mm, pixel 3,45 µm
K = np.array([[f_px, 0, 1224], [0, f_px, 1024], [0, 0, 1]]) # cảm biến 2448 × 2048
Z = 200.0 # FOV khoảng 68 × 57 mm
def do_doan(cx, cy, dist, goc_nghieng=0.0, cao=0.0, L=10.0):
"""Chiều dài (pixel) trên ảnh của đoạn L mm nằm ngang, tâm (cx, cy) mm, cao `cao` mm trên mặt phẳng."""
pts = np.array([[cx - L / 2, cy, -cao], [cx + L / 2, cy, -cao]], np.float64)
rvec = np.array([math.radians(goc_nghieng), 0, 0])
uv, _ = cv2.projectPoints(pts, rvec, np.array([0, 0, Z]), K, dist)
return np.linalg.norm(uv[1, 0] - uv[0, 0])
khong_meo = np.zeros(5)
meo = np.array([-0.1, 0, 0, 0, 0]) # méo thùng nhẹ: khoảng -0,5% ở góc ảnh
truong_hop = [("Lý tưởng", khong_meo, 0, 0), ("Méo thùng k1 = -0,1", meo, 0, 0),
("Camera nghiêng 2°", khong_meo, 2, 0), ("Vật cao hơn 1 mm", khong_meo, 0, 1)]
vi_tri = [(0, 0), (15, 12), (28, 24)]
print("Sai số đo đoạn 10 mm (µm), tỉ lệ hiệu chuẩn ở tâm ảnh trên mặt phẳng chuẩn")
print(f"{'trường hợp':22s}" + "".join(f" tại ({x:2d},{y:2d})" for x, y in vi_tri))
for ten, dist, nghieng, cao in truong_hop:
s0 = 10.0 / do_doan(0, 0, dist, nghieng) # mm/px đo bằng vật chuẩn ở tâm
sai = [1000 * (do_doan(x, y, dist, nghieng, cao) * s0 - 10.0) for x, y in vi_tri]
print(f"{ten:22s}" + "".join(f" {e:+11.1f}" for e in sai))
Sai số đo đoạn 10 mm (µm), tỉ lệ hiệu chuẩn ở tâm ảnh trên mặt phẳng chuẩn
trường hợp tại ( 0, 0) tại (15,12) tại (28,24)
Lý tưởng +0.0 +0.0 +0.0
Méo thùng k1 = -0,1 +0.0 -20.5 -73.1
Camera nghiêng 2° +0.0 -20.9 -41.7
Vật cao hơn 1 mm +50.3 +50.3 +50.3
| Tình huống | Đặc điểm sai số | Cách nhận ra | Giải pháp |
|---|---|---|---|
| Méo ống kính | 0 ở tâm, tăng nhanh ra mép (−73 µm gần góc ảnh, dù méo chỉ cỡ 0,5% ở góc) | Phần dư fit lưới lớn dần ra mép theo dạng xuyên tâm | Bài tiếp theo: Hiệu chuẩn camera: nội tại và méo ống kính |
| Camera nghiêng | Thay đổi tuyến tính theo một hướng (−42 µm ở y = 24 mm với 2°) | sx ≠ sy; phần dư nghiêng một chiều | Chỉnh lại giá camera; hoặc Homography: đo trên mặt phẳng khi camera nghiêng |
| Vật cao hơn mặt hiệu chuẩn | Như nhau ở mọi vị trí (+50 µm cho 1 mm chênh cao) | Đo căn mẫu kê cao sẽ thấy ngay | Hiệu chuẩn đúng độ cao, đồ gá, hoặc telecentric |
Kết luận thực hành. Quy đổi đơn giản (sx, sy cộng giá trị bù biên) là đủ khi tất cả các điều kiện sau đúng:
- Phần dư lớn nhất khi fit lưới chuẩn phủ toàn vùng đo nhỏ hơn khoảng 1/10 dải dung sai.
- Chiều cao các đặc trưng đo nằm trong một mặt phẳng ổn định: L · ΔZ / Z nhỏ so với dải dung sai.
- Thử nghiệm lặp lại ở mục 6 đạt tỉ số đã thống nhất, với chi tiết đặt ở nhiều vị trí khác nhau trong ảnh (tâm và mép), không chỉ ở tâm.
Rất nhiều trạm đo thực tế thỏa mãn được ba điều kiện này: ống kính tiêu cự dài hoặc telecentric, camera vuông góc, đồ gá tốt, dung sai cỡ vài phần trăm mm trở lên. Khi một điều kiện không đạt, đừng cố "chỉnh hệ số cho khớp" bằng tay: hãy chuyển sang mô hình đúng cho nguồn sai số đó.
8. Những lỗi thường gặp
- Lấy tỉ lệ từ đường kính chấm hoặc chiều dài vật thay vì khoảng cách tâm: tỉ lệ bị lẫn sai lệch biên, rồi lại dùng tỉ lệ đó đo lỗ (sai lệch biên ngược dấu) nên sai gấp đôi.
- Hiệu chuẩn bằng tấm chuẩn đặt trên mặt bàn trong khi chi tiết nằm trên đồ gá cao 5 mm.
- Chỉ đo lặp lại mà không nhấc chi tiết ra đặt lại: σ rất đẹp nhưng không phản ánh sản xuất thật (sai lệch vị trí, chiều cao, góc xoay).
- Đo lặp lại chỉ ở tâm ảnh, bỏ qua méo ở mép.
- Báo cáo kết quả với 4 chữ số thập phân khi σ là 5 µm: số chữ số không phải là độ chính xác.
- Quên hiệu chuẩn lại sau khi xoay vòng nét, đổi khẩu độ hoặc thay đèn.
9. Bài tập
Bài 1. Một trạm đo khe hở có dung sai ±0,03 mm. Thử lặp lại cho σ = 4 µm, bias = +6 µm. Tính tỉ số năng lực trước và sau khi bù bias. Để đạt 4:1 và 10:1 sau khi bù, σ phải nhỏ hơn bao nhiêu?
Xem đáp án
dung_sai = 0.03 # ±0,03 mm
sigma, bias = 0.004, 0.006 # mm
dai_sai_so = 2 * (abs(bias) + 3 * sigma)
print(f"Dải sai số đo {dai_sai_so:.3f} mm, tỉ số {2 * dung_sai / dai_sai_so:.2f}")
print(f"Sau khi bù bias: tỉ số {2 * dung_sai / (6 * sigma):.2f}")
print(f"σ cần để đạt 4:1 sau bù: ≤ {2 * dung_sai / 4 / 6 * 1000:.1f} µm; đạt 10:1: ≤ {2 * dung_sai / 10 / 6 * 1000:.1f} µm")
Dải sai số đo 0.036 mm, tỉ số 1.67
Sau khi bù bias: tỉ số 2.50
σ cần để đạt 4:1 sau bù: ≤ 2.5 µm; đạt 10:1: ≤ 1.0 µm
Hệ đo này không dùng được cho dung sai ±0,03 mm kể cả khi bù bias: cần σ ≤ 2,5 µm để đạt 4:1. Hướng cải thiện: giảm FOV để pixel nhỏ hơn, giảm dao động chiều cao, cải thiện chiếu sáng biên. Nếu tạm thời phải dùng hệ đo ở mức 4:1 đến 10:1, áp dụng dải bảo vệ (guard band): thu hẹp giới hạn chấp nhận vào trong mỗi phía một khoảng cỡ 3σ để giảm nguy cơ lọt hàng lỗi, đổi lại tăng loại oan.
Bài 2. Dùng ảnh căn mẫu và ảnh tấm chuẩn đã tạo ở mục 3, đo bề rộng căn mẫu và bước lưới (khoảng cách tâm chấm, tính bằng nhị phân hóa rồi lấy trọng tâm vùng) với mức cắt 30%, 50%, 70%. Kết quả chứng minh điều gì?
Xem đáp án
import glob
import json
import cv2
import numpy as np
tl = json.load(open("ti_le.json"))
cm = cv2.imread(sorted(glob.glob("can_mau/*.png"))[0], cv2.IMREAD_GRAYSCALE).astype(np.float64)
tc = cv2.imread(sorted(glob.glob("tam_chuan/*.png"))[0], cv2.IMREAD_GRAYSCALE)
nen, vat = np.median(cm), np.percentile(cm, 1)
print("mức cắt | rộng căn mẫu (mm) | bước lưới từ tâm chấm (px)")
for p in (0.3, 0.5, 0.7):
muc = vat + p * (nen - vat)
rong = []
for r in range(200, 280): # các hàng cắt ngang căn mẫu
prof = cm[r]
duoi = np.where(prof < muc)[0]
i, j = duoi[0], duoi[-1]
trai = i - 1 + (prof[i - 1] - muc) / (prof[i - 1] - prof[i])
phai = j + (prof[j] - muc) / (prof[j] - prof[j + 1])
rong.append(phai - trai)
_, nhi_phan = cv2.threshold(tc, muc, 255, cv2.THRESH_BINARY_INV)
n, _, st, tam = cv2.connectedComponentsWithStats(nhi_phan.astype(np.uint8))
tam = tam[1:][st[1:, cv2.CC_STAT_AREA] > 50]
hang_dau = np.sort(tam[tam[:, 1] < tam[:, 1].min() + 20][:, 0]) # 5 chấm hàng trên cùng
print(f" {int(100 * p)}% | {np.mean(rong) * tl['sx']:17.4f} | {np.diff(hang_dau).mean():10.3f}")
mức cắt | rộng căn mẫu (mm) | bước lưới từ tâm chấm (px)
30% | 9.9632 | 99.994
50% | 9.9877 | 100.003
70% | 10.0156 | 99.992
Bề rộng căn mẫu thay đổi hơn 50 µm chỉ vì đổi mức cắt (mức thấp thì biên vật tối "co" vào), trong khi bước lưới giữ nguyên khoảng 100,00 px vì tâm chấm không phụ thuộc mức cắt. Đó là lý do: lấy tỉ lệ từ khoảng cách tâm, lấy giá trị bù biên từ vật chuẩn cạnh tới cạnh, và giữ mức cắt cố định giữa lúc hiệu chuẩn và lúc đo.
Bài 3. Camera line scan có sx = 0,050 mm/px (đo bằng tấm chuẩn). Encoder 5000 xung/vòng gắn trên con lăn Ø50 mm, camera chụp một dòng mỗi 2 xung. Tính sy. Một chi tiết tròn Ø10 mm hiện ra trên ảnh như thế nào? Muốn pixel "vuông" thì cần bao nhiêu xung mỗi dòng?
Xem đáp án
import math
sx = 0.050 # mm/px theo chiều ngang, đo bằng tấm chuẩn
chu_vi = math.pi * 50.0 # con lăn encoder Ø50 mm
mm_moi_xung = chu_vi / 5000 # encoder 5000 xung/vòng
sy = 2 * mm_moi_xung # mỗi 2 xung chụp 1 dòng
print(f"mm/xung = {mm_moi_xung:.6f}, sy = {sy:.6f} mm/dòng, sy/sx = {sy / sx:.4f}")
print(f"Đồng xu Ø10 mm hiện thành elip {10 / sx:.0f} × {10 / sy:.1f} px")
print(f"Muốn sy = sx: {sx / mm_moi_xung:.4f} xung mỗi dòng")
mm/xung = 0.031416, sy = 0.062832 mm/dòng, sy/sx = 1.2566
Đồng xu Ø10 mm hiện thành elip 200 × 159.2 px
Muốn sy = sx: 1.5915 xung mỗi dòng
sy lớn hơn sx 26%: chi tiết tròn hiện thành elip 200 × 159 px. Có hai cách: dùng bộ chia/nhân xung có hệ số lẻ (nhiều camera line scan hỗ trợ) để được khoảng 1,59 xung/dòng, hoặc giữ nguyên và quy đổi riêng sx, sy. Lưu ý sy còn phụ thuộc trượt giữa băng tải và con lăn encoder, nên hiệu chuẩn sy bằng vật chuẩn chạy qua băng tải thật, không chỉ tính từ đường kính con lăn.
Tóm tắt
- Quy đổi đơn giản: mm = pixel × s, với sx, sy riêng. Đúng khi camera vuông góc, ống kính ít méo, đặc trưng nằm trên mặt phẳng hiệu chuẩn.
- Lấy tỉ lệ từ khoảng cách tâm trên tấm lưới chuẩn (không phụ thuộc ngưỡng); lấy giá trị bù biên từ căn mẫu, vòng chuẩn bằng chính thuật toán đo. Bù ngược dấu cho kích thước ngoài và kích thước trong.
- Đánh giá hệ đo: đo lặp lại ≥ 30 lần trong điều kiện thật, tính σ, ±3σ, bias, tỉ số dải dung sai / 2(|bias| + 3σ). Hướng dẫn: ≥ 10:1 tốt, 4:1 tới 10:1 có điều kiện, dưới 4:1 không đạt.
- Phần dư fit lưới là "máy dò" méo và nghiêng; sai số đều ở mọi vị trí là dấu hiệu chênh chiều cao.
- Khi méo, nghiêng hoặc chiều cao vượt ngân sách sai số: chuyển sang hiệu chuẩn đầy đủ, homography, đồ gá hoặc telecentric.
Bài tiếp theo: Hiệu chuẩn camera: nội tại và méo ống kính, nơi bạn mô hình hóa và khử chính loại sai số đã thấy ở mép ảnh trong mục 7.


