Использование масок в коде: площадь, перекрытие, контуры и вырезки
Научись правильно считывать masks.data и masks.xy, измерять площадь и заполнение объектов, проверять перекрытия с помощью масок, а не рамок, и вырезать объекты из кадра.
Модель сегментации окупается только тогда, когда код превращает её маски в решение. На фотографии автобуса рамки пешеходов перекрываются с рамкой автобуса на 56–74%, а их маски вообще не перекрываются с маской автобуса — оповещение «человек внутри зоны транспортного средства», основанное на рамках, сработало бы для всех четырёх людей, стоящих на тротуаре. Маски дают правильные ответы, но их представления по умолчанию имеют особенности размера и формата, из-за которых легко получить правдоподобные, но неверные числа.
Практические занятия
Два представления одной маски#

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) — размер изображения после letterbox-преобразования для инференса, — поэтому количество пикселей примерно в 2.85 раза меньше нужного, а маску нельзя наложить на исходное изображение. retina_masks=True увеличивает разрешение масок до исходного с небольшими затратами на постобработку. Используй его при измерении или кадрировании.
Измерь и вырежи каждый объект#
Этот скрипт считывает оба представления для каждого объекта: площадь в пикселях из masks.data, размер контура из masks.xy и прозрачную 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() × (см² на пиксель) | Один раз откалибруй см² на пиксель по объекту известного размера. При съёмке под углом площадь зависит от расстояния: калибруй отдельно для каждой зоны или измеряй только в одной области. |
| Какая часть области покрыта? | пиксели маски класса ÷ пиксели области | Реши, учитывать ли перекрывающиеся экземпляры один или два раза. |
| Находится ли он внутри зоны? | (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 вызывай .cpu() перед .numpy().
Показать решение
Без retina_masks=True изображение автобуса возвращает masks.data с формой (5, 640, 480): площадь каждого пикселя уменьшается в (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%» рамки отметили бы автобус, а маски — нет.