Ultralytics YOLO27: Встать в лист ожидания.
Get Started
Уроксредний

Использование масок в коде: площадь, перекрытие, контуры и вырезки

Научись правильно считывать masks.data и masks.xy, измерять площадь и заполнение объектов, проверять перекрытия с помощью масок, а не рамок, и вырезать объекты из кадра.

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

Результат

Запусти скрипты, которые для каждого объекта выводят площадь маски, коэффициент заполнения, размер контура и перекрытие с другим объектом, а также сохраняют прозрачную вырезку каждого объекта, используя для каждой задачи подходящее представление маски.

Ускоренное прохождение
Если ты уже освоился, вот краткая версия.
  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.dataТензор N × 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) — размер изображения после 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().

Попробовать

Запусти masks.py на собственном изображении как минимум с тремя объектами и укажи объект с наименьшей долей заполнения, а также объясни, почему его рамка завышает размер. Запусти модель ещё раз без retina_masks=True и отметь, как изменились masks.data.shape и площади. Затем адаптируй overlap.py, чтобы проверить, перекрывает ли какой-либо объект прямоугольник, который ты нарисуешь в нижней трети кадра.

Критерии готовности
Урок считается завершённым, если выполнены все условия ниже.
  • У тебя есть по одному прозрачному PNG-фрагменту для каждого обнаруженного объекта на изображении.

  • Ты можешь назвать наименьшую долю заполнения и объяснить разницу между masks.data с retina_masks=True и без него.

  • Тест зоны выводит для каждого объекта перекрытие, рассчитанное по рамке и по маске, а ты можешь объяснить, почему результаты для некоторых объектов различаются.

Показать решение

Без 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%» рамки отметили бы автобус, а маски — нет.

Что дальше

Далее мы полностью откажемся от экземпляров и будем размечать каждый пиксель с помощью семантической сегментации.

Войди, чтобы сохранять прогресс