在代码中使用掩码:面积、重叠区域、轮廓和抠图
正确读取 masks.data 和 masks.xy,测量对象面积和填充率,用掩码而非框测试重叠,并从画面中抠出对象。
只有当代码将掩码转化为决策时,分割模型的投入才有回报。在这张公交车照片中,每位行人的框与公交车框重叠 56–74%,而他们的掩码与公交车掩码重叠 0%——如果基于框构建“有人进入车辆区域”的警报,那么在人行道上站着的四个人都会触发警报。掩码能得出正确结果,但默认视图中存在尺寸和格式陷阱,可能导致看似合理却错误的数值。
实践操作
同一掩码的两种视图#

result.masks 以两种形式保存每个实例掩码。根据下一行代码要执行的操作选择视图;Results 参考文档列出了所有属性。
| 属性 | 形状 | 坐标 | 用途 |
|---|---|---|---|
masks.data | N × H × W 的 0/1 张量 | 默认使用推理尺寸;使用 retina_masks=True 时为原始尺寸 | 像素面积、掩码之间的重叠、抠图 |
masks.xy | 包含 N 个数组的列表,每个数组为 P × 2 | 原始图像像素 | 绘制轮廓、导出多边形 |
masks.xyn | 包含 N 个数组的列表,每个数组为 P × 2 | 归一化到 0–1 | 将 YOLO 多边形标签写出 |
对于这张 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()。
显示解答
不使用 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%”的规则时,框会将公交车标记出来,掩码则不会。