ML Learning Hub
Visionavancé

Segmentation d'Image : UNet & DeepLab

Classer chaque pixel — masques sémantiques, contours d'instances et compréhension panoptique

Classification au niveau pixel — segmentation sémantique, instance, panoptique, connexions skip dans UNet, convolutions dilatées dans DeepLab.

40 min
9 diagrammes
7 Concepts Couverts

Prérequis

Object Detection

Concepts Couverts

Semantic SegmentationInstance SegmentationUNetSkip ConnectionsDice LossmIoUDeepLab

Formules Clés

Perte Dice

Mesure le chevauchement entre les masques prédits et de référence — robuste au déséquilibre des classes

mIoU

Intersection sur Union Moyenne par classe — métrique standard de segmentation

Connexion de Saut

La connexion résiduelle UNet fusionne les caractéristiques de l'encodeur avec celles du décodeur — récupère les détails spatiaux perdus lors du sous-échantillonnage

Simulation Interactive

Loading visualization…
🎯

Segmentation : Compréhension au Niveau des Pixels

motivation

La détection d'objets donne des boîtes englobantes — des approximations grossières. La segmentation donne des masques au niveau du pixel. Cela importe pour : l'imagerie médicale (délimiter précisément le bord d'une tumeur pour la planification de la radiothérapie), la conduite autonome (distinguer la surface praticable du trottoir à chaque pixel), l'imagerie satellitaire (calculer la superficie cultivée à 1m² près), le mode portrait (séparer la personne de l'arrière-plan pour l'effet bokeh). Trois niveaux : **Segmentation sémantique** — étiqueter chaque pixel avec une classe (sans distinction d'instance). **Segmentation d'instance** — détecter et masquer des instances d'objets individuels (Mask R-CNN). **Segmentation panoptique** — combinée : choses (instances) + matière (régions amorphes comme le ciel/la route).

Un radiologue délimitant manuellement une tumeur prend 30–60 minutes par scan. Un modèle IA le fait en < 1 seconde — le goulot d'étranglement passe à la vérification de la sortie IA, pas à sa production.

💡

Architecture Encodeur–Décodeur (UNet)

intuition

UNet est l'architecture de segmentation canonique. L'encodeur (chemin contractant) est un CNN qui sous-échantillonne progressivement la carte de caractéristiques — apprenant « quoi » est dans l'image, perdant « où ». Le décodeur (chemin expansif) suréchantillonne progressivement jusqu'à la résolution d'origine via des convolutions transposées — récupérant « où ». L'innovation clé : les connexions de saut qui concaténent directement les cartes de caractéristiques de l'encodeur à chaque résolution à leur homologue dans le décodeur. Ces connexions permettent au modèle de combiner les caractéristiques sémantiques de haut niveau (des couches profondes) avec les détails spatiaux de bas niveau (des couches superficielles) — essentiels pour des frontières précises et nettes.

UNet a été conçu pour la segmentation biomédicale en 2015 avec très peu d'images d'entraînement (~30). L'efficacité des données vient des connexions de saut et de l'augmentation intensive des données.

</>

Segmentation avec torchvision et Albumentations

code
python85 lines
import torch
import torch.nn as nn
import torchvision.models as models
from torchvision.models.segmentation import DeepLabV3_ResNet101_Weights

class="tok-comment"># ── class="tok-num">1. Segmentation sémantique préentraînée (DeepLabV3) ──────────────────────
modele = models.segmentation.deeplabv3_resnet101(
    weights=DeepLabV3_ResNet101_Weights.DEFAULT
)
modele.eval()

from PIL import Image
import torchvision.transforms.functional as F
import numpy as np

img = Image.open(class="tok-str">"rue.jpg").convert(class="tok-str">"RGB")
img_t = F.to_tensor(img).unsqueeze(class="tok-num">0)   class="tok-comment"># (class="tok-num">1, class="tok-num">3, H, L)
img_t = F.normalize(img_t, mean=[class="tok-num">0.485,class="tok-num">0.456,class="tok-num">0.406], std=[class="tok-num">0.229,class="tok-num">0.224,class="tok-num">0.225])

with torch.no_grad():
    sortie = modele(img_t)[class="tok-str">"out"]         class="tok-comment"># (class="tok-num">1, class="tok-num">21, H, L) — class="tok-num">21 classes PASCAL VOC
masque_pred = sortie.argmax(dim=class="tok-num">1)[class="tok-num">0]    class="tok-comment"># (H, L) étiquettes de classes

class="tok-comment"># ── class="tok-num">2. UNet minimal ────────────────────────────────────────────────────────────
class DoubleConv(nn.Module):
    def __init__(self, entree, sortie):
        super().__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(entree, sortie, class="tok-num">3, padding=class="tok-num">1, bias=False),
            nn.BatchNorm2d(sortie), nn.ReLU(inplace=True),
            nn.Conv2d(sortie, sortie, class="tok-num">3, padding=class="tok-num">1, bias=False),
            nn.BatchNorm2d(sortie), nn.ReLU(inplace=True),
        )
    def forward(self, x): return self.conv(x)

class UNet(nn.Module):
    def __init__(self, canaux_entree=class="tok-num">3, n_classes=class="tok-num">2, canaux_base=class="tok-num">64):
        super().__init__()
        cb = canaux_base
        class="tok-comment"># Encodeur
        self.enc1 = DoubleConv(canaux_entree, cb)
        self.enc2 = DoubleConv(cb, cb*class="tok-num">2)
        self.enc3 = DoubleConv(cb*class="tok-num">2, cb*class="tok-num">4)
        self.pool = nn.MaxPool2d(class="tok-num">2)
        class="tok-comment"># Goulot d'étranglement
        self.goulot = DoubleConv(cb*class="tok-num">4, cb*class="tok-num">8)
        class="tok-comment"># Décodeur
        self.up3  = nn.ConvTranspose2d(cb*class="tok-num">8, cb*class="tok-num">4, class="tok-num">2, stride=class="tok-num">2)
        self.dec3 = DoubleConv(cb*class="tok-num">8, cb*class="tok-num">4)   class="tok-comment"># cb*class="tok-num">8 à cause de la connexion de saut
        self.up2  = nn.ConvTranspose2d(cb*class="tok-num">4, cb*class="tok-num">2, class="tok-num">2, stride=class="tok-num">2)
        self.dec2 = DoubleConv(cb*class="tok-num">4, cb*class="tok-num">2)
        self.up1  = nn.ConvTranspose2d(cb*class="tok-num">2, cb, class="tok-num">2, stride=class="tok-num">2)
        self.dec1 = DoubleConv(cb*class="tok-num">2, cb)
        class="tok-comment"># Sortie
        self.sortie_conv = nn.Conv2d(cb, n_classes, class="tok-num">1)

    def forward(self, x):
        e1 = self.enc1(x)
        e2 = self.enc2(self.pool(e1))
        e3 = self.enc3(self.pool(e2))
        g  = self.goulot(self.pool(e3))
        d3 = self.dec3(torch.cat([self.up3(g),  e3], dim=class="tok-num">1))
        d2 = self.dec2(torch.cat([self.up2(d3), e2], dim=class="tok-num">1))
        d1 = self.dec1(torch.cat([self.up1(d2), e1], dim=class="tok-num">1))
        return self.sortie_conv(d1)

unet = UNet(n_classes=class="tok-num">2)
x = torch.randn(class="tok-num">2, class="tok-num">3, class="tok-num">256, class="tok-num">256)
sortie = unet(x)
print(fclass="tok-str">"Sortie UNet : {sortie.shape}")   class="tok-comment"># (class="tok-num">2, class="tok-num">2, class="tok-num">256, class="tok-num">256)

class="tok-comment"># ── class="tok-num">3. Perte Dice ─────────────────────────────────────────────────────────────
def perte_dice(pred, cible, eps=class="tok-num">1e-6):
    class="tok-str">"""pred : (B, C, H, L) softmax, cible : (B, H, L) entier long"""
    pred_soft = torch.softmax(pred, dim=class="tok-num">1)
    cible_oh  = torch.zeros_like(pred_soft)
    cible_oh.scatter_(class="tok-num">1, cible.unsqueeze(class="tok-num">1), class="tok-num">1)
    inter = (pred_soft * cible_oh).sum(dim=(class="tok-num">2,class="tok-num">3))
    union = pred_soft.sum(dim=(class="tok-num">2,class="tok-num">3)) + cible_oh.sum(dim=(class="tok-num">2,class="tok-num">3))
    return class="tok-num">1 - (class="tok-num">2*inter + eps) / (union + eps)

pred   = torch.randn(class="tok-num">2, class="tok-num">2, class="tok-num">64, class="tok-num">64)
cible  = torch.randint(class="tok-num">0, class="tok-num">2, (class="tok-num">2, class="tok-num">64, class="tok-num">64))
perte  = perte_dice(pred, cible).mean()
print(fclass="tok-str">"Perte Dice : {perte.item():.4f}")
⚠️

Le Déséquilibre des Classes Détruit les Modèles de Segmentation

pitfall

Dans la plupart des tâches de segmentation, la classe d'arrière-plan domine — une scène de conduite pourrait être à 95% ciel+route et 5% piétons. L'entropie croisée traite tous les pixels de manière égale, donc le modèle apprend à prédire 'arrière-plan' partout et obtient 95% de précision en ratant tous les piétons. Solutions : (1) Entropie croisée pondérée — pondérer la perte inversement par la fréquence de classe. (2) Perte Dice — naturellement insensible au déséquilibre car elle mesure le rapport d'intersection, pas le nombre de pixels. (3) Focal loss (de RetinaNet) — réduit le poids des pixels bien classifiés pour que l'entraînement se concentre sur les exemples difficiles. En pratique, combinez : perte_totale = entropie_croisée + perte_dice fonctionne mieux pour l'imagerie médicale.

Vérifiez toujours l'IoU par classe dans vos métriques de validation — la précision globale cache les mauvaises performances sur les classes petites/rares qui sont souvent les plus importantes.

?Vérification des Connaissances

La progression est sauvegardée dans votre navigateur — aucun compte requis.

Besoin d'un ingénieur IA ou data scientist ?

Je conçois des modèles ML sur mesure, des agents IA, de la vision par ordinateur et de l'automatisation — de l'idée à la production.