Ultralytics YOLO27 : Inscris-toi sur la liste d’attente.
Get Started
Leçonintermédiaire

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.

Résultat

Exécute des scripts qui indiquent pour chaque objet la superficie de son masque, son taux de remplissage, la taille de son contour et son chevauchement avec un autre objet, puis enregistrent un détourage transparent par objet — en utilisant la bonne représentation du masque pour chaque opération.

Parcours accéléré
Si tu connais déjà les bases, voici la version courte.
  1. Lance les prédictions avec retina_masks=True chaque fois que tu mesures ou recadres une image, afin que les masques correspondent à sa taille d'origine.

  2. Utilise masks.data (N × H × W) pour calculer les superficies, les chevauchements et les détourages ; utilise masks.xy (polygones en pixels) pour dessiner et exporter les contours.

  3. La superficie est mask.sum() ; le remplissage correspond à la superficie du masque ÷ celle de la boîte ; le chevauchement est (mask_a & mask_b).sum().

  4. Vérifie result.masks is None avant de lire les masques : sa valeur est None si aucun objet n'est détecté.

Pratique

Deux représentations d'un même masque#

Une personne isolée à l'aide d'un masque de segmentation Ultralytics YOLO, conservée à sa place dans l'image complète sur un arrière-plan noir

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.

AttributFormeCoordonnéesUtilisation
masks.dataTenseur N × H × W de 0/1Taille d'inférence par défaut ; taille d'origine avec retina_masks=Truesuperficies en pixels, chevauchements entre masques, détourages
masks.xyliste de N tableaux, chacun de P × 2pixels de l'image d'originetracer les contours, exporter les polygones
masks.xynliste de N tableaux, chacun de P × 2normalisées de 0 à 1réécrire les annotations polygonales YOLO
Par défaut, masks.data utilise la taille d'inférence

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 pts

Voici ce qu'il faut lire dans le résultat :

  • masks.data: (5, 1080, 810) correspond à orig_shape ; retina_masks=True a donc rempli son rôle et chaque nombre de pixels est exprimé dans la résolution d'origine.
  • L'indice i relie 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 :

La même personne recadrée selon sa boîte englobante, chaque pixel situé à l'extérieur du masque étant mis en noir

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 mask

Les 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.

Règle de décision

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#

QuestionCalculAttention à
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égionDé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#

Lire les masques lorsqu'aucun objet n'a été détecté

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.

Aire des polygones pour les objets fragmentés

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.

Mélanger les résolutions

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().

Essayer

Exécute masks.py sur une image de ton choix contenant au moins trois objets, puis indique l’objet dont le taux de remplissage est le plus faible et explique pourquoi sa boîte surestime sa taille. Relance sans retina_masks=True et note les changements de masks.data.shape et des surfaces. Adapte ensuite overlap.py pour vérifier si un objet de ton image chevauche un rectangle que tu dessines sur le tiers inférieur de l’image.

Terminé lorsque
Tu as terminé la leçon lorsque tous les éléments suivants sont réunis.
  • Tu as une découpe PNG transparente pour chaque objet détecté dans ton image.

  • Tu peux indiquer le taux de remplissage le plus faible et expliquer la différence entre masks.data avec et sans retina_masks=True.

  • Ton test de zone affiche un chevauchement basé sur la boîte et un chevauchement basé sur le masque pour chaque objet, et tu peux expliquer les cas où les résultats diffèrent.

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.

Et ensuite ?

Ensuite, nous abandonnons complètement les instances et étiquetons chaque pixel avec la segmentation sémantique.