À la fin de ce chapitre, vous saurez :
Dans les chapitres précédents, nous avons travaillé sur la classification d'images : le modèle devait répondre à la question "Qu'est-ce qu'il y a dans cette image ?"
Exemple :
# Classification : une image → une classe
output = model(image) # Shape: [batch_size, num_classes]
predicted_class = torch.argmax(output, dim=1)
print(f"Cette image contient : {classes[predicted_class]}")
La détection d'objets va plus loin : le modèle doit répondre à "Qu'est-ce qu'il y a dans cette image ET où se trouve chaque objet ?"
Pour chaque objet détecté, le modèle doit fournir :
Exemple de sortie :
# Détection : une image → plusieurs objets localisés
outputs = model(image)
# outputs[0]['boxes']: tensor([[x1, y1, x2, y2], [x1, y1, x2, y2], ...])
# outputs[0]['labels']: tensor([1, 3, 1, ...]) # IDs des classes
# outputs[0]['scores']: tensor([0.95, 0.87, 0.76, ...]) # Confiances
💡 Intuition : imaginez que vous regardez une photo de famille. La classification dirait "photo de groupe", tandis que la détection indiquerait "3 personnes aux positions (x1,y1,x2,y2), (x3,y3,x4,y4), (x5,y5,x6,y6)".
Une boîte englobante est un rectangle défini par 4 valeurs. Il existe plusieurs façons de représenter ces coordonnées :
Format 1 : (x1, y1, x2, y2) —> coins de la boîte (PyTorch/torchvision)
x1, y1 : coordonnées du coin supérieur gauchex2, y2 : coordonnées du coin inférieur droitFormat 2 : (x, y, w, h) —> coin + dimensions (Label Studio format standard)
x, y : coordonnées du coin supérieur gauche (en % )w : largeur de la boîteh : hauteur de la boîteFormat 3 : (x_center, y_center, w, h) normalisé (Label Studio format utilisé pour YOLO)
x_center, y_center : coordonnées du centre (normalisées entre 0 et 1)w, h : largeur et hauteur (normalisées entre 0 et 1)Exemple d'une image $$640×480$$ pixels avec un objet :
Format PyTorch : [100, 50, 300, 250]
→ Rectangle du pixel (100,50) au pixel (300,250)
Format Label Studio : [15.625, 10.417, 31.25, 41.67]
→ Coin en (15.625%, 10.417%), taille 31.25%×41.67% de l'image
Format YOLO : [0.3125, 0.3125, 0.3125, 0.4167]
→ Centre à 31.25% de la largeur/hauteur, boîte de 31.25%×41.67% de l'image
⚠️ Format utilisé dans la suite du TP
Dans la suite de ce chapitre, nous utiliserons le Format 3 (YOLO) avec des coordonnées normalisées. C'est le format standard pour la détection d'objets, compatible avec YOLO et la plupart des frameworks modernes.
La détection d'objets est au cœur de nombreuses applications :
💡 Dans ce chapitre, nous allons apprendre à créer notre propre détecteur d'objets personnalisé, de A à Z !
Le pipeline complet pour créer un dataset de détection :
Voyons chaque étape en détail.
Objectif : filmer l'objet que vous voulez détecter sous différents angles et conditions.
Conseils pratiques :
💡 Astuce : plus vous capturez de variété, meilleur sera votre détecteur !
💡 Vous pouvez commencer avec beaucoup moins !
OpenCV (cv2) est une bibliothèque Python très puissante pour manipuler des vidéos. Elle s'utilise directement en Python sans installer d'outils externes.
# Installer OpenCV dans votre environnement virtuel
pip install opencv-python
Voici un script complet pour extraire toutes les frames d'une vidéo :
import cv2
import os
def extraire_frames(video_path, output_dir):
"""
Extrait toutes les frames d'une vidéo.
Args:
video_path: chemin vers la vidéo
output_dir: dossier où sauvegarder les images
"""
# Créer le dossier de sortie (exist_ok=True évite l'erreur si le dossier existe déjà)
os.makedirs(output_dir, exist_ok=True)
# Ouvrir la vidéo
cap = cv2.VideoCapture(video_path)
# Vérifier que la vidéo s'ouvre correctement
if not cap.isOpened():
print(f"❌ Erreur : impossible d'ouvrir {video_path}")
return
# Obtenir les propriétés de la vidéo (fps, nombre total de frames)
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print(f"📹 Vidéo : {total_frames} frames à {fps:.2f} fps")
frame_count = 0
while True:
# Lire la frame suivante
ret, frame = cap.read()
# Si plus de frames, sortir de la boucle
if not ret:
break
# Sauvegarder la frame en jpg pour compression
output_path = os.path.join(output_dir, f'frame_{frame_count:05d}.jpg')
cv2.imwrite(output_path, frame)
frame_count += 1
# Libérer les ressources
cap.release()
print(f"✓ {frame_count} frames extraites dans {output_dir}")
# Utilisation
extraire_frames('ma_video.mp4', 'frames/')
⚠️ Attention à la quantité !
Une vidéo de 30 secondes à 30 fps génère 900 images. C'est souvent trop pour annoter manuellement !
Pour réduire le nombre d'images à annoter, on extrait seulement certaines frames :
import cv2
import os
def extraire_frames_espacees(video_path, output_dir, intervalle=10):
"""
Extrait 1 frame tous les N frames.
Args:
video_path: chemin vers la vidéo
output_dir: dossier de sortie
intervalle: extraire 1 frame tous les N frames (ex: 10)
"""
os.makedirs(output_dir, exist_ok=True)
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"❌ Erreur : impossible d'ouvrir {video_path}")
return
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
print(f"📹 Extraction de 1 frame tous les {intervalle} frames")
print(f" Total attendu : ~{total_frames // intervalle} images")
frame_count = 0
saved_count = 0
while True:
ret, frame = cap.read()
if not ret:
break
# Sauvegarder seulement toutes les N frames
if frame_count % intervalle == 0:
output_path = os.path.join(output_dir, f'frame_{saved_count:05d}.jpg')
cv2.imwrite(output_path, frame)
saved_count += 1
frame_count += 1
cap.release()
print(f"✓ {saved_count} frames extraites sur {frame_count} totales")
# Exemple : extraire 1 frame toutes les 10 frames
extraire_frames_espacees('ma_video.mp4', 'frames/', intervalle=10)
Recommandation pratique : pour débuter, extraire 50-200 images est un bon compromis entre travail d'annotation et qualité du modèle.
Règles de calcul de l'intervalle :
intervalle=30intervalle=10intervalle=9Pour économiser l'espace disque et accélérer le traitement, on peut redimensionner directement.
⚠️ Attention à la déformation !
Si votre vidéo n'est pas carrée (ex : \(1920×1080\)) et que vous redimensionnez en carré (ex : \(224×224\)), l'image sera déformée (écrasée ou étirée).
Deux solutions :
Voici les deux approches :
Approche 1 : Crop au centre (recommandée - pas de déformation)
⚠️ Code utilisé dans la suite du TP
C'est cette fonction (extraire_frames_crop_redimensionner) que nous utiliserons dans tous les exercices du chapitre. Elle évite les déformations en découpant un carré au centre de l'image avant de redimensionner.
import cv2
import os
def extraire_frames_crop_redimensionner(video_path, output_dir, intervalle=10,
target_size=224):
"""
Extrait, crop au centre en carré, puis redimensionne.
ÉVITE la déformation en découpant l'image.
Args:
video_path: chemin vers la vidéo
output_dir: dossier de sortie
intervalle: extraire 1 frame tous les N frames
target_size: taille finale du carré (ex: 224 pour 224×224)
"""
os.makedirs(output_dir, exist_ok=True)
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"❌ Erreur : impossible d'ouvrir {video_path}")
return
# Obtenir les dimensions originales
original_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
original_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"📹 Résolution originale : {original_width}×{original_height}")
print(f"📐 Nouvelle résolution : {target_size}×{target_size} (carré)")
print(f"✂️ Méthode : Crop au centre (pas de déformation)")
frame_count = 0
saved_count = 0
while True:
ret, frame = cap.read()
if not ret:
break
if frame_count % intervalle == 0:
# ÉTAPE 1 : Crop au centre pour obtenir un carré
h, w = frame.shape[:2]
size = min(h, w) # Prendre la plus petite dimension
# Calculer les coordonnées du crop au centre
start_y = (h - size) // 2
start_x = (w - size) // 2
# Découper le carré au centre
cropped = frame[start_y:start_y+size, start_x:start_x+size]
# ÉTAPE 2 : Redimensionner le carré à la taille souhaitée
resized = cv2.resize(cropped, (target_size, target_size))
# Sauvegarder
output_path = os.path.join(output_dir, f'frame_{saved_count:05d}.jpg')
cv2.imwrite(output_path, resized)
saved_count += 1
frame_count += 1
cap.release()
print(f"✓ {saved_count} frames extraites, croppées et redimensionnées")
# Exemple : extraire 1 frame/seconde en 224×224 (format standard CNN)
extraire_frames_crop_redimensionner('ma_video.mp4', 'frames/',
intervalle=30, target_size=224)
Approche 2 : Redimensionnement direct (DÉCONSEILLÉ si ratio différent)
def extraire_frames_redimensionner_simple(video_path, output_dir, intervalle=10,
target_width=640, target_height=480):
"""
Redimensionne directement sans crop.
⚠️ ATTENTION : déforme l'image si le ratio change !
"""
os.makedirs(output_dir, exist_ok=True)
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"❌ Erreur : impossible d'ouvrir {video_path}")
return
original_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
original_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(f"📹 Résolution originale : {original_width}×{original_height}")
print(f"📐 Nouvelle résolution : {target_width}×{target_height}")
# Vérifier si le ratio va changer
original_ratio = original_width / original_height
target_ratio = target_width / target_height
if abs(original_ratio - target_ratio) > 0.01:
print(f"⚠️ ATTENTION : Le ratio va changer !")
print(f" Original : {original_ratio:.2f}")
print(f" Cible : {target_ratio:.2f}")
print(f" → L'image sera déformée !")
frame_count = 0
saved_count = 0
while True:
ret, frame = cap.read()
if not ret:
break
if frame_count % intervalle == 0:
# Redimensionner directement (PEUT DÉFORMER !)
resized = cv2.resize(frame, (target_width, target_height))
output_path = os.path.join(output_dir, f'frame_{saved_count:05d}.jpg')
cv2.imwrite(output_path, resized)
saved_count += 1
frame_count += 1
cap.release()
print(f"✓ {saved_count} frames extraites et redimensionnées")
# Exemple : ⚠️ Vidéo 16:9 → carré = DÉFORMATION !
# extraire_frames_redimensionner_simple('ma_video.mp4', 'frames/',
# intervalle=30,
# target_width=224, target_height=224)
💡 Recommandations :
Label Studio est un outil open-source d'annotation collaborative qui permet de créer des boîtes englobantes, de gérer plusieurs annotateurs et d'exporter dans différents formats.
# Installer Label Studio (dans votre environnement virtuel)
pip install label-studio
# Lancer Label Studio
label-studio start
# L'interface web s'ouvre automatiquement sur http://localhost:8080
Premier lancement : création du compte
Au premier lancement, Label Studio vous demande de créer un compte.
💡 Travail collaboratif
Si vous souhaitez travailler en équipe, vous pourrez inviter vos collègues via "Invite People" (voir section 3.5). Label Studio leur enverra automatiquement un email d'invitation.
Si vous avez déjà un compte : entrez simplement votre email et mot de passe pour vous connecter.
En cas de problème : si Label Studio ne s'ouvre pas automatiquement, ouvrez manuellement votre navigateur et allez sur http://localhost:8080
Étapes dans l'interface web :
frames/ (ou le dossier où vous avez extrait les images)Après avoir choisi le template, une interface apparaît où vous pouvez définir vos labels (classes d'objets).
Méthode simple : ajouter des labels via l'interface
Si vous préférez éditer le code XML directement, vous pouvez voir/modifier le code de configuration :
Exemple pour détecter des bouteilles et des gobelets :
<View>
<Image name="image" value="$image"/>
<RectangleLabels name="label" toName="image">
<Label value="bouteille" background="green"/>
<Label value="gobelet" background="blue"/>
</RectangleLabels>
</View>
💡 Astuce : commencez avec une seule classe pour simplifier. Vous pourrez toujours ajouter des classes plus tard.
Pour créer une annotation :
Modifier une annotation existante :
SupprBonnes pratiques d'annotation :
Pour travailler en équipe sur l'annotation, suivez ces étapes :
Étape 1 : Inviter des personnes
marie.dubois@exemple.com, paul.martin@exemple.com)Étape 2 : Ajouter les membres à votre projet
Une fois les invitations acceptées :
Étape 3 : Répartir le travail (optionnel mais recommandé)
Pour éviter que deux personnes annotent les mêmes images :
Exemple de workflow collaboratif :
💡 Astuce qualité : faites annoter 10 images par deux personnes différentes et comparez. Un IoU (Intersection over Union) > 0.7 indique une bonne cohérence entre annotateurs.
Pour accélérer l'annotation :
1, 2, 3... : sélectionner la classe 1, 2, 3...Ctrl + Enter ou Cmd + Enter : soumettre et passer à l'image suivanteCtrl + Z : annuler la dernière actionSuppr : supprimer la boîte sélectionnéeFlèches : ajuster finement la position d'une boîteAprès avoir terminé l'annotation de vos images dans Label Studio, vous devez exporter les données pour l'entraînement. Dans ce chapitre, nous allons utiliser le format "YOLO with Images".
Le format "YOLO with Images" est un export complet proposé par Label Studio qui contient :
images/ : toutes vos images annotéeslabels/ : un fichier texte .txt par image contenant les annotationsclasses.txt : la liste des noms de classes (un par ligne)notes.json : métadonnées sur l'exportC'est le format idéal car il regroupe tout ce dont vous avez besoin pour l'entraînement dans une seule archive ZIP.
Format d'annotation YOLO : un fichier texte par image avec des coordonnées normalisées.
Exemple de fichier labels/frame_00001.txt :
.3125 0.3125 0.3125 0.4167
.6250 0.5417 0.1562 0.2500
Format d'une ligne : class_id x_center y_center width height
Toutes les valeurs sont normalisées entre 0 et 1 :
class_id : entier (0, 1, 2...) correspondant à l'index de la classex_center : position X du centre de la boîte / largeur de l'imagey_center : position Y du centre de la boîte / hauteur de l'imagewidth : largeur de la boîte / largeur de l'imageheight : hauteur de la boîte / hauteur de l'imageExemple concret (image \(640×480\), objet de 100,50 à 300,200) :
# Coordonnées en pixels (format classique)
x1, y1, x2, y2 = 100, 50, 300, 200
img_width, img_height = 640, 480
# Conversion en format YOLO
x_center = ((x1 + x2) / 2) / img_width # Centre X : (100+300)/2 / 640 = 0.3125
y_center = ((y1 + y2) / 2) / img_height # Centre Y : (50+200)/2 / 480 = 0.2604
width = (x2 - x1) / img_width # Largeur : (300-100) / 640 = 0.3125
height = (y2 - y1) / img_height # Hauteur : (200-50) / 480 = 0.3125
# Résultat : "0 0.3125 0.2604 0.3125 0.3125"
Structure complète après export :
dataset_yolo/
├── images/ # Toutes vos images annotées
│ ├── frame_00001.jpg
│ ├── frame_00002.jpg
│ └── ...
├── labels/ # Fichiers .txt (même nom que l'image)
│ ├── frame_00001.txt
│ ├── frame_00002.txt
│ └── ...
├── classes.txt # Liste des classes : une par ligne
└── notes.json # Métadonnées (optionnel)
Fichier classes.txt exemple :
cube
bouteille
gobelet
💡 L'ordre des classes dans classes.txt définit les IDs : cube=0, bouteille=1, gobelet=2.
1. Accéder à l'export
2. Choisir le format
3. Télécharger l'archive
project-X-at-YYYY-MM-DD-HH-MM-XX.zip4. Extraire l'archive
# Décompresser l'archive
unzip project-1-at-2024-01-15-14-30-00.zip -d dataset_yolo/
# Vérifier le contenu
ls -R dataset_yolo/
# Vous devriez voir :
# dataset_yolo/
# ├── images/
# ├── labels/
# ├── classes.txt
# └── notes.json
Avant de commencer l'entraînement, vérifiez toujours que l'export est correct :
import os
def verify_yolo_export(dataset_path):
"""Vérifie la structure d'un export YOLO."""
images_dir = os.path.join(dataset_path, 'images')
labels_dir = os.path.join(dataset_path, 'labels')
classes_file = os.path.join(dataset_path, 'classes.txt')
# Vérifier que les dossiers existent
assert os.path.exists(images_dir), "❌ Dossier 'images/' manquant"
assert os.path.exists(labels_dir), "❌ Dossier 'labels/' manquant"
assert os.path.exists(classes_file), "❌ Fichier 'classes.txt' manquant"
# Compter les fichiers
images = [f for f in os.listdir(images_dir) if f.endswith(('.jpg', '.png'))]
labels = [f for f in os.listdir(labels_dir) if f.endswith('.txt')]
print(f"✅ Structure YOLO valide")
print(f" 📁 Images : {len(images)}")
print(f" 📁 Labels : {len(labels)}")
# Charger les classes
with open(classes_file, 'r') as f:
classes = [line.strip() for line in f.readlines()]
print(f" 📋 Classes ({len(classes)}) : {classes}")
# Vérifier la correspondance images/labels
missing_labels = []
for img in images:
label_name = os.path.splitext(img)[0] + '.txt'
if label_name not in labels:
missing_labels.append(img)
if missing_labels:
print(f"\n⚠️ {len(missing_labels)} images sans annotation :")
for img in missing_labels[:5]:
print(f" - {img}")
else:
print(f"\n✅ Toutes les images ont leurs annotations")
# Vérifier un fichier d'annotation
if labels:
sample_label = os.path.join(labels_dir, labels[0])
with open(sample_label, 'r') as f:
lines = f.readlines()
print(f"\n📄 Exemple d'annotation ({labels[0]}) :")
for line in lines[:3]:
print(f" {line.strip()}")
# 🎯 UTILISATION
verify_yolo_export('dataset_yolo/')
Maintenant que vous avez exporté votre dataset au format YOLO, créons un Dataset PyTorch personnalisé pour le charger.
Après extraction de l'archive ZIP, votre dataset doit avoir cette structure :
dataset_yolo/
├── images/ # Toutes vos images annotées
│ ├── frame_00001.jpg
│ ├── frame_00002.jpg
│ └── ...
├── labels/ # Fichiers .txt (même nom que l'image)
│ ├── frame_00001.txt
│ ├── frame_00002.txt
│ └── ...
└── classes.txt # Liste des classes (une par ligne)
Voici une implémentation complète qui charge le format YOLO et gère intelligemment le redimensionnement :
import torch
from torch.utils.data import Dataset
from PIL import Image
import os
from torchvision import transforms
class YOLODetectionDataset(Dataset):
"""
Dataset PyTorch pour le format YOLO (images + labels .txt).
Compatible avec l'export YOLO de Label Studio.
"""
def __init__(self, images_dir, labels_dir, classes_file, img_size=224, custom_transforms=None):
"""
Args:
images_dir: dossier contenant les images
labels_dir: dossier contenant les labels .txt au format YOLO
classes_file: chemin vers classes.txt
img_size: taille de redimensionnement (défaut: 224x224)
custom_transforms: transformations à appliquer (optionnel)
"""
self.images_dir = images_dir
self.labels_dir = labels_dir
self.img_size = img_size
self.custom_transforms = custom_transforms
# Pas de transformation automatique - on gérera le resize manuellement
# pour ajuster les coordonnées des bounding boxes en conséquence
self.to_tensor = transforms.ToTensor()
# Charger les noms de classes
with open(classes_file, 'r') as f:
self.classes = [line.strip() for line in f.readlines()]
# Liste des images (on suppose que chaque image a son label correspondant)
self.image_files = sorted([f for f in os.listdir(images_dir)
if f.endswith(('.jpg', '.jpeg', '.png'))])
print(f"✅ Dataset YOLO initialisé:")
print(f" - {len(self.image_files)} images")
print(f" - {len(self.classes)} classes : {self.classes}")
def __len__(self):
return len(self.image_files)
def __getitem__(self, idx):
"""
Charge une image et ses annotations au format YOLO.
Returns:
img: tensor [3, H, W]
target: dict avec 'boxes' (format [x1, y1, x2, y2] en pixels),
'labels', 'image_id'
"""
# Charger l'image
img_filename = self.image_files[idx]
img_path = os.path.join(self.images_dir, img_filename)
img = Image.open(img_path).convert('RGB')
orig_width, orig_height = img.size
# Calculer le ratio de resize pour garder l'aspect ratio
scale = self.img_size / max(orig_width, orig_height)
new_width = int(orig_width * scale)
new_height = int(orig_height * scale)
# Resize en gardant l'aspect ratio
img = img.resize((new_width, new_height), Image.BILINEAR)
# Créer une image carrée avec padding noir
padded_img = Image.new('RGB', (self.img_size, self.img_size), (0, 0, 0))
# Centrer l'image resizée
paste_x = (self.img_size - new_width) // 2
paste_y = (self.img_size - new_height) // 2
padded_img.paste(img, (paste_x, paste_y))
# Charger le label correspondant
label_filename = os.path.splitext(img_filename)[0] + '.txt'
label_path = os.path.join(self.labels_dir, label_filename)
boxes = []
labels = []
# Lire le fichier de labels si il existe
if os.path.exists(label_path):
with open(label_path, 'r') as f:
for line in f:
parts = line.strip().split()
if len(parts) == 5:
class_id = int(parts[0])
x_center = float(parts[1])
y_center = float(parts[2])
width = float(parts[3])
height = float(parts[4])
# Convertir du format YOLO normalisé vers pixels dans l'image originale
x_center_orig = x_center * orig_width
y_center_orig = y_center * orig_height
width_orig = width * orig_width
height_orig = height * orig_height
# Appliquer le scale et le padding
x_center_scaled = x_center_orig * scale + paste_x
y_center_scaled = y_center_orig * scale + paste_y
width_scaled = width_orig * scale
height_scaled = height_orig * scale
# Convertir en format [x1, y1, x2, y2]
x1 = x_center_scaled - width_scaled / 2
y1 = y_center_scaled - height_scaled / 2
x2 = x_center_scaled + width_scaled / 2
y2 = y_center_scaled + height_scaled / 2
boxes.append([x1, y1, x2, y2])
labels.append(class_id + 1) # +1 car background=0 dans certains modèles
# Convertir en tenseurs
boxes = torch.as_tensor(boxes, dtype=torch.float32)
labels = torch.as_tensor(labels, dtype=torch.int64)
# Créer le dictionnaire target
target = {}
target['boxes'] = boxes
target['labels'] = labels
target['image_id'] = torch.tensor([idx])
# Si aucune boîte, créer des tenseurs vides
if len(boxes) == 0:
target['boxes'] = torch.zeros((0, 4), dtype=torch.float32)
target['labels'] = torch.zeros((0,), dtype=torch.int64)
# Convertir l'image en tensor
img = self.to_tensor(padded_img)
# Appliquer les transformations personnalisées si fournies
if self.custom_transforms:
img = self.custom_transforms(img)
return img, target
def get_class_name(self, class_id):
"""Retourne le nom d'une classe depuis son ID (class_id - 1 car on a ajouté 1)."""
return self.classes[class_id - 1]
💡 Gestion intelligente du redimensionnement
Chargez le dataset et créez les splits train/val/test :
from torch.utils.data import DataLoader, random_split
# Charger le dataset YOLO
full_dataset = YOLODetectionDataset(
images_dir='dataset_yolo/images',
labels_dir='dataset_yolo/labels',
classes_file='dataset_yolo/classes.txt'
)
# Split : 70% train, 15% val, 15% test
total_size = len(full_dataset)
train_size = int(0.70 * total_size)
val_size = int(0.15 * total_size)
test_size = total_size - train_size - val_size
train_dataset, val_dataset, test_dataset = random_split(
full_dataset,
[train_size, val_size, test_size],
generator=torch.Generator().manual_seed(42)
)
print(f"\n📂 Split du dataset :")
print(f" Train : {len(train_dataset)} images")
print(f" Val : {len(val_dataset)} images")
print(f" Test : {len(test_dataset)} images")
# Créer les dataloaders
def collate_fn(batch):
"""Fonction nécessaire car chaque image a un nombre différent d'objets."""
return tuple(zip(*batch))
train_loader = DataLoader(
train_dataset,
batch_size=4,
shuffle=True,
num_workers=2,
collate_fn=collate_fn
)
val_loader = DataLoader(
val_dataset,
batch_size=4,
shuffle=False,
num_workers=2,
collate_fn=collate_fn
)
test_loader = DataLoader(
test_dataset,
batch_size=4,
shuffle=False,
num_workers=2,
collate_fn=collate_fn
)
print(f"\n✅ DataLoaders créés avec batch_size=4")
💡 Avantage de random_split : pas besoin de créer manuellement les listes d'images pour chaque split !
Toujours vérifier visuellement que le Dataset charge correctement les images et bounding boxes :
import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
def visualize_yolo_batch(dataset, num_samples=4):
"""Affiche quelques exemples du dataset avec leurs bounding boxes."""
fig, axes = plt.subplots(2, 2, figsize=(12, 12))
axes = axes.flatten()
for i in range(num_samples):
img, target = dataset[i]
# Convertir le tensor en numpy pour l'affichage
img_np = img.permute(1, 2, 0).numpy()
ax = axes[i]
ax.imshow(img_np)
ax.axis('off')
# Dessiner les bounding boxes
boxes = target['boxes'].numpy()
labels = target['labels'].numpy()
for box, label in zip(boxes, labels):
x1, y1, x2, y2 = box
width = x2 - x1
height = y2 - y1
# Créer le rectangle
rect = patches.Rectangle(
(x1, y1), width, height,
linewidth=2, edgecolor='red', facecolor='none'
)
ax.add_patch(rect)
# Ajouter le label
class_name = dataset.dataset.get_class_name(label) if hasattr(dataset, 'dataset') else f"Classe {label}"
ax.text(x1, y1-5, class_name,
bbox=dict(boxstyle='round', facecolor='red', alpha=0.7),
fontsize=10, color='white')
ax.set_title(f'Image {i}')
plt.tight_layout()
plt.show()
# Visualiser quelques exemples du train set
print("📸 Visualisation d'exemples du training set:\n")
visualize_yolo_batch(train_dataset, num_samples=4)
Points à vérifier :
💡 Astuce : si les bounding boxes ne correspondent pas aux objets, vérifiez que les fichiers .txt dans labels/ ont les mêmes noms que les images.