Dự án: đo kích thước chi tiết cơ khí bằng camera đã hiệu chuẩn
Hiệu chuẩn nội tại và mặt phẳng đo, caliper subpixel, fit đường và tròn trong mm, báo cáo theo dung sai và đánh giá độ lặp lại 10 lần đặt lại.
Học xong bài này, bạn sẽ
- Kết hợp hiệu chuẩn, đo cạnh, fitting
- Báo cáo kết quả theo dung sai
- Đánh giá độ lặp lại
Nên học trước: Kiểm tra hiệu chuẩn định kỳ
Nội dung bài viết
Đây là dự án tổng kết chặng Calibration. Bạn sẽ ghép mọi thứ của chặng này với chặng trước thành một chương trình đo hoàn chỉnh: hiệu chuẩn nội tại bằng bàn cờ (bài Hiệu chuẩn camera: nội tại và méo ống kính), hiệu chuẩn mặt phẳng đo bằng homography (bài Homography: đo trên mặt phẳng khi camera nghiêng), caliper subpixel và fitting (bài Đo cạnh subpixel, fitting đường thẳng và đường tròn), rồi báo cáo theo dung sai và đánh giá độ lặp lại (bài Quy đổi pixel sang mm và đánh giá sai số). Mọi ảnh được tạo bằng một camera ảo có méo ống kính và nghiêng nhẹ, nên bạn biết đáp án tới micromet và tự chấm được bài của mình. Dự án này là bước đệm trực tiếp cho Capstone 1: trạm đo kích thước hoàn chỉnh ở cuối lộ trình; phần giao tiếp PLC, lưu cơ sở dữ liệu và biên bản nghiệm thu để dành cho capstone đó và cho chặng tích hợp tiếp theo.
1. Bài toán
Một xưởng gia công cắt laser tấm kẹp bằng thép dày 1 mm: hình chữ nhật 40 × 25 mm có một lỗ Ø10 mm. QC muốn đo 100% sản phẩm trên một trạm đèn nền, camera 1,2 MP (1280 × 960) với ống kính 12 mm, độ phân giải khoảng 0,1 mm/px. Ống kính có méo thùng nhẹ (khoảng −0,8% ở góc ảnh), camera lắp lệch phương thẳng đứng khoảng 1°. Chi tiết được đặt tay lên bàn đèn nền, lệch vị trí tới ±5 mm và xoay tới ±10°.
| Ký hiệu | Kích thước | Danh định | Dung sai | Dải T |
|---|---|---|---|---|
| L | Chiều dài tấm (khoảng cách hai cạnh ngắn) | 40,00 mm | ±0,05 mm | 0,10 mm |
| W | Chiều rộng tấm (khoảng cách hai cạnh dài) | 25,00 mm | ±0,05 mm | 0,10 mm |
| D | Đường kính lỗ | 10,00 mm | ±0,03 mm | 0,06 mm |
| X | Khoảng cách từ cạnh trái tới tâm lỗ | 12,00 mm | ±0,05 mm | 0,10 mm |
2. Yêu cầu
| Mã | Yêu cầu | Tiêu chí đạt |
|---|---|---|
| Y1 | Hiệu chuẩn nội tại từ bộ ảnh bàn cờ, lưu YAML | RMS reprojection ≤ 0,1 px trên ảnh mô phỏng; fx sai < 0,2% so với đáp án |
| Y2 | Hiệu chuẩn mặt phẳng đo bằng bàn cờ đặt trên bàn đèn: homography từ pixel đã khử méo sang mm | Sai số trên góc bàn cờ (sau khử méo) ≤ 0,01 mm |
| Y3 | Đo L, W, D, X bằng caliper subpixel, fit đường/tròn trong hệ mm | Chi tiết ở vị trí và góc bất kỳ trong phạm vi cho phép |
| Y4 | Báo cáo theo dung sai: danh định, đo được, sai lệch, OK/NG cho từng kích thước và kết luận chung | Đúng kết luận cho 3 chi tiết thử (1 OK, 2 NG đúng kích thước lỗi) |
| Y5 | Độ lặp lại: đo 10 lần cùng một chi tiết, mỗi lần đặt lại | Báo cáo trung bình, σ, 6σ/T (%) và bias so với giá trị CMM; 6σ/T ≤ 20% cho mọi kích thước |
| Y6 | So sánh với cách đo "ngây thơ" (một hệ số mm/px, không khử méo) | Bảng sai số hai cách trên cùng bộ ảnh |
3. Kiến thức sử dụng
- Hiệu chuẩn camera: nội tại và méo ống kính:
findChessboardCornersSB,calibrateCameravớiCALIB_FIX_K3,undistortPoints(..., P=K), lưucv2.FileStorage. - Homography: đo trên mặt phẳng khi camera nghiêng:
findHomography,perspectiveTransform, nguyên tắc "đổi điểm biên sang mm rồi mới fit". - Đo cạnh subpixel, fitting đường thẳng và đường tròn: caliper với cực tính, nội suy parabol đỉnh gradient, fit đường bằng SVD, fit tròn Kasa.
- Đặc trưng contour và blob: Otsu, contour cha – con,
minAreaRectđể định vị thô. - Quy đổi pixel sang mm và đánh giá sai số: σ, 6σ/T, bias, dải bảo vệ.
4. Dữ liệu: camera ảo và bộ ảnh
Đoạn code này đóng vai "trạm thật": nó tạo ra mọi ảnh bạn cần trong thư mục du_an8/ và giữ đáp án trong du_an8/dap_an.json. Lời giải không được đọc file đáp án, trừ khi để chấm điểm. Cách vẽ giống các bài trước: mỗi pixel → tia sáng (bỏ méo) → giao mặt phẳng → tô màu theo khoảng cách có dấu tới biên hình, nên biên chính xác dưới pixel và có méo thật.
import json
import math
import os
import cv2
import numpy as np
W, H = 1280, 960
K_THAT = np.array([[2600.0, 0, 646.0], [0, 2600.0, 478.0], [0, 0, 1]])
DIST_THAT = np.array([-0.09, 0.03, 0.0004, -0.0003, 0.0])
# Mặt phẳng đo (mặt bàn đèn nền) trong hệ camera: nghiêng khoảng 1°, cách 260 mm
R_BAN = cv2.Rodrigues(np.radians([1.2, -0.6, 0.0]))[0] @ cv2.Rodrigues(np.radians([0, 0, 0.8]))[0]
T_BAN = np.array([-40.0, -25.0, 260.0])
COT, HANG, O = 9, 6, 10.0 # bàn cờ 9 × 6 góc trong, ô 10 mm
os.makedirs("du_an8", exist_ok=True)
uu, vv = np.meshgrid(np.arange(W, dtype=np.float64), np.arange(H, dtype=np.float64))
dung = (cv2.TERM_CRITERIA_COUNT | cv2.TERM_CRITERIA_EPS, 40, 1e-10)
TIA = cv2.undistortPoints(np.stack([uu.ravel(), vv.ravel()], 1).reshape(-1, 1, 2), K_THAT, DIST_THAT,
None, None, None, dung).reshape(H, W, 2)
def toa_do_mat_phang(R, t):
"""Tọa độ (X, Y) mm trên mặt phẳng Z = 0 của vật cho từng pixel, và cỡ pixel tại chỗ (mm)."""
Hi = np.linalg.inv(np.column_stack([R[:, 0], R[:, 1], t]))
x, y = TIA[..., 0], TIA[..., 1]
m = Hi[2, 0] * x + Hi[2, 1] * y + Hi[2, 2]
X = (Hi[0, 0] * x + Hi[0, 1] * y + Hi[0, 2]) / m
Y = (Hi[1, 0] * x + Hi[1, 1] * y + Hi[1, 2]) / m
return X, Y, np.maximum(np.hypot(*np.gradient(X)), np.hypot(*np.gradient(Y)))
def hoan_thien(anh, rng):
anh = cv2.GaussianBlur(anh, (0, 0), 0.9) + rng.normal(0, 2.0, anh.shape)
return np.clip(np.round(anh), 0, 255).astype(np.uint8)
def ve_ban_co(R, t, rng):
X, Y, p = toa_do_mat_phang(R, t)
sx = np.clip(O / math.pi * np.sin(math.pi * X / O) / (p / 2), -1, 1)
sy = np.clip(O / math.pi * np.sin(math.pi * Y / O) / (p / 2), -1, 1)
o_co = (X > -O) & (X < COT * O) & (Y > -O) & (Y < HANG * O)
giay = (X > -2 * O) & (X < (COT + 1) * O) & (Y > -2 * O) & (Y < (HANG + 1) * O)
anh = np.where(giay, np.where(o_co, 125 - 95 * sx * sy, 220.0), 60.0)
return hoan_thien(anh, rng)
def ve_tam_kep(ct, cx, cy, goc_do, rng):
"""Tấm kẹp kích thước ct = {L, W, D, X}, tâm hình chữ nhật (cx, cy) mm, xoay goc_do, trên đèn nền."""
X, Y, p = toa_do_mat_phang(R_BAN, T_BAN)
g = math.radians(goc_do)
xl = (X - cx) * math.cos(g) + (Y - cy) * math.sin(g) # tọa độ trong hệ chi tiết
yl = -(X - cx) * math.sin(g) + (Y - cy) * math.cos(g)
qx, qy = np.abs(xl) - ct["L"] / 2, np.abs(yl) - ct["W"] / 2 # khoảng cách có dấu tới hình chữ nhật
sd_hcn = np.hypot(np.maximum(qx, 0), np.maximum(qy, 0)) + np.minimum(np.maximum(qx, qy), 0)
sd_lo = np.hypot(xl - (ct["X"] - ct["L"] / 2), yl) - ct["D"] / 2
sd = np.maximum(sd_hcn, -sd_lo) # hình chữ nhật trừ lỗ
phu = np.clip(0.5 - sd / p, 0, 1)
return hoan_thien(230.0 - 205.0 * phu, rng)
rng = np.random.default_rng(8)
# (a) 12 ảnh bàn cờ ở nhiều tư thế để hiệu chuẩn nội tại
objp = np.zeros((COT * HANG, 3))
objp[:, :2] = np.mgrid[0:COT, 0:HANG].T.reshape(-1, 2) * O
k = 0
while k < 12:
R = cv2.Rodrigues(np.radians(rng.uniform(-25, 25, 3) * [1, 1, 0.8]))[0]
t = np.array([*rng.uniform(-55, 55, 2), rng.uniform(230, 300)]) - R @ np.array([40.0, 25.0, 0])
uv = cv2.projectPoints(objp, cv2.Rodrigues(R)[0], t, K_THAT, DIST_THAT)[0].reshape(-1, 2)
if uv.min() < 70 or uv[:, 0].max() > W - 70 or uv[:, 1].max() > H - 70:
continue
cv2.imwrite(f"du_an8/calib_{k:02d}.png", ve_ban_co(R, t, rng))
k += 1
# (b) bàn cờ nằm phẳng trên bàn đèn: định nghĩa hệ mm của mặt phẳng đo
cv2.imwrite("du_an8/mat_phang.png", ve_ban_co(R_BAN, T_BAN, rng))
# (c) chi tiết: A đo lặp lại 10 lần; A, B, C cho báo cáo dung sai (giá trị CMM)
CHI_TIET = {"A": {"L": 40.012, "W": 24.991, "D": 10.008, "X": 12.004},
"B": {"L": 39.996, "W": 25.004, "D": 10.041, "X": 11.990},
"C": {"L": 40.063, "W": 25.010, "D": 9.995, "X": 12.021}}
vi_tri = []
for lan in range(10):
cx, cy, goc = 40 + rng.uniform(-5, 5), 25 + rng.uniform(-5, 5), rng.uniform(-10, 10)
vi_tri.append([cx, cy, goc])
cv2.imwrite(f"du_an8/A_lan{lan:02d}.png", ve_tam_kep(CHI_TIET["A"], cx, cy, goc, rng))
for ten in "ABC":
cv2.imwrite(f"du_an8/{ten}.png", ve_tam_kep(CHI_TIET[ten], 40 + rng.uniform(-5, 5),
25 + rng.uniform(-5, 5), rng.uniform(-10, 10), rng))
json.dump({"K": K_THAT.tolist(), "dist": DIST_THAT.tolist(), "chi_tiet": CHI_TIET, "vi_tri_A": vi_tri},
open("du_an8/dap_an.json", "w"))
print("Đã tạo:", len(os.listdir("du_an8")), "file trong du_an8/")
Đã tạo: 27 file trong du_an8/
5. Gợi ý thiết kế
- Hai lần hiệu chuẩn, hai vai trò. Nội tại (K, dist) mô tả camera; homography mô tả mặt phẳng đo trong hệ camera. Homography phải tính từ góc bàn cờ đã khử méo (
undistortPointsvớiP=K), và về sau mọi điểm biên cũng phải đi đúng con đường đó: pixel → khử méo → H → mm. - Định vị thô trên ảnh, đo tinh trong mm. Dùng contour và
minAreaRectđể biết chi tiết ở đâu, xoay bao nhiêu, rồi đặt caliper theo hướng các cạnh. Hướng "trái" của chi tiết xác định bằng vị trí lỗ (lỗ gần cạnh trái), không tin vào góc củaminAreaRect. - Caliper quét từ ngoài vào trong, cực tính sáng → tối, cho cả bốn cạnh; với lỗ, quét từ tâm lỗ ra ngoài cũng là sáng → tối. Bỏ đoạn gần góc (khoảng 15% mỗi đầu cạnh) để tránh bo góc và ba via.
- Định nghĩa kích thước rõ ràng: L là khoảng cách giữa hai đường fit của hai cạnh ngắn, đo dọc pháp tuyến chung tại tâm; X là khoảng cách từ tâm lỗ tới đường fit cạnh trái.
- Hàm đo trả về dict, hàm báo cáo nhận dict: tách đo và phán xét để dùng lại ở capstone.
- Đo lặp lại đúng nghĩa: 10 ảnh khác nhau, mỗi ảnh một lần đặt (vị trí, góc khác nhau), không phải 10 lần chạy trên một ảnh.
6. Lời giải
Hãy tự làm trước. Lời giải gồm 5 bước, mỗi bước một khối code chạy được nối tiếp nhau.
Xem lời giải đầy đủ (5 bước)
Bước 1. Hiệu chuẩn nội tại
import glob
import cv2
import numpy as np
COT, HANG, O = 9, 6, 10.0
objp = np.zeros((COT * HANG, 3), np.float32)
objp[:, :2] = np.mgrid[0:COT, 0:HANG].T.reshape(-1, 2) * O
vat, anh_pts = [], []
for f in sorted(glob.glob("du_an8/calib_*.png")):
g = cv2.imread(f, cv2.IMREAD_GRAYSCALE)
ok, c = cv2.findChessboardCornersSB(g, (COT, HANG), flags=cv2.CALIB_CB_ACCURACY)
if ok:
vat.append(objp)
anh_pts.append(c)
rms, K, dist, _, _, _, _, e = cv2.calibrateCameraExtended(vat, anh_pts, g.shape[::-1], None, None,
flags=cv2.CALIB_FIX_K3)
fs = cv2.FileStorage("du_an8/camera.yaml", cv2.FILE_STORAGE_WRITE)
fs.write("camera_matrix", K)
fs.write("dist_coeffs", dist)
fs.write("rms_px", float(rms))
fs.release()
print(f"{len(anh_pts)} ảnh, RMS {rms:.3f} px, sai số từng ảnh lớn nhất {e.max():.3f} px")
print(f"fx {K[0, 0]:.1f}, fy {K[1, 1]:.1f}, cx {K[0, 2]:.1f}, cy {K[1, 2]:.1f}, k1 {dist[0, 0]:+.4f}, k2 {dist[0, 1]:+.4f}")
12 ảnh, RMS 0.019 px, sai số từng ảnh lớn nhất 0.029 px
fx 2599.9, fy 2599.9, cx 645.8, cy 478.1, k1 -0.0891, k2 +0.0211
Đối chiếu đáp án (fx = 2600, cx = 646, cy = 478, k1 = −0,09, k2 = +0,03): fx sai dưới 0,1%, đạt Y1.
Bước 2. Hiệu chuẩn mặt phẳng đo
import cv2
import numpy as np
def khu_meo(P):
"""Pixel ảnh gốc → pixel của camera không méo cùng K."""
return cv2.undistortPoints(np.asarray(P, np.float64).reshape(-1, 1, 2), K, dist, P=K).reshape(-1, 2)
g = cv2.imread("du_an8/mat_phang.png", cv2.IMREAD_GRAYSCALE)
ok, goc = cv2.findChessboardCornersSB(g, (COT, HANG), flags=cv2.CALIB_CB_ACCURACY)
Hmm, _ = cv2.findHomography(khu_meo(goc), objp[:, :2].astype(np.float64)) # pixel khử méo → mm
def px_sang_mm(P):
return cv2.perspectiveTransform(khu_meo(P).reshape(-1, 1, 2), Hmm).reshape(-1, 2)
du = np.linalg.norm(px_sang_mm(goc) - objp[:, :2], axis=1)
print(f"Sai số trên 54 góc: trung bình {1000 * du.mean():.1f} µm, lớn nhất {1000 * du.max():.1f} µm")
s_tam = np.mean(np.linalg.norm(np.diff(px_sang_mm(goc).reshape(HANG, COT, 2), axis=1), axis=2)) / \
np.mean(np.linalg.norm(np.diff(goc.reshape(HANG, COT, 2), axis=1), axis=2))
print(f"Tỉ lệ trung bình quanh tâm ảnh: {1000 * s_tam:.2f} µm/px")
# Nếu so với một điểm của bàn cờ mà không khử méo:
H_tho, _ = cv2.findHomography(goc.reshape(-1, 2).astype(np.float64), objp[:, :2].astype(np.float64))
du_tho = np.linalg.norm(cv2.perspectiveTransform(goc.reshape(-1, 1, 2).astype(np.float64), H_tho).reshape(-1, 2)
- objp[:, :2], axis=1)
print(f"Cùng homography nhưng KHÔNG khử méo: sai số lớn nhất {1000 * du_tho.max():.1f} µm")
Sai số trên 54 góc: trung bình 1.2 µm, lớn nhất 3.5 µm
Tỉ lệ trung bình quanh tâm ảnh: 100.62 µm/px
Cùng homography nhưng KHÔNG khử méo: sai số lớn nhất 52.0 µm
Thứ tự góc mà findChessboardCornersSB trả về quyết định gốc và chiều của hệ mm. Với phép đo kích thước (khoảng cách, đường kính) điều này không quan trọng vì mọi hệ trực chuẩn đều cho cùng khoảng cách. Nếu cần tọa độ tuyệt đối (gửi vị trí cho robot), phải cố định thứ tự như bài Hiệu chuẩn camera đã nhắc. Dòng cuối cho thấy homography không thay được khử méo: bỏ bước khử méo, sai số trên chính các góc dùng để fit đã lên hàng chục micromet.
Bước 3. Đo một chi tiết
import math
import cv2
import numpy as np
def tim_canh(anh, tam, huong, dai=40, cuc=-1, nguong=20):
"""Caliper: quét đoạn dài `dai` px qua `tam` theo `huong`; trả về điểm cạnh subpixel (x, y) hoặc None.
cuc = -1: cạnh sáng → tối theo chiều quét."""
hx, hy = np.asarray(huong, np.float64) / np.hypot(*huong)
t = np.arange(-dai / 2, dai / 2 + 1)
mx = (tam[0] + t * hx).astype(np.float32).reshape(1, -1)
my = (tam[1] + t * hy).astype(np.float32).reshape(1, -1)
p = cv2.remap(anh, mx, my, cv2.INTER_CUBIC).ravel().astype(np.float64)
gr = np.zeros_like(p)
gr[1:-1] = (p[2:] - p[:-2]) / 2 * cuc
k = int(np.argmax(gr))
if gr[k] < nguong or k < 2 or k > len(gr) - 3:
return None
a, b, c = gr[k - 1], gr[k], gr[k + 1]
s = t[k] + 0.5 * (a - c) / (a - 2 * b + c)
return tam[0] + s * hx, tam[1] + s * hy
def fit_duong(P):
c = P.mean(axis=0)
v = np.linalg.svd(P - c)[2][0]
return c, np.array([-v[1], v[0]]) # điểm trên đường, pháp tuyến đơn vị
def fit_tron(P):
A = np.column_stack([P, np.ones(len(P))])
D, E, F = np.linalg.lstsq(A, -(P ** 2).sum(axis=1), rcond=None)[0]
return np.array([-D / 2, -E / 2]), math.sqrt(D * D / 4 + E * E / 4 - F)
def do_tam_kep(anh8, doi_mm=None):
"""Đo L, W, D, X (mm). doi_mm: hàm đổi mảng điểm pixel → mm (mặc định: hiệu chuẩn đầy đủ)."""
doi_mm = doi_mm or px_sang_mm
anh = anh8.astype(np.float32)
_, bw = cv2.threshold(anh8, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
cs, hier = cv2.findContours(bw, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
i = max(range(len(cs)), key=lambda j: cv2.contourArea(cs[j]))
lo = [cs[j] for j in range(len(cs)) if hier[0][j][3] == i and cv2.contourArea(cs[j]) > 500]
if not lo:
raise ValueError("không thấy lỗ")
(cx, cy), (w, h), _ = cv2.minAreaRect(cs[i])
m = cv2.moments(lo[0])
lx, ly = m["m10"] / m["m00"], m["m01"] / m["m00"]
ux = np.array([cx - lx, cy - ly]) / math.hypot(cx - lx, cy - ly) # hướng từ lỗ → tâm: trục +x chi tiết
uy = np.array([-ux[1], ux[0]])
dai, rong = max(w, h), min(w, h)
canh = {}
for ten, goc_canh, phap, nua, doc in (("trai", -1, ux, dai / 2, rong), ("phai", +1, ux, dai / 2, rong),
("tren", -1, uy, rong / 2, dai), ("duoi", +1, uy, rong / 2, dai)):
doc_canh = np.array([phap[1], -phap[0]]) # hướng chạy dọc cạnh
diem = []
for s in np.linspace(-0.35, 0.35, 15) * doc:
tam = np.array([cx, cy]) + goc_canh * nua * phap + s * doc_canh
q = tim_canh(anh, tam, -goc_canh * phap) # quét từ ngoài vào trong
if q is not None:
diem.append(q)
canh[ten] = fit_duong(doi_mm(np.array(diem)))
r0 = math.sqrt(cv2.contourArea(lo[0]) / math.pi)
diem_lo = [tim_canh(anh, (lx + r0 * math.cos(a), ly + r0 * math.sin(a)), (math.cos(a), math.sin(a)))
for a in np.linspace(0, 2 * math.pi, 120, endpoint=False)]
tam_lo, ban_kinh = fit_tron(doi_mm(np.array([q for q in diem_lo if q is not None])))
def kc(c1, c2): # khoảng cách giữa hai đường gần song song
n = c1[1] if np.dot(c1[1], c2[1]) > 0 else -c1[1]
n = (n + c2[1]) / np.linalg.norm(n + c2[1])
return abs(np.dot(c2[0] - c1[0], n))
return {"L": kc(canh["trai"], canh["phai"]), "W": kc(canh["tren"], canh["duoi"]), "D": 2 * ban_kinh,
"X": abs(np.dot(tam_lo - canh["trai"][0], canh["trai"][1]))}
kq = do_tam_kep(cv2.imread("du_an8/A_lan00.png", cv2.IMREAD_GRAYSCALE))
print("Chi tiết A, lần 0:", {k: round(float(v), 4) for k, v in kq.items()})
Chi tiết A, lần 0: {'L': 40.0164, 'W': 24.9922, 'D': 10.0087, 'X': 12.0068}
Bước 4. Báo cáo theo dung sai
import cv2
BAN_VE = {"L": (40.00, 0.05), "W": (25.00, 0.05), "D": (10.00, 0.03), "X": (12.00, 0.05)}
def bao_cao(ten, kq):
print(f"Chi tiết {ten}")
print(" kích thước | danh định | dung sai | đo được | sai lệch | kết quả")
tong = "OK"
for k, (dd, ds) in BAN_VE.items():
lech = kq[k] - dd
ok = abs(lech) <= ds
tong = tong if ok else "NG"
print(f" {k} | {dd:7.2f} | ±{ds:.2f} | {kq[k]:8.3f} | {lech:+7.3f} | {'OK' if ok else 'NG'}")
print(f" KẾT LUẬN: {tong}")
return tong
for ten in "ABC":
bao_cao(ten, do_tam_kep(cv2.imread(f"du_an8/{ten}.png", cv2.IMREAD_GRAYSCALE)))
Chi tiết A
kích thước | danh định | dung sai | đo được | sai lệch | kết quả
L | 40.00 | ±0.05 | 40.016 | +0.016 | OK
W | 25.00 | ±0.05 | 24.992 | -0.008 | OK
D | 10.00 | ±0.03 | 10.006 | +0.006 | OK
X | 12.00 | ±0.05 | 12.007 | +0.007 | OK
KẾT LUẬN: OK
Chi tiết B
kích thước | danh định | dung sai | đo được | sai lệch | kết quả
L | 40.00 | ±0.05 | 40.000 | +0.000 | OK
W | 25.00 | ±0.05 | 25.006 | +0.006 | OK
D | 10.00 | ±0.03 | 10.042 | +0.042 | NG
X | 12.00 | ±0.05 | 11.993 | -0.007 | OK
KẾT LUẬN: NG
Chi tiết C
kích thước | danh định | dung sai | đo được | sai lệch | kết quả
L | 40.00 | ±0.05 | 40.066 | +0.066 | NG
W | 25.00 | ±0.05 | 25.011 | +0.011 | OK
D | 10.00 | ±0.03 | 9.998 | -0.002 | OK
X | 12.00 | ±0.05 | 12.023 | +0.023 | OK
KẾT LUẬN: NG
Kết quả hiển thị 3 chữ số thập phân (micromet) vì σ của hệ đo cỡ micromet (bước 5); hiển thị nhiều hơn là giả chính xác. B bị loại đúng vì lỗ Ø10,041 mm; C bị loại đúng vì dài 40,063 mm. Chú ý A: các kích thước nằm sát nhau quanh danh định nên rõ ràng OK; một chi tiết đo được, ví dụ, L = 40,048 mm thì sao? Với σ vài micromet, nó có thể là 40,052 thật. Đó là lý do dùng dải bảo vệ (bài Quy đổi pixel sang mm) khi kích thước đo được nằm trong khoảng ±3σ quanh giới hạn.
Bước 5. Độ lặp lại và so sánh với cách đo ngây thơ
import glob
import json
import cv2
import numpy as np
that = json.load(open("du_an8/dap_an.json"))["chi_tiet"]["A"] # giá trị CMM của chi tiết A
# Cách ngây thơ: một hệ số mm/px lấy từ bàn cờ ở tâm ảnh, không khử méo, không homography
def ngay_tho(P):
return np.asarray(P, np.float64) * s_tam
ket_qua = {"đầy đủ": [], "ngây thơ": []}
for f in sorted(glob.glob("du_an8/A_lan*.png")):
g = cv2.imread(f, cv2.IMREAD_GRAYSCALE)
ket_qua["đầy đủ"].append(do_tam_kep(g))
ket_qua["ngây thơ"].append(do_tam_kep(g, doi_mm=ngay_tho))
for cach, ds in ket_qua.items():
print(f"Cách đo {cach} (10 lần đặt lại chi tiết A)")
print(" kt | trung bình | σ (µm) | 6σ/T (%) | bias (µm) | đạt Y5")
for k, (dd, dsai) in BAN_VE.items():
x = np.array([d[k] for d in ds])
s = x.std(ddof=1)
ty_le = 100 * 6 * s / (2 * dsai)
print(f" {k} | {x.mean():9.4f} | {1000 * s:6.2f} | {ty_le:6.1f} | {1000 * (x.mean() - that[k]):+8.2f} | "
f"{'đạt' if ty_le <= 20 else 'KHÔNG'}")
Cách đo đầy đủ (10 lần đặt lại chi tiết A)
kt | trung bình | σ (µm) | 6σ/T (%) | bias (µm) | đạt Y5
L | 40.0156 | 0.82 | 4.9 | +3.58 | đạt
W | 24.9928 | 1.30 | 7.8 | +1.77 | đạt
D | 10.0091 | 1.66 | 16.6 | +1.13 | đạt
X | 12.0058 | 1.64 | 9.8 | +1.79 | đạt
Cách đo ngây thơ (10 lần đặt lại chi tiết A)
kt | trung bình | σ (µm) | 6σ/T (%) | bias (µm) | đạt Y5
L | 40.0879 | 12.14 | 72.8 | +75.85 | KHÔNG
W | 25.0406 | 9.59 | 57.5 | +49.62 | KHÔNG
D | 10.0357 | 3.55 | 35.5 | +27.73 | KHÔNG
X | 12.0393 | 4.19 | 25.1 | +35.30 | KHÔNG
Đọc hai bảng:
- Hiệu chuẩn đầy đủ: σ từ 0,8 tới 1,7 µm, 6σ/T từ 5% tới khoảng 17% (D có dải dung sai hẹp nhất nên tỉ lệ cao nhất), bias từ +1 tới +4 µm. Đây là câu trả lời cho bài thực hành: "Đo 10 lần cùng một chi tiết, tính độ lệch chuẩn và so với dung sai yêu cầu": hệ đo đạt Y5, và với D thì ở mức "chấp nhận có điều kiện" theo hướng dẫn 10:1 / 4:1 của bài Quy đổi pixel sang mm.
- Số tia đo lỗ quyết định σ của D: phiên bản đầu của lời giải dùng 48 tia và được σ = 3,6 µm (6σ/T = 36%, không đạt); tăng lên 120 tia giảm σ khoảng √(120/48) ≈ 1,6 lần, về khoảng 1,7 µm. Đây là "núm vặn" rẻ nhất, trước khi nghĩ tới camera độ phân giải cao hơn.
- Cách ngây thơ trên cùng 10 ảnh: σ tăng 2–15 lần (3,5–12 µm) vì sai số giờ phụ thuộc vị trí và góc chi tiết trong ảnh (méo và nghiêng thay đổi theo vị trí), và bias lên tới +28 … +76 µm vì tỉ lệ lấy ở tâm bàn cờ không đại diện cho cả vùng chi tiết. Không kích thước nào đạt; L còn bị đẩy sát giới hạn trên dù chi tiết thật chỉ dài hơn danh định 12 µm. Một kỹ sư chỉ thử chi tiết đặt ở giữa ảnh sẽ không bao giờ thấy vấn đề này.
- Bias còn lại của cách đầy đủ có nguồn gốc giống bài Quy đổi pixel sang mm: vị trí biên phụ thuộc mờ quang học và lấy mẫu; ở trạm thật cần thêm bước bù biên bằng căn mẫu hoặc vòng chuẩn. Capstone 1 làm bước này.
7. Tự đánh giá
| Tiêu chí | Mức đạt | Mức tốt |
|---|---|---|
| Hiệu chuẩn nội tại | RMS ≤ 0,1 px, có lưu YAML | In sai số từng ảnh, so K với đáp án, giải thích được vì sao cố định k3 |
| Mặt phẳng đo | Homography từ góc đã khử méo, sai số góc ≤ 10 µm | Chứng minh bằng số hậu quả của việc bỏ khử méo |
| Đo | Đo được L, W, D, X trên mọi ảnh, không lỗi | Caliper có cực tính, bỏ vùng góc, fit trong mm; xử lý ảnh không có chi tiết bằng ngoại lệ rõ ràng |
| Báo cáo dung sai | A OK, B NG (D), C NG (L) | Có số chữ số hợp lý, nêu được khi nào cần dải bảo vệ |
| Độ lặp lại | Bảng trung bình, σ, 6σ/T, bias cho 4 kích thước | So sánh được với cách ngây thơ và giải thích nguồn gốc khác biệt |
| Chất lượng code | Chạy từ đầu tới cuối không sửa tay | Hàm đo trả về dict, tách đo và phán xét, tham số ở một chỗ, dùng lại được cho capstone |
8. Mở rộng
- Bù sai lệch biên: thêm vào mô phỏng hiệu ứng đèn nền "ăn" biên 4 µm (như bài Quy đổi pixel sang mm), tạo ảnh một căn mẫu 30,000 mm và một vòng chuẩn Ø10,000 mm, tính giá trị bù cho kích thước ngoài và kích thước trong (ngược dấu).
- Chi tiết dày: đổi tấm 1 mm thành 5 mm và mô phỏng camera nhìn thấy cạnh trên; tính sai số khi hiệu chuẩn mặt phẳng ở mặt bàn (bài Homography, mục chiều cao).
- Đo nhiều chi tiết một ảnh: lặp qua mọi contour ngoài đủ lớn, báo cáo từng chi tiết theo vị trí.
- Kiểm tra đầu ca: thêm một ảnh mẫu chuẩn và hàm kiểm tra theo ngưỡng như bài Kiểm tra hiệu chuẩn định kỳ; khóa đo nếu không đạt.
- Hướng tới Capstone 1: tách thành mô-đun
hieu_chuan.py,do.py,bao_cao.py, cấu hình TOML; sau chặng tích hợp, thêm giao tiếp PLC và lưu kết quả, rồi làm nghiên cứu độ lặp lại 3 chi tiết × 30 lần như trong Capstone 1: trạm đo kích thước hoàn chỉnh.
Tóm tắt
- Chuỗi đo đúng: pixel → caliper subpixel → khử méo (K, dist) → homography mặt phẳng đo → mm → fit đường, fit tròn → kích thước. Không có "một hệ số mm/px chung" nào trong chuỗi.
- Hiệu chuẩn nội tại và hiệu chuẩn mặt phẳng là hai bước khác nhau; homography luôn tính trên điểm đã khử méo.
- Định vị thô bằng contour, đo tinh bằng caliper có cực tính, định nghĩa từng kích thước rõ ràng theo đường fit.
- Báo cáo theo dung sai với số chữ số phù hợp với σ; đánh giá độ lặp lại bằng 10 lần đặt lại, so 6σ với dải dung sai và bias với giá trị CMM.
- Cách ngây thơ có thể đúng ở tâm ảnh nhưng sai và kém lặp lại khi chi tiết dịch chuyển: luôn thử ở nhiều vị trí.
Chặng tiếp theo bắt đầu với bài I/O số và handshake với PLC: đưa kết quả OK/NG của trạm đo này tới dây chuyền, bước tiếp theo trên đường tới Capstone 1.
Bài tập thực hành
- Đo 10 lần cùng một chi tiết, tính độ lệch chuẩn và so với dung sai yêu cầu.
Làm xong bài tập mới chuyển sang bài tiếp theo: tự viết code là cách duy nhất để nhớ lâu.


