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.
Práctico
Dos vistas de la misma máscara#

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.
| Atributo | Forma | Coordenadas | Uso |
|---|---|---|---|
masks.data | tensor N × H × W de 0/1 | tamaño de inferencia de forma predeterminada; tamaño original con retina_masks=True | áreas en píxeles, solapamientos entre máscaras, recortes |
masks.xy | lista de N matrices, cada una de P × 2 | píxeles de la imagen original | dibujar contornos, exportar polígonos |
masks.xyn | lista de N matrices, cada una de P × 2 | normalizado de 0 a 1 | escribir etiquetas de polígonos de YOLO |
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 ptsQué debes observar:
masks.data: (5, 1080, 810)coincide conorig_shape, así queretina_masks=Trueha funcionado y todos los recuentos de píxeles usan las dimensiones originales.- El índice
ienlaza ambas.result.masks.data[i]pertenece aresult.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:

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 maskLas 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.
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#
| Pregunta | Cómputo | Ten 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ón | Decide 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#
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.
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.
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().
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.