⭐ Vision Engineer · Chặng 4Bài 2/4Bài học~60 phút

5 nhóm bài toán Machine Vision và cách giải: từ có/không đến dẫn hướng

Nhận diện nhóm bài toán, kỹ thuật và con số cần hỏi cho từng nhóm, ví dụ OpenCV chạy được và bảng chấm độ khó 0–16 cho một yêu cầu.

5 nhóm bài toán Machine Vision và cách giải: từ có/không đến dẫn hướng

Học xong bài này, bạn sẽ

  • Nhận diện bài toán: có/không, đo, kiểm lỗi, nhận dạng, dẫn hướng
  • Biết kỹ thuật điển hình cho từng nhóm
  • Ước lượng độ khó của một yêu cầu

Nên học trước: Hệ thống Machine Vision gồm những gì?

Nội dung bài viết
  1. 1. Năm nhóm bài toán
  2. 2. Bảng tra: nhóm → kỹ thuật → con số → yếu tố gây khó
  3. 3. Ví dụ nhóm 1 (có/không): kiểm tra đủ linh kiện trên bo mạch
  4. 4. Ví dụ nhóm 2 (đo lường): vì sao cần subpixel
  5. 5. Ví dụ nhóm 3 (kiểm lỗi): so với ảnh mẫu chuẩn
  6. 6. Nhóm 4 và 5 trong một đoạn
  7. 7. Ước lượng độ khó của một yêu cầu
  8. 8. Sai lầm thường gặp
  9. 9. Bài tập
  10. Tóm tắt

Khách hàng hiếm khi nói "tôi cần một thuật toán đo cạnh subpixel". Họ nói "chai bị thiếu nắp", "trục bị lệch kích thước", "vỏ bị xước", "in sai hạn sử dụng", "robot gắp trượt". Việc đầu tiên của kỹ sư Machine Vision là dịch câu nói đó thành một nhóm bài toán, vì mỗi nhóm có bộ kỹ thuật, bộ thông số và nguồn rủi ro riêng. Bài trước, "Hệ thống Machine Vision gồm những gì?", cho bạn bức tranh phần cứng; bài này cho bạn bức tranh bài toán. Cuối bài bạn có một bảng chấm điểm độ khó để trả lời câu hỏi khách hàng nào cũng hỏi: "làm được không, khó không?".

Mục tiêu cuối bàiNghe một yêu cầu là xếp được vào một hoặc vài nhóm trong 5 nhóm, nêu được kỹ thuật điển hình và các con số phải hỏi, chạy được ví dụ nhỏ cho ba nhóm, và chấm được độ khó của yêu cầu theo thang 0–16.

1. Năm nhóm bài toán

Gần như mọi ứng dụng Machine Vision trong nhà máy rơi vào một (hoặc kết hợp vài) nhóm sau:

  1. Có/không (presence/absence): đặc điểm có mặt hay vắng mặt. Thiếu nắp, thiếu ốc, thiếu linh kiện, thiếu nhãn, thiếu gioăng.
  2. Đo lường (gauging, measurement): kích thước, khoảng cách, góc, đường kính, độ đồng tâm có nằm trong dung sai không.
  3. Kiểm tra lỗi (defect inspection): bề mặt có vết xước, vết bẩn, sứt mẻ, bọt khí, in nhòe không. Lỗi không biết trước vị trí và hình dạng.
  4. Nhận dạng (identification): đọc mã vạch, mã 2D (QR, DataMatrix), đọc ký tự (OCR), xác nhận ký tự in đúng (OCV), phân biệt mã sản phẩm theo màu hay hình dạng.
  5. Dẫn hướng (guidance, alignment): tìm vị trí và góc của vật để robot gắp, để máy căn chỉnh, để bước kiểm tra sau đặt ROI đúng chỗ.

Một trạm thật thường kết hợp: trạm kiểm tra nhãn chai vừa dẫn hướng (tìm nhãn ở đâu), vừa có/không (có nhãn không), vừa đo (nhãn lệch bao nhiêu mm), vừa nhận dạng (đọc mã lô). Tách ra từng nhóm giúp bạn ước lượng đúng phần nào dễ, phần nào khó.

2. Bảng tra: nhóm → kỹ thuật → con số → yếu tố gây khó

NhómKỹ thuật điển hìnhCon số phải hỏi / tínhYếu tố làm bài toán khó
Có/khôngĐộ sáng trung bình trong ROI, đếm pixel sau phân ngưỡng, blob (diện tích, số lượng), điểm khớp mẫu (template matching)Số vị trí cần kiểm; kích thước đặc điểm nhỏ nhất; độ lệch vị trí của vật; năng suất (sản phẩm/phút)Đặc điểm cùng màu với nền; vật trôi vị trí nên ROI cố định không còn đúng; nhiều mã sản phẩm
Đo lườngProfile và tìm cạnh subpixel, công cụ thước kẹp (caliper), fitting đường thẳng/đường tròn, hiệu chuẩn pixel → mm, ống kính telecentricKích thước danh định và dung sai (ví dụ 12 ± 0,02 mm); độ lặp lại yêu cầu; mm/pixel; số phép đo mỗi vậtDung sai nhỏ so với kích thước vật; vật 3D, cạnh vát, bavia; phản xạ ở mép; nhiệt độ; vật không nằm cùng độ cao
Kiểm tra lỗiSo với ảnh mẫu chuẩn (golden template), lọc + phân ngưỡng + blob, phân tích texture, deep learning (phân loại, phân đoạn, phát hiện bất thường)Kích thước lỗi nhỏ nhất (mm) → số pixel trên lỗi; danh sách loại lỗi kèm ảnh; tỉ lệ lọt lỗi và loại nhầm chấp nhận đượcLỗi đa dạng, hiếm; định nghĩa lỗi bằng lời ("xước nhìn thấy được"); bề mặt bóng, cong, có hoa văn; lỗi nhỏ gần ngưỡng
Nhận dạngThư viện giải mã 1D/2D, OCR/OCV, phân loại màu, khớp mẫu hình dạngKích thước module mã / chiều cao ký tự → số pixel; chất lượng in; danh sách ký tự (font); dữ liệu đúng để so lấy từ đâuIn mờ, nhòe, khắc laser trên kim loại; bề mặt cong; font lạ; ký tự dễ nhầm (0/O, 1/I, 8/B)
Dẫn hướngKhớp mẫu hình dạng (shape matching), tâm và góc blob, fitting, hiệu chuẩn camera và hand-eye, vision 3DĐộ chính xác vị trí (mm) và góc (°); vùng làm việc; thời gian; số vật mỗi lần chụpVật chồng lên nhau; vật đối xứng (khó xác định góc); sai số hiệu chuẩn; vật có chiều cao thay đổi
Các con số kinh nghiệm cho độ phân giải (tham khảo)
  • Có/không, kiểm lỗi: đặc điểm hoặc lỗi nhỏ nhất nên phủ ít nhất khoảng 3×3 pixel; muốn ổn định và đo được kích thước lỗi thì cần nhiều hơn (5–10 pixel).
  • Đo lường: sai số của hệ đo nên nhỏ hơn khoảng 1/10 dải dung sai (quy tắc 10:1). Đo cạnh subpixel trong điều kiện tốt cho độ lặp lại cỡ 0,1–0,25 pixel, nên dải dung sai cần trải trên nhiều pixel.
  • Mã 2D (DataMatrix, QR): mỗi module thường cần vài pixel (khoảng 3–5 pixel trở lên); mã vạch 1D: vạch hẹp nhất khoảng 1,5–2 pixel trở lên. OCR: chiều cao ký tự thường cần vài chục pixel. Con số chính xác tùy thư viện, hãy đọc khuyến nghị của thư viện bạn dùng.
Công thức chung: mm/pixel = FOV (mm) / số pixel theo chiều đó, rồi số pixel trên đặc điểm = kích thước (mm) / (mm/pixel).

3. Ví dụ nhóm 1 (có/không): kiểm tra đủ linh kiện trên bo mạch

Bo mạch nằm trong đồ gá (jig) nên vị trí 8 linh kiện đã biết trước. Thuật toán đơn giản nhất: lấy ROI ở lõi mỗi vị trí, tính độ sáng trung bình, so ngưỡng. Thân linh kiện sáng, nền bo mạch tối.

import cv2
import numpy as np

# Bo mạch giả lập: 2 hàng × 4 vị trí linh kiện, vị trí 6 bị thiếu
rng = np.random.default_rng(3)
anh = np.full((240, 400), 60, np.uint8)                         # nền PCB tối
VI_TRI = [(50 + 90 * c, 50 + 100 * r) for r in range(2) for c in range(4)]   # góc trên-trái từng ô
for i, (x, y) in enumerate(VI_TRI, 1):
    cv2.rectangle(anh, (x, y), (x + 40, y + 40), 110, 2)        # khung pad (luôn có)
    if i != 6:
        cv2.rectangle(anh, (x + 6, y + 6), (x + 34, y + 34), 200, -1)   # thân linh kiện sáng
anh = np.clip(anh + rng.normal(0, 6, anh.shape), 0, 255).astype(np.uint8)
cv2.imwrite("bo_mach.png", anh)

NGUONG = 150                     # độ sáng trung bình tối thiểu khi có linh kiện
for i, (x, y) in enumerate(VI_TRI, 1):
    roi = anh[y + 10:y + 30, x + 10:x + 30]                     # chỉ lấy lõi, tránh viền pad
    tb = roi.mean()
    print(f"vị trí {i}: độ sáng {tb:6.1f} → {'CÓ' if tb > NGUONG else 'THIẾU'}")
vị trí 1: độ sáng  199.9 → CÓ
vị trí 2: độ sáng  199.4 → CÓ
vị trí 3: độ sáng  199.0 → CÓ
vị trí 4: độ sáng  199.3 → CÓ
vị trí 5: độ sáng  199.4 → CÓ
vị trí 6: độ sáng   59.4 → THIẾU
vị trí 7: độ sáng  198.9 → CÓ
vị trí 8: độ sáng  199.5 → CÓ

Những chi tiết nhỏ nhưng quan trọng:

  • ROI lấy ở lõi (cách viền 10 pixel), không phủ khung pad. Nếu ROI chạm viền pad, độ sáng của ô "thiếu" sẽ bị kéo lên và khoảng hở với ô "có" hẹp lại.
  • Khoảng hở ở đây khoảng 140 mức xám (khoảng 199 so với 59), ngưỡng 150 nằm thoải mái ở giữa. Đây là điều chiếu sáng tốt mang lại (bài trước).
  • Phương pháp chỉ đúng khi vật không trôi vị trí. Nếu bo mạch có thể lệch vài mm, bạn cần một bước dẫn hướng trước (tìm dấu định vị, dịch các ROI theo), là nhóm 5.

4. Ví dụ nhóm 2 (đo lường): vì sao cần subpixel

Đo bề rộng một chi tiết thật rộng 37,35 mm với độ phân giải 0,1 mm/pixel (tức 373,5 pixel). Trên dây chuyền, chi tiết dừng mỗi lần lệch một chút, nên mép rơi vào vị trí lẻ của pixel. Ta so hai cách: đếm số pixel sáng, và tìm vị trí mép subpixel bằng nội suy tuyến tính tại mức 50% giữa nền và vật.

import cv2
import numpy as np

TL = 0.1                                    # mm/pixel (đã hiệu chuẩn)
RONG_THAT = 37.35                           # mm = 373,5 pixel
rng = np.random.default_rng(0)


def chup(lech_px):
    """Ảnh 1 hàng của chi tiết sáng, mép trái tại 100 + lech_px (tạo ở độ phân giải ×10 rồi thu nhỏ)."""
    lon = np.full((10, 6000), 30, np.uint8)
    x1 = int(round((100 + lech_px) * 10))
    lon[:, x1:x1 + int(RONG_THAT / TL * 10)] = 220
    anh = cv2.resize(lon, (600, 1), interpolation=cv2.INTER_AREA)
    anh = cv2.GaussianBlur(anh, (5, 1), 1.0)
    return np.clip(anh[0] + rng.normal(0, 3, 600), 0, 255)


def vi_tri_mep(p, muc, tu_trai=True):
    """Vị trí subpixel đầu tiên profile p cắt mức 'muc', nội suy tuyến tính giữa 2 pixel."""
    idx = range(len(p) - 1) if tu_trai else range(len(p) - 2, -1, -1)
    for i in idx:
        a, b = p[i], p[i + 1]
        if (a - muc) * (b - muc) <= 0 and a != b:
            return i + (muc - a) / (b - a)
    return None


print(" lệch(px)  đếm pixel(mm)  subpixel(mm)")
kq_pixel, kq_sub = [], []
for k in range(10):                         # chi tiết dừng lệch 0,0 .. 0,9 pixel
    p = chup(k / 10)
    muc = (p.min() + p.max()) / 2           # mức 50% giữa nền và vật
    d_pixel = int((p > muc).sum()) * TL
    d_sub = (vi_tri_mep(p, muc, False) - vi_tri_mep(p, muc, True)) * TL
    kq_pixel.append(d_pixel)
    kq_sub.append(d_sub)
    print(f"   {k / 10:.1f}       {d_pixel:.3f}        {d_sub:.3f}")
for ten, d in (("đếm pixel", kq_pixel), ("subpixel", kq_sub)):
    print(f"{ten:9s}: sai lệch trung bình {np.mean(d) - RONG_THAT:+.3f} mm, dải dao động {np.ptp(d):.3f} mm")
 lệch(px)  đếm pixel(mm)  subpixel(mm)
   0.0       37.400        37.359
   0.1       37.400        37.353
   0.2       37.400        37.353
   0.3       37.400        37.357
   0.4       37.400        37.348
   0.5       37.300        37.350
   0.6       37.300        37.337
   0.7       37.300        37.340
   0.8       37.300        37.343
   0.9       37.300        37.348
đếm pixel: sai lệch trung bình +0.000 mm, dải dao động 0.100 mm
subpixel : sai lệch trung bình -0.001 mm, dải dao động 0.022 mm
  • Đếm pixel nhảy giữa 37,4 và 37,3 mm tùy chi tiết dừng ở đâu: sai số lượng tử ±1 pixel. Trung bình thì đúng, nhưng từng lần đo dao động 0,1 mm. Với dung sai ± 0,05 mm, cách này không dùng được.
  • Subpixel dao động khoảng 0,02 mm, tức khoảng 0,2 pixel, phù hợp con số kinh nghiệm 0,1–0,25 pixel ở trên.
  • Đây mới là ảnh mô phỏng sạch. Ngoài thực tế còn có mép vát, bavia, phản xạ, độ cao vật thay đổi (ống kính thường làm vật gần trông to hơn). Kỹ thuật đo cạnh, fitting và hiệu chuẩn được dạy kỹ ở chặng 7 và chặng 8.

5. Ví dụ nhóm 3 (kiểm lỗi): so với ảnh mẫu chuẩn

Với nhãn in, cách cổ điển là lưu một ảnh mẫu chuẩn (golden template) rồi trừ ảnh: chỗ nào khác nhiều là lỗi. Cách này rất nhạy, và chính vì quá nhạy nên có một cái bẫy: chỉ cần sản phẩm lệch 1 pixel, mọi mép chữ đều thành "lỗi".

import cv2
import numpy as np

rng = np.random.default_rng(5)


def nhan(dx=0, loi=False):
    """Nhãn in: khung, logo tròn, 3 dòng chữ giả. dx: lệch ngang (pixel)."""
    a = np.full((200, 300), 200, np.uint8)
    cv2.rectangle(a, (20 + dx, 20), (280 + dx, 180), 60, 3)
    cv2.circle(a, (70 + dx, 100), 30, 60, -1)
    for y in (70, 100, 130):
        cv2.line(a, (120 + dx, y), (250 + dx, y), 60, 6)
    if loi:
        cv2.line(a, (150 + dx, 150), (210 + dx, 165), 120, 2)       # vết xước mờ
        cv2.circle(a, (240 + dx, 40), 3, 90, -1)                     # chấm bẩn ~ 7 px
    return np.clip(a + rng.normal(0, 4, a.shape), 0, 255).astype(np.uint8)


def tim_loi(anh, mau, nguong=40, dien_tich_min=5):
    hieu = cv2.absdiff(anh, mau)
    _, m = cv2.threshold(hieu, nguong, 255, cv2.THRESH_BINARY)
    n, _, st, _ = cv2.connectedComponentsWithStats(m)
    return [int(s) for s in st[1:, cv2.CC_STAT_AREA] if s >= dien_tich_min]


def tim_loi_co_dung_sai(anh, mau, nguong=40, dien_tich_min=5, r=1):
    """So với 'ảnh min' và 'ảnh max' của mẫu (co/giãn r pixel): mép lệch nhẹ không bị coi là lỗi."""
    k = np.ones((2 * r + 1, 2 * r + 1), np.uint8)
    mau_min, mau_max = cv2.erode(mau, k), cv2.dilate(mau, k)
    vuot = np.maximum(cv2.subtract(mau_min, anh), cv2.subtract(anh, mau_max))
    _, m = cv2.threshold(vuot, nguong, 255, cv2.THRESH_BINARY)
    n, _, st, _ = cv2.connectedComponentsWithStats(m)
    return [int(s) for s in st[1:, cv2.CC_STAT_AREA] if s >= dien_tich_min]


mau = nhan()                                         # ảnh mẫu chuẩn (golden)
for ten, anh in (("OK, đúng vị trí", nhan()), ("lỗi, đúng vị trí", nhan(loi=True)),
                 ("OK, lệch 1 px", nhan(dx=1)), ("lỗi, lệch 1 px", nhan(dx=1, loi=True))):
    a, b = tim_loi(anh, mau), tim_loi_co_dung_sai(anh, mau)
    print(f"{ten:17s} | trừ ảnh: {len(a):2d} vùng | có dung sai mép: {len(b)} vùng {b}")
OK, đúng vị trí   | trừ ảnh:  0 vùng | có dung sai mép: 0 vùng []
lỗi, đúng vị trí  | trừ ảnh:  2 vùng | có dung sai mép: 2 vùng [29, 170]
OK, lệch 1 px     | trừ ảnh: 12 vùng | có dung sai mép: 0 vùng []
lỗi, lệch 1 px    | trừ ảnh: 14 vùng | có dung sai mép: 2 vùng [29, 170]
  • Trừ ảnh thuần túy bắt đúng 2 lỗi (chấm bẩn 29 pixel, vết xước 170 pixel) khi nhãn đúng vị trí, nhưng báo 12 vùng lỗi giả trên nhãn tốt chỉ lệch 1 pixel. Trên dây chuyền, đó là loại nhầm hàng loạt.
  • Cách sửa: tạo "ảnh min" và "ảnh max" bằng erode/dilate ảnh mẫu (bài morphology ở chặng 3). Một pixel chỉ là lỗi khi nó tối hơn ảnh min hoặc sáng hơn ảnh max quá ngưỡng. Mép lệch trong phạm vi r pixel được bỏ qua, lỗi thật vẫn bị bắt.
  • Cái giá: lỗi nằm sát mép chữ, trong vùng dung sai, sẽ khó bắt hơn. Mọi kỹ thuật kiểm lỗi đều là đánh đổi giữa lọt lỗi và loại nhầm, chủ đề chính của chặng 5.
  • Trước khi trừ ảnh, sản phẩm thường được căn chỉnh về đúng vị trí mẫu bằng khớp mẫu (nhóm 5). Lệch lớn hơn vài pixel thì dung sai mép không cứu được.

6. Nhóm 4 và 5 trong một đoạn

Nhận dạng thường dùng thư viện có sẵn: bộ giải mã mã vạch/2D (OpenCV có bộ đọc QR; các thư viện thương mại đọc DataMatrix khắc laser tốt hơn), OCR cho chữ. Phần việc của kỹ sư là bảo đảm đủ pixel trên module/ký tự, chiếu sáng cho mã rõ, và quyết định so với cái gì: mã đọc được phải khớp dữ liệu nào, lấy từ MES hay từ công thức sản phẩm. Lỗi hay gặp: đọc đúng nhưng so sai dữ liệu, hoặc chấp nhận kết quả đọc có độ tin cậy thấp.

Dẫn hướng trả về tọa độ (x, y, góc) thay vì OK/NG. Bài blob analysis ở chặng 3 đã cho bạn tâm và góc của vật. Phần khó thật sự là đổi tọa độ pixel sang tọa độ robot (hiệu chuẩn, chặng 8) và xử lý vật đối xứng hoặc chồng lấn. Sai số dẫn hướng cộng dồn từ: sai số tìm vật trên ảnh + sai số hiệu chuẩn + sai số robot. Khách hàng yêu cầu "robot gắp chính xác ± 0,1 mm" thì phần của vision phải nhỏ hơn nhiều.

7. Ước lượng độ khó của một yêu cầu

Khi khách hàng hỏi "làm được không?", đừng trả lời theo cảm giác. Chấm 8 yếu tố, mỗi yếu tố 0–2 điểm:

Yếu tố0 điểm1 điểm2 điểm
Độ phân giải (pixel trên đặc điểm nhỏ nhất; với đo lường: pixel trên dải dung sai)≥ 10 (đo: ≥ 20)3–10 (đo: 5–20)< 3 (đo: < 5)
Tương phản sau khi thử đènRõ, khoảng hở lớnThấy được nhưng hẹpChưa thử hoặc gần như không thấy
Biến động sản phẩm (màu, mã, lô vật liệu)Một loại, ổn địnhVài loạiNhiều loại, thay đổi thường xuyên
Vị trí, hướng vậtCố định trong đồ gáLệch nhỏ, cùng hướngNgẫu nhiên, xoay tự do, chồng nhau
Bề mặtMờ, phẳngBóng hoặc congBóng và cong, trong suốt, hoa văn
Chu kỳ (thời gian cho mỗi sản phẩm)> 500 ms100–500 ms< 100 ms
Định nghĩa NGBằng con số đo đượcCó ảnh mẫu lỗi, mô tả bằng lời"Nhìn thấy là lỗi", không có mẫu
Yêu cầu lọt lỗiCho phép lọt ít, có kiểm tra sauThấp, có thống kê"Không được lọt con nào"

Tổng 0–4: dễ, làm ngay. 5–9: trung bình, cần thử với mẫu thật trước khi chốt phần cứng. Từ 10 trở lên: khó, phải làm thử nghiệm khả thi (feasibility study, proof of concept) trước khi báo giá. Bảng chấm này không phải tiêu chuẩn ngành, mà là cách hệ thống hóa kinh nghiệm để bạn không quên yếu tố nào. Áp dụng cho ba yêu cầu:

def muc_do_phan_giai(fov_mm, so_pixel, kich_thuoc_mm, la_do_luong=False):
    """Điểm 0–2 cho độ phân giải. Đo lường: tính số pixel trên DẢI dung sai; còn lại: trên đặc điểm nhỏ nhất."""
    mm_px = fov_mm / so_pixel
    px = kich_thuoc_mm / mm_px
    if la_do_luong:
        diem = 0 if px >= 20 else 1 if px >= 5 else 2
    else:
        diem = 0 if px >= 10 else 1 if px >= 3 else 2
    return diem, px, mm_px


def danh_gia(ten, fov_mm, so_pixel, kich_thuoc_mm, la_do_luong, **cac_yeu_to):
    d_pg, px, mm_px = muc_do_phan_giai(fov_mm, so_pixel, kich_thuoc_mm, la_do_luong)
    tong = d_pg + sum(cac_yeu_to.values())
    muc = "DỄ" if tong <= 4 else "TRUNG BÌNH" if tong <= 9 else "KHÓ: làm thử trước khi báo giá"
    print(f"{ten}\n  {mm_px:.4f} mm/px, {px:.1f} px trên đặc điểm → điểm phân giải {d_pg}")
    khac = ", ".join(f"{k}={v}" for k, v in cac_yeu_to.items())
    print(f"  {khac}\n  TỔNG {tong}/16 → {muc}")


# Thang 0–2 cho mỗi yếu tố: tuong_phan, bien_dong, vi_tri, be_mat, chu_ky, dinh_nghia_ng, yeu_cau_lot
danh_gia("A. Có/không nắp chai (đèn nền)", 120, 1280, 10, False,
         tuong_phan=0, bien_dong=0, vi_tri=1, be_mat=0, chu_ky=0, dinh_nghia_ng=0, yeu_cau_lot=1)
danh_gia("B. Đo đường kính trục 12 ± 0,02 mm", 20, 2448, 0.04, True,
         tuong_phan=0, bien_dong=0, vi_tri=1, be_mat=1, chu_ky=1, dinh_nghia_ng=0, yeu_cau_lot=2)
danh_gia("C. Vết xước ≥ 0,1 mm trên vỏ nhôm bóng 150×75 mm", 160, 4096, 0.1, False,
         tuong_phan=2, bien_dong=1, vi_tri=1, be_mat=2, chu_ky=1, dinh_nghia_ng=2, yeu_cau_lot=2)
A. Có/không nắp chai (đèn nền)
  0.0938 mm/px, 106.7 px trên đặc điểm → điểm phân giải 0
  tuong_phan=0, bien_dong=0, vi_tri=1, be_mat=0, chu_ky=0, dinh_nghia_ng=0, yeu_cau_lot=1
  TỔNG 2/16 → DỄ
B. Đo đường kính trục 12 ± 0,02 mm
  0.0082 mm/px, 4.9 px trên đặc điểm → điểm phân giải 2
  tuong_phan=0, bien_dong=0, vi_tri=1, be_mat=1, chu_ky=1, dinh_nghia_ng=0, yeu_cau_lot=2
  TỔNG 7/16 → TRUNG BÌNH
C. Vết xước ≥ 0,1 mm trên vỏ nhôm bóng 150×75 mm
  0.0391 mm/px, 2.6 px trên đặc điểm → điểm phân giải 2
  tuong_phan=2, bien_dong=1, vi_tri=1, be_mat=2, chu_ky=1, dinh_nghia_ng=2, yeu_cau_lot=2
  TỔNG 13/16 → KHÓ: làm thử trước khi báo giá
  • A dễ: đặc điểm phủ hơn 100 pixel, đèn nền cho tương phản tuyệt đối.
  • B trung bình nhưng có một điểm đỏ: dải dung sai 0,04 mm chỉ trải trên 4,9 pixel. Theo quy tắc 10:1, sai số đo phải dưới 0,004 mm ≈ 0,5 pixel. Làm được với đo subpixel, đèn nền, ống kính telecentric và hiệu chuẩn cẩn thận; hoặc thu nhỏ FOV/tăng độ phân giải để dễ thở hơn. Phải nói rõ điều này với khách hàng từ đầu.
  • C khó: vết xước 0,1 mm chỉ phủ 2,6 pixel với một camera 4096 pixel nhìn toàn bộ vỏ, bề mặt nhôm bóng, định nghĩa lỗi mơ hồ. Hướng đi thực tế: chụp nhiều ảnh (nhiều camera, hoặc camera line scan quét), thử nhiều kiểu đèn, thu thập mẫu lỗi, và thống nhất với khách hàng định nghĩa lỗi bằng con số trước khi báo giá.

8. Sai lầm thường gặp

  • Nhận lời "kiểm tra mọi lỗi". Không có trạm nào kiểm "mọi lỗi". Yêu cầu khách hàng liệt kê từng loại lỗi, kích thước nhỏ nhất, kèm mẫu thật.
  • Tính độ phân giải theo chiều rộng sản phẩm, quên chiều cao FOV. FOV phải chứa cả sản phẩm lẫn độ lệch vị trí của nó, theo cả hai chiều, cộng thêm biên.
  • Coi đo lường như có/không. "Có lỗ không" thì dễ; "đường kính lỗ 5 ± 0,01 mm" là bài toán khác hẳn về quang học và hiệu chuẩn.
  • Dùng deep learning cho bài có/không đơn giản. Nếu ngưỡng độ sáng trong ROI đã tách sạch, thêm mô hình chỉ thêm rủi ro, thời gian huấn luyện và khó giải thích.

9. Bài tập

Bài 1. Xếp mỗi yêu cầu sau vào nhóm (có thể nhiều nhóm): (a) kiểm tra vỉ thuốc đủ 10 viên; (b) robot gắp bánh răng từ băng tải; (c) kiểm tra khe hở giữa nắp và thân hộp ≤ 0,3 mm; (d) đọc mã DataMatrix khắc laser trên chi tiết và gửi lên MES; (e) phát hiện bọt khí trong tấm nhựa trong; (f) kiểm tra nhãn dán đúng loại sản phẩm và không lệch quá 1 mm.

Xem đáp án
  • (a) Có/không (thêm kiểm lỗi nếu cần phát hiện viên vỡ).
  • (b) Dẫn hướng (cần hiệu chuẩn camera–robot).
  • (c) Đo lường.
  • (d) Nhận dạng.
  • (e) Kiểm lỗi (vật trong suốt: thường thử đèn nền hoặc chiếu sáng trường tối).
  • (f) Nhận dạng (đúng loại nhãn: khớp mẫu hoặc đọc mã) + đo lường (lệch bao nhiêu mm) + dẫn hướng (tìm vị trí nhãn).

Bài 2. Mã DataMatrix có module 0,25 mm. Camera 1280×1024 pixel. Tính số pixel trên một module khi FOV ngang là 40 mm và khi là 80 mm. Trường hợp nào an toàn?

Xem đáp án
module_mm, so_pixel = 0.25, 1280
for fov in (40, 80):
    mm_px = fov / so_pixel
    print(f"FOV {fov} mm: {mm_px:.4f} mm/px → {module_mm / mm_px:.1f} px/module")
FOV 40 mm: 0.0312 mm/px → 8.0 px/module
FOV 80 mm: 0.0625 mm/px → 4.0 px/module

FOV 40 mm cho 8 pixel/module: thoải mái. FOV 80 mm chỉ còn 4 pixel/module: nằm ở biên dưới của khuyến nghị, đọc được khi in đẹp nhưng dễ thất bại khi mã khắc laser mờ hoặc hơi mất nét. Nếu buộc phải có FOV 80 mm (vì vị trí mã thay đổi), hãy chọn camera độ phân giải cao hơn.

Bài 3. Trong ví dụ mục 5, nếu chấm bẩn nằm ngay sát mép chữ, phương pháp "có dung sai mép" có thể bỏ sót. Đề xuất hai cách giảm rủi ro này mà không quay lại trừ ảnh thuần túy.

Xem đáp án
  • Căn chỉnh chính xác hơn trước khi so (khớp mẫu subpixel, hoặc đồ gá tốt hơn), để giảm được bán kính dung sai r, thu hẹp vùng "mù".
  • Dùng ngưỡng khác nhau theo vùng: ngưỡng chặt ở vùng nền phẳng, ngưỡng nới ở vùng sát mép. Đây là ý tưởng của các mô hình biến thiên (variation model) trong thư viện thương mại: học độ biến động của từng pixel từ nhiều ảnh OK thay vì dùng một ảnh mẫu.
  • Thêm một tiêu chí khác, ví dụ đo độ dày nét chữ: chấm bẩn dính vào nét làm nét dày lên bất thường.

Tóm tắt

  • Năm nhóm: có/không, đo lường, kiểm lỗi, nhận dạng, dẫn hướng. Một trạm thật thường kết hợp vài nhóm.
  • Mỗi nhóm có con số riêng phải hỏi: kích thước đặc điểm, dung sai, kích thước lỗi nhỏ nhất, kích thước module/ký tự, độ chính xác vị trí.
  • Luôn tính mm/pixel và số pixel trên đặc điểm; đo lường cần quy tắc 10:1 và kỹ thuật subpixel.
  • Trừ ảnh mẫu rất nhạy với lệch vị trí; cần căn chỉnh và dung sai mép.
  • Chấm độ khó theo 8 yếu tố; từ 10/16 trở lên phải làm thử với mẫu thật trước khi cam kết.

Bài tiếp theo: Halcon, VisionPro, OpenCV, smart camera: chọn công cụ cho từng loại dự án và ngân sách.

Bài tiếp theo
Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.