Ultralytics YOLO27: Únete a la lista de espera.
Get Started
Lecciónintermedio

Usa máscaras en el código: área, solapamiento, contornos y recortes

Lee correctamente masks.data y masks.xy, mide el área y el relleno de los objetos, comprueba el solapamiento con máscaras en lugar de cajas y recorta objetos del fotograma.

Un modelo de segmentación solo compensa si el código convierte sus máscaras en una decisión. En la foto del autobús, la caja de cada peatón se solapa entre un 56 y un 74 % con la caja del autobús, mientras que sus máscaras no se solapan en absoluto: una alerta de «persona dentro de la zona del vehículo» basada en cajas se activaría para las cuatro personas que están en la acera. Las máscaras dan la respuesta correcta, pero sus vistas predeterminadas tienen trampas de tamaño y formato que producen cifras verosímiles, pero incorrectas.

Resultado

Ejecuta scripts que indiquen el área de la máscara, el porcentaje de relleno, el tamaño del contorno y el solapamiento con otro objeto, y guarda un recorte transparente de cada objeto usando la vista de máscara adecuada en cada caso.

Vía rápida
Si ya tienes experiencia, aquí tienes un resumen.
  1. Predice con retina_masks=True cada vez que midas o recortes, para que las máscaras coincidan con el tamaño de la imagen original.

  2. Usa masks.data (N × H × W) para las áreas, los solapamientos y los recortes; usa masks.xy (polígonos en píxeles) para dibujar y exportar contornos.

  3. El área es mask.sum(); el relleno es el área de la máscara ÷ el área de la caja; el solapamiento es (mask_a & mask_b).sum().

  4. Comprueba result.masks is None antes de leer las máscaras: su valor es None cuando no se encuentra nada.

Práctico

Dos vistas de la misma máscara#

Persona aislada con una máscara de segmentación de Ultralytics YOLO, en su lugar dentro del fotograma completo sobre un fondo negro

result.masks contiene todas las máscaras de instancias en dos representaciones. Elige según lo que haga la siguiente línea de código; la referencia de Results enumera todos los atributos.

AtributoFormaCoordenadasUso
masks.datatensor N × H × W de 0/1tamaño de inferencia de forma predeterminada; tamaño original con retina_masks=Trueáreas en píxeles, solapamientos entre máscaras, recortes
masks.xylista de N matrices, cada una de P × 2píxeles de la imagen originaldibujar contornos, exportar polígonos
masks.xynlista de N matrices, cada una de P × 2normalizado de 0 a 1escribir etiquetas de polígonos de YOLO
masks.data usa por defecto el tamaño de inferencia

En la imagen del autobús de 1080 × 810, masks.data devuelve un resultado de (5, 640, 480), el tamaño de inferencia con relleno de aspecto. Por tanto, los recuentos de píxeles son unas 2,85 veces menores y la máscara no se puede superponer a la imagen original. retina_masks=True amplía las máscaras a la resolución original con un pequeño coste de posprocesamiento. Úsalo siempre que midas o recortes.

Mide y recorta todos los objetos#

Este script lee ambas vistas para cada objeto: el área en píxeles de masks.data, el tamaño del contorno de masks.xy y un recorte PNG transparente que usa la máscara como canal alfa. La guía para aislar objetos con segmentación explica otras variantes de recorte.

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

Qué debes observar:

  • masks.data: (5, 1080, 810) coincide con orig_shape, así que retina_masks=True ha funcionado y todos los recuentos de píxeles usan las dimensiones originales.
  • El índice i enlaza ambas. result.masks.data[i] pertenece a result.boxes[i], así que los nombres y las confianzas proceden de la caja, y los píxeles, de la máscara.
  • El relleno revela algo que un detector no puede mostrar: el autobús ocupa el 62 % de su caja, mientras que la segunda persona, cortada por el borde derecho del fotograma y de pie junto al autobús, solo ocupa el 31 % de la suya.
  • Los recortes son archivos PNG BGRA que solo contienen a la persona o al autobús, sin nada más; están listos para un catálogo, un segundo modelo o una cola de revisión.

La variante recortada de la guía de aislamiento usa negro en lugar de un fondo transparente:

La misma persona recortada a su caja delimitadora, con todos los píxeles fuera de la máscara puestos en negro

Solapamiento: dónde quedan las cajas#

Reglas como «una persona está dentro de la zona restringida» o «el producto cubre la cinta transportadora» se basan en pruebas de solapamiento. Ejecuta la misma prueba con cajas y con máscaras:

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

Las personas están delante del autobús, así que la máscara del autobús se detiene en el contorno de cada persona y las máscaras no comparten píxeles. Las cajas son rectángulos dibujados alrededor de ambos, así que se solapan mucho. El mismo cálculo sirve para cualquier región que dibujes: rellena un polígono en una matriz de ceros con cv2.fillPoly y úsalo en lugar de la máscara del autobús.

Criterio de decisión

Cualquier regla formulada como dentro, tocando, cubriendo o bloqueando es una prueba de solapamiento. Aplícala a máscaras. Los cuadros sirven para cerca de y a la izquierda de, donde unos pocos píxeles de margen no cambian la respuesta.

De los píxeles a las respuestas del mundo real#

PreguntaCómputoTen cuidado con
¿Qué tamaño tiene?mask.sum() × (cm² por píxel)Calibra una vez los cm² por píxel con un objeto conocido. Con una cámara inclinada, el área cambia con la distancia: calibra cada zona o mide solo en una región.
¿Qué parte de la región está cubierta?píxeles de la máscara de la clase ÷ píxeles de la regiónDecide si las instancias superpuestas se cuentan una o dos veces.
¿Está dentro de la zona?(mask & zone).sum() / mask.sum()Dibuja la zona al tamaño original de la imagen.
¿Qué aspecto tiene su contorno?masks.xy[i]Antes de calcular el área a partir de este, consulta la advertencia sobre objetos divididos que aparece más abajo.

Errores habituales#

Leer máscaras cuando no se ha detectado nada

Síntoma: AttributeError: 'NoneType' object has no attribute 'xy' en fotogramas sin objetos. Causa: cuando el modelo no encuentra nada, result.masks es None, no un objeto vacío. Solución: recorre result.boxes (está vacío, nunca es None en los modelos de segmentación) e indexa las máscaras dentro del bucle, como hace masks.py, o comprueba primero result.masks is None.

Área del polígono en objetos separados

Síntoma: áreas muy distintas de las esperadas en objetos ocluidos. Causa: cuando un objeto queda dividido por otro que lo oculta, masks.xy une todos los contornos en una sola matriz y cv2.contourArea mide en esa matriz una forma que no existe. Solución: calcula las áreas a partir del recuento de píxeles de masks.data; reserva los polígonos para dibujar.

Mezclar resoluciones

Síntoma: las áreas de dos ejecuciones difieren en un factor de aproximadamente 2,85. Causa: una ejecución usó retina_masks=False, así que sus áreas están expresadas en píxeles de inferencia. Solución: mide siempre con retina_masks=True y, en CUDA, llama a .cpu() antes de .numpy().

Pruébalo

Ejecuta masks.py en una imagen propia con al menos tres objetos e indica cuál tiene el menor nivel de ocupación y por qué su caja sobrestima su tamaño. Vuelve a ejecutarlo sin retina_masks=True y anota cómo cambian masks.data.shape y las áreas. Después, adapta overlap.py para comprobar si algún objeto de tu imagen se solapa con un rectángulo que dibujes sobre el tercio inferior del fotograma.

Cuándo se considera terminado
Has terminado la lección cuando se cumplen todas estas condiciones.
  • Tienes un recorte PNG transparente por cada objeto detectado en tu imagen.

  • Puedes indicar el menor nivel de ocupación y explicar la diferencia entre masks.data con y sin retina_masks=True.

  • La prueba de zonas muestra una superposición basada en cajas y otra basada en máscaras para cada objeto, y puedes explicar cualquier caso en el que no coincidan.

Mostrar solución

Sin retina_masks=True, la imagen del autobús devuelve masks.data con la forma (5, 640, 480): el área en píxeles se reduce en todos los casos por un factor de (1080 × 810) / (640 × 480) ≈ 2.85, y la máscara deja de coincidir con orig_img para los recortes. Cuando la relación de aspecto de la imagen difiere de la forma de inferencia, el relleno de letterbox añade una segunda desalineación.

El menor nivel de ocupación en la imagen del autobús corresponde a la segunda persona (0.31): está cortada por el borde derecho del fotograma y de pie junto al autobús, así que la mayor parte de la caja es fondo y autobús.

Para la zona, créala una vez al tamaño original y reutiliza la prueba con máscaras:

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

En la imagen del autobús:

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%

Las piernas de las personas están realmente en el tercio inferior, así que los resultados de las cajas y las máscaras coinciden con una diferencia de unos diez puntos. El autobús es el falso positivo: su caja se adentra un 4 % en la zona, pero su máscara no. Con una regla de «más del 0 % dentro», las cajas marcarían el autobús y las máscaras no.

Qué viene después

A continuación, prescindimos por completo de las instancias y etiquetamos cada píxel con segmentación semántica.