Dùng mask trong code: diện tích, vùng chồng lấp, đường viền, phần cắt ảnh
Đọc đúng masks.data và masks.xy, đo diện tích và độ phủ của đối tượng, kiểm tra vùng chồng lấp bằng mask thay vì box, đồng thời cắt đối tượng khỏi khung hình.
Model phân đoạn chỉ phát huy giá trị khi code chuyển mask thành quyết định. Trong ảnh xe buýt, box của mỗi người đi bộ chồng lấp 56–74% với box xe buýt, trong khi mask của họ không chồng lấp với mask xe buýt (0%) — cảnh báo "người bên trong khu vực phương tiện" dựa trên box sẽ kích hoạt với cả bốn người đang đứng trên vỉa hè. Mask cho kết quả chính xác, nhưng cách xem mặc định có những bẫy về kích thước và định dạng, dễ tạo ra các con số có vẻ hợp lý nhưng sai.
Thực hành
Hai chế độ xem của cùng một mask#

result.masks chứa mọi mask instance dưới hai dạng biểu diễn. Chọn dạng dựa trên tác vụ mà dòng code tiếp theo thực hiện; tài liệu tham chiếu Results liệt kê mọi thuộc tính.
| Thuộc tính | Hình dạng | Tọa độ | Dùng cho |
|---|---|---|---|
masks.data | tensor N × H × W gồm các giá trị 0/1 | mặc định là kích thước inference; kích thước gốc khi dùng retina_masks=True | diện tích theo pixel, vùng chồng lấp giữa các mask, ảnh cắt |
masks.xy | danh sách gồm N mảng, mỗi mảng có kích thước P × 2 | pixel của ảnh gốc | vẽ đường viền, xuất polygon |
masks.xyn | danh sách gồm N mảng, mỗi mảng có kích thước P × 2 | được chuẩn hóa trong khoảng 0–1 | xuất lại nhãn polygon YOLO |
Trên ảnh xe buýt 1080 × 810, masks.data có kích thước (5, 640, 480) — kích thước suy luận sau letterbox — nên số pixel nhỏ hơn khoảng 2.85× và không thể đặt mask lên ảnh gốc. retina_masks=True phóng to mask về độ phân giải gốc với chi phí hậu xử lý nhỏ. Hãy dùng tham số này bất cứ khi nào bạn đo đạc hoặc crop.
Đo và cắt từng đối tượng#
Script này đọc cả hai chế độ xem cho từng đối tượng: diện tích theo pixel từ masks.data, kích thước đường viền từ masks.xy và ảnh cắt PNG trong suốt dùng mask làm kênh alpha. Hướng dẫn cô lập đối tượng bằng phân đoạn trình bày thêm các biến thể ảnh cắt.
import cv2
import numpy as np
from ultralytics import YOLO
model = YOLO("yolo26n-seg.pt")
result = model("https://ultralytics.com/images/bus.jpg", retina_masks=True)[0] # masks at original resolution
print("masks.data:", tuple(result.masks.data.shape), "orig_shape:", result.orig_shape)
for i, box in enumerate(result.boxes):
name = result.names[int(box.cls)]
mask = result.masks.data[i].cpu().numpy().astype(np.uint8) # H x W, 1 = object
polygon = result.masks.xy[i] # P x 2 outline in original pixels
x1, y1, x2, y2 = box.xyxy[0].int().tolist()
mask_area = int(mask.sum())
box_area = (x2 - x1) * (y2 - y1)
print(f"{name:6s} conf={float(box.conf):.2f} mask={mask_area:7d}px box={box_area:7d}px fill={mask_area / box_area:.2f} polygon={len(polygon)} pts")
# Cut the object out on a transparent background
cutout = np.dstack([result.orig_img, mask * 255])[y1:y2, x1:x2]
cv2.imwrite(f"{i}_{name}.png", cutout)masks.data: (5, 1080, 810) orig_shape: (1080, 810)
person conf=0.88 mask= 49331px box= 97219px fill=0.51 polygon=575 pts
person conf=0.86 mask= 21316px box= 67760px fill=0.31 polygon=408 pts
bus conf=0.86 mask= 255265px box= 409822px fill=0.62 polygon=1172 pts
person conf=0.81 mask= 33524px box= 55842px fill=0.60 polygon=350 pts
person conf=0.64 mask= 11109px box= 23828px fill=0.47 polygon=211 ptsCần chú ý những gì:
masks.data: (5, 1080, 810)khớp vớiorig_shape, nghĩa làretina_masks=Trueđã hoạt động đúng và mọi số đếm pixel đều tính theo ảnh gốc.- Chỉ số
iliên kết hai phần này.result.masks.data[i]thuộc vềresult.boxes[i], vì vậy tên và độ tin cậy lấy từ box, còn pixel lấy từ mask. - Độ phủ cho biết điều mà detector không thể hiện: xe buýt chiếm 62% box, trong khi người thứ hai — bị cắt ở mép phải khung hình và đứng sát xe buýt — chỉ chiếm 31%.
- Ảnh cắt là PNG BGRA chỉ chứa người hoặc xe buýt, không có gì khác — sẵn sàng dùng cho catalog, model thứ hai hoặc hàng đợi đánh giá.
Biến thể cắt ảnh trong hướng dẫn cô lập dùng nền đen thay cho nền trong suốt:

Vùng chồng lấp: vị trí các box#
Các quy tắc như "người nằm trong khu vực hạn chế" hoặc "sản phẩm che phủ băng chuyền" đều là kiểm tra vùng chồng lấp. Thực hiện cùng phép kiểm tra với box và mask:
import numpy as np
from ultralytics import YOLO
result = YOLO("yolo26n-seg.pt")("https://ultralytics.com/images/bus.jpg", retina_masks=True, verbose=False)[0]
names = [result.names[int(c)] for c in result.boxes.cls]
masks = result.masks.data.cpu().numpy().astype(bool) # N x H x W at original resolution
boxes = np.zeros_like(masks)
for i, (x1, y1, x2, y2) in enumerate(result.boxes.xyxy.int().tolist()):
boxes[i, y1:y2, x1:x2] = True # the same objects as filled rectangles
bus = names.index("bus")
for i, name in enumerate(names):
if name == "person":
by_box = (boxes[i] & boxes[bus]).sum() / boxes[i].sum()
by_mask = (masks[i] & masks[bus]).sum() / masks[i].sum()
print(f"person {i}: {by_box:4.0%} of box overlaps the bus box, {by_mask:4.0%} of mask overlaps the bus mask")person 0: 67% of box overlaps the bus box, 0% of mask overlaps the bus mask
person 1: 69% of box overlaps the bus box, 0% of mask overlaps the bus mask
person 3: 74% of box overlaps the bus box, 0% of mask overlaps the bus mask
person 4: 56% of box overlaps the bus box, 0% of mask overlaps the bus maskMọi người đứng phía trước xe buýt, vì vậy mask xe buýt dừng lại ở đường viền của từng người và các mask không có pixel chung. Box là các hình chữ nhật bao quanh cả hai, nên chúng chồng lấp nhiều. Có thể áp dụng phép tính tương tự cho mọi vùng do bạn tự vẽ: tô polygon vào mảng toàn số 0 bằng cv2.fillPoly và dùng mảng đó thay cho mask xe buýt.
Mọi quy tắc có cách diễn đạt như bên trong, chạm vào, che phủ hoặc chặn đều là phép kiểm tra chồng lấp. Hãy chạy phép kiểm tra trên mask. Bbox phù hợp với gần và bên trái, nơi sai số vài pixel không làm thay đổi kết quả.
Từ pixel đến kết quả thực tế#
| Câu hỏi | Tính toán | Cẩn thận với |
|---|---|---|
| Kích thước là bao nhiêu? | mask.sum() × (cm² trên mỗi pixel) | Hiệu chuẩn cm² trên mỗi pixel một lần bằng một vật thể có kích thước đã biết. Với camera đặt nghiêng, diện tích thay đổi theo khoảng cách: hiệu chuẩn riêng cho từng vùng hoặc chỉ đo trong một vùng. |
| Vùng được phủ bao nhiêu? | số pixel của mask lớp ÷ số pixel của vùng | Quyết định xem các instance chồng lấn được tính một hay hai lần. |
| Nó có nằm trong vùng không? | (mask & zone).sum() / mask.sum() | Vẽ vùng ở kích thước ảnh gốc. |
| Đường viền của nó trông như thế nào? | masks.xy[i] | Xem cảnh báo về đối tượng bị tách bên dưới trước khi tính diện tích từ đó. |
Các lỗi thường gặp#
Triệu chứng: AttributeError: 'NoneType' object has no attribute 'xy' trên các frame không có đối tượng. Nguyên nhân: result.masks là None khi model không tìm thấy gì, chứ không phải object rỗng. Cách khắc phục: lặp qua result.boxes (rỗng, không bao giờ là None với model segmentation) và truy cập mask bên trong vòng lặp như masks.py, hoặc kiểm tra result.masks is None trước.
Triệu chứng: diện tích của các đối tượng bị che khuất sai lệch rất nhiều. Nguyên nhân: khi vật che khuất chia một đối tượng thành nhiều phần, masks.xy nối mọi contour thành một mảng, và cv2.contourArea trên mảng đó đo diện tích của một hình dạng không tồn tại. Cách khắc phục: tính diện tích từ số pixel trong masks.data; giữ polygon để vẽ.
Triệu chứng: diện tích từ hai lần chạy chênh lệch khoảng 2.85 lần. Nguyên nhân: một lần chạy dùng retina_masks=False, nên diện tích được tính theo pixel suy luận. Cách khắc phục: luôn đo bằng retina_masks=True; trên CUDA, gọi .cpu() trước .numpy().
Hiển thị lời giải
Nếu không có retina_masks=True, ảnh xe buýt trả về masks.data với shape (5, 640, 480): diện tích mỗi pixel giảm theo hệ số (1080 × 810) / (640 × 480) ≈ 2.85, và mask không còn khớp với orig_img để tạo ảnh tách nền. Khi tỷ lệ khung hình của ảnh khác với shape suy luận, phần đệm letterbox gây ra thêm một sai lệch.
Tỷ lệ phủ thấp nhất trong ảnh xe buýt thuộc về người thứ hai (0.31): người này bị cắt mất một phần ở cạnh phải khung hình và đứng sát xe buýt, nên phần lớn box nằm trên nền và xe buýt.
Với vùng cần xét, hãy tạo vùng đó một lần ở kích thước gốc rồi dùng lại phép kiểm tra bằng mask:
import cv2
h, w = result.orig_shape
zone = np.zeros((h, w), np.uint8)
cv2.fillPoly(zone, [np.array([[0, 2 * h // 3], [w, 2 * h // 3], [w, h], [0, h]])], 1)
zone = zone.astype(bool)
for i, name in enumerate(names):
print(f"{name} {i}: box {(boxes[i] & zone).sum() / boxes[i].sum():.0%}, mask {(masks[i] & zone).sum() / masks[i].sum():.0%}")Trên ảnh xe buýt:
person 0: box 36%, mask 29%
person 1: box 33%, mask 37%
bus 2: box 4%, mask 0%
person 3: box 31%, mask 21%
person 4: box 47%, mask 53%Chân của mọi người thực sự nằm trong một phần ba dưới cùng, nên kết quả từ box và mask chênh nhau khoảng mười điểm phần trăm. Xe buýt là trường hợp báo động giả: box lấn 4% vào vùng, còn mask thì không. Với quy tắc "có bất kỳ phần nào nằm bên trong (> 0%)", box sẽ đánh dấu xe buýt còn mask thì không.