Utilise les masques dans le code : superficie, chevauchement, contours et détourages
Lis correctement masks.data et masks.xy, mesure la superficie et le remplissage des objets, teste le chevauchement avec des masques plutôt qu'avec des boîtes et détoure les objets de l'image.
Un modèle de segmentation n'est rentable que lorsque le code transforme ses masques en décisions. Sur la photo du bus, la boîte de chaque piéton chevauche celle du bus de 56 à 74 %, alors que leurs masques ne se chevauchent pas du tout : une alerte « personne dans la zone du véhicule » fondée sur les boîtes se déclencherait pour les quatre personnes debout sur le trottoir. Les masques donnent les bonnes réponses, mais leur représentation par défaut comporte des pièges de taille et de format qui produisent des chiffres plausibles, mais erronés.
Pratique
Deux représentations d'un même masque#

result.masks contient chaque masque d'instance sous deux représentations. Choisis celle qui convient à l'opération suivante dans le code ; la référence Results répertorie tous les attributs.
| Attribut | Forme | Coordonnées | Utilisation |
|---|---|---|---|
masks.data | Tenseur N × H × W de 0/1 | Taille d'inférence par défaut ; taille d'origine avec retina_masks=True | superficies en pixels, chevauchements entre masques, détourages |
masks.xy | liste de N tableaux, chacun de P × 2 | pixels de l'image d'origine | tracer les contours, exporter les polygones |
masks.xyn | liste de N tableaux, chacun de P × 2 | normalisées de 0 à 1 | réécrire les annotations polygonales YOLO |
Pour l'image de bus de 1080 × 810, masks.data renvoie un résultat de taille (5, 640, 480) — la taille d'inférence avec letterboxing —, si bien que les nombres de pixels sont environ 2.85× trop faibles et que le masque ne peut pas se superposer à l'image d'origine. retina_masks=True agrandit les masques jusqu'à la résolution d'origine, au prix d'un léger coût de post-traitement. Utilise-le chaque fois que tu mesures ou recadres.
Mesure et détoure chaque objet#
Ce script lit les deux représentations de chaque objet : la superficie en pixels à partir de masks.data, la taille du contour à partir de masks.xy et un détourage PNG transparent qui utilise le masque comme canal alpha. Le guide pour isoler des objets par segmentation présente d'autres variantes de détourage.
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 ptsVoici ce qu'il faut lire dans le résultat :
masks.data: (5, 1080, 810)correspond àorig_shape;retina_masks=Truea donc rempli son rôle et chaque nombre de pixels est exprimé dans la résolution d'origine.- L'indice
irelie les deux.result.masks.data[i]appartient àresult.boxes[i]; les noms et les scores de confiance proviennent donc de la boîte, et les pixels du masque. - Le remplissage révèle ce qu'un détecteur ne peut pas montrer : le bus occupe 62 % de sa boîte, tandis que la deuxième personne, coupée par le bord droit de l'image et debout contre le bus, n'en occupe que 31 %.
- Les détourages sont des images PNG BGRA contenant uniquement la personne ou le bus — prêts pour un catalogue, un second modèle ou une file de vérification.
La variante recadrée du guide d'isolation utilise un fond noir plutôt qu'un fond transparent :

Chevauchement : où se trouvent les boîtes#
Les règles telles que « une personne se trouve dans la zone interdite » ou « le produit recouvre le convoyeur » reposent sur des tests de chevauchement. Effectue le même test avec les boîtes et avec les masques :
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 maskLes personnes se tiennent devant le bus ; le masque du bus s'arrête donc au contour de chaque personne et les masques ne partagent aucun pixel. Les boîtes sont des rectangles tracés autour de ces objets ; elles se chevauchent donc fortement. Le même calcul fonctionne pour n'importe quelle région que tu dessines toi-même : remplis un polygone dans un tableau de zéros avec cv2.fillPoly et utilise-le à la place du masque du bus.
Toute règle formulée avec les termes à l'intérieur, touchant, couvrant ou bloquant correspond à un test de chevauchement. Effectue-le sur des masques. Les boîtes conviennent pour près de et à gauche de, où une marge de quelques pixels ne change pas la réponse.
Des pixels aux réponses concrètes#
| Question | Calcul | Attention à |
|---|---|---|
| Quelle est sa taille ? | mask.sum() × (cm² par pixel) | Calibre une fois les cm² par pixel à l’aide d’un objet connu. Avec une caméra inclinée, la surface varie selon la distance : calibre chaque zone ou mesure dans une seule région. |
| Quelle part de la région est couverte ? | pixels du masque de classe ÷ pixels de la région | Détermine si les instances qui se chevauchent sont comptées une ou deux fois. |
| Est-il dans la zone ? | (mask & zone).sum() / mask.sum() | Dessine la zone à la taille d’origine de l’image. |
| À quoi ressemble son contour ? | masks.xy[i] | Consulte l’avertissement ci-dessous sur les objets fragmentés avant d’en calculer la surface. |
Pièges#
Symptôme : AttributeError: 'NoneType' object has no attribute 'xy' sur des images sans objet. Cause : result.masks vaut None lorsque le modèle ne trouve rien ; ce n'est pas un objet vide. Solution : parcoure result.boxes (vide, jamais None, pour les modèles de segmentation) et accède aux masques dans la boucle, comme le fait masks.py, ou vérifie d'abord result.masks is None.
Symptôme : des aires très éloignées de la réalité pour les objets occultés. Cause : lorsqu'un objet est fragmenté par un élément qui l'occulte, masks.xy rassemble tous les contours dans un seul tableau et cv2.contourArea mesure, dans ce tableau, une forme qui n'existe pas. Solution : calcule les aires à partir du nombre de pixels de masks.data ; conserve les polygones pour le dessin.
Symptôme : les aires de deux exécutions diffèrent d'un facteur d'environ 2.85. Cause : une exécution utilisait retina_masks=False, donc ses aires sont exprimées en pixels d'inférence. Solution : effectue toutes les mesures avec retina_masks=True et, sur CUDA, appelle .cpu() avant .numpy().
Afficher la solution
Sans retina_masks=True, l’image du bus renvoie masks.data avec la forme (5, 640, 480) : la surface de chaque pixel est réduite d’un facteur (1080 × 810) / (640 × 480) ≈ 2.85, et le masque ne s’aligne plus sur orig_img pour les découpes. Lorsque le rapport hauteur/largeur de l’image diffère de la taille d’inférence, le remplissage letterbox ajoute un deuxième décalage.
Le taux de remplissage le plus faible sur l’image du bus est celui de la deuxième personne (0.31) : coupée par le bord droit de l’image et debout contre le bus, sa boîte contient donc surtout l’arrière-plan et le bus.
Pour la zone, crée-la une fois à la taille d’origine, puis réutilise le test du masque :
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%}")Sur l’image du bus :
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%Les jambes des personnes se trouvent bien dans le tiers inférieur ; les résultats de la boîte et du masque concordent donc à une dizaine de points près. Le bus est le faux positif : sa boîte empiète de 4 % sur la zone, mais pas son masque. Avec une règle « plus de 0 % à l’intérieur », les boîtes signaleraient le bus, contrairement aux masques.