⭐ Vision Engineer · Chặng 12Bài 4/8Bài học~45 phút

Phân đoạn ảnh: semantic, instance và panoptic khác nhau thế nào

Ba loại phân đoạn qua mảng NumPy, vì sao hộp bao sai khi đo vết lỗi, IoU và Dice trên mặt nạ, cách lưu mặt nạ và chọn mô hình cho trạm kiểm tra.

Phân đoạn ảnh: semantic, instance và panoptic khác nhau thế nào

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

  • Phân biệt ba loại phân đoạn
  • Biết khi nào cần phân đoạn thay vì phát hiện
  • Biết các mô hình phổ biến

Nên học trước: Từ R-CNN đến Faster R-CNN

Nội dung bài viết
  1. 1. Ba loại phân đoạn qua một ảnh mẫu
  2. 2. Khi nào hộp bao không đủ
  3. 3. Semantic + tách vùng liên thông có thay được instance?
  4. 4. Đánh giá mặt nạ: IoU và Dice
  5. 5. Lưu mặt nạ: ảnh nhị phân, đa giác, RLE
  6. 6. Chọn loại bài toán cho trạm kiểm tra
  7. 7. Các mô hình phổ biến
  8. 8. Sai lầm thường gặp
  9. 9. Bài tập
  10. Tóm tắt

Hai bài trước dạy bạn phát hiện vật bằng hộp bao và đánh giá bằng IoU, mAP. Hộp bao đủ để đếm vít hay khoanh vị trí vết móp, nhưng không đủ khi tiêu chuẩn chất lượng viết "vết xước dài quá 2 mm thì loại" hay "tổng diện tích vết dầu không quá 3 mm²": một vết xước chéo nằm trong hộp bao mà phần lớn diện tích hộp là bề mặt tốt. Phân đoạn ảnh (image segmentation) trả lời câu hỏi chính xác hơn: từng pixel thuộc về cái gì. Bài này phân biệt ba loại phân đoạn (semantic, instance, panoptic) bằng chính các mảng NumPy mà mỗi loại tạo ra, cho thấy bằng số khi nào phải phân đoạn thay vì phát hiện, cách đánh giá mặt nạ (IoU, Dice) và các mô hình phổ biến. Đây là bài nền cho hai bài thực hành tiếp theo: "Phân đoạn vết lỗi với YOLO26-seg" (instance) và "U-Net cho phân đoạn ảnh" (semantic).

Mục tiêu cuối bàiNêu được đầu ra của semantic, instance, panoptic segmentation và dựng được cả ba từ một ảnh mẫu; chỉ ra bằng số vì sao hộp bao sai khi đo diện tích, chiều dài vết lỗi; biết khi nào semantic + tách vùng liên thông là đủ và khi nào bắt buộc instance; tính IoU, Dice trên mặt nạ; biết các cách lưu mặt nạ (ảnh nhị phân, đa giác, RLE); chọn được loại phân đoạn và mô hình phù hợp cho một bài toán nhà máy.

1. Ba loại phân đoạn qua một ảnh mẫu

Ảnh mẫu: tấm kim loại 300×200 px có ba vết xước (hai vết cắt chéo nhau), một vết dầu, và một nhãn dán ở góc. Hệ số quy đổi đã hiệu chuẩn (bài "Quy đổi pixel sang mm và đánh giá sai số" ở chặng 8) là 0,05 mm/px. Ta vẽ từng vật riêng, giống như người gán nhãn vẽ từng đa giác, rồi dựng ba loại mặt nạ.

import cv2
import numpy as np

H, W, MM_PX = 200, 300, 0.05
LOP = {0: "nen", 1: "xuoc", 2: "dau", 3: "nhan_dan"}


def ve(fn):
    m = np.zeros((H, W), np.uint8)
    fn(m)
    return m > 0


# từng đối tượng: (lớp, mặt nạ riêng)
doi_tuong = [
    (1, ve(lambda m: cv2.line(m, (40, 40), (140, 120), 1, 3))),      # xước A
    (1, ve(lambda m: cv2.line(m, (60, 130), (160, 50), 1, 3))),      # xước B, cắt chéo A
    (1, ve(lambda m: cv2.line(m, (200, 150), (270, 160), 1, 3))),    # xước C
    (2, ve(lambda m: cv2.ellipse(m, (230, 60), (25, 15), 20, 0, 360, 1, -1))),   # vết dầu
]
nhan_dan = ve(lambda m: cv2.rectangle(m, (0, 165), (80, 199), 1, -1))  # vùng "stuff", không đếm

# 1) SEMANTIC: một số nguyên lớp cho mỗi pixel
sem = np.zeros((H, W), np.uint8)
sem[nhan_dan] = 3
for lop, m in doi_tuong:
    sem[m] = lop

# 2) INSTANCE: danh sách (lớp, mặt nạ nhị phân, điểm tin cậy nếu là dự đoán), chỉ cho vật đếm được
inst = [(lop, m) for lop, m in doi_tuong]

# 3) PANOPTIC: mỗi pixel một cặp (lớp, id); stuff có id 0, mỗi vật một id riêng, không chồng nhau
pan_lop, pan_id = sem.copy(), np.zeros((H, W), np.int32)
for k, (lop, m) in enumerate(doi_tuong, 1):
    pan_id[m & (pan_id == 0)] = k                                  # pixel giao nhau thuộc vật vẽ trước

print("semantic : mảng", sem.shape, sem.dtype, "giá trị", {LOP[v]: int((sem == v).sum()) for v in np.unique(sem)})
print("instance :", len(inst), "mặt nạ:", [(LOP[l], int(m.sum())) for l, m in inst])
print("panoptic : cặp (lớp, id) xuất hiện:",
      sorted({(LOP[int(a)], int(b)) for a, b in zip(pan_lop.ravel(), pan_id.ravel())}))
print("pixel thuộc 2 vết xước cùng lúc:", int((inst[0][1] & inst[1][1]).sum()))

MAU = np.array([(60, 60, 60), (0, 0, 255), (0, 200, 255), (200, 120, 0)], np.uint8)
mau_inst = np.zeros((H, W, 3), np.uint8)
for k, (_, m) in enumerate(inst):
    mau_inst[m] = [(0, 0, 255), (0, 255, 0), (255, 0, 255), (0, 200, 255)][k]
cv2.imwrite("ba_loai_phan_doan.png", cv2.hconcat([MAU[sem], mau_inst, MAU[pan_lop] // 2 + mau_inst // 2]))
print("đã lưu ba_loai_phan_doan.png: semantic | instance | panoptic")
semantic : mảng (200, 300) uint8 giá trị {'nen': 54240, 'xuoc': 1694, 'dau': 1231, 'nhan_dan': 2835}
instance : 4 mặt nạ: [('xuoc', 675), ('xuoc', 685), ('xuoc', 363), ('dau', 1231)]
panoptic : cặp (lớp, id) xuất hiện: [('dau', 4), ('nen', 0), ('nhan_dan', 0), ('xuoc', 1), ('xuoc', 2), ('xuoc', 3)]
pixel thuộc 2 vết xước cùng lúc: 29
đã lưu ba_loai_phan_doan.png: semantic | instance | panoptic
LoạiĐầu raTrả lời câu hỏiKhông trả lời được
SemanticMột ảnh nhãn H×W, mỗi pixel một lớpPixel nào là xước, pixel nào là dầu; tổng diện tích mỗi loại lỗiCó mấy vết xước, mỗi vết dài bao nhiêu (các vết chạm nhau dính thành một vùng)
InstanceDanh sách đối tượng, mỗi cái một mặt nạ nhị phân + lớp (+ điểm tin cậy)Có 3 vết xước, vết A dài x mm, vết B dài y mmVùng nền, vùng không đếm được (thường bỏ qua)
PanopticMỗi pixel một cặp (lớp, id): "stuff" (nền, nhãn dán) có lớp, "thing" (vật đếm được) có thêm idCả hai: phân vùng toàn ảnh và từng vật, không pixel nào bị bỏ hoặc thuộc hai vậtChi phí gán nhãn, mô hình nặng hơn; ít khi cần trong kiểm tra lỗi

Thuật ngữ "stuff" (vùng không đếm được: nền, trời, đường, lớp sơn) và "thing" (vật đếm được: người, xe, vết xước) đến từ các bộ dữ liệu như COCO. Trong nhà máy, gần như mọi thứ bạn quan tâm là "thing" (vết lỗi, linh kiện), còn "stuff" chỉ là bề mặt sản phẩm. Vì vậy hai loại dùng nhiều nhất là semantic và instance; panoptic xuất hiện chủ yếu ở xe tự hành, robot di động, phân tích cảnh.

2. Khi nào hộp bao không đủ

Đo trên chính ảnh mẫu: diện tích hộp bao so với diện tích thật của từng vết, và chiều dài đo từ hộp so với chiều dài đo từ mặt nạ.

import cv2
import numpy as np

print("vật    | hộp bao mm² | mặt nạ mm² | hộp/mặt nạ | dài theo cạnh hộp | dài theo mặt nạ")
for k, (lop, m) in enumerate(inst):
    ys, xs = np.nonzero(m)
    w, h = xs.max() - xs.min() + 1, ys.max() - ys.min() + 1
    dt_hop, dt_mn = w * h * MM_PX ** 2, m.sum() * MM_PX ** 2
    cnt = cv2.findContours(m.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0][0]
    dai = max(cv2.minAreaRect(cnt)[1]) * MM_PX                  # cạnh dài của hình chữ nhật xoay
    print(f"{'ABCD'[k]} {LOP[lop]:4s} | {dt_hop:11.2f} | {dt_mn:10.2f} | {dt_hop / dt_mn:10.1f} | "
          f"{max(w, h) * MM_PX:17.2f} | {dai:15.2f}")
vật    | hộp bao mm² | mặt nạ mm² | hộp/mặt nạ | dài theo cạnh hộp | dài theo mặt nạ
A xuoc |       22.31 |       1.69 |       13.2 |              5.25 |            6.56
B xuoc |       22.31 |       1.71 |       13.0 |              5.25 |            6.56
C xuoc |        2.81 |       0.91 |        3.1 |              3.75 |            3.73
D dau  |        4.04 |       3.08 |        1.3 |              2.45 |            2.50

Với vết xước chéo A, hộp bao lớn gấp khoảng 13 lần diện tích thật, và "chiều dài" đọc từ cạnh hộp (5,25 mm) thấp hơn chiều dài thật theo đường chéo (khoảng 6,5 mm). Một tiêu chí "dài quá 6 mm thì loại" sẽ cho kết quả sai nếu đo từ hộp. Với vết C gần nằm ngang, hai cách gần giống nhau; với vết dầu tròn trịa, tỉ lệ hộp/mặt nạ chỉ khoảng 1,3. Quy tắc thực tế: vật dài, mảnh, nghiêng, hình dạng bất thường cần mặt nạ; vật gọn, hình dạng ổn định thì hộp bao thường đủ.

3. Semantic + tách vùng liên thông có thay được instance?

Mô hình semantic (như U-Net) rẻ hơn khi gán nhãn và huấn luyện. Từ mặt nạ semantic, ta có thể tách từng vết bằng vùng liên thông (connected components, đã học ở chặng xử lý ảnh). Cách này đúng khi các vết không chạm nhau, và sai khi chúng chạm:

import cv2
import numpy as np

xuoc = (sem == 1).astype(np.uint8)
n, nhan, thong_ke, _ = cv2.connectedComponentsWithStats(xuoc, connectivity=8)
print(f"semantic + vùng liên thông: {n - 1} vết xước")
for k in range(1, n):
    print(f"  vùng {k}: {thong_ke[k, cv2.CC_STAT_AREA] * MM_PX ** 2:.2f} mm²")
print(f"instance (đúng)          : {sum(l == 1 for l, _ in inst)} vết xước")
for k, (l, m) in enumerate(inst):
    if l == 1:
        print(f"  vết {'ABC'[k]}: {m.sum() * MM_PX ** 2:.2f} mm²")
semantic + vùng liên thông: 2 vết xước
  vùng 1: 3.33 mm²
  vùng 2: 0.91 mm²
instance (đúng)          : 3 vết xước
  vết A: 1.69 mm²
  vết B: 1.71 mm²
  vết C: 0.91 mm²

Hai vết A và B cắt nhau nên semantic + vùng liên thông thấy một "vết" hình chữ X có diện tích gần gấp đôi. Nếu tiêu chí là "không quá 2 vết xước" thì kết quả đếm sai (thấy 2 thay vì 3), nếu tiêu chí là diện tích từng vết thì vết "X" bị tính quá lớn. Kết luận:

  • Tiêu chí theo tổng (tổng diện tích lỗi trên sản phẩm, có lỗi hay không) hoặc lỗi hiếm khi chạm nhau: semantic + vùng liên thông là đủ, gán nhãn đơn giản hơn (tô mặt nạ, không cần tách từng vật).
  • Tiêu chí theo từng vết (dài nhất, số lượng) và vết có thể chạm, chồng nhau: instance.
  • Hai vết chồng nhau thì instance mask cho phép một pixel thuộc cả hai vết (ở đây có pixel giao), panoptic thì bắt buộc mỗi pixel thuộc đúng một vật.

4. Đánh giá mặt nạ: IoU và Dice

IoU ở bài "IoU, mAP: đánh giá Object Detection" áp dụng nguyên vẹn cho mặt nạ, chỉ thay diện tích hộp bằng số pixel. Lĩnh vực phân đoạn (nhất là y tế) còn dùng hệ số Dice (còn gọi F1 trên pixel):

IoU  = |A ∩ B| / |A ∪ B|
Dice = 2·|A ∩ B| / (|A| + |B|)        quan hệ: Dice = 2·IoU / (1 + IoU)

Dice luôn lớn hơn hoặc bằng IoU, nên đừng so Dice của mô hình này với IoU của mô hình khác. Thử với dự đoán bị "phình" 1 pixel (biên dày thêm 1 px), sai số rất thường gặp:

import cv2
import numpy as np


def iou_dice(a, b):
    giao, hop = (a & b).sum(), (a | b).sum()
    return giao / hop, 2 * giao / (a.sum() + b.sum())


k3 = np.ones((3, 3), np.uint8)
for ten, m in (("xước A (mảnh)", inst[0][1]), ("vết dầu (to)", inst[3][1])):
    du_doan = cv2.dilate(m.astype(np.uint8), k3) > 0                # biên dày thêm 1 px
    i, d = iou_dice(m, du_doan)
    print(f"{ten:14s}: IoU {i:.2f} | Dice {d:.2f} | 2·IoU/(1+IoU) = {2 * i / (1 + i):.2f}")
xước A (mảnh) : IoU 0.64 | Dice 0.78 | 2·IoU/(1+IoU) = 0.78
vết dầu (to)  : IoU 0.88 | Dice 0.94 | 2·IoU/(1+IoU) = 0.94

Cùng sai 1 pixel ở biên: vết dầu to vẫn có IoU cao, vết xước rộng 3 px rơi xuống khoảng 0,64, dù về mặt sử dụng (đo chiều dài) dự đoán vẫn rất tốt. Với vật mảnh và nhỏ, IoU/Dice pixel thấp chưa chắc là mô hình tệ; hãy đánh giá thêm bằng đại lượng nhà máy quan tâm: sai số chiều dài, diện tích theo mm, và escape/overkill theo tiêu chí. Với instance segmentation, công cụ (như Ultralytics) còn báo mAP mặt nạ: giống mAP ở bài "IoU, mAP" nhưng IoU tính trên mặt nạ thay vì hộp.

5. Lưu mặt nạ: ảnh nhị phân, đa giác, RLE

Nhãn phân đoạn được lưu theo ba cách chính, và bạn sẽ phải chuyển qua lại giữa chúng:

  • Ảnh mặt nạ (PNG, mỗi pixel một mã lớp): dùng cho semantic (U-Net). Phải lưu PNG, không lưu JPEG: nén mất mát làm sai mã lớp ở biên.
  • Đa giác (danh sách đỉnh): gọn, dễ sửa trong công cụ gán nhãn; định dạng nhãn YOLO-seg dùng đa giác chuẩn hóa 0–1.
  • RLE (run-length encoding, mã hóa độ dài chuỗi): COCO dùng cho mặt nạ phức tạp, có lỗ.
import cv2
import numpy as np


def rle_ma_hoa(m):
    """RLE kiểu COCO (đơn giản): độ dài xen kẽ 0,1,0,1… theo thứ tự cột (Fortran)."""
    v = np.r_[0, m.ravel(order="F").astype(np.int8), 0]
    doi = np.flatnonzero(np.diff(v)) + 1
    dem = np.diff(np.r_[1, doi, len(v) - 1])
    return dem.tolist()


def rle_giai_ma(dem, shape):
    v = np.repeat(np.arange(len(dem)) % 2, dem).astype(bool)
    return v.reshape(shape, order="F")


m = inst[3][1]                                                     # vết dầu
cnt = cv2.findContours(m.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0][0]
da_giac = cv2.approxPolyDP(cnt, 0.7, True)[:, 0]                  # đơn giản hóa đa giác, sai lệch ≤ 0,7 px
ve_lai = cv2.fillPoly(np.zeros((H, W), np.uint8), [da_giac], 1) > 0
rle = rle_ma_hoa(m)
cv2.imwrite("mat_na_dau.png", m.astype(np.uint8) * 255)
print(f"ảnh mặt nạ PNG : {len(open('mat_na_dau.png', 'rb').read())} byte")
print(f"đa giác        : {len(da_giac)} đỉnh, IoU với mặt nạ gốc {iou_dice(m, ve_lai)[0]:.3f}")
print(f"RLE            : {len(rle)} số, giải mã khớp hoàn toàn: {np.array_equal(rle_giai_ma(rle, m.shape), m)}")
ảnh mặt nạ PNG : 562 byte
đa giác        : 17 đỉnh, IoU với mặt nạ gốc 0.982
RLE            : 99 số, giải mã khớp hoàn toàn: True

RLE giữ chính xác từng pixel; đa giác là xấp xỉ, sai lệch phụ thuộc số đỉnh (tham số của approxPolyDP). Bài "Phân đoạn vết lỗi với YOLO26-seg" chuyển mặt nạ sang nhãn đa giác YOLO và kiểm tra sai số này.

6. Chọn loại bài toán cho trạm kiểm tra

Câu hỏi của trạmBài toán phù hợp
Sản phẩm có lỗi hay không, không cần biết ở đâuPhân loại (chặng 10) hoặc anomaly detection (bài "Anomaly detection: học từ ảnh OK với PaDiM, PatchCore")
Đếm vật, kiểm tra đủ/thiếu, vị trí gần đúngPhát hiện đối tượng (hộp bao)
Tổng diện tích lỗi, tỉ lệ phủ keo, vùng thiếu sơnSemantic segmentation
Kích thước, số lượng từng vết lỗi theo tiêu chí; vết có thể chạm nhauInstance segmentation
Hiểu toàn cảnh: robot di động, phân vùng kho, xe tự hànhPanoptic segmentation
Vật tương phản rõ với nền, ánh sáng ổn địnhPhân ngưỡng + contour, watershed (không cần học sâu)

Hàng cuối không phải để cho có: nếu đèn backlight tạo được bóng vật đen tuyền trên nền trắng, phân ngưỡng và contour cho mặt nạ chính xác từng pixel, chạy vài mili giây, không cần dữ liệu huấn luyện. Học sâu cần thiết khi ranh giới vật phụ thuộc kết cấu, ngữ cảnh mà luật đơn giản không mô tả được (vết xước mờ trên kim loại chải xước, vết dầu trên nền vân), như bài "Rule-based hay Deep Learning?" ở chặng 11 đã phân tích.

7. Các mô hình phổ biến

Mô hìnhLoạiGhi chú thực tế
FCN (2015)SemanticMạng đầu tiên phân đoạn "toàn tích chập": thay lớp Linear của mạng phân loại bằng conv 1×1, phóng to bản đồ điểm số về kích thước ảnh và nối thêm đặc trưng tầng nông (FCN-32s, 16s, 8s). Mở đầu kiến trúc encoder–decoder.
U-Net (2015)SemanticEncoder–decoder với skip connection; hiệu quả với ít dữ liệu, dễ tự cài và tùy biến; rất phổ biến cho lỗi bề mặt. Bài "U-Net cho phân đoạn ảnh".
DeepLabv3+SemanticDùng tích chập giãn (atrous) để nhìn rộng; có sẵn trong torchvision (DeepLabv3) và các thư viện phân đoạn.
Mask R-CNN (2017)InstanceHai giai đoạn: phát hiện hộp rồi dự đoán mặt nạ trong hộp; chính xác, chậm hơn YOLO; có trong torchvision, Detectron2.
YOLO-seg (YOLO26-seg…)InstanceMột giai đoạn, thời gian thực, quy trình huấn luyện giống hệt YOLO phát hiện. Bài "Phân đoạn vết lỗi với YOLO26-seg".
Mask2Former và các mô hình "universal"Semantic, instance và panopticMột kiến trúc cho cả ba loại; mạnh nhưng nặng, thường dùng trong nghiên cứu hoặc khi có GPU mạnh.
SAM (Segment Anything) và các phiên bản sauPhân đoạn theo gợi ý (điểm, hộp)Mô hình nền tảng không cần huấn luyện cho từng bài; rất hữu ích để gán nhãn nhanh (bấm một điểm ra mặt nạ) trong các công cụ như CVAT, Label Studio; ít khi dùng trực tiếp làm mô hình kiểm tra lỗi vì nó không biết "lỗi" là gì.

Từ mạng phân loại sang mạng phân đoạn: phóng to bản đồ đặc trưng

Mạng phân loại thu nhỏ ảnh 32 lần; mạng phân đoạn phải trả về đúng kích thước ảnh, nên nửa sau của mạng (decoder) cần phóng to (upsample). Bốn cách hay gặp:

CáchÝ tưởngHọc được?PyTorch
Nearest neighborNhân bản mỗi điểm thành khối 2×2Khôngnn.Upsample(scale_factor=2, mode="nearest")
BilinearNội suy tuyến tính theo hai chiều, mượt hơnKhôngF.interpolate(x, scale_factor=2, mode="bilinear")
Max unpoolingNhớ vị trí max lúc pooling, trả giá trị về đúng chỗ (SegNet)Khôngnn.MaxUnpool2d
Transposed convolutionMỗi điểm vào nhân với kernel rồi "rải" ra vùng lớn hơn, cộng dồn chỗ chồng lấnCónn.ConvTranspose2d

Kích thước ra của transposed convolution: (N − 1)·S − 2P + K + output_padding. Hai cấu hình gấp đôi kích thước thường dùng là K = 2, S = 2, P = 0 (U-Net) và K = 4, S = 2, P = 1. Khi kernel không chia hết cho stride, transposed conv dễ tạo vân bàn cờ (checkerboard); cách tránh là phóng to bilinear rồi mới conv 3×3.

Nhóm DeepLab đi theo hướng ngược lại: bớt thu nhỏ ở các tầng cuối và dùng tích chập giãn để giữ tầm nhìn rộng, kèm khối ASPP (nhiều conv giãn với độ giãn 6, 12, 18 chạy song song) để nắm ngữ cảnh ở nhiều thang đo. Mask R-CNN thì thêm một nhánh FCN nhỏ dự đoán mặt nạ 28×28 cho từng vùng, và thay RoI Pooling bằng RoI Align (không làm tròn tọa độ) để mặt nạ khớp đúng vị trí; xem bài Từ R-CNN đến Faster R-CNN.

Đọc sâu hơn: Image Segmentation: FCN, U-Net, DeepLab, Mask R-CNN (có code U-Net, DeepLabV3 và Mask R-CNN của torchvision, ví dụ mIoU và Dice tính tay).

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

  • Chọn instance khi chỉ cần tổng diện tích: tốn công vẽ đa giác tách từng vết mà không dùng tới.
  • Chọn semantic khi tiêu chí theo từng vết và vết có thể chạm nhau: đếm sai, đo sai như mục 3.
  • Lưu mặt nạ semantic bằng JPEG: biên bị nhiễu thành các mã lớp lạ.
  • Đánh giá vết mảnh chỉ bằng IoU pixel: kết luận sai rằng mô hình tệ; hãy đo sai số theo mm.
  • Quy ước gán nhãn biên không thống nhất (người vẽ sát mép vết, người vẽ chừa viền): với vết rộng 3 px, đó là chênh lệch hơn 60% diện tích.

9. Bài tập

Bài 1. (Thực hành của bài) Trạm kiểm tra tấm nhựa ép: tiêu chuẩn của khách hàng là "không vết lõm nào có diện tích lớn hơn 0,5 mm²; tổng diện tích lõm không quá 2 mm²". Vết lõm đôi khi nằm sát nhau thành cụm. Chọn loại phân đoạn phù hợp và giải thích. Nếu tiêu chuẩn chỉ còn vế sau thì sao?

Xem đáp án

Instance segmentation. Vế "từng vết không quá 0,5 mm²" cần diện tích của từng vết; vì các vết có thể sát nhau, semantic + vùng liên thông sẽ gộp cụm thành một vùng lớn và báo NG sai (overkill), hoặc ngược lại nếu tách bằng xử lý hình thái thì dễ cắt một vết thành hai (escape). Instance cho mỗi vết một mặt nạ; diện tích = số pixel × (mm/px)², tổng diện tích = diện tích hợp các mặt nạ (không cộng trùng pixel giao). Nếu tiêu chuẩn chỉ còn "tổng diện tích không quá 2 mm²", semantic là đủ và rẻ hơn: gán nhãn bằng tô mặt nạ, mô hình U-Net, tổng diện tích = số pixel lớp "lõm" × (mm/px)². Trong cả hai trường hợp, cần đánh giá sai số diện tích theo mm² trên ảnh có nhãn, vì vết nhỏ (vài chục pixel) có sai số tương đối lớn ở biên.

Bài 2. Mặt nạ thật có 400 pixel, mặt nạ dự đoán có 360 pixel, phần giao 300 pixel. Tính IoU và Dice bằng tay, kiểm tra công thức Dice = 2·IoU/(1 + IoU).

Xem đáp án

Hợp = 400 + 360 − 300 = 460. IoU = 300/460 ≈ 0,652. Dice = 600/760 ≈ 0,789. Kiểm tra: 2 × 0,652 / 1,652 ≈ 0,789.

giao, a, b = 300, 400, 360
i, d = giao / (a + b - giao), 2 * giao / (a + b)
print(f"IoU {i:.3f} | Dice {d:.3f} | 2·IoU/(1+IoU) {2 * i / (1 + i):.3f}")
IoU 0.652 | Dice 0.789 | 2·IoU/(1+IoU) 0.789

Bài 3. Chọn bài toán (phân loại, phát hiện, semantic, instance, hoặc không cần học sâu) cho: (a) đếm số viên thuốc trên vỉ dưới đèn nền, viên trắng trên nền đen; (b) đo tỉ lệ phủ keo trên mép hộp giấy, keo trong suốt hơi bóng; (c) phát hiện và đo chiều dài từng vết nứt trên gạch men, vết nứt có thể phân nhánh và giao nhau.

Xem đáp án
  • (a) Không cần học sâu: đèn nền cho tương phản rất rõ, phân ngưỡng + đếm contour (hoặc watershed nếu viên chạm nhau) là đủ, nhanh và dễ nghiệm thu.
  • (b) Semantic: chỉ cần tỉ lệ diện tích "có keo" trên vùng mép, không cần tách từng vệt. Keo trong suốt có thể cần chiếu sáng đặc biệt (chặng 6) trước khi nghĩ tới mô hình.
  • (c) Instance (hoặc semantic rồi phân tích khung xương nếu quy tắc tách nhánh rõ ràng): cần chiều dài từng vết, mà vết có thể giao nhau. Chiều dài vết cong, phân nhánh nên đo bằng khung xương (skeleton) của mặt nạ, không phải cạnh hộp.

Tóm tắt

  • Semantic: mỗi pixel một lớp, không phân biệt vật cùng lớp. Instance: mỗi vật một mặt nạ + lớp. Panoptic: mỗi pixel một cặp (lớp, id), gồm cả "stuff" và "thing", không chồng nhau.
  • Cần phân đoạn thay vì hộp bao khi đo diện tích, chiều dài, hình dạng; với vết xước chéo, hộp bao sai diện tích hơn 10 lần và sai cả chiều dài.
  • Semantic + vùng liên thông thay được instance chỉ khi các vật không chạm nhau hoặc tiêu chí theo tổng.
  • Đánh giá mặt nạ bằng IoU, Dice (Dice = 2·IoU/(1+IoU)), mAP mặt nạ; với vật mảnh, đo thêm sai số theo mm.
  • Mặt nạ lưu dạng PNG, đa giác hoặc RLE; mô hình phổ biến: U-Net, DeepLabv3+ (semantic), Mask R-CNN, YOLO-seg (instance), Mask2Former (cả ba), SAM (gán nhãn nhanh).

Bài tiếp theo: Phân đoạn vết lỗi với YOLO26-seg, gán nhãn đa giác, huấn luyện instance segmentation và đo từng vết lỗi theo mm.

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

  1. Với bài toán đo diện tích vết lỗi, chọn loại phân đoạn phù hợp 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.

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