Rule-based hay Deep Learning: chọn cách nào cho bài toán kiểm tra lỗi?
Bảng tiêu chí và hàm chấm điểm, thí nghiệm luật phân ngưỡng so với CNN nhỏ, thiết kế hybrid có cổng kiểm tra và cách ước lượng chi phí dữ liệu.
Học xong bài này, bạn sẽ
- Biết khi nào dùng thuật toán cổ điển, khi nào dùng AI
- Kết hợp hai cách trong một hệ thống
- Ước lượng chi phí dữ liệu
Nên học trước: Dự án chặng 10: phân loại ảnh OK/NG bằng transfer learning
Nội dung bài viết
"Bài toán này có nên dùng AI không?" là câu hỏi đầu tiên của mọi dự án kiểm tra lỗi, và câu trả lời đúng hiếm khi là "luôn luôn AI" hay "không bao giờ AI". Ở chặng 10, dự án "Phân loại ảnh OK/NG bằng transfer learning" cho bạn thấy một mô hình học sâu học được lỗi từ ảnh; các chặng trước đó cho bạn bộ công cụ cổ điển: phân ngưỡng, blob, đo cạnh subpixel, so khớp mẫu. Bài mở đầu chặng 11 này dạy cách chọn giữa hai trường phái bằng tiêu chí cụ thể, chứng minh bằng một thí nghiệm chạy được vì sao luật cổ điển thắng tuyệt đối ở bài toán này nhưng thua ở bài toán khác, thiết kế hệ thống hybrid (rule-based làm cổng kiểm tra và đo, AI phán lỗi bề mặt), và ước lượng chi phí dữ liệu trước khi hứa với khách hàng. Bài sau, "Tự xây dataset ảnh công nghiệp", biến ước lượng đó thành kế hoạch thu thập thật.
1. Hai cách tiếp cận
Rule-based (xử lý ảnh cổ điển): kỹ sư tự thiết kế chuỗi xử lý (phân ngưỡng, lọc, contour, đo cạnh, so khớp mẫu) và viết luật rõ ràng, ví dụ "vết tối có diện tích ≥ 0,30 mm² trong vùng A là NG". Mọi tham số đều đọc được, chỉnh được, và kết quả lặp lại y hệt với cùng một ảnh.
Deep Learning: mô hình tự học đặc trưng từ ảnh. Có bốn dạng hay gặp trong nhà máy:
| Dạng | Đầu ra | Dữ liệu cần | Khi nào dùng |
|---|---|---|---|
| Phân loại (classification) | OK/NG hoặc loại lỗi cho cả ảnh/ROI | Ảnh có nhãn cấp ảnh | Chỉ cần biết có lỗi hay không, ROI nhỏ đã cắt sẵn |
| Phát hiện (object detection) | Khung bao + loại lỗi | Khung bao từng vết | Cần vị trí, đếm số vết, lỗi nhỏ trên ảnh lớn |
| Phân đoạn (segmentation) | Mặt nạ từng pixel | Đa giác/mặt nạ từng vết | Cần diện tích, chiều dài vết để áp tiêu chí mm, mm² |
| Phát hiện bất thường (anomaly detection) | Điểm bất thường + bản đồ nhiệt | Chủ yếu ảnh OK | Ảnh lỗi hiếm, loại lỗi chưa biết trước |
Điểm mấu chốt: AI không thay thế được phép đo. Một CNN phân loại không trả lời "đường kính bao nhiêu mm"; một mô hình phân đoạn cho mặt nạ vết lỗi nhưng việc quy đổi pixel sang mm, áp tiêu chí theo vùng vẫn là rule-based.
2. Bảng tiêu chí quyết định
| Tiêu chí | Nghiêng về rule-based khi… | Nghiêng về AI khi… |
|---|---|---|
| Bản chất yêu cầu | Đo kích thước, vị trí, góc; đếm; có/không có chi tiết; đọc mã | Phán đoán ngoại hình: "vết bẩn", "lỗi dệt", "nấm mốc" |
| Mô tả lỗi bằng con số | Viết được thành luật: diện tích, độ tương phản, số lượng | QC chỉ nói "nhìn là biết", định nghĩa phải kèm ảnh mẫu |
| Nền, kết cấu | Nền đồng nhất, chiếu sáng tạo tương phản cao (đèn nền, đèn góc thấp) | Kết cấu tự nhiên biến thiên: gỗ, vải, da, đá, kim loại đúc, phun cát |
| Độ đa dạng của lỗi | Ít loại, hình dạng ổn định | Nhiều hình dạng, kích thước, hướng; lỗi mới xuất hiện theo lô |
| Dữ liệu | Vài ảnh mẫu để chỉnh tham số | Hàng trăm ảnh lỗi gán nhãn (có giám sát) hoặc hàng trăm ảnh OK (anomaly) |
| Giải thích, truy xuất | Bắt buộc giải thích từng quyết định bằng con số (ô tô, y tế, hàng không) | Chấp nhận điểm số + bản đồ nhiệt + nghiệm thu thống kê |
| Phần cứng, cycle time | CPU, vài ms; không muốn thêm GPU | Có ngân sách GPU/bộ tăng tốc hoặc mô hình đủ nhỏ cho CPU |
| Thay đổi sản phẩm | Đổi mã thường xuyên nhưng chỉ đổi kích thước: sửa công thức (recipe) | Đổi mã ít, mỗi lần đổi chấp nhận thu thêm ảnh và huấn luyện lại |
Hàm chấm điểm giúp ra quyết định
Biến bảng trên thành một hàm nhỏ để cả nhóm trả lời cùng một bộ câu hỏi cho từng loại lỗi. Mỗi tiêu chí trả lời 0 (không), 1 (một phần), 2 (rõ ràng); trọng số âm kéo về rule-based, dương kéo về AI. Đây là công cụ gợi ý để thảo luận có cấu trúc, không phải công thức chính xác: hãy chỉnh trọng số theo kinh nghiệm của nhóm bạn.
TRONG_SO = {
"can_do_kich_thuoc": -2, # yêu cầu đo mm/µm, so dung sai bản vẽ
"mo_ta_bang_luat": -2, # lỗi viết được thành luật: diện tích, độ tương phản, số lượng
"can_giai_thich": -1, # khách hàng đòi giải thích từng quyết định bằng con số
"nen_bien_thien": +2, # nền/kết cấu tự nhiên thay đổi: gỗ, vải, da, đúc
"loi_da_dang": +2, # hình dạng lỗi đa dạng, khó liệt kê
"phan_doan_chu_quan": +1, # QC phán "nhìn là biết", định nghĩa dựa vào ảnh mẫu
}
def goi_y(tra_loi, so_anh_loi_thu_duoc):
"""tra_loi: dict tiêu chí -> 0/1/2. so_anh_loi_thu_duoc: số ảnh lỗi có thể có trong thời hạn dự án."""
diem = sum(TRONG_SO[k] * v for k, v in tra_loi.items())
if diem <= -2:
ket_luan = "RULE-BASED"
elif diem >= 3:
ket_luan = "AI có giám sát" if so_anh_loi_thu_duoc >= 200 else "AI: bắt đầu bằng anomaly detection"
else:
ket_luan = "HYBRID / thử cả hai"
if diem > -2 and tra_loi.get("can_do_kich_thuoc", 0) >= 1:
ket_luan += " + đo kích thước bằng rule-based"
return diem, ket_luan
BAI_TOAN = {
"Đếm đủ 12 chân IC": ({"can_do_kich_thuoc": 1, "mo_ta_bang_luat": 2, "can_giai_thich": 2}, 50),
"Lỗi dệt trên vải mộc": ({"mo_ta_bang_luat": 0, "nen_bien_thien": 2, "loi_da_dang": 2,
"phan_doan_chu_quan": 1}, 800),
"Nhãn chai: lệch và nhăn": ({"can_do_kich_thuoc": 2, "mo_ta_bang_luat": 1, "loi_da_dang": 1,
"phan_doan_chu_quan": 2, "nen_bien_thien": 1}, 300),
"Vết rỗ trên vỏ nhôm đúc": ({"can_do_kich_thuoc": 1, "nen_bien_thien": 2, "loi_da_dang": 2,
"phan_doan_chu_quan": 1}, 60),
}
for ten, (tl, n_loi) in BAI_TOAN.items():
diem, kl = goi_y(tl, n_loi)
print(f"{ten:26s} điểm {diem:+3d} → {kl}")
Đếm đủ 12 chân IC điểm -8 → RULE-BASED
Lỗi dệt trên vải mộc điểm +9 → AI có giám sát
Nhãn chai: lệch và nhăn điểm +0 → HYBRID / thử cả hai + đo kích thước bằng rule-based
Vết rỗ trên vỏ nhôm đúc điểm +7 → AI: bắt đầu bằng anomaly detection + đo kích thước bằng rule-based
Đọc kết quả: đếm chân IC là bài toán đo và đếm thuần túy, rule-based thắng tuyệt đối. Lỗi dệt trên vải có nền biến thiên và lỗi đa dạng, nhiều ảnh lỗi, nên dùng AI có giám sát. Nhãn chai vừa có phần đo (lệch bao nhiêu mm) vừa có phần ngoại hình (nhăn, rách) nên hybrid. Vết rỗ trên nhôm đúc nghiêng hẳn về AI nhưng chỉ thu được 60 ảnh lỗi trong thời hạn, nên bắt đầu bằng anomaly detection rồi chuyển dần sang có giám sát (chặng 12 dạy cả hai).
3. Thí nghiệm: luật cổ điển thắng ở đâu, thua ở đâu
Hai bài toán, cùng kích thước ảnh 64×64, cùng một luật đơn giản "đếm pixel tối hơn ngưỡng, nếu diện tích vượt mức thì NG":
- Bài toán A: mặt kim loại phẳng, chiếu sáng ổn định, lỗi là đốm rỗ tối. Lỗi có kiểm soát: tối hơn nền rõ rệt và nền không có gì tối.
- Bài toán B: mặt gỗ có vân (thớ sáng tối xen kẽ), mỗi tấm sáng tối khác nhau, có mắt gỗ tối nhưng không phải lỗi; lỗi là vết nứt mảnh cắt ngang thớ.
Để công bằng, ngưỡng xám và diện tích của luật được chọn tự động: thử mọi tổ hợp trên tập train, giữ tổ hợp có độ chính xác cao nhất, rồi đánh giá trên tập test riêng.
import cv2
import numpy as np
rng = np.random.default_rng(0)
S = 64
def be_mat_kim_loai(loi):
"""Bài toán A: mặt kim loại phẳng, đèn ổn định; lỗi = đốm rỗ tối."""
img = rng.normal(150, 6, (S, S))
if loi:
c = rng.integers(10, S - 10, 2)
cv2.circle(img, (int(c[0]), int(c[1])), int(rng.integers(3, 6)), 70, -1)
return np.clip(img, 0, 255).astype(np.uint8)
def van_go(loi):
"""Bài toán B: vân gỗ, mắt gỗ tối (không phải lỗi); lỗi = vết nứt mảnh cắt ngang thớ."""
yy, xx = np.mgrid[0:S, 0:S].astype(np.float32)
chu_ky = rng.uniform(7, 12)
song = 3 * np.sin(xx / rng.uniform(8, 14) + rng.uniform(0, 6))
img = 150 + 30 * np.sin(2 * np.pi * (yy + song) / chu_ky) + rng.normal(0, 8, (S, S))
img += rng.uniform(-25, 25) # mỗi tấm gỗ sáng tối khác nhau
for _ in range(int(rng.integers(0, 3))): # 0-2 mắt gỗ
c = rng.integers(8, S - 8, 2)
truc = (int(rng.integers(3, 7)), int(rng.integers(3, 6)))
cv2.ellipse(img, (int(c[0]), int(c[1])), truc, 0, 0, 360, float(rng.uniform(60, 90)), -1)
if loi:
x0, y0 = rng.integers(12, S - 12, 2)
goc = np.deg2rad(90 + rng.uniform(-25, 25)) # gần vuông góc với thớ
L = rng.uniform(14, 26)
dx, dy = L / 2 * np.cos(goc), L / 2 * np.sin(goc)
vet = np.zeros((S, S), np.float32)
cv2.line(vet, (int(x0 - dx), int(y0 - dy)), (int(x0 + dx), int(y0 + dy)), 1.0, 1, cv2.LINE_AA)
img -= 55 * vet
return np.clip(img, 0, 255).astype(np.uint8)
def tao_bo(ham, n):
y = np.array([i % 2 for i in range(n)]) # xen kẽ OK/NG, 50/50
return np.stack([ham(bool(v)) for v in y]), y
def luat_dom_toi(X, nguong_xam, dien_tich_min):
return ((X < nguong_xam).sum(axis=(1, 2)) >= dien_tich_min).astype(int)
def chinh_luat(X, y):
tot, tham_so = 0, None
for t in range(50, 131, 5):
for a in (3, 5, 10, 20, 40, 80, 150, 300):
acc = np.mean(luat_dom_toi(X, t, a) == y)
if acc > tot:
tot, tham_so = acc, (t, a)
return tham_so
def bao_cao(ten, y, du_doan):
print(f"{ten:24s} escape {np.mean(du_doan[y == 1] == 0):6.1%} | overkill {np.mean(du_doan[y == 0] == 1):6.1%}")
for ten_bt, ham in (("A. đốm rỗ trên kim loại", be_mat_kim_loai), ("B. vết nứt trên vân gỗ", van_go)):
Xtr, ytr = tao_bo(ham, 600)
Xte, yte = tao_bo(ham, 400)
t, a = chinh_luat(Xtr, ytr)
print(f"{ten_bt}: luật tốt nhất trên train = pixel < {t}, diện tích >= {a} px")
bao_cao(" rule-based trên test", yte, luat_dom_toi(Xte, t, a))
if ham is van_go:
np.savez_compressed("go.npz", Xtr=Xtr, ytr=ytr, Xte=Xte, yte=yte)
cv2.imwrite("vi_du_go.png", cv2.resize(cv2.hconcat(list(Xte[:6])), None, fx=3, fy=3,
interpolation=cv2.INTER_NEAREST))
A. đốm rỗ trên kim loại: luật tốt nhất trên train = pixel < 75, diện tích >= 3 px
rule-based trên test escape 0.0% | overkill 0.0%
B. vết nứt trên vân gỗ: luật tốt nhất trên train = pixel < 65, diện tích >= 3 px
rule-based trên test escape 40.5% | overkill 17.0%
Ở bài toán A, luật hai tham số đạt 0% lọt, 0% loại nhầm, không cần một ảnh lỗi gán nhãn nào ngoài vài ảnh để chỉnh ngưỡng. Ở bài toán B, luật tốt nhất có thể vẫn lọt khoảng 40% vết nứt và loại nhầm khoảng 17% tấm tốt. Mở vi_du_go.png (ảnh chẵn là OK, lẻ là NG) để thấy lý do: mắt gỗ và rãnh thớ tối không kém vết nứt, và tấm gỗ tối có rãnh thớ tối hơn cả vết nứt trên tấm gỗ sáng. Không ngưỡng xám nào tách được, vì thứ phân biệt lỗi là hình dạng và hướng (một đường mảnh cắt ngang thớ), không phải độ tối.
Một kỹ sư giỏi vẫn có thể viết luật tốt hơn cho B: lọc theo hướng vuông góc với thớ, chuẩn hóa độ sáng từng tấm, loại blob tròn. Nhưng mỗi biến thể mới (gỗ loại khác, vết nứt chéo, mắt gỗ dài) lại cần thêm luật, và chuỗi luật ngày càng mong manh. Đó là tín hiệu nên để mô hình học.
Một CNN nhỏ học bài toán B
Mạng ba lớp tích chập, kết thúc bằng AdaptiveMaxPool2d: lấy giá trị lớn nhất trên toàn ảnh nên vết lỗi ở đâu cũng được "nhìn thấy". Ta huấn luyện với số ảnh train tăng dần để thấy ngay chi phí dữ liệu, mỗi lần cùng 300 bước cập nhật. Augmentation duy nhất là lật ngang, an toàn vì thớ gỗ không có chiều trái/phải (bài "Dataset, DataLoader và augmentation" ở chặng 10 giải thích cách chọn).
import time
import numpy as np
import torch
from torch import nn
torch.manual_seed(0)
torch.set_num_threads(4)
d = np.load("go.npz")
def sang_tensor(X):
return (torch.from_numpy(X).float().unsqueeze(1) / 255.0 - 0.5) / 0.25
Xtr_all, ytr_all = sang_tensor(d["Xtr"]), torch.from_numpy(d["ytr"]).float()
Xte, yte = sang_tensor(d["Xte"]), d["yte"]
def tao_mo_hinh():
return nn.Sequential(
nn.Conv2d(1, 8, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 32×32
nn.Conv2d(8, 16, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), # 16×16
nn.Conv2d(16, 16, 3, padding=1), nn.ReLU(),
nn.AdaptiveMaxPool2d(1), nn.Flatten(), nn.Linear(16, 1))
def huan_luyen(n_anh, so_buoc=300):
torch.manual_seed(0)
X, y = Xtr_all[:n_anh], ytr_all[:n_anh]
mo_hinh = tao_mo_hinh()
toi_uu = torch.optim.Adam(mo_hinh.parameters(), lr=3e-3)
ham_mat = nn.BCEWithLogitsLoss()
mo_hinh.train()
for _ in range(so_buoc):
idx = torch.randint(0, n_anh, (32,))
xb = X[idx]
lat = torch.rand(32) < 0.5 # lật ngang ngẫu nhiên
xb = torch.where(lat[:, None, None, None], xb.flip(3), xb)
loss = ham_mat(mo_hinh(xb).squeeze(1), y[idx])
toi_uu.zero_grad()
loss.backward()
toi_uu.step()
mo_hinh.eval()
with torch.no_grad():
p = torch.sigmoid(mo_hinh(Xte).squeeze(1)).numpy()
return mo_hinh, (p >= 0.5).astype(int)
t0 = time.time()
print("ảnh train (NG) | escape | overkill")
for n in (40, 100, 200, 600):
mo_hinh, du_doan = huan_luyen(n)
print(f"{n:5d} ({n // 2:3d} NG) | {np.mean(du_doan[yte == 1] == 0):6.1%} | {np.mean(du_doan[yte == 0] == 1):6.1%}")
torch.save(mo_hinh.state_dict(), "cnn_go.pt") # mô hình 600 ảnh, dùng ở mục 4
print(f"thời gian huấn luyện: {time.time() - t0:.0f} s (thay đổi theo máy)")
ảnh train (NG) | escape | overkill
40 ( 20 NG) | 17.5% | 17.5%
100 ( 50 NG) | 0.0% | 0.0%
200 (100 NG) | 0.0% | 0.0%
600 (300 NG) | 0.0% | 0.5%
thời gian huấn luyện: 19 s (thay đổi theo máy)
Từ khoảng 50 ảnh NG trở lên, CNN tí hon gần như không lọt và không loại nhầm trên tập test, điều luật phân ngưỡng không làm được. Nhưng với 20 ảnh NG, nó lọt và loại nhầm khoảng 17%: chất lượng phụ thuộc trực tiếp vào số ảnh lỗi. Con số cụ thể có thể lệch đôi chút giữa các máy và phiên bản thư viện; xu hướng thì không đổi. Đây cũng là bài toán rất dễ (một loại lỗi, ảnh mô phỏng sạch, một kiểu gỗ); bài toán thật có nhiều loại lỗi, nhiều lô vật liệu và thường cần nhiều ảnh hơn hẳn, mục 5 sẽ tính chi phí đó.
4. Thiết kế hybrid: rule-based làm cổng, AI phán lỗi bề mặt
Hệ thống hiệu quả nhất trong nhà máy hầu như luôn là hybrid. Mỗi phần làm đúng việc nó giỏi:
- Cổng chất lượng ảnh (rule-based): ảnh quá tối/quá sáng (đèn hỏng, trigger lệch), ảnh mờ (rung, lệch nét). Ảnh hỏng không được đưa vào AI, vì mô hình sẽ trả về một điểm số vô nghĩa nhưng trông rất tự tin.
- Định vị và kiểm tra có chi tiết (rule-based): tìm chi tiết, cắt ROI chuẩn hóa. Mô hình chỉ phải học một việc trên ảnh đã căn chỉnh, nên cần ít dữ liệu hơn.
- Đo kích thước (rule-based): áp dung sai bản vẽ bằng mm.
- AI phán lỗi bề mặt trong ROI.
- Luật tổng hợp: ghép kết quả, phân biệt "NG sản phẩm" với "lỗi hệ thống", gửi mã cho PLC và ghi log.
Phân biệt NG sản phẩm và lỗi hệ thống rất quan trọng: nếu đèn hỏng mà trạm báo NG hàng loạt, sản phẩm tốt bị loại và không ai biết đèn hỏng. Mã riêng cho lỗi hệ thống cho phép PLC dừng băng tải và gọi kỹ thuật. Đoạn code dưới ghép năm bước trên khung hình 128×128 (tấm gỗ 64×64 trên nền băng tải tối), dùng CNN vừa lưu:
import cv2
import numpy as np
import torch
from torch import nn
torch.set_num_threads(4)
mo_hinh = nn.Sequential(
nn.Conv2d(1, 8, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(8, 16, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(16, 16, 3, padding=1), nn.ReLU(),
nn.AdaptiveMaxPool2d(1), nn.Flatten(), nn.Linear(16, 1))
mo_hinh.load_state_dict(torch.load("cnn_go.pt", weights_only=True))
mo_hinh.eval()
MA_PLC = {"OK": 1, "NG_BE_MAT": 2, "NG_KICH_THUOC": 3, "LOI_HE_THONG": 9}
MM_PX = 0.5 # mm/pixel sau hiệu chuẩn (giả định)
DAI_DANH_NGHIA, DUNG_SAI = 32.0, 1.5 # mm: chiều dài tấm 32 ± 1,5 mm
def kiem_tra(khung):
# 1. cổng chất lượng ảnh
sang = float(khung.mean())
if not 35 <= sang <= 120:
return "LOI_HE_THONG", f"độ sáng trung bình {sang:.0f} ngoài [35, 120]"
# 2. định vị: tìm tấm gỗ sáng trên nền tối
_, nhi_phan = cv2.threshold(khung, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
cs, _ = cv2.findContours(nhi_phan, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cs = [c for c in cs if 2000 < cv2.contourArea(c) < 8000] # tấm 64×64 ≈ 4096 px
if not cs:
return "LOI_HE_THONG", "không thấy chi tiết trong FOV"
x, y, w, h = cv2.boundingRect(max(cs, key=cv2.contourArea))
roi = khung[y:y + h, x:x + w]
do_net = cv2.Laplacian(roi, cv2.CV_64F).var()
if do_net < 300:
return "LOI_HE_THONG", f"ảnh mờ (phương sai Laplacian {do_net:.0f} < 300)"
# 3. đo kích thước bằng rule-based
dai_mm = w * MM_PX
if abs(dai_mm - DAI_DANH_NGHIA) > DUNG_SAI:
return "NG_KICH_THUOC", f"dài {dai_mm:.1f} mm, ngoài {DAI_DANH_NGHIA} ± {DUNG_SAI}"
# 4. AI trên ROI đã chuẩn hóa kích thước
roi = cv2.resize(roi, (64, 64), interpolation=cv2.INTER_AREA)
x_t = (torch.from_numpy(roi).float()[None, None] / 255.0 - 0.5) / 0.25
with torch.no_grad():
diem = torch.sigmoid(mo_hinh(x_t)).item()
# 5. luật tổng hợp
if diem >= 0.5:
return "NG_BE_MAT", f"điểm AI {diem:.2f}"
return "OK", f"dài {dai_mm:.1f} mm, điểm AI {diem:.2f}"
d = np.load("go.npz")
Xte, yte = d["Xte"], d["yte"]
def dat_len_bang_tai(tam, x, y):
khung = np.full((128, 128), 30, np.uint8)
khung[y:y + tam.shape[0], x:x + tam.shape[1]] = tam
return khung
ca_thu = {
"tấm OK, lệch vị trí": dat_len_bang_tai(Xte[0], 50, 20),
"tấm có vết nứt": dat_len_bang_tai(Xte[1], 30, 40),
"tấm bị cắt ngắn": dat_len_bang_tai(Xte[2][:, :54], 40, 30),
"đèn hỏng (ảnh tối)": (dat_len_bang_tai(Xte[4], 32, 32) * 0.2).astype(np.uint8),
"băng tải trống": np.full((128, 128), 30, np.uint8) + 40,
"rung máy (ảnh mờ)": cv2.GaussianBlur(dat_len_bang_tai(Xte[6], 32, 32), (0, 0), 2.5),
}
for ten, khung in ca_thu.items():
ket_qua, ly_do = kiem_tra(khung)
print(f"{ten:20s} → {ket_qua:13s} (PLC {MA_PLC[ket_qua]}) | {ly_do}")
tấm OK, lệch vị trí → OK (PLC 1) | dài 32.0 mm, điểm AI 0.01
tấm có vết nứt → NG_BE_MAT (PLC 2) | điểm AI 1.00
tấm bị cắt ngắn → NG_KICH_THUOC (PLC 3) | dài 27.0 mm, ngoài 32.0 ± 1.5
đèn hỏng (ảnh tối) → LOI_HE_THONG (PLC 9) | độ sáng trung bình 13 ngoài [35, 120]
băng tải trống → LOI_HE_THONG (PLC 9) | không thấy chi tiết trong FOV
rung máy (ảnh mờ) → LOI_HE_THONG (PLC 9) | ảnh mờ (phương sai Laplacian 123 < 300)
Bốn trong sáu trường hợp được quyết định trước khi tới AI, và ba trong số đó (ảnh tối, băng tải trống, ảnh mờ) nếu đưa vào AI sẽ nhận một điểm số vô nghĩa mà không ai biết. Cách chia này còn có lợi về dữ liệu: vì ROI đã được định vị và chuẩn hóa, mô hình không phải học "tấm gỗ có thể nằm ở đâu", và tập dữ liệu không cần bao phủ mọi vị trí đặt.
| Mẫu hybrid hay gặp | Rule-based làm | AI làm |
|---|---|---|
| Định vị + phân loại ROI | So khớp mẫu, căn chỉnh, cắt ROI | Phân loại lỗi trong ROI |
| AI phân đoạn + đo | Quy đổi mặt nạ sang mm, mm²; áp tiêu chí theo vùng | Phân đoạn vết lỗi |
| Rule-based sàng lọc + AI xác nhận | Bắt mọi vết khả nghi (ngưỡng thấp, ít lọt) | Loại các vết khả nghi thực ra là OK (giảm overkill) |
| AI phát hiện + OCR/đọc mã cổ điển | Giải mã DataMatrix, so khớp ký tự | Tìm vùng mã trên nền phức tạp |
5. Ước lượng chi phí dữ liệu
Trước khi hứa "dùng AI", hãy tính ba con số: bao lâu mới có đủ ảnh lỗi, bao nhiêu giờ công gán nhãn, và chi phí đó so với phương án rule-based. Lỗi hiếm nhất quyết định lịch: nếu dây chuyền tốt chỉ sinh 0,03% hở lớp phủ, 10.000 sản phẩm/ngày cho bạn 3 chi tiết/ngày.
| Loại nhãn | Thời gian gán điển hình (tham khảo, đo lại trên 50 ảnh đầu) |
|---|---|
| Nhãn cấp ảnh (OK/NG, loại lỗi) | 2–5 giây/ảnh khi xem lưới ảnh nhỏ |
| Khung bao | 10–30 giây/vết |
| Đa giác, mặt nạ | 30–120 giây/vết (nhanh hơn nhiều nếu có công cụ hỗ trợ kiểu SAM) |
Số ảnh cần cũng chỉ có khoảng kinh nghiệm: phân loại ROI đã căn chỉnh thường bắt đầu dùng được từ vài trăm ảnh mỗi lớp; phát hiện/phân đoạn cần vài trăm vết mỗi loại lỗi, nhiều hơn khi lỗi đa dạng; anomaly detection cần vài trăm ảnh OK đại diện cộng một ít ảnh NG thật để đánh giá. Công cụ dưới đây tính lịch và giờ công từ các giả định của bạn:
import math
def chi_phi_du_lieu(san_luong_ngay, ti_le_loi, giu_lai, muc_tieu, n_ok, giay_moi_vet,
vet_moi_anh=1.2, giay_ok=3, ti_le_kiem_cheo=0.2, gia_gio=60_000):
"""ti_le_loi, muc_tieu: dict loại lỗi -> tỉ lệ / số ảnh NG cần. gia_gio: đồng/giờ công (giả định)."""
print(f"{'loại lỗi':14s} {'NG/ngày':>8s} {'thu được/ngày':>14s} {'số ngày':>8s}")
so_ngay_max = 0
for loai, p in ti_le_loi.items():
thu = san_luong_ngay * p * giu_lai
ngay = math.ceil(muc_tieu[loai] / thu)
so_ngay_max = max(so_ngay_max, ngay)
print(f"{loai:14s} {san_luong_ngay * p:8.1f} {thu:14.1f} {ngay:8d}")
n_ng = sum(muc_tieu.values())
giay = n_ng * vet_moi_anh * giay_moi_vet + n_ok * giay_ok
giay *= 1 + ti_le_kiem_cheo # người thứ hai kiểm lại một phần
gio = giay / 3600
print(f"→ đủ dữ liệu sau {so_ngay_max} ngày sản xuất (lỗi hiếm nhất quyết định)")
print(f"→ gán nhãn {n_ng} ảnh NG + {n_ok} ảnh OK: {gio:.0f} giờ công ≈ {gio * gia_gio / 1e6:.1f} triệu đồng")
return so_ngay_max, gio
TI_LE = {"xuoc": 0.0015, "rỗ_khí": 0.0006, "hở_lớp_phủ": 0.0003}
MUC_TIEU = {"xuoc": 300, "rỗ_khí": 200, "hở_lớp_phủ": 150}
for ten, giay_vet in (("khung bao", 20), ("đa giác", 60)):
print(f"=== Phương án gán nhãn {ten} ({giay_vet} s/vết)")
chi_phi_du_lieu(10_000, TI_LE, giu_lai=0.7, muc_tieu=MUC_TIEU, n_ok=3000, giay_moi_vet=giay_vet)
=== Phương án gán nhãn khung bao (20 s/vết)
loại lỗi NG/ngày thu được/ngày số ngày
xuoc 15.0 10.5 29
rỗ_khí 6.0 4.2 48
hở_lớp_phủ 3.0 2.1 72
→ đủ dữ liệu sau 72 ngày sản xuất (lỗi hiếm nhất quyết định)
→ gán nhãn 650 ảnh NG + 3000 ảnh OK: 8 giờ công ≈ 0.5 triệu đồng
=== Phương án gán nhãn đa giác (60 s/vết)
loại lỗi NG/ngày thu được/ngày số ngày
xuoc 15.0 10.5 29
rỗ_khí 6.0 4.2 48
hở_lớp_phủ 3.0 2.1 72
→ đủ dữ liệu sau 72 ngày sản xuất (lỗi hiếm nhất quyết định)
→ gán nhãn 650 ảnh NG + 3000 ảnh OK: 19 giờ công ≈ 1.1 triệu đồng
Hai bài học từ con số. Thứ nhất, giờ công gán nhãn thường không phải nút thắt; thời gian chờ lỗi hiếm mới là: 72 ngày sản xuất để có 150 mẫu hở lớp phủ, trong khi gán nhãn chỉ khoảng 8–19 giờ công (chưa kể viết hướng dẫn, họp thống nhất và sửa nhãn, thường cộng thêm vài chục phần trăm). Muốn rút ngắn, hãy thu cả hàng NG tồn kho, nhờ QC giữ lại mọi chi tiết NG, dùng anomaly detection giai đoạn đầu, hoặc bổ sung dữ liệu tổng hợp (bài "Khi thiếu ảnh lỗi: augmentation và dữ liệu tổng hợp"). Thứ hai, giờ công trên là một lần; mỗi lần đổi mã sản phẩm, đổi camera hay đèn, bạn cần một phần dữ liệu mới. Phương án rule-based ngược lại tốn giờ kỹ sư để thiết kế và chỉnh luật nhưng gần như không tốn chi phí dữ liệu.
| Khoản chi phí | Rule-based | AI có giám sát |
|---|---|---|
| Dữ liệu | Vài chục ảnh để chỉnh và kiểm tra | Hàng trăm đến hàng nghìn ảnh, gán nhãn, kiểm chéo |
| Thời gian chờ lỗi | Thấp (cần mẫu giới hạn để nghiệm thu) | Cao với lỗi hiếm |
| Kỹ sư | Cao với bài toán khó (luật mong manh) | Trung bình: pipeline dữ liệu, huấn luyện, đánh giá |
| Phần cứng | CPU | Có thể cần GPU/bộ tăng tốc (chặng 14) |
| Bảo trì khi đổi mã | Chỉnh recipe | Thu thêm ảnh, huấn luyện lại, nghiệm thu lại trên golden set |
6. Sai lầm thường gặp
- Dùng AI để bù chiếu sáng kém. Sửa ánh sáng trước; nhiều bài toán "cần AI" trở thành bài toán phân ngưỡng.
- Dùng AI để đo. Hồi quy kích thước bằng CNN cho sai số lớn hơn và khó truy xuất hơn đo cạnh subpixel. Để AI tìm, để rule-based đo.
- Không có cổng chất lượng ảnh. Ảnh tối, mờ, không có chi tiết vẫn được đưa vào mô hình và sinh NG hàng loạt (hoặc tệ hơn, OK hàng loạt).
- Hứa thời hạn mà không tính lịch thu thập lỗi hiếm. Lỗi 0,03% không cho bạn 150 ảnh trong 2 tuần.
- Đánh giá bằng accuracy chung chung. Nhà máy nói bằng escape và overkill tại một ngưỡng cụ thể (bài "Chọn ngưỡng và đánh giá theo escape, overkill, cycle time").
- Tập test cùng đợt chụp với tập train. Kết quả đẹp trên giấy, thất bại khi đổi ca hoặc đổi lô vật liệu. Bài sau dạy cách chia không rò rỉ.
7. Bài tập
Bài 1. Với 4 bài toán: (a) đo đường kính trục 12,00 ± 0,02 mm; (b) phát hiện vết xước trên mặt gỗ ván sàn; (c) đọc mã DataMatrix khắc laser trên chi tiết kim loại; (d) phân loại bánh mì có nấm mốc hay không. Chọn rule-based, AI hay hybrid và giải thích. Dùng hàm chấm điểm ở mục 2 để kiểm tra lập luận.
Xem đáp án
TRONG_SO = {"can_do_kich_thuoc": -2, "mo_ta_bang_luat": -2, "can_giai_thich": -1,
"nen_bien_thien": +2, "loi_da_dang": +2, "phan_doan_chu_quan": +1}
def goi_y(tra_loi, so_anh_loi):
diem = sum(TRONG_SO[k] * v for k, v in tra_loi.items())
if diem <= -2:
kl = "RULE-BASED"
elif diem >= 3:
kl = "AI có giám sát" if so_anh_loi >= 200 else "AI: bắt đầu bằng anomaly detection"
else:
kl = "HYBRID / thử cả hai"
if diem > -2 and tra_loi.get("can_do_kich_thuoc", 0) >= 1:
kl += " + đo kích thước bằng rule-based"
return diem, kl
BON_BAI = {
"(a) đường kính trục": ({"can_do_kich_thuoc": 2, "mo_ta_bang_luat": 2, "can_giai_thich": 2}, 0),
"(b) xước trên gỗ": ({"nen_bien_thien": 2, "loi_da_dang": 2, "phan_doan_chu_quan": 1,
"can_do_kich_thuoc": 1}, 400),
"(c) DataMatrix khắc laser": ({"mo_ta_bang_luat": 2, "can_giai_thich": 1, "nen_bien_thien": 1}, 0),
"(d) nấm mốc trên bánh": ({"nen_bien_thien": 2, "loi_da_dang": 2, "phan_doan_chu_quan": 2}, 500),
}
for ten, (tl, n) in BON_BAI.items():
diem, kl = goi_y(tl, n)
print(f"{ten:26s} điểm {diem:+3d} → {kl}")
(a) đường kính trục điểm -10 → RULE-BASED
(b) xước trên gỗ điểm +7 → AI có giám sát + đo kích thước bằng rule-based
(c) DataMatrix khắc laser điểm -3 → RULE-BASED
(d) nấm mốc trên bánh điểm +10 → AI có giám sát
- (a) Rule-based: đèn nền (backlight) + ống kính telecentric, đo cạnh subpixel, quy đổi mm bằng hiệu chuẩn (chặng 7–8). Dung sai ±0,02 mm đòi độ lặp lại mà AI không cho được, và cần truy xuất giá trị đo.
- (b) AI + rule-based đo: vân gỗ biến thiên, vết xước đủ hình dạng; dùng phát hiện hoặc phân đoạn, rồi rule-based quy đổi chiều dài vết sang mm để áp tiêu chí "xước ≥ x mm". Nếu chưa đủ ảnh xước, bắt đầu bằng anomaly detection.
- (c) Rule-based (bộ giải mã chuyên dụng: thư viện giải mã hoặc đầu đọc mã công nghiệp) cùng chiếu sáng phù hợp với mã khắc (đèn góc thấp, đèn vòm). Giải mã là bài toán có đáp án đúng/sai tuyệt đối và có tiêu chuẩn chấm chất lượng mã. AI chỉ đáng cân nhắc để tìm vùng mã trên nền rất phức tạp trước khi đưa cho bộ giải mã.
- (d) AI (phân loại hoặc phân đoạn): nấm mốc có màu, hình dạng, kích thước đa dạng trên bề mặt bánh vốn đã không đồng nhất; luật màu sắc sẽ nhầm với vết nướng cháy. Cần camera màu, ánh sáng ổn định và dữ liệu từ nhiều mẻ bánh.
Bài 2. Dây chuyền 4.000 sản phẩm/ngày, hai loại lỗi: móp 0,2% và nứt 0,05%. QC giữ lại được 80% số NG. Bạn cần 250 ảnh mỗi loại, 2.000 ảnh OK, gán nhãn khung bao 15 giây/vết, 1,5 vết/ảnh NG. Bao nhiêu ngày để đủ dữ liệu và bao nhiêu giờ công gán nhãn (kiểm chéo 20%, ảnh OK 3 giây)? Nếu khách hàng muốn chạy thử sau 4 tuần (20 ngày sản xuất), bạn đề xuất gì?
Xem đáp án
import math
san_luong, giu_lai = 4000, 0.8
for loai, p in (("móp", 0.002), ("nứt", 0.0005)):
thu = san_luong * p * giu_lai
print(f"{loai}: {thu:.1f} ảnh/ngày → {math.ceil(250 / thu)} ngày")
giay = (500 * 1.5 * 15 + 2000 * 3) * 1.2
print(f"giờ công gán nhãn: {giay / 3600:.1f} giờ")
móp: 6.4 ảnh/ngày → 40 ngày
nứt: 1.6 ảnh/ngày → 157 ngày
giờ công gán nhãn: 5.8 giờ
Vết nứt cần 157 ngày sản xuất, gấp gần 8 lần thời hạn chạy thử, trong khi gán nhãn chỉ khoảng 6 giờ. Đề xuất: (1) chạy thử bằng anomaly detection huấn luyện trên ảnh OK (có ngay) kết hợp luật cho phần đo được; (2) song song thu mọi chi tiết NG, kể cả hàng NG tồn kho; (3) chuyển sang mô hình có giám sát cho lỗi móp sau khoảng 40 ngày, lỗi nứt sau khoảng 157 ngày, có thể bổ sung dữ liệu tổng hợp cho huấn luyện; (4) ghi rõ trong hợp đồng rằng tiêu chí nghiệm thu lỗi nứt phụ thuộc số mẫu thật thu được.
Bài 3. Trạm kiểm tra nắp chai: phải xác nhận có nắp, nắp vặn đủ chặt (khe hở nắp–cổ chai ≤ 0,5 mm), và không có vết móp, trầy trên mặt nắp in logo nhiều màu. Thiết kế pipeline hybrid: liệt kê từng bước, bước nào rule-based, bước nào AI, và mã gửi PLC.
Xem đáp án
- Cổng chất lượng ảnh (rule-based): độ sáng trung bình trong khoảng cho phép, độ nét (phương sai Laplacian) trên ngưỡng; sai thì mã "lỗi hệ thống".
- Có chai, có nắp (rule-based): blob/contour trong ROI cổ chai; không có nắp là NG ngay, không cần AI.
- Khe hở nắp–cổ chai (rule-based): camera nhìn ngang với đèn nền, đo khoảng cách cạnh subpixel, quy đổi mm; > 0,5 mm là "NG kích thước".
- Định vị mặt nắp (rule-based): tìm vòng tròn nắp, cắt ROI và xoay về hướng chuẩn theo logo (so khớp mẫu).
- Móp, trầy trên logo nhiều màu (AI): mặt nắp in nhiều màu làm luật phân ngưỡng nhầm với nét in; dùng anomaly detection trên ROI đã căn chỉnh (ảnh OK nhiều) hoặc mô hình có giám sát khi đã đủ ảnh lỗi.
- Luật tổng hợp: mã PLC 1 = OK, 2 = NG bề mặt, 3 = NG lắp ráp/khe hở, 9 = lỗi hệ thống; ghi log điểm AI và giá trị đo cho mỗi chai.
Tóm tắt
- Rule-based thắng khi yêu cầu là đo, đếm, đọc mã, có/không, và khi chiếu sáng tạo được tương phản rõ; AI thắng khi lỗi được định nghĩa bằng hình dạng trên nền biến thiên.
- Dùng bảng tiêu chí và một hàm chấm điểm để cả nhóm trả lời cùng một bộ câu hỏi cho từng loại lỗi; kết quả là gợi ý để thảo luận, không phải công thức.
- Thí nghiệm: luật phân ngưỡng hoàn hảo trên đốm rỗ kim loại nhưng lọt khoảng 40% vết nứt trên vân gỗ; CNN nhỏ học được vết nứt nhưng chất lượng phụ thuộc số ảnh lỗi.
- Hệ thống hybrid: cổng chất lượng ảnh, định vị, đo bằng rule-based; AI phán lỗi bề mặt trên ROI đã chuẩn hóa; tách mã "lỗi hệ thống" khỏi "NG".
- Ước lượng chi phí dữ liệu: lỗi hiếm nhất quyết định lịch thu thập; giờ công gán nhãn tính theo loại nhãn; nhớ chi phí lặp lại khi đổi mã sản phẩm.
Bài tiếp theo: Tự xây dataset ảnh công nghiệp, biến ước lượng hôm nay thành kế hoạch thu thập, cấu trúc thư mục, phiên bản và cách chia tập không rò rỉ.
Bài tập thực hành
- Với 4 bài toán (đo đường kính, phát hiện vết xước gỗ, đọc mã DataMatrix, phân loại nấm mốc), chọn rule-based hay AI và giải thích.
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.


