Notebook : Segmentation d'image¶
La segmentation d'images consiste à attribuer une étiquette (comme "cheveux", "vêtement", "arrière-plan") à chaque pixel d'une image.
1. Configuration Initiale et Importations¶
Commençons par importer les bibliothèques Python nécessaires. Nous aurons besoin de :
ospour interagir avec le système de fichiers (lister les images).requestspour effectuer des requêtes HTTP vers l'API.PIL (Pillow)pour manipuler les images.matplotlib.pyplotpour afficher les images et les masques.numpypour la manipulation des tableaux (les images sont des tableaux de pixels).tqdm.notebookpour afficher une barre de progression (utile pour plusieurs images).base64etiopour décoder les masques renvoyés par l'API.
import os
import requests
from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
#from tqdm.notebook import tqdm (pas sur VS Code)
from tqdm import tqdm
import base64
import io
#ajout pour le token :
from dotenv import load_dotenv
load_dotenv()
True
Variables de Configuration¶
Nous devons définir quelques variables :
image_dir: Le chemin vers le dossier contenant vos images. Assurez-vous de modifier ce chemin si nécessaire.max_images: Le nombre maximum d'images à traiter (pour ne pas surcharger l'API ou attendre trop longtemps).api_token: Votre jeton d'API Hugging Face. IMPORTANT : Gardez ce jeton secret !
Comment obtenir un token API Hugging Face ?
- Créez un compte sur huggingface.co.
- Allez dans votre profil -> Settings -> Access Tokens.
- Créez un nouveau token (par exemple, avec le rôle "read").
- Copiez ce token ici.
#chemin vers les images
image_dir = "../assets/images/IMG"
#Nombre max d'images à traiter (pour tester)
max_images = 50
#Token API Hugging Face (chargé depuis .env)
api_token = os.getenv("HF_TOKEN")
#Vérification 1 : dossier
if not os.path.exists(image_dir):
raise FileNotFoundError(f"Le dossier '{image_dir}' n'existe pas.")
else:
print(f"Dossier '{image_dir}' existant.")
#verification 2 : token
if not api_token:
raise ValueError(
"HF_TOKEN introuvable. Vérifie ton fichier .env et son contenu."
)
else:
print("Token Hugging Face chargé avec succès.")
#affichage
print(f"Dossier images : {image_dir}")
print(f"Nombre max d'images à traiter : {max_images}")
Dossier '../assets/images/IMG' existant. Token Hugging Face chargé avec succès. Dossier images : ../assets/images/IMG Nombre max d'images à traiter : 50
2. Comprendre l'API d'Inférence Hugging Face¶
L'API d'inférence permet d'utiliser des modèles hébergés sur Hugging Face sans avoir à les télécharger ou à gérer l'infrastructure.
- Modèle utilisé : Nous allons utiliser le modèle
sayeed99/segformer_b3_clothes, spécialisé dans la segmentation de vêtements et de parties du corps. - URL de l'API : L'URL pour un modèle est généralement
https://api-inference.huggingface.co/models/NOM_DU_MODELE. - Headers (En-têtes) : Pour s'authentifier et spécifier le type de contenu, nous envoyons des en-têtes avec notre requête.
Authorization: Contient notre token API (précédé deBearer).Content-Type: Indique que nous envoyons une image au format JPEG (ou PNG selon le cas).
API_URL = f"https://router.huggingface.co/hf-inference/models/sayeed99/segformer_b3_clothes"
headers = {
"Authorization": f"Bearer {api_token}"
# Le "Content-Type" sera ajouté dynamiquement lors de l'envoi de l'image
}
# Lister les chemins des images à traiter
# Assurez-vous d'avoir des images dans le dossier 'image_dir'!
image_paths = [
os.path.join(image_dir,f)
for f in os.listdir(image_dir)
if f.lower().endswith((".jpg", ".png", ".jpeg"))
] [:max_images]
#verification
if not image_paths:
print(f"Aucune image trouvée dans '{image_dir}'. Veuillez y ajouter des images.")
else:
print(f"{len(image_paths)} image(s) à traiter : {image_paths}")
50 image(s) à traiter : ['../assets/images/IMG\\image_0.png', '../assets/images/IMG\\image_1.png', '../assets/images/IMG\\image_10.png', '../assets/images/IMG\\image_11.png', '../assets/images/IMG\\image_12.png', '../assets/images/IMG\\image_13.png', '../assets/images/IMG\\image_14.png', '../assets/images/IMG\\image_15.png', '../assets/images/IMG\\image_16.png', '../assets/images/IMG\\image_17.png', '../assets/images/IMG\\image_18.png', '../assets/images/IMG\\image_19.png', '../assets/images/IMG\\image_2.png', '../assets/images/IMG\\image_20.png', '../assets/images/IMG\\image_21.png', '../assets/images/IMG\\image_22.png', '../assets/images/IMG\\image_23.png', '../assets/images/IMG\\image_24.png', '../assets/images/IMG\\image_25.png', '../assets/images/IMG\\image_26.png', '../assets/images/IMG\\image_27.png', '../assets/images/IMG\\image_28.png', '../assets/images/IMG\\image_29.png', '../assets/images/IMG\\image_3.png', '../assets/images/IMG\\image_30.png', '../assets/images/IMG\\image_31.png', '../assets/images/IMG\\image_32.png', '../assets/images/IMG\\image_33.png', '../assets/images/IMG\\image_34.png', '../assets/images/IMG\\image_35.png', '../assets/images/IMG\\image_36.png', '../assets/images/IMG\\image_37.png', '../assets/images/IMG\\image_38.png', '../assets/images/IMG\\image_39.png', '../assets/images/IMG\\image_4.png', '../assets/images/IMG\\image_40.png', '../assets/images/IMG\\image_41.png', '../assets/images/IMG\\image_42.png', '../assets/images/IMG\\image_43.png', '../assets/images/IMG\\image_44.png', '../assets/images/IMG\\image_45.png', '../assets/images/IMG\\image_46.png', '../assets/images/IMG\\image_47.png', '../assets/images/IMG\\image_48.png', '../assets/images/IMG\\image_49.png', '../assets/images/IMG\\image_5.png', '../assets/images/IMG\\image_6.png', '../assets/images/IMG\\image_7.png', '../assets/images/IMG\\image_8.png', '../assets/images/IMG\\image_9.png']
3. Fonctions Utilitaires pour le Traitement des Masques¶
Le modèle que nous utilisons (sayeed99/segformer_b3_clothes) renvoie des masques pour différentes classes (cheveux, chapeau, etc.). Ces masques sont encodés en base64. Les fonctions :
CLASS_MAPPING: Un dictionnaire qui associe les noms de classes (ex: "Hat") à des identifiants numériques.get_image_dimensions: Récupérer les dimensions d'une image.decode_base64_mask: Décoder un masque de base64 en une image (tableau NumPy) et le redimensionner.create_masks: Combiner les masques de toutes les classes détectées en un seul masque de segmentation final, où chaque pixel a la valeur de l'ID de sa classe.
CLASS_MAPPING = {
"Background": 0,
"Hat": 1,
"Hair": 2,
"Sunglasses": 3,
"Upper-clothes": 4,
"Skirt": 5,
"Pants": 6,
"Dress": 7,
"Belt": 8,
"Left-shoe": 9,
"Right-shoe": 10,
"Face": 11,
"Left-leg": 12,
"Right-leg": 13,
"Left-arm": 14,
"Right-arm": 15,
"Bag": 16,
"Scarf": 17
}
def get_image_dimensions(img_path):
"""
Get the dimensions of an image.
Args:
img_path (str): Path to the image.
Returns:
tuple: (width, height) of the image.
"""
original_image = Image.open(img_path)
return original_image.size
def decode_base64_mask(base64_string, width, height):
"""
Decode a base64-encoded mask into a NumPy array.
Args:
base64_string (str): Base64-encoded mask.
width (int): Target width.
height (int): Target height.
Returns:
np.ndarray: Single-channel mask array.
"""
mask_data = base64.b64decode(base64_string)
mask_image = Image.open(io.BytesIO(mask_data))
mask_array = np.array(mask_image)
if len(mask_array.shape) == 3:
mask_array = mask_array[:, :, 0] # Take first channel if RGB
mask_image = Image.fromarray(mask_array).resize((width, height), Image.NEAREST)
return np.array(mask_image)
def create_masks(results, width, height):
"""
Combine multiple class masks into a single segmentation mask.
Args:
results (list): List of dictionaries with 'label' and 'mask' keys.
width (int): Target width.
height (int): Target height.
Returns:
np.ndarray: Combined segmentation mask with class indices.
"""
combined_mask = np.zeros((height, width), dtype=np.uint8) # Initialize with Background (0)
# Process non-Background masks first
for result in results:
label = result['label']
class_id = CLASS_MAPPING.get(label, 0)
if class_id == 0: # Skip Background
continue
mask_array = decode_base64_mask(result['mask'], width, height)
combined_mask[mask_array > 0] = class_id
# Process Background last to ensure it doesn't overwrite other classes unnecessarily
# (Though the model usually provides non-overlapping masks for distinct classes other than background)
for result in results:
if result['label'] == 'Background':
mask_array = decode_base64_mask(result['mask'], width, height)
# Apply background only where no other class has been assigned yet
# This logic might need adjustment based on how the model defines 'Background'
# For this model, it seems safer to just let non-background overwrite it first.
# A simple application like this should be fine: if Background mask says pixel is BG, set it to 0.
# However, a more robust way might be to only set to background if combined_mask is still 0 (initial value)
combined_mask[mask_array > 0] = 0 # Class ID for Background is 0
return combined_mask
Le modèle renvoie plusieurs masques encodés en base64, un par classe. Les fonctions utilitaires servent à décoder ces masques, les aligner sur l'image d'origine et les fusionner en un masque de segmentation final exploitable pour la visualisation et l'analyse.
Fonctions utilitaires pour affichage :¶
# Colormap personnalisé
# Palette stable : class_id -> (R, G, B)
custom_colormap = {
0: (0, 0, 0), # Background -> noir
1: (255, 0, 0), # Hat -> rouge
2: (255, 128, 0), # Hair -> orange
3: (255, 255, 0), # Sunglasses -> jaune
4: (0, 128, 255), # Upper-clothes -> bleu clair
5: (255, 0, 255), # Skirt -> magenta
6: (0, 255, 255), # Pants -> cyan
7: (0, 255, 0), # Dress -> vert
8: (128, 64, 0), # Belt -> marron
9: (128, 128, 128), # Left-shoe -> gris
10:(128, 128, 128), # Right-shoe -> gris
11:(200, 170, 140), # Face -> peau (approx)
12:(200, 170, 140), # Left-leg -> peau (approx)
13:(200, 170, 140), # Right-leg -> peau (approx)
14:(200, 170, 140), # Left-arm -> peau (approx)
15:(200, 170, 140), # Right-arm -> peau (approx)
16:(128, 0, 128), # Bag -> violet
17:(0, 100, 0), # Scarf -> vert foncé
}
# Légendes associées aux labels
legend_labels = {
"0": "Background",
"1": "Hat",
"2": "Hair",
"3": "Sunglasses",
"4": "Upper-clothes",
"5": "Skirt",
"6": "Pants",
"7": "Dress",
"8": "Belt",
"9": "Left-shoe",
"10": "Right-shoe",
"11": "Face",
"12": "Left-leg",
"13": "Right-leg",
"14": "Left-arm",
"15": "Right-arm",
"16": "Bag",
"17": "Scarf"
}
# Fonctions pour coloriser le masque et ajouter la légende
def colorize_mask(mask, colormap):
"""
Applique le colormap personnalisé au masque.
Pour chaque pixel, s'il correspond à un label défini dans colormap,
la couleur correspondante est assignée.
"""
colored_mask = np.zeros((mask.shape[0], mask.shape[1], 3), dtype=np.uint8)
for label, color in colormap.items():
colored_mask[mask == label] = color
return colored_mask
import matplotlib.patches as mpatches
def build_legend(colormap, labels):
"""
Pour chaque classe définie dans le colormap, cette fonction crée un patch
coloré avec le nom lisible de la classe, afin de pouvoir afficher une légende
lors de la visualisation du masque segmenté.
"""
patches = []
for class_id, color in colormap.items():
label_name = labels.get(str(class_id), f"class {class_id}")
color_norm = tuple(c / 255 for c in color) #couleur comprehensible par matplotlib
patches.append(
mpatches.Patch(color=color_norm, label=label_name)
)
return patches
#récupération du masque réel d'une image
from pathlib import Path
import re
def load_gt_mask_L(image_path, masks_dir="../assets/masks/Mask"):
"""
Charge le masque réel associé à une image de type image_X.png -> mask_X.png
Retourne None si le masque est introuvable (batch-safe).
"""
image_path = Path(image_path)
# Extraire l’index (ex: image_0.png → 0)
match = re.search(r"(\d+)", image_path.stem)
if match is None:
print(f"[WARN] Index introuvable dans {image_path.name}")
return None
idx = match.group(1)
mask_name = f"mask_{idx}.png"
mask_path = Path(masks_dir) / mask_name
if not mask_path.exists():
print(f"[WARN] Masque réel introuvable : {mask_path}")
return None
mask = np.array(Image.open(mask_path))
return mask
#verification de mes masques réels pour savoir si ma fonction "load_gt_mask_L" est bien adaptée à mon cas de masque
im = Image.open("../assets/masks/Mask/mask_0.png")
arr = np.array(im)
print("PIL mode:", im.mode) # 'L', 'P', 'RGB', 'RGBA'...
print("shape:", arr.shape) # (H,W) ou (H,W,3)
print("dtype:", arr.dtype)
print("unique (first 30):", np.unique(arr)[:30])
print("unique count:", len(np.unique(arr)))
PIL mode: L shape: (600, 400) dtype: uint8 unique (first 30): [ 0 2 4 6 9 10 11 12 13 15 16] unique count: 11
4. Segmentation d'une Seule Image¶
Avant de traiter toutes les images, concentrons-nous sur une seule pour bien comprendre le processus.
Étapes :
- Choisir une image.
- Ouvrir l'image en mode binaire (
"rb") et lire son contenu (data). - Déterminer le
Content-Type(par exemple,"image/jpeg"ou"image/png"). - Envoyer la requête POST à l'API avec
requests.post()en passant l'URL, les headers et les données. - Vérifier le code de statut de la réponse. Une erreur sera levée si le code n'est pas 2xx (succès) grâce à
response.raise_for_status(). - Convertir la réponse JSON en un dictionnaire Python avec
response.json(). - Utiliser nos fonctions
get_image_dimensionsetcreate_maskspour obtenir le masque final. - Afficher l'image originale et le masque segmenté.
MAX_W, MAX_H = 2000, 2000
if image_paths:
# 1.Choisir une image
single_image_path = image_paths[0] # Prenons la première image de notre liste
print(f"Traitement de l'image : {single_image_path}")
#verifier taille de l'image
with Image.open(single_image_path) as im:
w, h = im.size
if w > MAX_W or h > MAX_H:
raise ValueError(f"Image trop grande: {w}x{h} (max {MAX_W}x{MAX_H}).")
try:
# 2. Lire l'image en binaire
with open(single_image_path, "rb") as f:
image_data = f.read()
# 3. Content-Type selon extension
ext= single_image_path.lower()
if ext.endswith(".png"):
content_type = "image/png"
elif ext.endswith((".jpg",".jpeg")):
content_type = "image/jpeg"
else :
raise ValueError(f"Format d'image non supporté : {single_image_path}")
headers["Content-Type"]= content_type
# 4. Requête API
response = requests.post(API_URL, headers=headers, data=image_data, timeout=(5, 60))
# 5. Vérification du statut HTTP
response.raise_for_status()
# 6. Convertir la réponse JSON
result = response.json()
# 7. Construire le masque final (en utilisant les fonctions utilitaires pour le traitement des masques)
w, h = get_image_dimensions(single_image_path)
combined_mask= create_masks(result,w,h)
# 8. Affichage de l'image d'origine et de l'image segmentée
img= Image.open(single_image_path).convert("RGB")
plt.figure(figsize=(15,5))
plt.subplot(1,3,1)
plt.title("Image originale")
plt.imshow(img)
plt.axis("off")
colored_mask = colorize_mask(combined_mask, custom_colormap)
plt.subplot(1,3,2)
plt.title("Masque de segmentation (prédit)")
plt.imshow(colored_mask)
plt.axis("off")
legend_patches = build_legend(custom_colormap, legend_labels)
plt.legend(handles=legend_patches,bbox_to_anchor=(1.05, 1),loc="upper left",borderaxespad=0.,fontsize=10)
#masque réel
gt_mask = load_gt_mask_L(single_image_path)
gt_colored = colorize_mask(gt_mask, custom_colormap)
plt.subplot(1,3,3)
plt.title("Masque de segmentation (réel)")
plt.imshow(gt_colored)
plt.axis("off")
plt.show()
#plt.close("all")
except Exception as e:
print(f"Une erreur est survenue : {e}")
else:
print("Aucune image à traiter. Vérifiez la configuration de 'image_dir' et 'max_images'.")
5. Segmentation de Plusieurs Images (Batch)¶
Maintenant que nous savons comment traiter une image, nous pouvons créer une fonction pour en traiter plusieurs.
Cette fonction va boucler sur la liste image_paths et appliquer la logique de segmentation à chaque image.
Nous utiliserons tqdm pour avoir une barre de progression.
import time
from io import BytesIO
def resize_for_api(img: Image.Image, max_size=1024):
w, h = img.size
if max(w, h) <= max_size:
return img
ratio = max_size / max(w, h)
return img.resize((int(w*ratio), int(h*ratio)), Image.BILINEAR)
def segment_images_batch(list_of_image_paths, sleep_s=0.3):
"""
Segmente une liste d'images en utilisant l'API Hugging Face.
Args:
list_of_image_paths (list): Liste des chemins vers les images.
sleep_s (float): pause entre appel API (évite rate-limit/surcharge)
Returns:
list: Liste des masques de segmentation (tableaux NumPy).
Contient None si une image n'a pas pu être traitée.
"""
batch_segmentations = []
for img_path in tqdm(list_of_image_paths, desc="Segmentation en batch"):
try:
# 0. Verifier taille de l'image
with Image.open(img_path) as im:
im = im.convert("RGB") # optionnel mais évite des soucis (mode P/RGBA)
im = resize_for_api(im, max_size=1024)
# 1. Lire l'image en binaire
buf = BytesIO()
ext = img_path.lower()
fmt = "PNG" if ext.endswith(".png") else "JPEG"
im.save(buf, format=fmt)
image_data = buf.getvalue()
# 2. Content-Type selon extension
content_type = "image/png" if fmt == "PNG" else "image/jpeg"
# 3. Préparer headers (copie pour éviter effets de bords)
local_headers = headers.copy()
local_headers["Content-Type"]= content_type
# 4. Requête API
response = requests.post(API_URL, headers=local_headers, data=image_data, timeout=(5, 60) ) # (5,60) -> (connect_timeout, read_timeout)
# 5. Vérification du statut HTTP
response.raise_for_status()
# 6. Convertir la réponse JSON
result = response.json()
# 7. Construire le masque final
w, h = im.size
combined_mask= create_masks(result,w,h)
# 8. Ajout
batch_segmentations.append(combined_mask)
except Exception as e:
print(f"[ERREUR] {img_path} -> {e}")
batch_segmentations.append(None)
# 9. Petite pause entre appels
time.sleep(sleep_s)
return batch_segmentations
# Appeler la fonction pour segmenter les images listées dans image_paths
if image_paths:
print(f"\nTraitement de {len(image_paths)} image(s) en batch...")
batch_seg_results = segment_images_batch(image_paths)
print("Traitement en batch terminé.")
else:
batch_seg_results = []
print("Aucune image à traiter en batch.")
Traitement de 50 image(s) en batch...
Segmentation en batch: 100%|██████████| 50/50 [01:53<00:00, 2.27s/it]
Traitement en batch terminé.
6. Affichage des Résultats en Batch¶
Nous allons maintenant créer une fonction pour afficher les images originales et leurs segmentations correspondantes côte à côte, dans une grille.
import math
def display_segmented_images_batch(original_image_paths, segmentation_masks):
"""
Affiche les images originales et leurs masques segmentés.
Args:
original_image_paths (list): Liste des chemins des images originales.
segmentation_masks (list): Liste des masques segmentés (NumPy arrays).
"""
# On garde uniquement les paires valides (mask != None)
pairs = [(p, m) for p, m in zip(original_image_paths, segmentation_masks) if m is not None]
if not pairs:
print("Aucun masque valide à afficher.")
return
n = len(pairs)
nrows = n
ncols = 3 # image / masque prédit / masque réel
plt.figure(figsize=(10, 6 * nrows))
for i, (img_path, mask) in enumerate(pairs):
# image originale
ax1 = plt.subplot(nrows, ncols, i * 3 + 1)
img = Image.open(img_path).convert("RGB")
ax1.imshow(img)
ax1.set_title(f"Image {i+1}")
ax1.axis("off")
# masque prédit
colored_mask = colorize_mask(mask, custom_colormap)
ax2 = plt.subplot(nrows, ncols, i * 3 + 2)
ax2.imshow(colored_mask)
ax2.set_title("Masque prédit")
legend_patches = build_legend(custom_colormap, legend_labels)
plt.legend(handles=legend_patches,bbox_to_anchor=(1.05, 1),loc="upper left",borderaxespad=0., fontsize=8)
ax2.axis("off")
#masque réel
gt_mask = load_gt_mask_L(img_path)
ax3 = plt.subplot(nrows, ncols, i * 3 + 3)
if gt_mask is not None:
gt_colored = colorize_mask(gt_mask, custom_colormap)
ax3.imshow(gt_colored)
ax3.set_title("Masque réel")
else:
plt.text(0.5, 0.5, "Masque réel manquant", ha="center", va="center")
ax3.axis("off")
plt.tight_layout()
plt.show()
#plt.close("all")
# Afficher les résultats du batch
if batch_seg_results:
display_segmented_images_batch(image_paths, batch_seg_results)
else:
print("Aucun résultat de segmentation à afficher.")
#verification
print("len(image_paths) =", len(image_paths))
print("max_images =", max_images)
print("images =", len(image_paths))
print("masks =", len(batch_seg_results))
print("valid =", sum(m is not None for m in batch_seg_results))
print("none =", sum(m is None for m in batch_seg_results))
len(image_paths) = 50 max_images = 50 images = 50 masks = 50 valid = 50 none = 0
7. Analyse des performances du modèle sur les 50 images¶
- A - Analyse qualitative des résultats de ségmentation :
Le modèle SegFormer permet de segmenter efficacement les différentes parties du corps et les grandes catégories de vêtements. Les résultats montrent une bonne séparation entre le fond (background), le corps humain et les vêtements portés par les personnes.
Cependant, le modèle repose sur des catégories de vêtements larges. Par exemple, les vestes, manteaux, pulls et t-shirt sont regroupés dans une seule classe appelée Upper-clothes. Il n'est donc pas possible de distinguer précisement les types de hauts.
Cette limitation est directement liée aux classes définies dans le jeu de données d'entraînement du modèle et non à une erreur d'implémentation.
Les petites pièces (sunglasses, belt) sont parfois mal détectées.
- B - Justification du code couleur :
Un code couleur cohérent a été défini afin de représenter de manière stable chaque classe de segmentation. Chaque catégorie de vêtement est associée à une couleur fixe (par exemple: bleu pour Upperclothes, cyan pour Pants, jaune pour Sunglasses, orange pour Hair etc.), ce qui facilite la lecture et la compréhension des résultats entre différentes images.
Ce choix permet une meilleure interprétation visuelle des résultats, notamment lors de l'analyse en batch.
- C - Qualité des masques réels et comparaison visuelle :
- Lorsque les masques réels sont disponibles, une comparaison visuelle est effectuée entre le masque prédit par le modèle et le masque GT (de vérité terrain). Si les masques réels sont absents, cela est signalé explicitement dans la visualisation. Mais ici on a bien 50 masque GT disponibles.
- Lors de l'analyse image par image, on observe que les masques réels (GT) ne sont pas toujours parfaitement précis : certains présentent des contours plus grossiers, des détails manquants, ou des erreurs d'annotation. Ce point est important car un masque "réel" n'est pas forcément une vérité parfaite : il réflète la qualité de l'annotation du dataset, qui peut varier selon les images. En conséquence, la comparaison "masque prédit vs masque réel" montre des erreurs dans les 2 sens :
- parfois le masque prédit est visuellement plus propre (contours, séparation fond/sujet) et capture des éléments absents du masque réel;
- parfois, au contraire, le masque réel est plus fidèle et la prédiction rate des détails (subtils)
Autrement dit, l'écart observé ne correspond pas uniquement aux erreurs du modèle : il peut aussi provenir des limites/variations des annotations réelles.
- D - Tendance générales observées :
- Globalement, sur ce lot de 50 images, les contours du masque prédit sont souvent plus nets et la séparation background vs personne est fréquemment meilleure que dans les masques réels, surtout quand l'arrière plan est complexe. Cela donne l'impression que la prédiction "surpasse" le masque réel visuellement . Cela peut être expliqué par :
- une annotation réelle approximative
- un modèle qui "lisse" naturellement les frontières
Cependant, la prédiction peut aussi introduire des confusions sémantiques, notamment sur des éléments petits ou ambigus.
- Points forts du modèle :
- Les grandes régions (fond, silhouette, cheveux, haut, pantalon...) sont bien séparés
- Robustesse à certains motifs. Par exemple, lorsqu'un vêtement contient un visage imprimé (ou un motif ressemblant à une autre personne), le modèle ne le segmente pas comme un humain distinct; il reste cohérent avec la classe du vêtement.
- Accessoires souvent bien détectés
- Quand les cheveux et l'arrière plan ont des couleurs proches (ex : cheveux noirs sur fond noir), le masque réel peut sous-segmenter les cheveux. Le modèle prédit parvient à mieux récupérer cette zone, ce qui améliore le rendu global.
- Erreurs typiques et cas ambigus :
- Ceinture ("belt") est une classe difficile car fine et proche d'autres éléments. On observe des cas où elle est incomplète ou "bruité" dans le masque prédit.
- Des objets tenus en main (exemple : boisson) peuvent être confondus avec un sac "bag". Car le modèle apprend parfois "objet tenu" = accessoire, surtout si la forme est proche ou ambigue.
- Les lunettes de soleil sont plutôt bien reconnues. Mais les lunettes de vue sont plus incertaines : elles peuvent être ratées ou détectées sous forme de petits pixels isolés.
- Les détails comme bijoux, montres, bracelets ne sont pas segementés, ce qui est cohérent avec les classes attendues. Les bandeaux sont parfois captés comme des chapeaux et parfois comme des cheveux.
- Les chaussettes constituent un cas ambigu récurrent : selon les images elles sont parfois segementées comme faisant partie du pantalon ou des chaussures.En raison de leur faible visibilité ou de leur continuité visuelle avec ces éléments.
8. Méthode de validation du modèle¶
- A - Choix d'une métrique pertinente :
Nous utilisons le mean Intersection over Union (mIoU) comme métrique d'évaluation. L'IoU est une métrique standard pour la segmentation sémantique et elle permet d'évaluer strictement la qualité du recouvrement spatial entre les masques prédits et réels. L'IoU est calculée séparément pour chaque classe afin d'éviter que les classes majoritaires (notamment le fond) n'écrasent l'évaluation des classes minoritaires. La moyenne sur l'ensemble des classes présentes permet d'obtenir une métrique globale équitable. Ce choix permet d'évaluer rigoureusement la qualité de la segmentation tout en maîtrisant l'effet du déséquilibre de classes.
- B - Constitution d'un jeu d'entrainement/de validation/de test :
Avec 50 images :
Train : 30 images (pour éventuellement ajuster des choix)
Validation : 10 images (pour choisir les meilleurs paramètres)
Test : 10 images (score final "rapporté")
Nous utilisons déjà un modéle entrainé (API), ici le split sert à une évaluation honnête et non pas à entrainer le modèle.
- C - Evaluation du modèle
#verification taille masque GT = taille masque prédit . si ok on pourra directement utiliser IoU
for p, pred in zip(image_paths, batch_seg_results):
if pred is None:
continue
gt = load_gt_mask_L(p)
if gt is None:
continue
print("pred:", pred.shape, "gt:", gt.shape, "file:", p)
break
pred: (600, 400) gt: (600, 400) file: ../assets/images/IMG\image_0.png
fonctions utilitaires :
import random
def split_paths(paths, seed=42, train_n=30, val_n=10, test_n=10):
"""
Sépare une liste de chemins d'images en 3 sous-ensembles : entrainement/validation/test
"""
paths = list(paths)
random.Random(seed).shuffle(paths)
train = paths[:train_n]
val = paths[train_n:train_n+val_n]
test = paths[train_n+val_n:train_n+val_n+test_n]
return train, val, test
def iou_per_class(pred, gt, num_classes=18):
"""
calcul l'Intersection over Union (IoU) pour chaque classe présente entre un masque prédit et un masque réel.
"""
ious = {}
for c in range(num_classes):
pred_c = (pred == c)
gt_c = (gt == c)
inter = np.logical_and(pred_c, gt_c).sum()
union = np.logical_or(pred_c, gt_c).sum()
ious[c] = np.nan if union == 0 else inter / union
return ious
def miou_from_ious(ious_dict):
"""
calcul le mean IoU 0 PARTIR DES IoU par classe
"""
vals = [v for v in ious_dict.values() if not np.isnan(v)]
return float(np.mean(vals)) if vals else np.nan
def build_pred_by_path(image_paths, batch_segmentations):
"""
Associe chaque image à son masque prédit correspondant
"""
return {p: m for p, m in zip(image_paths, batch_segmentations) if m is not None}
def evaluate_on_test(test_paths, pred_by_path, num_classes=18):
"""
Evalue les performances du modèle sur le jeu de test en calculant le mIoU moyen sur toutes les images valides"""
per_image_miou = []
per_class_vals = {c: [] for c in range(num_classes)}
n_used = 0
for p in test_paths:
pred = pred_by_path.get(p)
if pred is None:
continue
gt = load_gt_mask_L(p)
if gt is None:
continue
# si shapes différents, on ne calcule pas (au cas où)
if pred.shape != gt.shape:
print(f"[SKIP shape mismatch] pred={pred.shape} gt={gt.shape} -> {p}")
continue
ious = iou_per_class(pred, gt, num_classes=num_classes)
per_image_miou.append(miou_from_ious(ious))
for c, v in ious.items():
if not np.isnan(v):
per_class_vals[c].append(v)
n_used += 1
miou_global = float(np.mean(per_image_miou)) if per_image_miou else np.nan
miou_per_class = {c: (float(np.mean(v)) if v else np.nan) for c, v in per_class_vals.items()}
return miou_global, miou_per_class, n_used
évaluation :
train_paths, val_paths, test_paths = split_paths(image_paths, seed=42)
pred_by_path = build_pred_by_path(image_paths, batch_seg_results)
miou_global, miou_per_class, n_used = evaluate_on_test(test_paths, pred_by_path)
print("Nb images test utilisées:", n_used, "/", len(test_paths))
print("mIoU global (test):", miou_global)
Nb images test utilisées: 10 / 10 mIoU global (test): 0.5851445874187551
# tri en ignorant NaN
items = [(c, v) for c, v in miou_per_class.items() if not np.isnan(v)]
items_sorted = sorted(items, key=lambda x: x[1], reverse=True)
print("Top 5 classes (IoU):", items_sorted[:5])
print("Flop 5 classes (IoU):", items_sorted[-5:])
Top 5 classes (IoU): [(0, 0.9808970297516758), (5, 0.9397323917335844), (7, 0.9296819218931693), (4, 0.9048105778156871), (6, 0.9026475292086987)] Flop 5 classes (IoU): [(9, 0.33373009101238077), (12, 0.26962102223341616), (14, 0.2558407514635559), (13, 0.14107514680998134), (10, 0.07210873468070322)]
- Le modèle obtient un mIoU global de 0.59 sur le jeu de test (10 images).
- Les meilleures performances concernent les classes dominantes et spacialement étendues (background "0", skirt "5", Dress "7", upper-clothes "4", pants "6").
- Tandis que les classes les plus fines et ambigues (chaussures, bras, jambes) affichent les IoU les plus faibles. Cette baisse pourrait s'expliquer par :
- Des ambiguités d'annotation (chaussettes/chaussures/jambes)
- Des erreurs gauche/droite
- Sensibilité aux contours
- La présence de bruit et d'imprécisions dans les masques réels peut conduire à une sous-estimation du mIoU, notamment pour les classes fines/ambigues.
- Ici, le score obtenu du mIoU est donc très acceptable. Il indique que le modèle sait obtenir des masques de segmentation de bonne qualité globale.
Coût d'utilisation :¶
L'agence souhaite avoir une idée sur l'estimation du coût pour le traitement de 500.000 images en 30 jours.¶
Les temps d’exécution ont été observés via les Inference Providers de Hugging Face, qui reposent sur une infrastructure mutualisée et présentent donc une forte variabilité. Afin d’obtenir une estimation prudente et réaliste, un temps moyen volontairement conservateur de 3 secondes par image a été retenu.
Dans ces conditions, le traitement de 500 000 images représente environ 18 jours de calcul, ce qui reste largement compatible avec une contrainte de 30 jours, même en tenant compte d’éventuels ralentissements ou erreurs ponctuelles.
Pour l’estimation du coût, l’hypothèse retenue est le déploiement d’un endpoint dédié CPU (1 vCPU – 2 Go de RAM), facturé 0,033 dollars par heure, actif 24h/24 pendant 30 jours. Le coût total est alors estimé à environ 24$, ce qui rend la solution économiquement très compétitive pour un tel volume.
Pistes d'amélioration ou d'exploration :¶
- Gestion d'erreurs plus fine : Implémenter des tentatives multiples (retry) en cas d'échec de l'API (par exemple, si le modèle est en cours de chargement).
- Obtention d'un Endpoint dédié : recommandé
- Appels asynchrones, parralélisation controlée : Pour un grand nombre d'images, des appels asynchrones (avec
asyncioetaiohttp) seraient beaucoup plus rapides. - Autres modèles : Explorer d'autres modèles de segmentation ou d'autres tâches sur Hugging Face Hub.
- Amélioration du modèle en lui-même en raffinant certaines classes (Upper clothes par exemple) ou en regroupant certaines classes (distinction gauche/droite)
- Autres images : J'ai également testé sur des images de dos ou de profil, pour préserver le visage des individus et valider si le modèle pouvait traiter ces poses.
- Données sensibles : Sensibilisation au RGDP et Ai-Act lors de la présentation du projet
- Piste d'applications métiers à présenter: analyse des tendances, veille concurrentielle, catégorisation d'images, aide à la décision produit.
Note sur l’affichage des résultats¶
Dans ce notebook partagé, l’affichage des images originales contenant des individus a été volontairement désactivé.
En conséquence, les cellules d’exécution dédiées à la visualisation de la segmentation (sur une image unique et en batch) ne présentent pas de sortie graphique.
Ce choix vise à garantir une présentation plus légère du notebook et à limiter l’exposition directe des images sources.
Un aperçu des masques de segmentation prédits pour certaines images est néanmoins disponible via le carrousel d’images intégré à la page portfolio associée à ce projet.
Bouzouita Hayette