⭐ Vision Engineer · Chặng 7Bài 4/8Thực hành~75 phút

Đọc mã QR và DataMatrix trên bao bì bằng OpenCV và libdmtx

Đọc QR bằng OpenCV, DataMatrix bằng pylibdmtx, kiểm tra chuỗi GS1 với lệnh sản xuất, đo giới hạn mờ, nghiêng và tăng tỉ lệ đọc bằng chiến lược bậc thang.

Đọc mã QR và DataMatrix trên bao bì bằng OpenCV và libdmtx

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

  • Đọc mã 2D bằng OpenCV và libdmtx
  • Kiểm tra nội dung hợp lệ
  • Xử lý mã mờ, nghiêng

Nên học trước: Feature matching và shape matching khi vật xoay

Nội dung bài viết
  1. 1. QR Code và DataMatrix: cấu tạo cần biết
  2. 2. Cài đặt và kiểm tra OpenCV có gì
  3. 3. Tạo nhãn thử và đọc QR đầu tiên
  4. 4. Nhiều mã trên một ảnh và bộ đọc thứ hai
  5. 5. DataMatrix với pylibdmtx (chạy trên máy bạn)
  6. 6. Kiểm tra nội dung trước khi chấp nhận
  7. 7. Mã mờ, nghiêng, tương phản thấp: đo giới hạn
  8. 8. Chiến lược đọc bậc thang
  9. 9. Thực hành: 20 ảnh, đo tỉ lệ đọc thành công
  10. 10. Đưa vào dây chuyền
  11. 11. Lỗi thường gặp
  12. Bài tập
  13. Tóm tắt

Ở bài feature matching và shape matching, bạn đã định vị được vật xoay bất kỳ góc nào. Mã 2D là trường hợp đặc biệt dễ chịu: QR Code và DataMatrix được thiết kế sẵn các mẫu định vị để máy tự tìm vị trí, góc xoay và lưới ô, rồi có cả mã sửa lỗi Reed-Solomon. Chúng là "chứng minh thư" của sản phẩm: mã sản phẩm, số lô, hạn dùng, số seri. Trạm đọc mã trên dây chuyền đóng gói là một trong những ứng dụng vision phổ biến nhất ở nhà máy Việt Nam (thực phẩm, dược, điện tử, linh kiện ô tô). Bài lab này dạy bạn đọc mã bằng OpenCV và libdmtx, kiểm tra nội dung trước khi chấp nhận, đo giới hạn khi mã mờ hoặc nghiêng, và tự làm bộ thử 20 ảnh để đo tỉ lệ đọc. Bài sau, OCR và OCV trong công nghiệp, chuyển sang đọc chữ in, việc khó hơn nhiều vì chữ không có cơ chế sửa lỗi.

Mục tiêu cuối bàiĐọc được QR bằng cv2.QRCodeDetector và cv2.QRCodeDetectorAruco, đọc DataMatrix bằng pylibdmtx (libdmtx); tách và kiểm tra chuỗi GS1 (chữ số kiểm tra, hạn dùng, lô, trùng seri); đo được mã đọc tới mức mờ, nghiêng, tương phản nào; dùng chiến lược đọc "bậc thang" để tăng tỉ lệ đọc mà không tăng cycle time.

1. QR Code và DataMatrix: cấu tạo cần biết

Đơn vị nhỏ nhất của mã 2D là module (một ô vuông đen hoặc trắng). Mọi quy tắc thiết kế trạm đọc đều tính theo module: camera phải nhìn mỗi module bằng vài pixel, độ mờ phải nhỏ hơn một phần module, và quanh mã phải có vùng yên tĩnh (quiet zone) không in gì.

QR Code (ISO/IEC 18004)DataMatrix ECC 200 (ISO/IEC 16022)
Mẫu định vị3 ô vuông lớn (finder pattern) ở 3 góc, thêm ô căn chỉnh (alignment) từ phiên bản 2Hai cạnh liền tạo hình chữ L (finder), hai cạnh đối diện chấm đen trắng xen kẽ (clock/timing)
Kích thướcPhiên bản 1–40: từ 21×21 đến 177×177 moduleVuông 10×10 đến 144×144 module, có cả dạng chữ nhật (ví dụ 8×18)
Sửa lỗiReed-Solomon, chọn mức L/M/Q/H: phục hồi khoảng 7/15/25/30% dữ liệu hỏngReed-Solomon, mức cố định theo kích thước mã
Vùng yên tĩnh4 module1 module
Dùng nhiều ởBao bì tiêu dùng, thanh toán, marketing, nhãn thùngLinh kiện điện tử, ô tô, dược phẩm; khắc trực tiếp lên kim loại, nhựa (DPM)
Ưu điểmĐọc được bằng điện thoại, thư viện mở hỗ trợ tốtNhỏ gọn hơn với cùng dữ liệu, chịu được khắc chấm (dot peen)

Trong chuỗi cung ứng, nội dung mã thường theo chuẩn GS1: dữ liệu ghép từ các trường có mã định danh ứng dụng (Application Identifier, AI) đứng trước, ví dụ (01) là GTIN 14 chữ số, (17) hạn dùng dạng YYMMDD, (10) số lô, (21) số seri. Trường có độ dài thay đổi (lô, seri) kết thúc bằng ký tự phân cách GS (ASCII 29) nếu còn trường khác theo sau. Mã GS1 DataMatrix và GS1 QR thật bắt đầu bằng ký tự đặc biệt FNC1; khi được cấu hình, đầu đọc gửi kèm tiền tố nhận dạng như ]d2 (GS1 DataMatrix) hoặc ]Q3 (GS1 QR) để phần mềm biết đây là chuỗi GS1.

2. Cài đặt và kiểm tra OpenCV có gì

pip install opencv-python numpy
# DataMatrix: pylibdmtx là lớp Python bọc thư viện C libdmtx
pip install pylibdmtx
# Ubuntu/Debian: cài thêm thư viện hệ thống (bản Windows của pylibdmtx đã kèm sẵn DLL)
sudo apt install libdmtx0b
# macOS
brew install libdmtx

Trên Windows, nếu import pylibdmtx báo lỗi thiếu DLL, thường là máy thiếu gói Visual C++ Redistributable; tài liệu của pylibdmtx ghi rõ phiên bản cần cài. Trước khi viết code, hãy kiểm tra bản OpenCV của bạn thực sự có những bộ đọc nào, đừng đoán:

import cv2

print("OpenCV", cv2.__version__)
for ten in ("QRCodeEncoder", "QRCodeDetector", "QRCodeDetectorAruco"):
    print(f"cv2.{ten:28s}", hasattr(cv2, ten))
print(f"{'cv2.barcode.BarcodeDetector':32s}", hasattr(cv2, "barcode") and hasattr(cv2.barcode, "BarcodeDetector"))
dm = [n for n in dir(cv2) if "datamatrix" in n.lower() or "dmtx" in n.lower()]
print(f"{'Bộ đọc DataMatrix trong cv2':32s}", dm if dm else "không có")
OpenCV 5.0.0
cv2.QRCodeEncoder                True
cv2.QRCodeDetector               True
cv2.QRCodeDetectorAruco          True
cv2.barcode.BarcodeDetector      True
Bộ đọc DataMatrix trong cv2      không có

Bài viết chạy với OpenCV 5.0. OpenCV có bộ tạo và hai bộ đọc QR, bộ đọc mã vạch 1D, nhưng không có bộ giải mã DataMatrix (hàm findDataMatrix của OpenCV 2.x đã bị bỏ từ OpenCV 3). Vì vậy DataMatrix cần thư viện khác: pylibdmtx (mã nguồn mở), zxing-cpp (gói pip zxing-cpp, đọc được nhiều loại mã), hoặc thư viện của các hãng vision thương mại. Môi trường chạy thử của bài không có libdmtx, nên phần chạy được dùng QR; phần DataMatrix ở mục 5 là mã để bạn chạy trên máy mình. Mọi kỹ thuật còn lại (kiểm tra nội dung, đo giới hạn, đọc bậc thang) áp dụng y hệt cho DataMatrix.

3. Tạo nhãn thử và đọc QR đầu tiên

Ta tạo một nhãn thùng 640×480: mã QR chứa chuỗi GS1 (GTIN, hạn dùng, lô, seri), mức sửa lỗi M, mỗi module 5 pixel. cv2.QRCodeEncoder trả về ảnh có 1 pixel cho mỗi module, nên ta phóng to bằng INTER_NEAREST để giữ cạnh sắc.

import cv2
import numpy as np

GS = "\x1d"                                                   # ký tự phân cách GS1 (ASCII 29)
NOI_DUNG = "01" + "08931234567897" + "17" + "271231" + "10" + "L2610A" + GS + "21" + "VN000123"

p = cv2.QRCodeEncoder_Params()
p.correction_level = cv2.QRCODE_ENCODER_CORRECT_LEVEL_M
qr = cv2.QRCodeEncoder.create(p).encode(NOI_DUNG)             # 1 pixel = 1 module
den = np.where(qr.min(axis=0) == 0)[0]
print("ảnh mã:", qr.shape, "| vùng có module đen: cột", den.min(), "đến", den.max())

MOD = 5
ma = cv2.resize(qr, None, fx=MOD, fy=MOD, interpolation=cv2.INTER_NEAREST)
nhan = np.full((480, 640), 190, np.uint8)                     # thùng carton
cv2.rectangle(nhan, (40, 60), (600, 420), 245, -1)            # nhãn trắng
nhan[100:100 + ma.shape[0], 380:380 + ma.shape[1]] = ma
cv2.putText(nhan, "GTIN 08931234567897", (60, 140), cv2.FONT_HERSHEY_SIMPLEX, 0.7, 20, 2, cv2.LINE_AA)
cv2.putText(nhan, "HSD 31/12/2027", (60, 190), cv2.FONT_HERSHEY_SIMPLEX, 0.7, 20, 2, cv2.LINE_AA)
cv2.putText(nhan, "LOT L2610A", (60, 240), cv2.FONT_HERSHEY_SIMPLEX, 0.7, 20, 2, cv2.LINE_AA)
rng = np.random.default_rng(0)
nhan = np.clip(nhan + rng.normal(0, 3, nhan.shape), 0, 255).astype(np.uint8)
cv2.imwrite("nhan_thung.png", nhan)

bo_doc = cv2.QRCodeDetector()
chuoi, goc, ma_thang = bo_doc.detectAndDecode(nhan)
print("nội dung:", repr(chuoi))
print("4 góc (px):", goc.reshape(4, 2).round(1).tolist())
canh = np.linalg.norm(goc[0, 1] - goc[0, 0])
print(f"cạnh mã {canh:.1f} px, lưới {ma_thang.shape[0]} module → {canh / ma_thang.shape[0]:.2f} px/module")
ảnh mã: (37, 37) | vùng có module đen: cột 2 đến 34
nội dung: '01089312345678971727123110L2610A\x1d21VN000123'
4 góc (px): [[390.0, 110.0], [554.0, 110.0], [554.0, 274.0], [390.0, 274.0]]
cạnh mã 164.0 px, lưới 33 module → 4.97 px/module

Ba điều cần để ý:

  • Mã có 33×33 module (phiên bản 4, vì chuỗi GS1 dài 43 ký tự ở mức sửa lỗi M). Bộ tạo của OpenCV chỉ chừa vùng yên tĩnh 2 module mỗi bên (ảnh 37×37), ít hơn chuẩn 4 module; nhãn thật phải chừa đủ. Ở đây nền nhãn trắng quanh mã đóng vai vùng yên tĩnh.
  • detectAndDecode trả về 3 thứ: chuỗi (rỗng nếu không đọc được), 4 góc theo thứ tự trên-trái, trên-phải, dưới-phải, dưới-trái của mã (không phải của ảnh: mã xoay 90° thì "trên-trái" là góc có finder pattern tương ứng), và ảnh mã đã nắn thẳng, 1 pixel mỗi module.
  • Từ cạnh mã và số module, ta tính được px/module. Con số này nên ghi vào log mỗi lần đọc: nó tụt dần là dấu hiệu camera bị xô lệch hoặc khách đổi mẫu nhãn.

Ký tự GS (\x1d) đi qua mã nguyên vẹn. Nếu bạn in chuỗi ra màn hình mà không dùng repr, ký tự này vô hình và bạn sẽ không hiểu vì sao phép so sánh chuỗi thất bại.

4. Nhiều mã trên một ảnh và bộ đọc thứ hai

Thùng hàng thường có nhiều mã (mã thùng, mã pallet, mã của từng hộp nhìn qua cửa sổ). detectAndDecodeMulti đọc tất cả. OpenCV còn có QRCodeDetectorAruco, dùng thuật toán tìm finder pattern kiểu ArUco, có giao diện giống hệt. Hai bộ đọc mạnh yếu khác nhau, ta sẽ tận dụng điều đó ở mục 8.

import cv2
import numpy as np

rng = np.random.default_rng(1)
anh = np.full((480, 640), 200, np.uint8)
for i, (x, y, goc) in enumerate(((40, 60, 0), (250, 180, 30), (450, 40, 90))):
    qr = cv2.QRCodeEncoder.create().encode(f"21VN00012{i}")
    ma = cv2.resize(qr, None, fx=5, fy=5, interpolation=cv2.INTER_NEAREST)
    ma = cv2.copyMakeBorder(ma, 10, 10, 10, 10, cv2.BORDER_CONSTANT, value=255)
    h, w = ma.shape
    M = cv2.getRotationMatrix2D((w / 2, h / 2), goc, 1.0)
    xoay = cv2.warpAffine(ma, M, (w, h), flags=cv2.INTER_LINEAR, borderValue=200)
    anh[y:y + h, x:x + w] = xoay
anh = np.clip(anh + rng.normal(0, 3, anh.shape), 0, 255).astype(np.uint8)

for ten, bo_doc in (("QRCodeDetector", cv2.QRCodeDetector()), ("QRCodeDetectorAruco", cv2.QRCodeDetectorAruco())):
    ok, chuoi, goc, _ = bo_doc.detectAndDecodeMulti(anh)
    ket_qua = sorted(zip(chuoi, goc.mean(axis=1).round().astype(int).tolist())) if ok else []
    print(f"{ten:20s} đọc được {sum(1 for c, _ in ket_qua if c)} mã:")
    for c, tam in ket_qua:
        print(f"    {c!r:16s} tâm {tam}")
QRCodeDetector       đọc được 3 mã:
    '21VN000120'     tâm [112, 132]
    '21VN000121'     tâm [322, 252]
    '21VN000122'     tâm [522, 112]
QRCodeDetectorAruco  đọc được 3 mã:
    '21VN000120'     tâm [112, 132]
    '21VN000121'     tâm [322, 253]
    '21VN000122'     tâm [522, 113]

Thứ tự kết quả của detectAndDecodeMulti không theo vị trí, nên luôn sắp xếp lại (theo nội dung hoặc theo tọa độ) trước khi so với kỳ vọng. Một phần tử chuỗi rỗng nghĩa là tìm thấy mã nhưng không giải được; đếm riêng trường hợp này, nó thường báo hiệu mã in kém chứ không phải thiếu mã.

5. DataMatrix với pylibdmtx (chạy trên máy bạn)

Khối dưới đây cần pylibdmtx và libdmtx (mục 2). Nó sinh một mã DataMatrix rồi đọc lại, đúng cách bạn sẽ dùng ở trạm thật.

# (minh họa) — cần: pip install pylibdmtx (+ libdmtx của hệ điều hành)
import time

import cv2
import numpy as np
from pylibdmtx.pylibdmtx import decode, encode

GS = "\x1d"
noi_dung = "0108931234567897" + "17271231" + "10L2610A" + GS + "21VN000123"
enc = encode(noi_dung.encode("ascii"))                 # Encoded(width, height, bpp, pixels), ảnh RGB
dm = np.frombuffer(enc.pixels, np.uint8).reshape(enc.height, enc.width, 3)
xam = cv2.cvtColor(dm, cv2.COLOR_RGB2GRAY)
xam = cv2.resize(xam, None, fx=2, fy=2, interpolation=cv2.INTER_NEAREST)
xam = cv2.copyMakeBorder(xam, 40, 40, 40, 40, cv2.BORDER_CONSTANT, value=255)

t0 = time.perf_counter()
ket_qua = decode(xam, timeout=200, max_count=1)       # timeout tính bằng mili giây; max_count: dừng khi đủ số mã
print(f"{(time.perf_counter() - t0) * 1000:.0f} ms")
for r in ket_qua:
    print(repr(r.data.decode("ascii")), r.rect)     # data là bytes; rect = Rect(left, top, width, height)
    x, w, h = r.rect.left, r.rect.width, r.rect.height
    y_tren = xam.shape[0] - r.rect.top - h          # libdmtx đặt gốc tọa độ ở góc DƯỚI-trái
    cv2.rectangle(xam, (x, y_tren), (x + w, y_tren + h), 128, 2)   # vẽ thử để kiểm tra khung

Những điểm khác với bộ đọc QR của OpenCV:

  • Luôn đặt timeout (mili giây). Không có mã trong ảnh, libdmtx có thể quét rất lâu trên ảnh lớn, đủ để trễ cả chục chu kỳ dây chuyền. max_count=1 dừng ngay khi đọc được một mã.
  • shrink=2 cho libdmtx giảm ảnh trước khi tìm (nhanh hơn, cần module đủ lớn). Các tham số khác như shape, min_edge, max_edge, threshold giúp thu hẹp tìm kiếm; ý nghĩa và giá trị hợp lệ xem trong tài liệu của phiên bản bạn cài.
  • Kết quả chỉ có một hình chữ nhật rect, không có 4 góc và hướng như QR. Trường top tính theo hệ tọa độ của libdmtx (gốc dưới-trái), nên phải đổi như dòng y_tren; hãy vẽ khung lên ảnh để xác nhận với phiên bản bạn đang dùng.
  • Hiệu quả nhất là cắt ROI quanh vùng có mã (nhờ đồ gá hoặc định vị bằng template matching như bài trước) rồi mới gọi decode.

6. Kiểm tra nội dung trước khi chấp nhận

Đọc được mã mới là một nửa. Trạm phải trả lời: nội dung có đúng cho lô đang chạy không? Máy in nhãn cài nhầm dữ liệu lô cũ, đổi ca quên cập nhật hạn dùng, máy in lặp lại một seri: các lỗi này đều cho mã đọc "đẹp" nhưng sai. Quy trình kiểm tra:

  1. Tách chuỗi GS1 thành các trường. Trường độ dài cố định (01: 14 chữ số, 17: 6 chữ số) cắt theo độ dài; trường độ dài thay đổi (10, 21: tối đa 20 ký tự) đọc tới ký tự GS hoặc hết chuỗi.
  2. Kiểm tra chữ số kiểm tra GTIN (thuật toán GS1 mod 10): nhân các chữ số với trọng số 3, 1, 3, 1… tính từ phải sang (không tính chữ số kiểm tra), chữ số kiểm tra = (10 − tổng mod 10) mod 10.
  3. Hạn dùng phải là ngày hợp lệ và khớp lệnh sản xuất; GTIN và lô khớp lệnh sản xuất.
  4. Seri chưa từng xuất hiện (chống trùng).
from datetime import date

GS = "\x1d"
DO_DAI_CO_DINH = {"01": 14, "17": 6, "11": 6}                 # AI có độ dài cố định dùng trong bài
DO_DAI_TOI_DA = {"10": 20, "21": 20}                          # AI độ dài thay đổi, kết thúc bằng GS


def chu_so_kiem_tra_gs1(than):
    """Chữ số kiểm tra GS1 (GTIN-8/12/13/14, SSCC): trọng số 3,1,3,1... tính từ phải sang."""
    tong = sum(int(c) * (3 if i % 2 == 0 else 1) for i, c in enumerate(reversed(than)))
    return str((10 - tong % 10) % 10)


def tach_gs1(chuoi):
    """'0108931234567897172712...' → {'01': '0893...', '17': '271231', ...}; lỗi → ValueError."""
    truong, i = {}, 0
    while i < len(chuoi):
        ai = chuoi[i:i + 2]
        if ai in DO_DAI_CO_DINH:
            n = DO_DAI_CO_DINH[ai]
            gia_tri = chuoi[i + 2:i + 2 + n]
            if len(gia_tri) != n:
                raise ValueError(f"AI {ai} thiếu ký tự")
            i += 2 + n
        elif ai in DO_DAI_TOI_DA:
            j = chuoi.find(GS, i + 2)
            j = len(chuoi) if j < 0 else j
            gia_tri = chuoi[i + 2:j]
            if not 1 <= len(gia_tri) <= DO_DAI_TOI_DA[ai]:
                raise ValueError(f"AI {ai} sai độ dài")
            i = j + 1
        else:
            raise ValueError(f"AI không hỗ trợ: {ai!r}")
        truong[ai] = gia_tri
    return truong


LENH_SX = {"gtin": "08931234567897", "lo": "L2610A", "hsd": date(2027, 12, 31)}
da_doc = set()


def kiem_tra(chuoi):
    try:
        t = tach_gs1(chuoi)
    except ValueError as e:
        return "NG", f"sai cấu trúc GS1: {e}"
    if not {"01", "17", "10", "21"} <= t.keys():
        return "NG", "thiếu trường bắt buộc"
    gtin = t["01"]
    if not gtin.isdigit() or chu_so_kiem_tra_gs1(gtin[:-1]) != gtin[-1]:
        return "NG", f"GTIN {gtin} sai chữ số kiểm tra"
    if gtin != LENH_SX["gtin"]:
        return "NG", f"GTIN {gtin} không thuộc lệnh sản xuất"
    try:
        hsd = date(2000 + int(t["17"][:2]), int(t["17"][2:4]), int(t["17"][4:]))
    except ValueError:
        return "NG", f"hạn dùng {t['17']} không phải ngày hợp lệ"
    if hsd != LENH_SX["hsd"]:
        return "NG", f"hạn dùng {hsd} khác lệnh ({LENH_SX['hsd']})"
    if t["10"] != LENH_SX["lo"]:
        return "NG", f"lô {t['10']} khác lệnh ({LENH_SX['lo']})"
    if t["21"] in da_doc:
        return "NG", f"seri {t['21']} bị trùng"
    da_doc.add(t["21"])
    return "OK", f"seri {t['21']}"


THU = {
    "nhãn đúng": "0108931234567897" + "17271231" + "10L2610A" + GS + "21VN000123",
    "đọc lại nhãn trên": "0108931234567897" + "17271231" + "10L2610A" + GS + "21VN000123",
    "seri kế tiếp": "0108931234567897" + "17271231" + "10L2610A" + GS + "21VN000124",
    "GTIN in sai 1 số": "0108931234567887" + "17271231" + "10L2610A" + GS + "21VN000125",
    "ngày 31/02": "0108931234567897" + "17270231" + "10L2610A" + GS + "21VN000126",
    "lô cũ chưa đổi": "0108931234567897" + "17271231" + "10L2609C" + GS + "21VN000127",
    "chuỗi không phải GS1": "PN:VB-500;SN:123",
}
print("Chữ số kiểm tra của 0893123456789:", chu_so_kiem_tra_gs1("0893123456789"))
for ten, chuoi in THU.items():
    kq, ly_do = kiem_tra(chuoi)
    print(f"{ten:22s} {kq}  {ly_do}")
Chữ số kiểm tra của 0893123456789: 7
nhãn đúng              OK  seri VN000123
đọc lại nhãn trên      NG  seri VN000123 bị trùng
seri kế tiếp           OK  seri VN000124
GTIN in sai 1 số       NG  GTIN 08931234567887 sai chữ số kiểm tra
ngày 31/02             NG  hạn dùng 270231 không phải ngày hợp lệ
lô cũ chưa đổi         NG  lô L2609C khác lệnh (L2610A)
chuỗi không phải GS1   NG  sai cấu trúc GS1: AI không hỗ trợ: 'PN'

Thứ tự kiểm tra có chủ ý: kiểm cấu trúc và chữ số kiểm tra trước (lỗi đọc hoặc lỗi in), rồi mới so với lệnh sản xuất (lỗi dữ liệu), và chống trùng sau cùng: chỉ ghi nhận seri khi sản phẩm qua hết các bước khác, nếu không một hộp bị loại vì sai lô sẽ "chiếm" seri và hộp làm lại bị báo trùng oan. Trên trạm thật, tập da_doc phải nằm trong cơ sở dữ liệu (SQLite hoặc MES) để không mất khi khởi động lại phần mềm. Cách in chuỗi GS1 dễ đọc cho người: thay mỗi AI bằng dạng có ngoặc, ví dụ (01)08931234567897(17)271231(10)L2610A(21)VN000123.

Mã vạch 1D cũng dùng chữ số kiểm tra nàyEAN-13 trên bao bì là GTIN-13, chữ số cuối tính bằng đúng hàm chu_so_kiem_tra_gs1. cv2.barcode.BarcodeDetector().detectAndDecodeWithType(anh) đọc mã vạch họ EAN/UPC và trả về loại mã (ví dụ EAN_13); bộ đọc tự kiểm tra chữ số cuối, nên mã in sai chữ số kiểm tra sẽ không giải được thay vì trả về chuỗi sai.

7. Mã mờ, nghiêng, tương phản thấp: đo giới hạn

Thay vì tin vào "thư viện đọc tốt lắm", hãy đo. Hàm chup mô phỏng camera nhìn mã với 5 yếu tố: xoay trong mặt phẳng, nghiêng (mặt nhãn không vuông góc trục camera, gây méo phối cảnh), mờ (lệch nét hoặc rung, mô phỏng bằng Gaussian với độ lệch chuẩn σ pixel), tương phản và kích thước module. Mỗi ô kết quả ghi hai ký hiệu: bộ đọc QRCodeDetector rồi QRCodeDetectorAruco, + là đọc đúng, . là không đọc được.

import math

import cv2
import numpy as np

p = cv2.QRCodeEncoder_Params()
p.correction_level = cv2.QRCODE_ENCODER_CORRECT_LEVEL_M
NOI_DUNG = "0108931234567897" + "17271231" + "10L2610A\x1d" + "21VN000123"
QR = cv2.QRCodeEncoder.create(p).encode(NOI_DUNG)


def chup(goc=0, nghieng=0, sigma=0.0, tuong_phan=1.0, mod=5, seed=0):
    """Ảnh 640×480 của mã QR trên nhãn trắng, tâm (320, 240)."""
    rng = np.random.default_rng(seed)
    ma = cv2.resize(QR, None, fx=mod, fy=mod, interpolation=cv2.INTER_NEAREST).astype(np.float32)
    n = ma.shape[0]
    c, k = math.cos(math.radians(nghieng)), 0.25 * math.sin(math.radians(nghieng))
    dich = np.float32([[n * k / 2, 0], [n - n * k / 2, 0], [n, n * c], [0, n * c]])   # cạnh xa co lại
    dich -= dich.mean(axis=0)
    a = math.radians(goc)
    R = np.float32([[math.cos(a), -math.sin(a)], [math.sin(a), math.cos(a)]])
    dich = dich @ R.T + np.float32([320, 240])
    H = cv2.getPerspectiveTransform(np.float32([[0, 0], [n, 0], [n, n], [0, n]]), dich)
    anh = cv2.warpPerspective(ma, H, (640, 480), flags=cv2.INTER_AREA, borderValue=255)
    anh = 128 + (anh - 128) * tuong_phan
    if sigma > 0:
        anh = cv2.GaussianBlur(anh, (0, 0), sigma)
    return np.clip(anh + rng.normal(0, 4, anh.shape), 0, 255).astype(np.uint8)


BO_DOC = (cv2.QRCodeDetector(), cv2.QRCodeDetectorAruco())


def thu(anh):
    return "".join("+" if b.detectAndDecode(anh)[0] == NOI_DUNG else "." for b in BO_DOC)


print("xoay (độ)      :", "  ".join(f"{g}:{thu(chup(goc=g))}" for g in (0, 15, 45, 90, 135, 180)))
print("nghiêng (độ)   :", "  ".join(f"{g}:{thu(chup(nghieng=g))}" for g in (0, 20, 40, 50, 60, 70)))
print("tương phản     :", "  ".join(f"{t}:{thu(chup(tuong_phan=t))}" for t in (1, 0.5, 0.2, 0.1, 0.05)))
print()
print("px/module  tỉ lệ đọc (6 ảnh, góc khác nhau)")
for mod in (2, 3, 4, 5):
    kq = [thu(chup(goc=7.3 * s, mod=mod, seed=s)) for s in range(6)]
    print(f"   {mod}       {' '.join(kq)}   QRCodeDetector {sum(k[0] == '+' for k in kq)}/6, Aruco {sum(k[1] == '+' for k in kq)}/6")
print()
print("độ mờ σ (px) theo px/module:")
for mod in (4, 5, 6, 8):
    print(f"   {mod} px/module:", "  ".join(f"σ={s}:{thu(chup(sigma=s, mod=mod))}" for s in (0, 1, 1.5, 2, 2.5, 3)))
xoay (độ)      : 0:++  15:++  45:++  90:++  135:++  180:++
nghiêng (độ)   : 0:++  20:++  40:++  50:++  60:.+  70:..
tương phản     : 1:++  0.5:++  0.2:++  0.1:++  0.05:..

px/module  tỉ lệ đọc (6 ảnh, góc khác nhau)
   2       .+ .+ .+ .. .. .+   QRCodeDetector 0/6, Aruco 4/6
   3       .. ++ .+ ++ ++ ++   QRCodeDetector 4/6, Aruco 5/6
   4       ++ ++ ++ ++ ++ ++   QRCodeDetector 6/6, Aruco 6/6
   5       ++ ++ ++ ++ ++ ++   QRCodeDetector 6/6, Aruco 6/6

độ mờ σ (px) theo px/module:
   4 px/module: σ=0:++  σ=1:++  σ=1.5:..  σ=2:..  σ=2.5:..  σ=3:..
   5 px/module: σ=0:++  σ=1:++  σ=1.5:++  σ=2:..  σ=2.5:..  σ=3:..
   6 px/module: σ=0:++  σ=1:++  σ=1.5:+.  σ=2:..  σ=2.5:..  σ=3:..
   8 px/module: σ=0:++  σ=1:++  σ=1.5:++  σ=2:++  σ=2.5:++  σ=3:+.

Kết luận rút ra từ bảng (với nhiễu σ = 4 mức xám như camera công nghiệp bình thường):

Yếu tốKết quả đoÝ nghĩa cho thiết kế trạm
Xoay trong mặt phẳngMọi góc đều đọc đượcKhông cần đồ gá giữ góc mã; khác hẳn template matching
NghiêngĐọc tới khoảng 50°, hỏng từ khoảng 60°Giữ camera gần vuông góc nhãn; nghiêng nhẹ 10–15° lại có lợi với nhãn bóng (tránh phản xạ gương)
Tương phảnChênh lệch đen trắng còn 10% vẫn đọc, 5% thì hỏngTương phản là tỉ lệ với nhiễu; mã khắc laser mờ cần chiếu sáng tạo tương phản (mục 9)
Kích thước module2 px: bộ đọc thường hỏng hết, Aruco chập chờn; 3 px chập chờn; từ 4 px ổn địnhChọn FOV và độ phân giải để module ≥ 4 px với thư viện mã nguồn mở
Độ mờNgưỡng σ tăng theo module: 4 px chịu σ = 1, 5 px chịu σ = 1,5, 8 px chịu σ = 2,5Quy tắc gần đúng: σ nhỏ hơn khoảng 0,3 module

Từ bảng này bạn tính ngược được yêu cầu cho camera. Ví dụ mã DataMatrix 16×16 module, mỗi module 0,25 mm: muốn 4 px/module thì cần 16 px/mm, tức FOV rộng 50 mm cần cảm biến ít nhất 800 px theo chiều đó. Độ mờ do chuyển động: băng tải 0,5 m/s, thời gian phơi sáng 100 µs làm vật dịch 0,05 mm = 0,8 px, chấp nhận được; phơi sáng 1 ms thì dịch 8 px, mã không thể đọc dù thuật toán nào.

8. Chiến lược đọc bậc thang

Ở bảng trên, đôi khi bộ này đọc được còn bộ kia không. Đầu đọc thương mại xử lý chuyện này bằng cách thử nhiều cách giải trên cùng một ảnh. Ta làm tương tự với một bậc thang: thử cách nhanh nhất trước, chỉ khi thất bại mới thử cách tốn kém hơn. Ảnh tốt (đa số) chỉ tốn một lần đọc, nên cycle time trung bình gần như không đổi; chỉ ảnh khó mới tốn thêm.

import cv2
import numpy as np

BO_DOC = (("QR", cv2.QRCodeDetector()), ("Aruco", cv2.QRCodeDetectorAruco()))


def bien_the(anh):
    """Sinh lần lượt các biến thể ảnh, rẻ trước đắt sau."""
    yield "gốc", anh
    _, otsu = cv2.threshold(anh, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    yield "otsu", otsu
    mo = cv2.GaussianBlur(anh, (0, 0), 2)
    sac = cv2.addWeighted(anh, 2.0, mo, -1.0, 0)                # unsharp mask: tăng độ sắc cạnh
    _, sac_otsu = cv2.threshold(sac, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    yield "sắc+otsu", sac_otsu
    yield "thích nghi", cv2.adaptiveThreshold(anh, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 5)


def doc_bac_thang(anh):
    """Trả (chuỗi, 4 góc, bậc đã dùng, số lần thử) hoặc (None, None, None, số lần thử)."""
    lan = 0
    for ten_anh, x in bien_the(anh):
        for ten_bo, bo in BO_DOC:
            lan += 1
            chuoi, goc, _ = bo.detectAndDecode(x)
            if chuoi:
                return chuoi, goc, f"{ten_bo}/{ten_anh}", lan
    return None, None, None, lan


anh = cv2.imread("nhan_thung.png", cv2.IMREAD_GRAYSCALE)
chuoi, goc, bac, lan = doc_bac_thang(anh)
print(f"ảnh rõ : {chuoi!r} bậc {bac}, {lan} lần thử")
mo = cv2.GaussianBlur(anh, (0, 0), 1.8)
chuoi, goc, bac, lan = doc_bac_thang(mo)
print(f"ảnh mờ : {chuoi!r} bậc {bac}, {lan} lần thử")
ảnh rõ : '01089312345678971727123110L2610A\x1d21VN000123' bậc QR/gốc, 1 lần thử
ảnh mờ : '01089312345678971727123110L2610A\x1d21VN000123' bậc Aruco/gốc, 2 lần thử

Ảnh rõ đọc ngay lần thử đầu. Ảnh mờ σ = 1,8 px (module 5 px) làm bộ đọc thứ nhất thất bại, bộ Aruco đọc được ở lần thử thứ hai; ảnh mờ hơn sẽ phải xuống tới các bậc nhị phân hóa. Hãy ghi lại bậc đã dùng cho mỗi sản phẩm: tỉ lệ sản phẩm phải dùng bậc 2 trở lên tăng dần theo ngày là cảnh báo sớm rằng ống kính bẩn, lệch nét hoặc đèn yếu, trước khi tỉ lệ không đọc được tăng lên.

9. Thực hành: 20 ảnh, đo tỉ lệ đọc thành công

Bài thực hành của bài lab: tạo 20 ảnh mã QR với góc xoay và độ mờ khác nhau, lưu kèm đáp án, rồi đo tỉ lệ đọc đúng của bộ đọc đơn và bậc thang. Khối đầu tạo dữ liệu, khối sau đánh giá: tách riêng như vậy để bạn thay bộ ảnh tổng hợp bằng ảnh chụp thật mà không sửa phần đánh giá.

import csv
import os

import cv2
import numpy as np

os.makedirs("bo_20_anh", exist_ok=True)
GOC = (0, 30, 60, 135)
SIGMA = (0.0, 1.0, 1.5, 2.0, 2.5)
rng = np.random.default_rng(42)

with open("bo_20_anh/dap_an.csv", "w", newline="", encoding="utf-8") as f:
    ghi = csv.writer(f)
    ghi.writerow(["file", "goc", "sigma", "noi_dung"])
    for i, (g, s) in enumerate((g, s) for g in GOC for s in SIGMA):
        noi_dung = "0108931234567897" + "17271231" + f"10L2610A\x1d21VN{i + 200:06d}"
        qr = cv2.QRCodeEncoder.create().encode(noi_dung)
        ma = cv2.resize(qr, None, fx=5, fy=5, interpolation=cv2.INTER_NEAREST)
        ma = cv2.copyMakeBorder(ma, 15, 15, 15, 15, cv2.BORDER_CONSTANT, value=250)
        anh = np.full((480, 640), 185, np.float32)                  # carton
        h, w = ma.shape
        M = cv2.getRotationMatrix2D((w / 2, h / 2), g, 1.0)
        M[:, 2] += (320 - w / 2 + rng.uniform(-40, 40), 240 - h / 2 + rng.uniform(-30, 30))
        nhan = cv2.warpAffine(ma.astype(np.float32), M, (640, 480), flags=cv2.INTER_LINEAR, borderValue=-1)
        anh = np.where(nhan >= 0, nhan, anh)
        if s > 0:
            anh = cv2.GaussianBlur(anh, (0, 0), s)
        anh = np.clip(anh + rng.normal(0, 4, anh.shape), 0, 255).astype(np.uint8)
        ten = f"qr_{i:02d}_g{g:03d}_s{s:.1f}.png"
        cv2.imwrite(f"bo_20_anh/{ten}", anh)
        ghi.writerow([ten, g, s, noi_dung])
print("Đã tạo", len(os.listdir("bo_20_anh")) - 1, "ảnh và bo_20_anh/dap_an.csv")
Đã tạo 20 ảnh và bo_20_anh/dap_an.csv

Khối đánh giá đọc lại từng ảnh, so với đáp án (đọc ra chuỗi khác đáp án được tính là đọc sai, nguy hiểm hơn không đọc được), in bảng theo góc × độ mờ và đo thời gian. Hàm doc_bac_thang được viết lại cho khối chạy độc lập.

import csv
import time

import cv2
import numpy as np

BO_DOC = (("QR", cv2.QRCodeDetector()), ("Aruco", cv2.QRCodeDetectorAruco()))


def bien_the(anh):
    yield "gốc", anh
    _, otsu = cv2.threshold(anh, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    yield "otsu", otsu
    sac = cv2.addWeighted(anh, 2.0, cv2.GaussianBlur(anh, (0, 0), 2), -1.0, 0)
    yield "sắc+otsu", cv2.threshold(sac, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
    yield "thích nghi", cv2.adaptiveThreshold(anh, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 31, 5)


def doc_don(anh):
    return BO_DOC[0][1].detectAndDecode(anh)[0] or None


def doc_bac_thang(anh):
    for _, x in bien_the(anh):
        for _, bo in BO_DOC:
            chuoi = bo.detectAndDecode(x)[0]
            if chuoi:
                return chuoi
    return None


with open("bo_20_anh/dap_an.csv", encoding="utf-8") as f:
    mau = list(csv.DictReader(f))

for ten, ham in (("QRCodeDetector đơn", doc_don), ("bậc thang", doc_bac_thang)):
    bang, dung, sai, thoi_gian = {}, 0, 0, []
    for m in mau:
        anh = cv2.imread("bo_20_anh/" + m["file"], cv2.IMREAD_GRAYSCALE)
        t0 = time.perf_counter()
        chuoi = ham(anh)
        thoi_gian.append((time.perf_counter() - t0) * 1000)
        ky_hieu = "." if chuoi is None else ("+" if chuoi == m["noi_dung"] else "X")
        dung += ky_hieu == "+"
        sai += ky_hieu == "X"
        bang[(int(m["goc"]), float(m["sigma"]))] = ky_hieu
    print(f"== {ten}: đọc đúng {dung}/20 = {dung / 20:.0%}, đọc sai {sai}, "
          f"thời gian trung bình {np.mean(thoi_gian):.1f} ms, lâu nhất {max(thoi_gian):.1f} ms")
    print("   góc \\ σ   " + "  ".join(f"{s:3.1f}" for s in (0.0, 1.0, 1.5, 2.0, 2.5)))
    for g in (0, 30, 60, 135):
        print(f"   {g:3d}°      " + "    ".join(bang[(g, s)] for s in (0.0, 1.0, 1.5, 2.0, 2.5)))
== QRCodeDetector đơn: đọc đúng 7/20 = 35%, đọc sai 0, thời gian trung bình 22.1 ms, lâu nhất 30.2 ms
   góc \ σ   0.0  1.0  1.5  2.0  2.5
     0°      +    .    .    .    .
    30°      +    +    .    .    .
    60°      +    +    .    .    .
   135°      +    +    .    .    .
== bậc thang: đọc đúng 16/20 = 80%, đọc sai 0, thời gian trung bình 69.0 ms, lâu nhất 181.9 ms
   góc \ σ   0.0  1.0  1.5  2.0  2.5
     0°      +    +    +    +    .
    30°      +    +    +    +    .
    60°      +    +    +    +    .
   135°      +    +    +    +    .

Thời gian phụ thuộc máy (con số trên chỉ để tham khảo), nhưng xu hướng thì ổn định: góc xoay gần như không ảnh hưởng, độ mờ quyết định tất cả. Bộ đọc đơn chỉ chắc chắn với ảnh nét, chập chờn từ σ = 1; bậc thang đọc hết mọi ảnh tới σ = 2 (với module 5 px), không đọc sai ảnh nào, và tốn thời gian chủ yếu ở các ảnh thất bại, vì phải thử hết mọi bậc. Ở σ = 2,5 (một nửa module) thông tin đã mất, không bậc nào cứu được: giải pháp là chỉnh nét, giảm thời gian phơi sáng hoặc tăng độ phân giải, không phải thuật toán.

Giới hạn thời gian cho bậc thangẢnh không có mã (sản phẩm bị lật, nhãn rơi) sẽ đi hết bậc thang, là trường hợp chậm nhất. Hãy đo thời gian của trường hợp xấu nhất này và so với ngân sách cycle time; nếu vượt, cắt bớt bậc hoặc dừng khi hết thời gian và báo "không đọc được".

10. Đưa vào dây chuyền

  • Trigger: cảm biến quang báo thùng tới, camera chụp đúng lúc mã nằm giữa khung hình. Băng tải nhanh cần phơi sáng ngắn (vài chục đến vài trăm µs) và đèn đủ mạnh, có thể chớp (strobe).
  • ROI: nếu vị trí nhãn tương đối cố định, chỉ đọc trong vùng quanh nhãn. Ảnh nhỏ hơn thì đọc nhanh hơn và ít bị nhiễu bởi chữ, hình in khác trên thùng.
  • Không đọc được = NG: thùng không đọc được mã phải bị đẩy ra (reject) hoặc dừng băng tải để người xử lý, không được cho qua. Lưu ảnh mọi trường hợp NG để phân tích.
  • Chất lượng in: ISO/IEC 15415 chấm điểm mã 2D in trên nhãn (tương phản, độ đều module, sai lệch lưới, hỏng mẫu định vị, mức dùng sửa lỗi), xếp hạng A–F hoặc 4–0; ISO/IEC TR 29158 cho mã khắc trực tiếp (DPM). Kết quả chính thức cần máy kiểm định (verifier) có hiệu chuẩn. Trạm tự viết vẫn nên theo dõi các chỉ số gần đúng: px/module, tương phản trong vùng mã, bậc đã dùng.
  • Chiếu sáng mã DPM: mã khắc laser trên kim loại phẳng, bóng thường dùng đèn đồng trục (coaxial) hoặc đèn vòm; mã khắc chấm (dot peen) thường dùng đèn góc thấp (dark field) để các chấm lõm sáng lên trên nền tối.
  • Truy xuất: gửi nội dung mã, kết quả OK/NG, thời điểm và đường dẫn ảnh lên MES hoặc cơ sở dữ liệu.

11. Lỗi thường gặp

LỗiTriệu chứngCách sửa
Module quá nhỏ (dưới 3 px)Đọc chập chờn, phụ thuộc vị trí mãThu hẹp FOV hoặc camera độ phân giải cao hơn; tính trước px/module
Phơi sáng dài trên băng tảiĐứng yên đọc tốt, chạy thì hỏngGiảm thời gian phơi sáng, tăng đèn hoặc gain
Nhãn bóng, màng coMột vệt sáng trắng che mất moduleNghiêng camera 10–15°, đèn khuếch tán, kính lọc phân cực
So chuỗi mà quên ký tự GS hoặc khoảng trắngMã đọc đúng nhưng bị báo NGIn bằng repr, tách trường GS1 thay vì so cả chuỗi
Chỉ kiểm tra "đọc được"Thùng sai lô, sai hạn dùng vẫn quaKiểm tra nội dung với lệnh sản xuất, chống trùng seri
Gọi libdmtx không có timeout trên ảnh lớnThỉnh thoảng một chu kỳ kéo dài hàng giâyĐặt timeout, max_count, cắt ROI

Bài tập

Bài 1. Mở rộng bộ thử ở mục 9 thành 40 ảnh: thêm độ nghiêng 0° và 55° (dùng hàm chup ở mục 7 hoặc tự viết phép biến đổi phối cảnh). Dự đoán trước: tỉ lệ đọc ở hàng nghiêng 55° sẽ thấp hơn hay bằng hàng 0°? Vì sao độ nghiêng làm giảm khả năng chịu mờ?

Xem đáp án

Thấp hơn. Nghiêng 55° làm mã co theo chiều nghiêng còn khoảng cos 55° ≈ 0,57 lần, và cạnh xa camera co thêm do phối cảnh. Module 5 px theo chiều đó chỉ còn khoảng 2,5–3 px, nên theo bảng ở mục 7 (σ phải nhỏ hơn khoảng 0,3 module) mức mờ chịu được giảm xuống dưới σ = 1. Thử với hàm chup của mục 7: ở nghiêng 0°, cả hai bộ đọc chịu được σ = 1,5; ở nghiêng 55°, cả hai đã hỏng ngay từ σ = 1. Bài học: kích thước module phải tính theo chiều nhỏ nhất của mã trên ảnh, sau khi đã tính nghiêng.

Bài 2. Viết hàm gs1_de_doc(t) nhận dict từ tach_gs1 và trả về chuỗi dạng (01)...(17)...(10)...(21).... Sau đó kiểm tra: GTIN-13 trên mã vạch EAN của sản phẩm là 893123456789?; chữ số cuối phải là bao nhiêu? GTIN-14 tương ứng (thêm số 0 ở đầu) có cùng chữ số kiểm tra không?

Xem đáp án
def chu_so_kiem_tra_gs1(than):
    tong = sum(int(c) * (3 if i % 2 == 0 else 1) for i, c in enumerate(reversed(than)))
    return str((10 - tong % 10) % 10)


def gs1_de_doc(t):
    return "".join(f"({ai}){gia_tri}" for ai, gia_tri in t.items())


print(gs1_de_doc({"01": "08931234567897", "17": "271231", "10": "L2610A", "21": "VN000123"}))
print("EAN-13 :", "893123456789" + chu_so_kiem_tra_gs1("893123456789"))
print("GTIN-14:", "0893123456789" + chu_so_kiem_tra_gs1("0893123456789"))
(01)08931234567897(17)271231(10)L2610A(21)VN000123
EAN-13 : 8931234567897
GTIN-14: 08931234567897

Chữ số kiểm tra là 7 cho cả hai: trọng số tính từ phải sang, nên thêm số 0 ở đầu không làm thay đổi tổng. Vì thế GTIN-13 đổi sang GTIN-14 chỉ cần thêm 0 ở đầu.

Bài 3. Dây chuyền dược phẩm chạy 300 hộp/phút, mỗi hộp có một DataMatrix 14×14 module, mỗi module 0,3 mm, hộp có thể lệch ±10 mm so với tâm camera theo mỗi chiều. Đề xuất FOV tối thiểu, độ phân giải camera, thời gian phơi sáng tối đa nếu băng tải chạy 0,4 m/s, và ngân sách thời gian cho việc đọc.

Xem đáp án
  • Mã rộng 14 × 0,3 = 4,2 mm; cộng vùng yên tĩnh và lệch ±10 mm, FOV khoảng 30 × 30 mm là đủ (có dư).
  • Muốn 4 px/module thì cần 4 / 0,3 ≈ 13,3 px/mm; FOV 30 mm cần khoảng 400 px. Camera 640×480 (VGA) đã đủ; dùng 1280×1024 để dư (khoảng 6–8 px/module với FOV 30–40 mm), chịu được mờ và nghiêng tốt hơn.
  • Độ mờ chuyển động nên dưới khoảng 0,3 module = 0,09 mm: thời gian phơi sáng ≤ 0,09 mm / 400 mm/s ≈ 225 µs. Chọn khoảng 100 µs và đèn chớp đủ sáng.
  • 300 hộp/phút = 200 ms mỗi hộp. Trừ thời gian chụp, truyền ảnh, gửi kết quả cho PLC, ngân sách đọc mã khoảng 50–100 ms, kể cả trường hợp xấu nhất của bậc thang và timeout của libdmtx (đặt khoảng 50 ms).

Tóm tắt

  • QR có 3 finder pattern và 4 mức sửa lỗi; DataMatrix có viền chữ L và nhỏ gọn, phổ biến cho linh kiện và mã khắc trực tiếp. Mọi quy tắc tính theo module và vùng yên tĩnh.
  • OpenCV 5 có QRCodeDetector, QRCodeDetectorAruco (cùng detectAndDecode, detectAndDecodeMulti) và barcode.BarcodeDetector, nhưng không có bộ đọc DataMatrix; dùng pylibdmtx với timeout, max_count và nhớ gốc tọa độ dưới-trái.
  • Đọc được chưa đủ: tách trường GS1, kiểm tra chữ số kiểm tra, ngày hợp lệ, khớp lệnh sản xuất, chống trùng ở bước cuối.
  • Đo được: xoay không ảnh hưởng, nghiêng tới khoảng 50°, module ≥ 4 px, độ mờ σ dưới khoảng 0,3 module.
  • Đọc bậc thang tăng tỉ lệ đọc mà không làm chậm ảnh tốt; theo dõi bậc đã dùng để phát hiện sớm suy giảm quang học.

Bài tiếp theo: OCR và OCV trong công nghiệp, khi thông tin không nằm trong mã 2D mà được in thành chữ.

Bài tập thực hành

  1. Tạo 20 ảnh mã QR với góc xoay và độ mờ khác nhau, đo tỉ lệ đọc thành công.

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.

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í.