Ultralytics YOLO27: 加入候补名单。
Get Started
课程中级

在代码中使用掩码:面积、重叠区域、轮廓和抠图

正确读取 masks.data 和 masks.xy,测量对象面积和填充率,用掩码而非框测试重叠,并从画面中抠出对象。

只有当代码将掩码转化为决策时,分割模型的投入才有回报。在这张公交车照片中,每位行人的框与公交车框重叠 56–74%,而他们的掩码与公交车掩码重叠 0%——如果基于框构建“有人进入车辆区域”的警报,那么在人行道上站着的四个人都会触发警报。掩码能得出正确结果,但默认视图中存在尺寸和格式陷阱,可能导致看似合理却错误的数值。

学习成果

运行脚本,报告每个对象的掩码面积、填充率、轮廓尺寸以及与其他对象的重叠情况,并为每个对象保存透明抠图——每项操作都要使用正确的掩码视图。

快速通道
如果你已经熟悉相关内容,以下是简要版。
  1. 每次测量或裁剪时,都使用 retina_masks=True 进行预测,以确保掩码与原始图像尺寸一致。

  2. 面积、重叠和抠图使用 masks.data(N × H × W);绘制和导出轮廓使用 masks.xy(像素多边形)。

  3. 面积是 mask.sum();填充率是掩码面积 ÷ 框面积;重叠值是 (mask_a & mask_b).sum()。

  4. 读取掩码前先检查 result.masks is None:没有检测到对象时,它的值为 None。

实践操作

同一掩码的两种视图#

使用 Ultralytics YOLO 分割掩码分离出的人物,仍位于完整画面中的原位置,背景为黑色

result.masks 以两种形式保存每个实例掩码。根据下一行代码要执行的操作选择视图;Results 参考文档列出了所有属性。

属性形状坐标用途
masks.dataN × H × W 的 0/1 张量默认使用推理尺寸;使用 retina_masks=True 时为原始尺寸像素面积、掩码之间的重叠、抠图
masks.xy包含 N 个数组的列表,每个数组为 P × 2原始图像像素绘制轮廓、导出多边形
masks.xyn包含 N 个数组的列表,每个数组为 P × 2归一化到 0–1将 YOLO 多边形标签写出
masks.data 默认使用推理尺寸

对于这张 1080 × 810 的公交车图像,masks.data 返回的尺寸是 (5, 640, 480)——也就是添加填充后的推理尺寸——因此像素计数约小了 2.85 倍,而且掩码无法叠加到原始图像上。retina_masks=True 会以少量后处理开销将掩码放大到原始分辨率。测量或裁剪时都应使用它。

测量并抠出每个对象#

此脚本会为每个对象读取两种视图:从 masks.data 获取像素面积,从 masks.xy 获取轮廓尺寸,并使用掩码作为 alpha 通道生成透明 PNG 抠图。分割对象抠图指南介绍了更多抠图方式。

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

查看结果时需要注意:

  • masks.data: (5, 1080, 810) 与 orig_shape 相匹配,因此 retina_masks=True 已生效,所有像素计数都使用原始图像像素。
  • 索引 i 将两者关联起来。 result.masks.data[i] 属于 result.boxes[i],因此名称和置信度来自框,像素则来自掩码。
  • 填充率反映了检测器无法表达的信息:公交车占其框的 62%,而第二个人被画面右侧边缘截断、站在公交车旁,因此只占其框的 31%。
  • 这些抠图是 BGRA PNG,只包含人物或公交车本身,不含其他内容——可以直接用于商品目录、第二个模型或审核队列。

抠图指南中的裁剪版本使用黑色背景,而不是透明背景:

同一个人被裁剪到其边界框内,掩码以外的所有像素都设为黑色

重叠:框的位置#

“有人进入限制区域”或“产品覆盖传送带”等规则,本质上都是重叠测试。分别用框和掩码执行相同的测试:

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

行人站在公交车前面,因此公交车掩码会在每个人的轮廓处截止,两种掩码没有共享像素。框是围绕两者绘制的矩形,因此重叠区域很大。同样的计算也适用于你自行绘制的任何区域:使用 cv2.fillPoly 将多边形填充到零数组中,再用它替代公交车掩码。

判断规则

任何包含内部、接触、覆盖或阻挡的规则,都是重叠测试。应使用掩码进行判断。对于附近和左侧这类规则,几像素的误差余量不会改变结果,因此使用边界框即可。

从像素到真实世界的答案#

问题计算注意
它有多大?mask.sum() ×(每像素 cm²)用一个已知物体校准一次每像素 cm²。对于倾斜拍摄的相机,面积会随距离变化:按区域分别校准,或只在一个区域内测量。
区域被覆盖了多少?类别掩码像素数 ÷ 区域像素数确定重叠实例是计一次还是两次。
它在区域内吗?(mask & zone).sum() / mask.sum()按原始图像尺寸绘制区域。
它的轮廓是什么样的?masks.xy[i]根据它计算面积前,请先查看下方关于对象被拆分的警告。

常见问题#

没有检测到目标时读取掩码

症状:在没有物体的帧上出现 AttributeError: 'NoneType' object has no attribute 'xy'。原因:模型未检测到任何物体时,result.masks 的值是 None,而不是空对象。解决方法:循环处理 result.boxes(分割模型中为空,绝不会是 None),并在循环中索引掩码,如 masks.py 所示;或者先检查 result.masks is None。

被分割物体的多边形面积

症状:被遮挡物体的面积数值严重错误。原因:当遮挡物将物体分割开时,masks.xy 会把所有轮廓合并到一个数组中,而在该数组上使用 cv2.contourArea 测量出的形状并不存在。解决方法:根据 masks.data 的像素计数计算面积;多边形只用于绘制。

混用分辨率

症状:两次运行的面积相差约 2.85 倍。原因:其中一次使用了 retina_masks=False,因此面积以推理像素为单位。解决方法:始终使用 retina_masks=True 进行测量;在 CUDA 上,在 .numpy() 之前调用 .cpu()。

试试看

在你自己的图像上运行 masks.py,图中至少包含三个对象;报告填充率最低的对象,并说明它的框为何高估了尺寸。不使用 retina_masks=True 重新运行,并记录 masks.data.shape 和面积如何变化。然后调整 overlap.py,测试图像中的对象是否与绘制在画面下方三分之一处的矩形重叠。

完成条件
满足以下所有条件,即表示你已完成本课程。
  • 你有一张图像,其中每个检测到的对象都对应一个透明 PNG 抠图。

  • 你可以说出最低填充率,并解释有无 retina_masks=True 时 masks.data 的差异。

  • 你的区域测试会为每个对象分别打印基于框和基于掩码的重叠情况,而且你可以解释它们结果不一致的对象。

显示解答

不使用 retina_masks=True 时,公交车图像会返回形状为 (5, 640, 480) 的 masks.data:每个像素面积会缩小 (1080 × 810) / (640 × 480) ≈ 2.85 倍,而且掩码不再与抠图用的 orig_img 对齐。如果图像的宽高比与推理尺寸不同,letterbox 填充还会造成第二处错位。

公交车图像中填充率最低的是第二个人(0.31):此人被画面右边缘截断,且站在公交车旁边,因此框的大部分区域是背景和公交车。

对于区域,在原始尺寸下创建一次,然后重复使用掩码测试:

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%}")

在公交车图像上:

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%

人的腿确实位于下方三分之一处,所以框和掩码的结果相差约十个百分点以内。公交车则是误报:它的框有 4% 延伸到区域内,而掩码完全没有。采用“区域内超过 0%”的规则时,框会将公交车标记出来,掩码则不会。

接下来做什么

接下来,我们完全不再区分实例,而是用语义分割为每个像素标注类别。

登录以保存学习进度