Instance và Universal Segmentation: Mask R-CNN, Mask2Former, SAM
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Phân biệt được ba bài toán: semantic segmentation, instance segmentation (phân vùng từng đối tượng) và panoptic segmentation (phân vùng toàn cảnh). Tính được độ đo PQ (panoptic quality) và tách nó thành hai phần SQ, RQ.
- Giải thích được Mask R-CNN: phát hiện hộp trước, rồi vẽ mask trong từng hộp; và vì sao RoIAlign thay RoIPool.
- Hiểu hướng phân loại mask (mask classification) bằng truy vấn: DETR, MaskFormer, rồi Mask2Former với masked attention, một kiến trúc cho cả ba bài toán.
- Dùng được SAM với điểm gợi ý (point prompt), và giải thích vì sao SAM trả về ba mask cho một điểm.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Semantic Segmentation: FCN, UNet, FPN, DeepLab, HRNet, thuộc phần Thị giác máy tính (Đã học)
Trực giác
Phần tiêu đề “Trực giác”Hai người đứng sát nhau
Phần tiêu đề “Hai người đứng sát nhau”Bạn có một ảnh hai người đứng vai kề vai. Một mô hình semantic segmentation tốt tô đúng mọi pixel người bằng cùng một màu. IoU của lớp “người” gần bằng 1.
Giờ hỏi: trong ảnh có mấy người? Mô hình không trả lời được. Nó chỉ biết “pixel này là người”, không biết “pixel này thuộc người nào”. Với xe tự lái đếm người qua đường, robot nhặt từng quả táo, hay phần mềm đếm tế bào, câu hỏi “người nào, quả nào” mới là thứ cần.
Cách đơn giản nhất: lấy vùng “người”, tách thành các mảng liền nhau, mỗi mảng là một người. Cách này đúng khi các vật nằm rời nhau. Nhưng hai người chạm vai thành một mảng liền, nên bị đếm là một. Ô NumPy đầu tiên của bài cho thấy đúng chỗ hỏng này.
Hai hướng giải
Phần tiêu đề “Hai hướng giải”Hướng thứ nhất: tìm vật trước, rồi tô từng vật. Một bộ phát hiện vật thể (object detector) vẽ một hộp bao (bounding box) quanh mỗi người, rồi trong mỗi hộp ta tô pixel nào là người đó. Hai người đứng sát vẫn có hai hộp, nên vẫn ra hai mask. Đây là Mask R-CNN.
Hướng thứ hai: cho mô hình một số “khe” cố định, ví dụ 100 khe, và yêu cầu mỗi khe trả về một mask kèm một nhãn lớp (hoặc “không có gì”). Mỗi khe tự “nhận” một vật. Cách này không cần hộp, và cùng một mô hình làm được cả semantic lẫn instance. Đây là MaskFormer và Mask2Former.
Hướng thứ ba khác hẳn: người dùng chỉ vào vật cần tách (bấm một điểm, vẽ một hộp), mô hình trả về mask của vật đó, kể cả loại vật chưa từng có trong dữ liệu huấn luyện. Đây là SAM.
Bài toán của bài này là: làm sao tách từng vật riêng lẻ, kể cả khi chúng chạm nhau, đo chất lượng việc tách đó thế nào, và ba hướng trên giải nó ra sao?
Lý thuyết
Phần tiêu đề “Lý thuyết”Ba bài toán, ba kiểu đáp án
Phần tiêu đề “Ba bài toán, ba kiểu đáp án”Kirillov và cộng sự (2019) chia lớp trong ảnh thành hai loại:
- Thing (vật đếm được): người, xe, quả táo. Mỗi cái có hình dạng riêng, đếm được.
- Stuff (vùng không đếm được): bầu trời, mặt đường, cỏ. Không ai hỏi “có mấy bầu trời”.
Từ đó có ba bài toán trên cùng một ảnh :
| Bài toán | Đầu ra | Thing | Stuff |
|---|---|---|---|
| Semantic segmentation | Mỗi pixel một lớp | Gộp mọi vật cùng lớp | Có |
| Instance segmentation | Một tập : mask, lớp, điểm tin cậy | Mỗi vật một mask | Không |
| Panoptic segmentation | Mỗi pixel một cặp (lớp, mã đối tượng) | Mỗi vật một mã riêng | Một vùng mỗi lớp |
Ở đây là mask nhị phân của vật thứ , là lớp, là điểm tin cậy. Mask của instance segmentation được phép chồng lên nhau (mô hình có thể trả về nhiều mask cho cùng một vùng, xếp theo điểm). Panoptic thì không: mỗi pixel thuộc đúng một đoạn (segment).
Đo bằng PQ
Phần tiêu đề “Đo bằng PQ”Với semantic segmentation, bài trước dùng mIoU. Nhưng mIoU không thấy lỗi gộp hai người: tô đúng mọi pixel người thì IoU lớp người vẫn bằng 1. Panoptic cần độ đo nhìn vào từng đoạn. Kirillov và cộng sự (2019) đề xuất PQ (panoptic quality), tính cho từng lớp rồi lấy trung bình.
Bước 1: ghép đoạn. Một đoạn dự đoán ghép với một đoạn thật khi . Vì sao 0.5? Các đoạn thật không chồng nhau, các đoạn dự đoán cũng vậy. Nếu chiếm hơn nửa phần hợp với , thì không còn chỗ cho một khác cũng chiếm hơn nửa phần hợp với . Bài báo chứng minh điều này: ngưỡng 0.5 cho ghép duy nhất, không cần thuật toán ghép đôi nào.
Bước 2: chia ba nhóm. Cặp ghép được là TP (true positive). Đoạn dự đoán không ghép với ai là FP. Đoạn thật không ai ghép là FN.
Bước 3: công thức.
Tử số cộng IoU của các cặp ghép được: ghép càng khít, tử số càng lớn. Mẫu số đếm số cặp, cộng thêm nửa điểm phạt cho mỗi đoạn thừa và mỗi đoạn sót.
Bước 4: tách làm hai. Nhân rồi chia cho , ta được hai thừa số dễ đọc:
Thừa số đầu, SQ (segmentation quality), là IoU trung bình của các cặp đã ghép: mask khít tới đâu. Thừa số sau, RQ (recognition quality), chính là điểm F1 quen thuộc của bài toán phát hiện: tìm đúng bao nhiêu vật.
Áp vào ví dụ của ô NumPy: ba đồng xu A, B, C, trong đó B và C chạm nhau. Cách “mảng liền nhau” cho hai đoạn: A khớp hoàn toàn (IoU 1), còn mảng B+C có IoU 0.58 với C và 0.42 với B. Vậy TP gồm (A, A) và (B+C, C), không có FP, còn B là FN:
Trong khi đó, mask semantic của lớp “xu” vẫn đúng từng pixel.
Nhắc nhanh về phát hiện vật thể
Phần tiêu đề “Nhắc nhanh về phát hiện vật thể”Bài về phát hiện vật thể chưa có, nên đây là ba ý bài này cần. Một bộ phát hiện trả về các hộp , mỗi hộp có lớp và điểm tin cậy. Độ trùng giữa hai hộp đo bằng IoU, như với mask, chỉ là tính trên hình chữ nhật: phần giao rộng và cao tính tương tự.
Bộ phát hiện thường trả về nhiều hộp chồng nhau cho cùng một vật. NMS (non-maximum suppression, triệt tiêu điểm không cực đại) dọn chúng: sắp hộp theo điểm giảm dần, giữ hộp cao nhất, bỏ mọi hộp còn lại có IoU với nó lớn hơn một ngưỡng, rồi lặp lại. Ngưỡng là con dao hai lưỡi: thấp quá thì xóa nhầm người thật đứng sát nhau, cao quá thì sót hộp trùng. Ô NumPy thứ hai cho thấy điều này.
Mask R-CNN: tìm hộp trước, tô mask sau
Phần tiêu đề “Mask R-CNN: tìm hộp trước, tô mask sau”Mask R-CNN (He và cộng sự, 2017) xây trên Faster R-CNN (Ren và cộng sự, 2015), một bộ phát hiện hai giai đoạn:
- Backbone (mạng xương sống, thường là ResNet kèm FPN) biến ảnh thành bản đồ đặc trưng.
- RPN (region proposal network, mạng đề xuất vùng) đề xuất vài trăm tới vài nghìn hộp có thể chứa vật.
- Với mỗi hộp đề xuất, gọi là RoI (region of interest, vùng quan tâm), lấy ra một bản đồ đặc trưng nhỏ cỡ cố định, ví dụ hoặc .
- Các đầu ra trên đặc trưng đó: đầu phân loại (lớp nào), đầu hồi quy hộp (chỉnh hộp cho khít), và phần Mask R-CNN thêm vào: đầu mask.
Đầu mask là một mạng tích chập nhỏ, cho ra mask cỡ (bài báo dùng ), mỗi lớp một mask. Mỗi pixel đi qua sigmoid và học bằng cross-entropy nhị phân. Chỉ mask của lớp đúng được tính vào mất mát; lúc suy luận, mask được chọn theo lớp mà đầu phân loại đoán. Tác giả thấy tách việc “là lớp gì” khỏi việc “pixel nào thuộc vật” quan trọng: dùng softmax cạnh tranh giữa các lớp ở từng pixel cho kết quả kém hơn rõ. Mất mát của mỗi RoI là tổng ba phần:
Mask sau đó được phóng về đúng cỡ hộp trên ảnh. Vì mỗi hộp có mask riêng, hai người đứng sát vẫn ra hai mask. Đổi lại, chất lượng phụ thuộc vào hộp: hộp sót thì vật mất, hộp bị NMS xóa nhầm thì vật cũng mất.
RoIAlign: đừng làm tròn
Phần tiêu đề “RoIAlign: đừng làm tròn”Bước 3 nghe đơn giản nhưng có bẫy. Backbone thu nhỏ ảnh, ví dụ stride 16: một ô đặc trưng ứng với pixel. Hộp có tọa độ số thực trên ảnh, nên trên bản đồ đặc trưng tọa độ thành .
RoIPool (cách cũ) làm tròn hai lần. Lần một: tọa độ hộp. Cạnh trái ở pixel 33 thành , làm tròn thành ô 2, tức pixel 32 trên ảnh. Lần hai: khi chia hộp thành lưới ô, ranh giới mỗi ô lại bị làm tròn. Mỗi lần làm tròn có thể lệch tới nửa ô, tức tới 8 pixel trên ảnh. Phân loại chịu được lệch nhỏ như vậy: con mèo dịch vài pixel vẫn là con mèo. Mask thì không: lệch 8 pixel là đường viền lệch hẳn.
RoIAlign bỏ mọi phép làm tròn. Giữ nguyên 2.06, chia hộp thành ô bằng số thực. Trong mỗi ô, lấy giá trị tại 4 điểm đặt đều, mỗi điểm tính bằng nội suy song tuyến tính (bilinear interpolation) từ 4 ô đặc trưng gần nhất, rồi gộp lại bằng max hoặc trung bình. Thay đổi nhỏ, hiệu quả lớn: trong bảng thí nghiệm của bài báo (ResNet-50-C4), AP của mask tăng từ 26.9 (RoIPool) lên 30.3 (RoIAlign).
Từ phân loại pixel sang phân loại mask
Phần tiêu đề “Từ phân loại pixel sang phân loại mask”DETR (Carion và cộng sự, 2020) làm phát hiện vật thể theo kiểu khác. Mô hình có truy vấn vật thể (object query), bài báo dùng : các vectơ học được, đi qua một decoder Transformer, nhìn sang đặc trưng ảnh bằng cross-attention. Mỗi truy vấn trả ra một hộp và một lớp, hoặc lớp đặc biệt (“không có vật”). Khi huấn luyện, mỗi vật thật được ghép đôi (bipartite matching) với đúng một truy vấn, theo cách ghép có tổng chi phí nhỏ nhất. Truy vấn thứ hai cũng đoán cùng vật đó sẽ không được ghép và bị phạt, nên mô hình tự học không đoán trùng. Kết quả: không cần NMS, không cần hộp neo.
MaskFormer (Cheng và cộng sự, 2021) đổi hộp thành mask. Cùng truy vấn, mỗi truy vấn cho ra một phân phối trên lớp (thêm ) và một mask embedding . Song song, một bộ giải mã pixel (pixel decoder) cho mỗi pixel một vectơ . Mask của truy vấn là:
Tích vô hướng lớn nghĩa là pixel giống “vật” mà truy vấn đang giữ; sigmoid đưa nó về khoảng . Đây là ý tưởng cốt lõi: bài toán nào cũng là “dự đoán một tập mask, mỗi mask một nhãn”. Semantic segmentation: mỗi lớp một mask. Instance: mỗi vật một mask. Panoptic: cả hai. Chỉ cách ghép kết quả lúc suy luận là khác. Bài báo tên là “Per-Pixel Classification is Not All You Need”, đúng ý: phân loại từng pixel không phải cách duy nhất, kể cả cho semantic segmentation.
Mask2Former: masked attention
Phần tiêu đề “Mask2Former: masked attention”MaskFormer học chậm và yếu với vật nhỏ. Mask2Former (Cheng và cộng sự, 2022) sửa bằng một ý: ở mỗi lớp decoder, truy vấn chỉ được nhìn vào vùng mask mà chính nó vừa đoán ở lớp trước.
Gọi là truy vấn sau lớp thứ . Cross-attention thường cho truy vấn nhìn mọi pixel. Masked attention thêm một ma trận vào trước softmax (bài báo viết công thức không có hệ số , ở đây giữ đúng như vậy):
lấy từ các truy vấn, và lấy từ đặc trưng ảnh. Phần tử của ở hàng , vị trí bằng 0 nếu mask lớp trước của truy vấn (cắt ngưỡng 0.5) chứa pixel đó, và bằng nếu không. Vì sao ? Vì : sau softmax, trọng số attention ở các pixel ngoài mask bằng đúng 0. Truy vấn đang giữ “con mèo” chỉ gom đặc trưng từ vùng con mèo, không bị phân tán khắp ảnh. là kết nối tắt quen thuộc.
Thêm vài cải tiến (đặc trưng nhiều độ phân giải luân phiên giữa các lớp decoder, tính mất mát mask trên một số điểm lấy mẫu thay vì cả mask để tiết kiệm bộ nhớ), Mask2Former đạt 57.8 PQ trên COCO panoptic, 50.1 AP trên COCO instance và 57.7 mIoU trên ADE20K semantic (bài báo, 2022, backbone Swin-L), cùng một kiến trúc. Với backbone ResNet-50, nó đạt 51.9 PQ sau 50 epoch, trong khi MaskFormer cần 300 epoch để đạt 46.5.
“Universal” ở đây là một kiến trúc, không phải một bộ trọng số: chính bài báo ghi rằng mô hình vẫn phải huấn luyện riêng cho từng bài toán, và bản chỉ học panoptic hơi kém bản học riêng instance hay semantic.
SAM: phân vùng theo gợi ý
Phần tiêu đề “SAM: phân vùng theo gợi ý”Mask R-CNN và Mask2Former chỉ biết các lớp có trong dữ liệu huấn luyện (COCO có 80 lớp thing). SAM (Kirillov và cộng sự, 2023) đặt bài toán khác: phân vùng theo gợi ý (promptable segmentation). Đầu vào là ảnh cộng một prompt: một hoặc vài điểm, một hộp, hay một mask thô. Đầu ra là mask hợp lệ cho thứ được chỉ vào, bất kể đó là gì. SAM không trả về nhãn lớp.
SAM có ba phần, thiết kế xoay quanh tốc độ:
- Bộ mã hóa ảnh (image encoder): một ViT lớn (bản lớn nhất là ViT-H), pre-train bằng MAE, nhận ảnh và cho ra embedding với 256 kênh. Phần này nặng, nhưng chỉ chạy một lần cho mỗi ảnh.
- Bộ mã hóa prompt (prompt encoder): đổi điểm và hộp thành vectơ (mã hóa vị trí cộng embedding học được).
- Bộ giải mã mask (mask decoder): vài khối Transformer nhẹ, attention hai chiều giữa token prompt và embedding ảnh. Theo bài báo, khi đã có embedding ảnh, bộ mã hóa prompt và bộ giải mã chạy khoảng 50 ms trên CPU trong trình duyệt.
Một điểm gợi ý thường mơ hồ: bấm vào cái áo thì ý bạn là cái áo, hay cả người? Nếu chỉ có một đầu ra, mô hình sẽ đoán một mask lưng chừng, trung bình của các đáp án. SAM trả về 3 mask cho một điểm (bài báo: các mask lồng nhau thường sâu tối đa ba tầng, nguyên vật, phần, phần nhỏ), kèm một điểm IoU dự đoán (iou_scores) cho mỗi mask, để xếp hạng. Khi huấn luyện, chỉ mask khớp nhất với đáp án được lan truyền ngược.
Dữ liệu là phần lớn của câu chuyện. Mask theo pixel đắt, nên nhóm tác giả dựng một động cơ dữ liệu (data engine) ba giai đoạn: người gán nhãn dùng SAM để gán nhanh, rồi SAM tự đề xuất mask, cuối cùng SAM tự gán toàn bộ. Kết quả là SA-1B: 11 triệu ảnh, 1.1 tỉ mask.
SAM 2 (Ravi và cộng sự, 2024) mở rộng sang video: thêm bộ nhớ (memory bank) lưu đặc trưng và dự đoán của các khung hình trước, và một khối memory attention cho khung hiện tại nhìn vào bộ nhớ đó. Bạn bấm vào vật ở một khung, mô hình theo vật đó qua cả video. Bài báo xây bộ SA-V (35.5 triệu mask trên 50.9 nghìn video) và báo cáo trên ảnh SAM 2 chính xác hơn và nhanh hơn 6 lần so với SAM.
Giới hạn
Phần tiêu đề “Giới hạn”- Mask R-CNN phụ thuộc hộp và NMS. Cảnh đông, vật chồng nhiều lên nhau dễ bị xóa nhầm (ô NMS của bài, câu hỏi thứ tư).
- Mask2Former là một kiến trúc, không phải một mô hình cho mọi thứ. Vẫn huấn luyện riêng từng bài toán, và chỉ biết các lớp đã học.
- SAM không biết vật là gì. Muốn có nhãn lớp, phải ghép với một mô hình khác, ví dụ một bộ phát hiện theo câu chữ như GLIP ở bài CLIP, BLIP, GLIP, hoặc một bộ phân loại.
- Bài không đi sâu vào phát hiện vật thể (các họ R-CNN, YOLO, hộp neo); chỉ nhắc phần Mask R-CNN cần.
Minh họa
Phần tiêu đề “Minh họa”Hình dưới đặt ba đáp án của cùng một cảnh cạnh nhau. Hãy nhìn hai người đứng sát: ở ô semantic họ cùng một màu, ở ô instance và panoptic họ hai màu khác nhau; còn bầu trời và mặt đường thì biến mất ở ô instance.
Bảng dưới so ba họ mô hình của bài theo cùng các câu hỏi. Số liệu lấy từ bài báo của từng mô hình.
| Tiêu chí | Mask R-CNN (2017) | Mask2Former (2022) | SAM (2023) |
|---|---|---|---|
| Ý chính | Tìm hộp, rồi tô mask trong mỗi hộp | truy vấn, mỗi truy vấn một mask và một lớp | Tách thứ được gợi ý (điểm, hộp, mask) |
| Bài toán | Instance (và phát hiện) | Semantic, instance, panoptic, cùng kiến trúc | Mask không nhãn, theo gợi ý |
| Có nhãn lớp không | Có, các lớp đã học | Có, các lớp đã học | Không |
| Bỏ trùng lặp bằng | NMS trên hộp | Ghép đôi khi huấn luyện, không NMS | Người dùng chọn vật; 3 mask kèm điểm IoU dự đoán |
| Điểm mạnh | Đơn giản, đầu mask rẻ, mở rộng sang điểm khớp người | Một thiết kế cho cả ba bài toán, masked attention hội tụ nhanh | Làm được với vật chưa từng thấy; gán nhãn nhanh |
| Cái giá | Phụ thuộc chất lượng hộp; mask chỉ | Vẫn huấn luyện riêng từng bài toán; tốn bộ nhớ | Bộ mã hóa ảnh nặng; không biết vật là gì |
Ví dụ code
Phần tiêu đề “Ví dụ code”Code trong bài chạy thử lần cuối tháng 11/2026: hai ô NumPy với Python 3.14, NumPy 2.4 và SciPy 1.18 (bản có trong Pyodide), ô SAM với Python 3.14, PyTorch 2.15 và transformers 5.22 trên CPU. Hai ô NumPy chạy ngay trên trang, ô SAM chạy trên máy bạn. Các ô độc lập, không dùng biến của nhau. Ô đầu tiên lần đầu chạy lâu hơn, vì trình duyệt phải tải thêm SciPy.
Từ mask semantic tới từng vật, và PQ
Phần tiêu đề “Từ mask semantic tới từng vật, và PQ”Ô dưới vẽ ba đồng xu cùng lớp trên lưới : xu A nằm riêng, xu B và xu C chạm nhau. Mask semantic là hợp của ba mask thật. Cách 1 tách bằng scipy.ndimage.label (mỗi mảng liền nhau theo 4 hướng là một vật). Cách 2 co mask một pixel (binary_erosion) để cắt chỗ chạm, đánh số các mảng còn lại làm “hạt giống”, rồi trả mỗi pixel của mask về hạt giống gần nhất. Mỗi cách in ma trận IoU giữa đoạn dự đoán (hàng) và vật thật (cột), rồi tính PQ như mục Lý thuyết.
Tách vật từ mask semantic, ghép theo IoU, tính PQ
import numpy as npfrom scipy import ndimage
# Ảnh 20 x 32 có ba đồng xu cùng một lớp; xu B và xu C chạm nhauyy, xx = np.indices((20, 34))def disk(cy, cx, r): return (yy - cy) ** 2 + (xx - cx) ** 2 <= r ** 2gt = [disk(6, 5, 4), disk(11, 15, 5), disk(11, 27, 6)] # 3 mask thậtsemantic = gt[0] | gt[1] | gt[2] # mask semantic: chỉ biết "là xu"
def iou(a, b): return (a & b).sum() / (a | b).sum()
def pq(pred): # Ghép dự đoán với vật thật khi IoU > 0.5, rồi tính PQ M = np.array([[iou(p, g) for g in gt] for p in pred]) tp = [M[i, j] for i in range(len(pred)) for j in range(3) if M[i, j] > 0.5] fp, fn = len(pred) - len(tp), 3 - len(tp) print("IoU (pred x true):") print(np.round(M, 2)) print(f"TP={len(tp)} FP={fp} FN={fn} " f"PQ={sum(tp) / (len(tp) + 0.5 * fp + 0.5 * fn):.3f}")
# Cách 1: mỗi vùng liền nhau là một vậtlabels, n = ndimage.label(semantic)print(f"components: {n}")pq([labels == k for k in range(1, n + 1)])
# Cách 2: co mask để tách chỗ chạm, đánh số, rồi trả mỗi pixel# về hạt giống gần nhất (một dạng watershed rất thô)seeds, n2 = ndimage.label(ndimage.binary_erosion(semantic, iterations=1))_, (iy, ix) = ndimage.distance_transform_edt(seeds == 0, return_indices=True)labels2 = seeds[iy, ix] * semanticprint(f"components after erosion: {n2}")pq([labels2 == k for k in range(1, n2 + 1)])Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc kết quả:
- Cách 1,
components: 2: ba đồng xu nhưng chỉ hai mảng. Hàng thứ hai của ma trận là mảng B+C: IoU 0.42 với B, 0.58 với C. Chỉ cặp với C vượt 0.5, nênTP=2 FP=0 FN=1vàPQ=0.633, đúng số đã tính tay. - Cách 2,
components after erosion: 3: co một pixel cắt đứt chỗ chạm, ba hạt giống mọc lại thành ba mask khớp hoàn toàn,PQ=1.000. Thứ tự hàng đổi (C trước B) vìlabelđánh số theo thứ tự quét từ trên xuống, và C to hơn nên chạm hàng trên sớm hơn.
Mẹo co mask là phiên bản rất thô của thuật toán watershed. Nó chạy được ở đây vì chỗ chạm chỉ mỏng một pixel. Hai vật chồng lên nhau một mảng lớn, hay hai vật dính nhau suốt một cạnh dài, thì co bao nhiêu cũng không tách đúng. Vì thế các mô hình instance học trực tiếp “vật nào là vật nào”, thay vì đoán lại từ mask semantic.
NMS trên vài hộp
Phần tiêu đề “NMS trên vài hộp”Ô dưới có 4 hộp: hộp 0 và 1 cùng khoanh người A, hộp 2 khoanh người B đứng sát A, hộp 3 khoanh người C ở xa. Code tính IoU giữa mọi cặp hộp, rồi chạy NMS với ba ngưỡng.
NMS bằng NumPy với ba ngưỡng
import numpy as np
# Hộp [x1, y1, x2, y2] và điểm tin cậy của bộ phát hiện# 0, 1: hai hộp cho cùng người A; 2: người B đứng sát A; 3: người Cboxes = np.array([[10, 10, 50, 90], [14, 12, 54, 92], [22, 10, 62, 90], [100, 20, 140, 100]], dtype=float)scores = np.array([0.95, 0.80, 0.90, 0.85])
def box_iou(a, b): # Phần giao: cạnh trái lớn nhất, cạnh phải nhỏ nhất w = max(0.0, min(a[2], b[2]) - max(a[0], b[0])) h = max(0.0, min(a[3], b[3]) - max(a[1], b[1])) inter = w * h area = lambda r: (r[2] - r[0]) * (r[3] - r[1]) return inter / (area(a) + area(b) - inter)
def nms(thr): order = list(np.argsort(-scores)) # điểm cao trước keep = [] while order: i = order.pop(0) keep.append(int(i)) # bỏ mọi hộp còn lại chồng lên hộp vừa giữ quá ngưỡng order = [j for j in order if box_iou(boxes[i], boxes[j]) <= thr] return sorted(keep)
M = np.array([[box_iou(a, b) for b in boxes] for a in boxes])print(np.round(M, 2))for thr in (0.5, 0.6, 0.8): print(f"thr={thr}: keep {nms(thr)}")Lần chạy đầu cần tải Python, khoảng 10 MB.
Đọc kết quả:
- Ma trận IoU: hai hộp của người A có IoU 0.78. Người B đứng sát nên hộp của B có IoU 0.54 với hộp 0 và 0.64 với hộp 1. Người C không chạm ai.
thr=0.5: giữ[0, 3]. Hộp 1 bị bỏ (đúng), nhưng hộp 2 cũng bị bỏ vì 0.54 vượt ngưỡng: người B biến mất.thr=0.6: giữ[0, 2, 3], đúng ba người. Lưu ý hộp 1 bị hộp 0 xóa trước, nên IoU 0.64 giữa hộp 1 và hộp 2 không còn ảnh hưởng.thr=0.8: giữ cả 4 hộp, người A bị đếm hai lần.
Không có ngưỡng nào đúng cho mọi cảnh. Mask R-CNN sống chung với điều này; DETR và Mask2Former tránh nó bằng cách ghép mỗi vật với đúng một truy vấn khi huấn luyện.
SAM với điểm gợi ý
Phần tiêu đề “SAM với điểm gợi ý”File dưới vẽ bằng PIL một ảnh : hai hình tròn đỏ chạm nhau (A, B) và một hình vuông xanh (C), cùng mask thật của từng hình. Bộ mã hóa ảnh chạy một lần để lấy embedding; sau đó mỗi hình nhận một điểm gợi ý ở giữa, và chỉ bộ giải mã chạy lại. Với mỗi điểm, code in diện tích ba mask SAM trả về, điểm iou_scores mô hình tự chấm, IoU thật so với mask đúng, và mask có iou_scores cao nhất. Lần đầu chạy, transformers tải checkpoint facebook/sam-vit-base (bản ViT-B, nhỏ nhất của SAM), khoảng 375 MB. Cách cài PyTorch bản CPU có ở bài Xây dựng LLM đầu tiên.
Chạy trên máy bạn
SAM: một embedding ảnh, ba điểm gợi ý
- Cần cài
pip install torch transformers pillow- Phần cứng
- CPU là đủ. Khoảng 1–2 phút trên máy phát triển khi đã có checkpoint. Bước attention của bộ mã hóa ảnh cần cấp một khối bộ nhớ khoảng 0.8 GB, nên hãy để trống vài GB RAM
import numpy as npimport torchfrom PIL import Image, ImageDrawfrom transformers import SamModel, SamProcessor
torch.manual_seed(0)# Ảnh 256 x 256 tự vẽ: hai hình tròn chạm nhau và một hình vuôngimg = Image.new("RGB", (256, 256), (235, 230, 220))draw = ImageDraw.Draw(img)draw.ellipse((20, 40, 100, 120), fill=(200, 60, 50)) # tròn Adraw.ellipse((100, 40, 180, 120), fill=(200, 60, 50)) # tròn Bdraw.rectangle((60, 160, 140, 230), fill=(50, 90, 170)) # vuông C
# Mask thật, vẽ lại từng vật trên ảnh đen trắngdef true_mask(shape, box): m = Image.new("L", (256, 256), 0) getattr(ImageDraw.Draw(m), shape)(box, fill=1) return np.array(m, dtype=bool)gt = {"A": true_mask("ellipse", (20, 40, 100, 120)), "B": true_mask("ellipse", (100, 40, 180, 120)), "C": true_mask("rectangle", (60, 160, 140, 230))}points = {"A": [60, 80], "B": [140, 80], "C": [100, 195]} # (x, y)
name = "facebook/sam-vit-base" # khoảng 375 MBprocessor = SamProcessor.from_pretrained(name)model = SamModel.from_pretrained(name).eval()
# Bộ mã hóa ảnh (ViT) chạy đúng một lần cho cả ảnhinputs = processor(img, return_tensors="pt")with torch.no_grad(): emb = model.get_image_embeddings(inputs["pixel_values"])print("image embedding:", tuple(emb.shape))
for key, xy in points.items(): # Mỗi điểm gợi ý chỉ chạy lại phần giải mã nhẹ p = processor(img, input_points=[[[xy]]], return_tensors="pt") with torch.no_grad(): out = model(image_embeddings=emb, input_points=p["input_points"]) masks = processor.image_processor.post_process_masks( out.pred_masks, p["original_sizes"], p["reshaped_input_sizes"])[0][0] # 3 mask cho 1 điểm scores = out.iou_scores[0, 0] ious = [(m.numpy() & gt[key]).sum() / (m.numpy() | gt[key]).sum() for m in masks] print(key, "area:", [int(m.sum()) for m in masks], "truth:", int(gt[key].sum())) print(" iou_scores:", np.round(scores.numpy(), 3)) print(" IoU vs truth:", np.round(ious, 3), "best:", int(scores.argmax()))Kết quả mẫu
Lấy từ một lần chạy thật.
image embedding: (1, 256, 64, 64)A area: [10250, 10214, 5127] truth: 5145 iou_scores: [0.731 0.933 1.016] IoU vs truth: [0.501 0.5 0.997] best: 2B area: [11701, 10241, 5111] truth: 5145 iou_scores: [0.727 0.954 1.004] IoU vs truth: [0.438 0.501 0.993] best: 2C area: [15843, 5751, 5747] truth: 5751 iou_scores: [0.89 1.015 0.999] IoU vs truth: [0.363 1. 0.999] best: 1Đọc kết quả:
(1, 256, 64, 64): ảnh được phóng lên rồi thu 16 lần thành lưới , mỗi ô 256 kênh, đúng như bài báo mô tả. Ba điểm gợi ý dùng lại embedding này.- Điểm trên hình tròn A: mask thứ ba có diện tích 5127 pixel, gần bằng 5145 của mask thật, IoU 0.997. Hai mask đầu rộng khoảng gấp đôi (10250 và 10214 pixel) và có IoU với A khoảng 0.5: chúng phủ cả hai hình tròn. Đây đúng là sự mơ hồ SAM được thiết kế để xử lý: một điểm trên hình tròn đỏ có thể có nghĩa “hình tròn này” hoặc “khối đỏ gồm hai hình tròn chạm nhau”.
iou_scoresxếp mask đúng lên đầu (best: 2). - Hình tròn B: giống A, mask tốt nhất có IoU 0.993.
- Hình vuông C: mask thứ hai khớp hoàn toàn (IoU 1.0, diện tích 5751 bằng đúng mask thật), và cũng là mask có điểm cao nhất. Mask đầu tiên rộng gần gấp ba mask thật, IoU chỉ 0.363.
iou_scorescó thể lớn hơn 1 (1.016, 1.015): đây là đầu ra của một đầu hồi quy học bằng sai số bình phương, không bị chặn ở 1. Dùng nó để xếp hạng các mask, đừng coi là xác suất.
Để ý hai hình tròn chạm nhau, giống B và C của ô NumPy đầu. Một điểm gợi ý đủ để SAM tách từng hình, việc mà ndimage.label không làm được.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- Semantic segmentation gán lớp cho pixel; instance segmentation tách từng vật đếm được (thing); panoptic làm cả hai, mỗi pixel một cặp (lớp, mã đối tượng). PQ ghép đoạn ở IoU lớn hơn 0.5 rồi tính : độ khít của mask nhân với F1 của việc tìm vật.
- Mask R-CNN thêm đầu mask vào Faster R-CNN: tìm hộp, rồi tô mask trong hộp. RoIAlign bỏ phép làm tròn tọa độ của RoIPool bằng nội suy song tuyến tính, đủ để AP của mask tăng rõ.
- Hướng phân loại mask: truy vấn, mỗi truy vấn một mask và một lớp, ghép đôi với vật thật khi huấn luyện nên không cần NMS. Mask2Former thêm masked attention (giá trị ngoài vùng mask trước softmax) và dùng một kiến trúc cho cả ba bài toán.
- SAM tách theo gợi ý: bộ mã hóa ảnh ViT chạy một lần, bộ giải mã nhẹ chạy cho mỗi điểm hay hộp, trả 3 mask kèm điểm IoU dự đoán khi gợi ý mơ hồ, học trên SA-1B (1.1 tỉ mask). SAM không cho nhãn lớp; SAM 2 mở rộng sang video bằng bộ nhớ.
Đọc thêm:
- Kirillov, A., He, K., Girshick, R., Rother, C., Dollár, P. (2019). Panoptic Segmentation, arXiv:1801.00868, CVPR 2019, mục 3 và 4. Định nghĩa thing, stuff, bài toán panoptic, và chứng minh ngắn vì sao ngưỡng 0.5 cho ghép duy nhất.
- He, K., Gkioxari, G., Dollár, P., Girshick, R. (2017). Mask R-CNN, arXiv:1703.06870, mục 3 và bảng 2. Đầu mask, RoIAlign, và các thí nghiệm bỏ từng thành phần.
- Carion, N. và cộng sự (2020). End-to-End Object Detection with Transformers, arXiv:2005.12872, mục 3. Truy vấn vật thể và mất mát ghép đôi, nền của MaskFormer.
- Cheng, B., Schwing, A. G., Kirillov, A. (2021). Per-Pixel Classification is Not All You Need for Semantic Segmentation, arXiv:2107.06278, NeurIPS 2021; và Cheng, B., Misra, I., Schwing, A. G., Kirillov, A., Girdhar, R. (2022). Masked-attention Mask Transformer for Universal Image Segmentation, arXiv:2112.01527, CVPR 2022, mục 3 và 4.6. MaskFormer, Mask2Former, và chính tác giả nói về giới hạn “vẫn huấn luyện riêng từng bài toán”.
- Kirillov, A. và cộng sự (2023). Segment Anything, arXiv:2304.02643, mục 3 và 4; và Ravi, N. và cộng sự (2024). SAM 2: Segment Anything in Images and Videos, arXiv:2408.00714. Kiến trúc ba phần, xử lý mơ hồ, động cơ dữ liệu; và phần mở rộng sang video.
Trình duyệt đang chặn lưu dữ liệu. Tiến độ sẽ mất khi bạn tải lại trang.
Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu