Ultralytics YOLO27: Inscreve-te na lista de espera.
Get Started
Aulaintermediário

Usar máscaras em código: área, sobreposição, contornos e recortes

Lê corretamente masks.data e masks.xy, mede a área e o preenchimento dos objetos, testa a sobreposição com máscaras em vez de caixas e recorta objetos da imagem.

Um modelo de segmentação só compensa quando o código transforma as máscaras numa decisão. Na fotografia do autocarro, a caixa de cada peão sobrepõe-se à caixa do autocarro em 56–74%, enquanto as respetivas máscaras têm 0% de sobreposição com a do autocarro — um alerta de «pessoa dentro da zona do veículo» baseado em caixas seria acionado para as quatro pessoas no passeio. As máscaras dão as respostas certas, mas as suas representações predefinidas têm armadilhas de tamanho e formato que produzem valores plausíveis, mas errados.

Resultado

Executa scripts que indiquem a área da máscara, a proporção de preenchimento, o tamanho do contorno e a sobreposição com outro objeto para cada objeto, e guarda um recorte com transparência para cada um — usando a representação correta da máscara em cada caso.

Via rápida
Se já sabes orientar-te, aqui está a versão resumida.
  1. Faz a previsão com retina_masks=True sempre que medires ou recortares, para que as máscaras correspondam ao tamanho da imagem original.

  2. Usa masks.data (N × H × W) para áreas, sobreposições e recortes; usa masks.xy (polígonos em píxeis) para desenhar e exportar contornos.

  3. A área é mask.sum(); o preenchimento é a área da máscara ÷ a área da caixa; a sobreposição é (mask_a & mask_b).sum().

  4. Verifica result.masks is None antes de ler as máscaras: é None quando nada é detetado.

Prático

Duas representações da mesma máscara#

Uma pessoa isolada com uma máscara de segmentação Ultralytics YOLO, no mesmo lugar e num fundo preto

result.masks contém todas as máscaras de instâncias em duas representações. Escolhe com base no que a linha de código seguinte faz; a referência de Results lista todos os atributos.

AtributoFormaCoordenadasUtilização
masks.datatensor N × H × W de valores 0/1tamanho da inferência por predefinição; tamanho original com retina_masks=Trueáreas em píxeis, sobreposições entre máscaras, recortes
masks.xylista de N matrizes, cada uma com P × 2píxeis da imagem originaldesenhar contornos, exportar polígonos
masks.xynlista de N matrizes, cada uma com P × 2normalizado de 0 a 1escrever novamente rótulos poligonais YOLO
masks.data usa por predefinição o tamanho de inferência

Na imagem do autocarro com 1080 × 810, masks.data é devolvido como (5, 640, 480) — o tamanho da inferência com letterbox —, pelo que as contagens de píxeis ficam cerca de 2.85× abaixo do valor correto e não é possível sobrepor a máscara à imagem original. retina_masks=True aumenta as máscaras para a resolução original, com um pequeno custo de pós-processamento. Usa-o sempre que medires ou recortares.

Mede e recorta todos os objetos#

Este script lê as duas representações de cada objeto: a área em píxeis de masks.data, o tamanho do contorno de masks.xy e um recorte PNG com transparência que usa a máscara como canal alfa. O guia de isolamento de objetos de segmentação apresenta mais 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

O que deves observar:

  • masks.data: (5, 1080, 810) corresponde a orig_shape, por isso retina_masks=True fez o seu trabalho e todas as contagens de píxeis usam os píxeis originais.
  • O índice i liga as duas representações. result.masks.data[i] pertence a result.boxes[i], por isso os nomes e as confianças vêm da caixa e os píxeis vêm da máscara.
  • O preenchimento revela algo que um detetor não consegue: o autocarro ocupa 62% da sua caixa, enquanto a segunda pessoa, cortada pela margem direita da imagem e encostada ao autocarro, ocupa apenas 31%.
  • Os recortes são imagens PNG BGRA que contêm apenas a pessoa ou o autocarro — prontas para um catálogo, um segundo modelo ou uma fila de revisão.

A variante recortada do guia de isolamento usa preto em vez de um fundo transparente:

A mesma pessoa recortada à sua caixa delimitadora, com todos os píxeis fora da máscara definidos como preto

Sobreposição: onde as caixas falham#

Regras como «uma pessoa está dentro da zona restrita» ou «o produto cobre a passadeira» são testes de sobreposição. Executa o mesmo teste com caixas e com 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

As pessoas estão à frente do autocarro, por isso a máscara do autocarro termina no contorno de cada pessoa e as máscaras não partilham píxeis. As caixas são retângulos desenhados à volta de ambos, por isso sobrepõem-se bastante. O mesmo cálculo funciona com qualquer região que desenhes: preenche um polígono numa matriz de zeros com cv2.fillPoly e usa-o no lugar da máscara do autocarro.

Regra de decisão

Qualquer regra formulada como dentro, a tocar, a cobrir ou a bloquear é um teste de sobreposição. Executa-a em máscaras. As caixas são adequadas para perto de e à esquerda de, situações em que uma margem de alguns píxeis não altera a resposta.

Dos píxeis às respostas do mundo real#

PerguntaCálculoAtenção a
Qual é o tamanho?mask.sum() × (cm² por pixel)Calibra cm² por pixel uma vez usando um objeto conhecido. Numa câmera inclinada, a área muda com a distância: calibra por zona ou mede apenas numa região.
Que parte da região está coberta?pixels da máscara da classe ÷ pixels da regiãoDecide se as instâncias sobrepostas contam uma ou duas vezes.
Está dentro da zona?(mask & zone).sum() / mask.sum()Desenha a zona no tamanho original da imagem.
Qual é o formato do contorno?masks.xy[i]Consulta o aviso abaixo sobre objetos divididos antes de calcular a área com base nele.

Armadilhas#

Ler máscaras quando nada foi detetado

Sintoma: AttributeError: 'NoneType' object has no attribute 'xy' em fotogramas sem objetos. Causa: result.masks é None quando o modelo não encontra nada; não é um objeto vazio. Solução: percorre result.boxes (vazio, nunca None, para modelos de segmentação) e acede às máscaras dentro do loop, como faz masks.py, ou verifica primeiro result.masks is None.

Área do polígono em objetos divididos

Sintoma: áreas extremamente incorretas para objetos ocluídos. Causa: quando um objeto é dividido por um oclusor, masks.xy junta todos os contornos num único array, e cv2.contourArea nesse array mede uma forma que não existe. Solução: calcula as áreas a partir das contagens de píxeis de masks.data; usa os polígonos para desenhar.

Misturar resoluções

Sintoma: as áreas de duas execuções diferem por um fator de cerca de 2.85. Causa: uma execução usou retina_masks=False, pelo que as áreas estão em píxeis de inferência. Solução: mede sempre com retina_masks=True e, em CUDA, chama .cpu() antes de .numpy().

Experimente

Executa masks.py numa imagem tua com pelo menos três objetos e indica o objeto com o menor preenchimento e por que a caixa superestima o seu tamanho. Executa novamente sem retina_masks=True e anota como masks.data.shape e as áreas mudam. Depois, adapta overlap.py para testar se algum objeto na tua imagem se sobrepõe a um retângulo que desenhes sobre o terço inferior do quadro.

Concluído quando
Você concluiu a aula quando todos estes critérios forem atendidos.
  • Tens um recorte PNG transparente para cada objeto detetado na tua imagem.

  • Consegues indicar a menor proporção de preenchimento e explicar a diferença entre masks.data com e sem retina_masks=True.

  • O teu teste de zona apresenta uma sobreposição baseada na caixa e outra baseada na máscara para cada objeto, e consegues explicar qualquer objeto em que os resultados sejam diferentes.

Mostrar solução

Sem retina_masks=True, a imagem do ônibus retorna masks.data com formato (5, 640, 480): a área de cada pixel diminui por (1080 × 810) / (640 × 480) ≈ 2.85, e a máscara deixa de se alinhar com orig_img para os recortes. Quando a proporção da imagem difere do formato de inferência, o preenchimento letterbox adiciona um segundo desalinhamento.

O menor preenchimento na imagem do ônibus é o da segunda pessoa (0.31): cortada pela borda direita do quadro e de pé junto ao ônibus, a maior parte da caixa é fundo e ônibus.

Para a zona, cria-a uma vez no tamanho original e reutiliza o teste da máscara:

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

Na imagem do ônibus:

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%

As pernas das pessoas estão mesmo no terço inferior, então caixa e máscara concordam com uma diferença de cerca de dez pontos. O ônibus é o falso positivo: a caixa entra 4% na zona, mas a máscara não entra. Com uma regra de «mais de 0% dentro», as caixas assinalariam o ônibus e as máscaras não.

Próximos passos

A seguir, deixamos de lado as instâncias e rotulamos cada pixel com segmentação semântica.

Entre para salvar seu progresso