Ultralytics YOLO27: Tham gia danh sách chờ.
Get Started
Bài họctrung cấp

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.

Kết quả

Chạy các script báo cáo diện tích mask, tỷ lệ độ phủ, kích thước đường viền và mức độ chồng lấp với đối tượng khác cho từng đối tượng, đồng thời lưu ảnh cắt trong suốt cho từng đối tượng — sử dụng đúng chế độ xem mask cho mỗi tác vụ.

Lộ trình cấp tốc
Nếu bạn đã nắm rõ cách thực hiện, đây là phiên bản tóm tắt.
  1. Dùng retina_masks=True khi dự đoán cho mọi phép đo hoặc thao tác cắt để mask khớp với kích thước ảnh gốc.

  2. Dùng masks.data (N × H × W) để tính diện tích, vùng chồng lấp và tạo ảnh cắt; dùng masks.xy (polygon theo pixel) để vẽ và xuất đường viền.

  3. Diện tích là mask.sum(); độ phủ là diện tích mask ÷ diện tích box; vùng chồng lấp là (mask_a & mask_b).sum().

  4. Kiểm tra result.masks is None trước khi đọc mask: giá trị này là None khi không tìm thấy đối tượng nào.

Thực hành

Hai chế độ xem của cùng một mask#

Một người được tách riêng bằng mask phân đoạn Ultralytics YOLO, vẫn ở nguyên vị trí trong toàn khung hình trên nền đen

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ínhHình dạngTọa độDùng cho
masks.datatensor N × H × W gồm các giá trị 0/1mặc định là kích thước inference; kích thước gốc khi dùng retina_masks=Truediện tích theo pixel, vùng chồng lấp giữa các mask, ảnh cắt
masks.xydanh sách gồm N mảng, mỗi mảng có kích thước P × 2pixel của ảnh gốcvẽ đường viền, xuất polygon
masks.xyndanh 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–1xuất lại nhãn polygon YOLO
masks.data mặc định dùng kích thước suy luận

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 pts

Cần chú ý những gì:

  • masks.data: (5, 1080, 810) khớp với orig_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ố i liê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:

Cùng người đó được cắt theo box bao quanh, mọi pixel bên ngoài mask đều được chuyển thành màu đen

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 mask

Mọ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.

Quy tắc quyết định

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ỏiTính toánCẩ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ùngQuyế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#

Đọc mask khi không tìm thấy đối tượng

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.

Diện tích polygon trên đối tượng bị chia tách

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

Trộn lẫn độ phân giải

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().

Dùng thử

Chạy masks.py trên một ảnh của riêng bạn có ít nhất ba đối tượng, rồi báo cáo đối tượng có tỷ lệ phủ thấp nhất và giải thích vì sao bounding box phóng đại kích thước của nó. Chạy lại mà không có retina_masks=True và ghi nhận masks.data.shape cùng diện tích thay đổi thế nào. Sau đó điều chỉnh overlap.py để kiểm tra xem có đối tượng nào trong ảnh chồng lấn với hình chữ nhật bạn vẽ trên một phần ba dưới cùng của khung hình hay không.

Điều kiện hoàn thành
Bạn đã hoàn thành bài học khi tất cả các điều kiện sau đều đúng.
  • Bạn có một ảnh PNG trong suốt đã tách nền cho mỗi đối tượng được phát hiện trong ảnh.

  • Bạn có thể nêu tỷ lệ phủ thấp nhất và giải thích sự khác biệt giữa masks.data khi có và không có retina_masks=True.

  • Bài kiểm tra vùng của bạn in ra mức độ chồng lấn dựa trên box và mask cho từng đối tượng, và bạn có thể giải thích mọi trường hợp hai kết quả không khớp nhau.

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.

Tiếp theo là gì

Tiếp theo, chúng ta bỏ hẳn instance và gán nhãn cho mọi pixel bằng segmentation ngữ nghĩa.

Đăng nhập để lưu tiến độ