Computer Vision avec PyTorch : entraîner un modèle de détection d’objets
Résumé rapide
📑 Sommaire Introduction à la détection d'objets Théorie des réseaux convolutifs (CNN) Datasets : COCO, Pascal VOC et au-delà Data Augmentation avec Albumentations Transfer Learning : ResNet et EfficientNet YOLO v8 et v9 : architectures modernes Code complet d'entraînement Métriques : mAP et IoU Inférence en temps réel Export ONNX et déploiement Conclusion et ressources 1. Introduction à la détection d'objets La détection d'objets représente l'un des domaines les plus...
1. Introduction à la détection d’objets
La détection d’objets représente l’un des domaines les plus passionnants et les plus actifs de l’intelligence artificielle contemporaine. Contrairement à la simple classification d’images — qui consiste à attribuer une étiquette unique à une image entière — la détection d’objets combine deux tâches fondamentales : la localisation (où se trouve l’objet dans l’image ?) et la classification (de quel type d’objet s’agit-il ?). Cette dualité en fait un problème nettement plus complexe, mais aussi infiniment plus utile dans les applications du monde réel.
Les applications de la détection d’objets sont omniprésentes : véhicules autonomes qui doivent identifier piétons, panneaux et autres véhicules en temps réel ; systèmes de surveillance intelligente capables de détecter des comportements anormaux ; contrôle qualité industriel sur les lignes de production ; imagerie médicale pour la détection de tumeurs ou d’anomalies ; agriculture de précision pour surveiller l’état des cultures ; et bien d’autres domaines encore. Le marché mondial de la vision par ordinateur devrait atteindre 41,11 milliards de dollars d’ici 2030, ce qui témoigne de l’importance croissante de ces technologies.
PyTorch, développé par Meta AI Research, s’est imposé comme le framework de référence pour la recherche et le développement en deep learning. Sa conception basée sur le calcul dynamique (define-by-run) le rend particulièrement intuitif pour le prototypage et le débogage. L’écosystème PyTorch offre également torchvision, une bibliothèque dédiée à la vision par ordinateur qui fournit des modèles pré-entraînés, des transformations d’images et des utilitaires pour les datasets les plus courants.
Cet article s’adresse aux développeurs ayant une connaissance intermédiaire de Python et des bases en machine learning. Une familiarité avec les tenseurs PyTorch et les concepts fondamentaux du deep learning (rétropropagation, descente de gradient) est recommandée. Un GPU NVIDIA avec au moins 8 Go de VRAM est fortement conseillé pour les entraînements présentés.
Dans cet article approfondi, nous allons parcourir l’ensemble du pipeline de détection d’objets avec PyTorch : de la théorie fondamentale des CNN aux architectures les plus récentes comme YOLO v8 et v9, en passant par la préparation des données, l’augmentation, le transfer learning, l’entraînement complet, l’évaluation et le déploiement en production. Chaque section sera accompagnée de code fonctionnel que vous pourrez reproduire directement dans vos projets.
2. Théorie des réseaux convolutifs (CNN)
Les réseaux de neurones convolutifs (Convolutional Neural Networks, ou CNN) constituent la pierre angulaire de toute approche moderne en vision par ordinateur. Pour comprendre la détection d’objets, il est essentiel de maîtriser les mécanismes fondamentaux qui permettent à ces réseaux d’extraire des caractéristiques visuelles pertinentes à partir d’images brutes.
2.1 L’opération de convolution
L’opération de convolution consiste à faire glisser un filtre (ou kernel) de petite taille — typiquement 3×3 ou 5×5 pixels — sur l’ensemble de l’image d’entrée. À chaque position, le filtre effectue une multiplication élément par élément suivie d’une somme, produisant une valeur unique dans la carte de caractéristiques (feature map) de sortie. Ce mécanisme permet au réseau de détecter des motifs locaux comme des bords, des textures ou des formes, indépendamment de leur position dans l’image — c’est ce qu’on appelle l’invariance par translation.
Un CNN typique empile plusieurs couches de convolution. Les premières couches capturent des caractéristiques de bas niveau (bords horizontaux, verticaux, diagonaux), tandis que les couches profondes combinent ces primitives pour former des représentations de plus haut niveau (yeux, roues, visages). Cette hiérarchie de caractéristiques est ce qui rend les CNN si puissants pour comprendre le contenu visuel.
2.2 Pooling et sous-échantillonnage
Entre les couches de convolution, on insère généralement des couches de pooling (le plus souvent du max pooling 2×2) qui réduisent la résolution spatiale des feature maps par un facteur 2. Cette opération a plusieurs avantages : elle réduit le nombre de paramètres et le coût computationnel, elle introduit une forme de robustesse aux petites translations, et elle augmente le champ réceptif effectif des couches suivantes.
2.3 Architecture pour la détection
Dans le contexte de la détection d’objets, l’architecture CNN est généralement décomposée en deux parties distinctes. Le backbone est la partie qui extrait les caractéristiques visuelles — c’est typiquement un réseau de classification pré-entraîné comme ResNet, EfficientNet ou VGG dont on retire les couches finales de classification. Le neck est une structure intermédiaire (comme le Feature Pyramid Network, FPN) qui combine les feature maps de différentes résolutions pour détecter des objets de tailles variées. Enfin, la head est la partie qui produit les prédictions finales : coordonnées des boîtes englobantes, scores de confiance et probabilités de classe.
import torch
import torch.nn as nn
class SimpleConvBlock(nn.Module):
"""Bloc convolutif basique avec BatchNorm et ReLU."""
def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, padding=1):
super().__init__()
self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, bias=False)
self.bn = nn.BatchNorm2d(out_channels)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
return self.relu(self.bn(self.conv(x)))
class FeatureExtractor(nn.Module):
"""Extracteur de caractéristiques multi-échelle simplifié."""
def __init__(self):
super().__init__()
self.stage1 = nn.Sequential(
SimpleConvBlock(3, 64),
SimpleConvBlock(64, 64),
nn.MaxPool2d(2, 2)
)
self.stage2 = nn.Sequential(
SimpleConvBlock(64, 128),
SimpleConvBlock(128, 128),
nn.MaxPool2d(2, 2)
)
self.stage3 = nn.Sequential(
SimpleConvBlock(128, 256),
SimpleConvBlock(256, 256),
SimpleConvBlock(256, 256),
nn.MaxPool2d(2, 2)
)
def forward(self, x):
c1 = self.stage1(x) # Résolution / 2
c2 = self.stage2(c1) # Résolution / 4
c3 = self.stage3(c2) # Résolution / 8
return c1, c2, c3 # Feature maps multi-échelle
« Le véritable pouvoir des CNN réside dans leur capacité à apprendre automatiquement les représentations visuelles pertinentes, sans ingénierie manuelle de caractéristiques. C’est ce qui a rendu obsolètes des décennies de travail en feature engineering. »
— Yann LeCun, Chief AI Scientist chez Meta
2.4 Approches one-stage vs two-stage
Les détecteurs d’objets se divisent historiquement en deux familles. Les détecteurs two-stage (comme Faster R-CNN et Mask R-CNN) génèrent d’abord des propositions de régions d’intérêt (Region Proposal Network), puis classifient et affinent chaque proposition individuellement. Ils sont généralement plus précis mais plus lents. Les détecteurs one-stage (comme YOLO, SSD, RetinaNet) prédisent directement les boîtes englobantes et les classes en une seule passe sur l’image. Ils sont beaucoup plus rapides et particulièrement adaptés aux applications temps réel, au prix d’une légère perte de précision — un écart qui s’est considérablement réduit avec les architectures récentes.
3. Datasets : COCO, Pascal VOC et au-delà
La qualité d’un modèle de détection d’objets dépend fondamentalement de la qualité et de la diversité des données d’entraînement. Deux datasets ont joué un rôle central dans l’évolution du domaine et servent encore aujourd’hui de références incontournables pour l’évaluation comparative des modèles.
3.1 MS COCO (Common Objects in Context)
Le dataset COCO, créé par Microsoft Research, est le standard de facto pour l’évaluation des modèles de détection. Il contient plus de 330 000 images avec plus de 1,5 million d’instances d’objets annotées dans 80 catégories différentes. Les annotations incluent non seulement les boîtes englobantes mais aussi les masques de segmentation d’instance, les points clés pour la pose humaine et les descriptions textuelles. La richesse et la complexité des scènes COCO — avec de nombreux objets par image, des occlusions fréquentes et des tailles d’objets très variées — en font un benchmark exigeant qui reflète bien les défis du monde réel.
3.2 Pascal VOC (Visual Object Classes)
Pascal VOC, plus ancien et plus petit que COCO (environ 11 500 images pour 20 catégories), reste largement utilisé pour le prototypage rapide et l’enseignement. Son format d’annotation XML est devenu un standard repris par de nombreux outils. Le challenge VOC a été organisé annuellement de 2005 à 2012 et a catalysé les progrès en détection d’objets, segmentation sémantique et classification d’actions.
3.3 Charger les données avec PyTorch
import torchvision
from torchvision import transforms
from torch.utils.data import DataLoader
# Charger COCO avec torchvision
transform = transforms.Compose([
transforms.Resize((640, 640)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# Dataset COCO
coco_dataset = torchvision.datasets.CocoDetection(
root='./data/coco/train2017',
annFile='./data/coco/annotations/instances_train2017.json',
transform=transform
)
# Dataset Pascal VOC
voc_dataset = torchvision.datasets.VOCDetection(
root='./data',
year='2012',
image_set='trainval',
download=True,
transform=transform
)
# DataLoader avec collate_fn personnalisé pour la détection
def collate_fn(batch):
"""Gère les images avec un nombre variable d'annotations."""
images = [item[0] for item in batch]
targets = [item[1] for item in batch]
images = torch.stack(images, dim=0)
return images, targets
train_loader = DataLoader(
coco_dataset,
batch_size=16,
shuffle=True,
num_workers=4,
collate_fn=collate_fn,
pin_memory=True
)
print(f"Nombre d'images d'entraînement : {len(coco_dataset)}")
print(f"Nombre de batches : {len(train_loader)}")
3.4 Datasets spécialisés et personnalisés
Au-delà de COCO et VOC, de nombreux datasets spécialisés existent pour des domaines spécifiques : Open Images V7 de Google (9 millions d’images, 600 catégories), Objects365 (365 catégories, 2 millions d’images), LVIS (1 200+ catégories avec distribution long-tail). Pour les projets industriels, vous aurez souvent besoin de créer vos propres datasets annotés. Des outils comme Label Studio, CVAT ou Roboflow facilitent grandement ce processus d’annotation.
La qualité des annotations est critique. Un dataset mal annoté (boîtes imprécises, classes erronées, objets oubliés) produira un modèle médiocre, quelle que soit la sophistication de l’architecture utilisée. Prévoyez un processus de relecture et de validation de vos annotations. Un taux d’erreur d’annotation supérieur à 5% peut sérieusement dégrader les performances.
4. Data Augmentation avec Albumentations
L’augmentation de données est une technique essentielle pour améliorer la robustesse et la capacité de généralisation des modèles de détection. Elle consiste à appliquer des transformations aléatoires aux images d’entraînement pour simuler la variabilité des conditions réelles : changements d’éclairage, rotations, occlusions partielles, variations de contraste, etc. La bibliothèque Albumentations s’est imposée comme la référence pour l’augmentation en vision par ordinateur grâce à sa rapidité (implémentation en C++ via OpenCV), sa richesse fonctionnelle et sa gestion native des boîtes englobantes et des masques de segmentation.
import albumentations as A
from albumentations.pytorch import ToTensorV2
import cv2
import numpy as np
# Pipeline d'augmentation pour la détection d'objets
train_transform = A.Compose([
# Redimensionnement avec préservation du ratio
A.LongestMaxSize(max_size=640),
A.PadIfNeeded(min_height=640, min_width=640,
border_mode=cv2.BORDER_CONSTANT, value=0),
# Transformations géométriques
A.HorizontalFlip(p=0.5),
A.RandomRotate90(p=0.2),
A.ShiftScaleRotate(
shift_limit=0.1,
scale_limit=0.2,
rotate_limit=15,
border_mode=cv2.BORDER_CONSTANT,
p=0.5
),
# Transformations colorimétriques
A.OneOf([
A.RandomBrightnessContrast(
brightness_limit=0.3,
contrast_limit=0.3,
p=1.0
),
A.HueSaturationValue(
hue_shift_limit=20,
sat_shift_limit=30,
val_shift_limit=20,
p=1.0
),
A.CLAHE(clip_limit=4.0, p=1.0),
], p=0.7),
# Bruit et flou
A.OneOf([
A.GaussNoise(var_limit=(10, 50), p=1.0),
A.GaussianBlur(blur_limit=(3, 7), p=1.0),
A.MotionBlur(blur_limit=7, p=1.0),
], p=0.3),
# Cutout / occlusion simulée
A.CoarseDropout(
max_holes=8, max_height=64, max_width=64,
min_holes=1, min_height=16, min_width=16,
fill_value=0, p=0.3
),
# Normalisation et conversion
A.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
ToTensorV2()
], bbox_params=A.BboxParams(
format='pascal_voc', # [x_min, y_min, x_max, y_max]
label_fields=['class_labels'],
min_area=256, # Ignorer les boîtes trop petites
min_visibility=0.3 # Boîte visible à au moins 30%
))
# Transformation de validation (sans augmentation aléatoire)
val_transform = A.Compose([
A.LongestMaxSize(max_size=640),
A.PadIfNeeded(min_height=640, min_width=640,
border_mode=cv2.BORDER_CONSTANT, value=0),
A.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
ToTensorV2()
], bbox_params=A.BboxParams(
format='pascal_voc',
label_fields=['class_labels']
))
# Exemple d'utilisation
image = cv2.imread('image.jpg')
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
bboxes = [[50, 30, 200, 180], [300, 100, 450, 350]]
class_labels = [0, 1] # IDs de classe
augmented = train_transform(
image=image,
bboxes=bboxes,
class_labels=class_labels
)
aug_image = augmented['image'] # Tensor PyTorch
aug_bboxes = augmented['bboxes'] # Boîtes transformées
aug_labels = augmented['class_labels'] # Labels préservés
Le pipeline ci-dessus combine des transformations géométriques (flip, rotation, scale) qui modifient la position spatiale des objets — et donc leurs boîtes englobantes — avec des transformations colorimétriques (luminosité, contraste, teinte) qui n’affectent que l’apparence sans modifier les annotations. L’utilisation de A.OneOf permet de sélectionner aléatoirement une seule transformation parmi un groupe, évitant ainsi la sur-augmentation qui pourrait rendre les images irréalistes.
La technique de Mosaic augmentation, popularisée par YOLO v4, consiste à assembler 4 images d’entraînement en une seule mosaïque. Elle est particulièrement efficace pour la détection d’objets car elle augmente artificiellement le nombre d’objets par image et expose le modèle à des contextes visuels plus variés. La MixUp augmentation superpose deux images avec transparence, encourageant le modèle à apprendre des représentations plus robustes.
5. Transfer Learning : ResNet et EfficientNet
Le transfer learning est sans doute la technique la plus impactante en deep learning appliqué. Plutôt que d’entraîner un réseau de neurones from scratch sur votre dataset — ce qui nécessiterait des millions d’images et des semaines de calcul GPU — le transfer learning consiste à partir d’un modèle déjà entraîné sur un large dataset (typiquement ImageNet avec ses 14 millions d’images et 1 000 catégories) et à l’adapter à votre tâche spécifique. Les couches profondes du réseau ont déjà appris à reconnaître des caractéristiques visuelles universelles (bords, textures, formes, parties d’objets) qui sont transférables à pratiquement n’importe quelle tâche de vision.
5.1 ResNet comme backbone
ResNet (Residual Network), introduit par Kaiming He et al. en 2015, a révolutionné le deep learning avec son concept de connexions résiduelles (skip connections). Ces connexions permettent au gradient de circuler directement à travers les couches, résolvant le problème de la dégradation du gradient dans les réseaux très profonds. ResNet-50 (25,6 millions de paramètres) et ResNet-101 (44,5 millions) offrent un excellent compromis entre performance et coût computationnel pour servir de backbone de détection.
import torchvision.models as models
import torch.nn as nn
def create_resnet_backbone(pretrained=True, frozen_stages=2):
"""
Crée un backbone ResNet-50 pré-entraîné pour la détection.
Args:
pretrained: Utiliser les poids ImageNet
frozen_stages: Nombre de stages à geler (0-4)
"""
# Charger ResNet-50 pré-entraîné
resnet = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2)
# Supprimer les couches de classification
# On garde: conv1, bn1, relu, maxpool, layer1-4
backbone_modules = list(resnet.children())[:-2] # Retire avgpool et fc
# Geler les premiers stages pour stabiliser l'entraînement
stages = [
nn.Sequential(*backbone_modules[:4]), # Stage 0: conv1 + bn + relu + maxpool
backbone_modules[4], # Stage 1: layer1 (256 channels)
backbone_modules[5], # Stage 2: layer2 (512 channels)
backbone_modules[6], # Stage 3: layer3 (1024 channels)
backbone_modules[7], # Stage 4: layer4 (2048 channels)
]
for i in range(frozen_stages + 1):
for param in stages[i].parameters():
param.requires_grad = False
print(f"Stage {i} gelé ({sum(p.numel() for p in stages[i].parameters()):,} params)")
backbone = nn.Sequential(*stages)
return backbone
backbone = create_resnet_backbone(pretrained=True, frozen_stages=1)
# Test avec une image aléatoire
dummy = torch.randn(1, 3, 640, 640)
features = backbone(dummy)
print(f"Shape des features: {features.shape}") # [1, 2048, 20, 20]
5.2 EfficientNet : l’efficacité optimisée
EfficientNet, proposé par Tan et Le (Google Brain, 2019), utilise un compound scaling qui optimise simultanément la profondeur, la largeur et la résolution du réseau via un coefficient unique. EfficientNet-B0 à B7 offrent une gamme complète de modèles, du plus léger au plus puissant. EfficientNet-B4 et B5 sont particulièrement populaires comme backbones de détection, offrant des performances supérieures à ResNet avec moins de paramètres et de FLOPs.
# EfficientNet comme backbone
efficientnet = models.efficientnet_b4(
weights=models.EfficientNet_B4_Weights.IMAGENET1K_V1
)
# Extraire les features intermédiaires pour le FPN
class EfficientNetBackbone(nn.Module):
def __init__(self, model):
super().__init__()
self.features = model.features
def forward(self, x):
outputs = []
for i, block in enumerate(self.features):
x = block(x)
# Capturer les sorties aux résolutions clés
if i in [2, 4, 6, 8]: # Stages P2-P5
outputs.append(x)
return outputs
eff_backbone = EfficientNetBackbone(efficientnet)
dummy = torch.randn(1, 3, 640, 640)
multi_scale_features = eff_backbone(dummy)
for i, feat in enumerate(multi_scale_features):
print(f"Feature P{i+2}: {feat.shape}")
« En pratique, le transfer learning réduit le temps d’entraînement d’un facteur 10 à 100 et permet d’atteindre des performances compétitives avec seulement quelques centaines d’images annotées, là où un entraînement from scratch nécessiterait des dizaines de milliers d’exemples. »
— Andrew Ng, fondateur de DeepLearning.AI
6. YOLO v8 et v9 : architectures modernes
YOLO (You Only Look Once) est la famille de détecteurs d’objets la plus populaire pour les applications temps réel. Depuis sa création par Joseph Redmon en 2016, l’architecture a connu de nombreuses évolutions. Les versions 8 et 9, développées respectivement par Ultralytics et le groupe de Chien-Yao Wang, représentent l’état de l’art actuel en termes de compromis vitesse-précision.
6.1 YOLOv8 : la maturité de l’écosystème
YOLOv8 (janvier 2023, Ultralytics) introduit plusieurs innovations majeures. L’architecture adopte un design anchor-free, abandonnant les boîtes d’ancrage prédéfinies au profit d’une prédiction directe des centres et dimensions des objets. Le backbone CSPDarknet est optimisé avec des blocs C2f (Cross Stage Partial avec Fusion), et la head utilise des branches séparées (decoupled head) pour la classification et la régression. YOLOv8 est disponible en cinq tailles : Nano (3,2M params), Small (11,2M), Medium (25,9M), Large (43,7M) et XLarge (68,2M).
from ultralytics import YOLO
# Charger un modèle pré-entraîné YOLOv8
model = YOLO('yolov8m.pt') # Medium - bon compromis
# Entraîner sur un dataset personnalisé
results = model.train(
data='dataset.yaml', # Configuration du dataset
epochs=100,
imgsz=640,
batch=16,
lr0=0.01, # Learning rate initial
lrf=0.01, # Learning rate final (fraction de lr0)
momentum=0.937,
weight_decay=0.0005,
warmup_epochs=3,
warmup_momentum=0.8,
warmup_bias_lr=0.1,
box=7.5, # Poids de la loss de boîte
cls=0.5, # Poids de la loss de classification
dfl=1.5, # Poids de la Distribution Focal Loss
mosaic=1.0, # Augmentation mosaïque
mixup=0.15, # Augmentation MixUp
copy_paste=0.1, # Augmentation Copy-Paste
device='0', # GPU 0
workers=8,
project='runs/detect',
name='yolov8m_custom',
patience=20, # Early stopping
save_period=10, # Sauvegarder tous les 10 epochs
amp=True, # Mixed precision training
close_mosaic=10, # Désactiver mosaïque les 10 derniers epochs
resume=False
)
Le fichier de configuration dataset.yaml définit la structure du dataset avec les chemins vers les images et labels, ainsi que les classes à détecter. Voici un exemple type :
# dataset.yaml - Configuration du dataset
path: /data/custom_dataset
train: images/train
val: images/val
test: images/test
# Classes (indices commençant à 0)
names:
0: personne
1: voiture
2: velo
3: moto
4: bus
5: camion
# Nombre de classes
nc: 6
6.2 YOLOv9 : Programmable Gradient Information
YOLOv9 (février 2024) introduit le concept révolutionnaire de Programmable Gradient Information (PGI), conçu pour résoudre le problème de la perte d’information (information bottleneck) dans les réseaux profonds. Le PGI utilise un réseau auxiliaire réversible qui préserve l’intégralité des informations du gradient pendant l’entraînement. Combiné avec l’architecture GELAN (Generalized Efficient Layer Aggregation Network), YOLOv9 atteint des performances supérieures à YOLOv8 sur COCO avec un nombre comparable de paramètres.
# YOLOv9 avec Ultralytics
model_v9 = YOLO('yolov9c.pt') # YOLOv9 Compact
# Entraînement YOLOv9
results_v9 = model_v9.train(
data='dataset.yaml',
epochs=150,
imgsz=640,
batch=8, # Batch plus petit (modèle plus lourd)
optimizer='AdamW',
lr0=0.001,
cos_lr=True, # Cosine annealing LR scheduler
label_smoothing=0.1,
device='0',
amp=True,
cache='ram', # Mettre le dataset en RAM pour accélérer
multi_scale=True, # Entraînement multi-résolution
)
Sur COCO val2017 : YOLOv8-M atteint 50,2% mAP@0.5:0.95 à 234 FPS (T4), tandis que YOLOv9-C atteint 53,0% mAP avec un nombre similaire de paramètres. YOLOv9 est donc plus précis, mais YOLOv8 bénéficie d’un écosystème plus mature et d’une meilleure compatibilité d’export.
7. Code complet d’entraînement
Voici un pipeline d’entraînement complet et production-ready pour un détecteur Faster R-CNN avec PyTorch natif. Ce code couvre la gestion du dataset, l’entraînement avec mixed precision, la validation, le logging et la sauvegarde des checkpoints.
import torch
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torchvision.models.detection import fasterrcnn_resnet50_fpn_v2
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from torch.cuda.amp import GradScaler, autocast
import json
import os
import time
from pathlib import Path
class CustomDetectionDataset(Dataset):
"""Dataset personnalisé pour la détection d'objets."""
def __init__(self, root_dir, annotations_file, transforms=None):
self.root_dir = Path(root_dir)
self.transforms = transforms
with open(annotations_file, 'r') as f:
self.annotations = json.load(f)
self.image_ids = list(self.annotations.keys())
def __len__(self):
return len(self.image_ids)
def __getitem__(self, idx):
img_id = self.image_ids[idx]
img_info = self.annotations[img_id]
# Charger l'image
img_path = self.root_dir / img_info['filename']
image = cv2.imread(str(img_path))
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Préparer les annotations
boxes = torch.tensor(img_info['boxes'], dtype=torch.float32)
labels = torch.tensor(img_info['labels'], dtype=torch.int64)
area = (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0])
iscrowd = torch.zeros(len(boxes), dtype=torch.int64)
target = {
'boxes': boxes,
'labels': labels,
'image_id': torch.tensor([idx]),
'area': area,
'iscrowd': iscrowd
}
if self.transforms:
augmented = self.transforms(
image=image,
bboxes=boxes.numpy().tolist(),
class_labels=labels.numpy().tolist()
)
image = augmented['image']
if len(augmented['bboxes']) > 0:
target['boxes'] = torch.tensor(augmented['bboxes'], dtype=torch.float32)
target['labels'] = torch.tensor(augmented['class_labels'], dtype=torch.int64)
return image, target
def create_model(num_classes, pretrained=True):
"""Crée un Faster R-CNN avec backbone ResNet50-FPN v2."""
model = fasterrcnn_resnet50_fpn_v2(
weights='DEFAULT' if pretrained else None,
trainable_backbone_layers=3
)
# Remplacer la tête de classification
in_features = model.roi_heads.box_predictor.cls_score.in_features
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
return model
def train_one_epoch(model, optimizer, data_loader, device, scaler, epoch):
"""Entraîne le modèle pour une epoch complète."""
model.train()
total_loss = 0
num_batches = 0
for batch_idx, (images, targets) in enumerate(data_loader):
images = [img.to(device) for img in images]
targets = [{k: v.to(device) for k, v in t.items()} for t in targets]
optimizer.zero_grad()
with autocast():
loss_dict = model(images, targets)
losses = sum(loss for loss in loss_dict.values())
scaler.scale(losses).backward()
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10.0)
scaler.step(optimizer)
scaler.update()
total_loss += losses.item()
num_batches += 1
if batch_idx % 50 == 0:
lr = optimizer.param_groups[0]['lr']
print(f" Epoch {epoch} [{batch_idx}/{len(data_loader)}] "
f"Loss: {losses.item():.4f} | LR: {lr:.6f}")
return total_loss / num_batches
@torch.no_grad()
def evaluate(model, data_loader, device):
"""Évalue le modèle sur le jeu de validation."""
model.eval()
all_predictions = []
all_targets = []
for images, targets in data_loader:
images = [img.to(device) for img in images]
predictions = model(images)
for pred, target in zip(predictions, targets):
all_predictions.append({
k: v.cpu() for k, v in pred.items()
})
all_targets.append(target)
return all_predictions, all_targets
def main():
"""Pipeline d'entraînement principal."""
# Configuration
config = {
'num_classes': 7, # 6 classes + background
'batch_size': 8,
'num_epochs': 50,
'lr': 0.005,
'momentum': 0.9,
'weight_decay': 0.0005,
'lr_step_size': 15,
'lr_gamma': 0.1,
'save_dir': './checkpoints',
'device': 'cuda' if torch.cuda.is_available() else 'cpu'
}
device = torch.device(config['device'])
os.makedirs(config['save_dir'], exist_ok=True)
# Créer le modèle
model = create_model(config['num_classes'], pretrained=True)
model.to(device)
print(f"Paramètres totaux: {sum(p.numel() for p in model.parameters()):,}")
print(f"Paramètres entraînables: "
f"{sum(p.numel() for p in model.parameters() if p.requires_grad):,}")
# Optimiseur SGD avec momentum
params = [p for p in model.parameters() if p.requires_grad]
optimizer = optim.SGD(
params,
lr=config['lr'],
momentum=config['momentum'],
weight_decay=config['weight_decay']
)
# Scheduler : réduction du LR
scheduler = optim.lr_scheduler.CosineAnnealingWarmRestarts(
optimizer, T_0=10, T_mult=2, eta_min=1e-6
)
# Mixed precision scaler
scaler = GradScaler()
# Boucle d'entraînement
best_map = 0.0
for epoch in range(1, config['num_epochs'] + 1):
start = time.time()
train_loss = train_one_epoch(
model, optimizer, train_loader, device, scaler, epoch
)
scheduler.step()
elapsed = time.time() - start
print(f"Epoch {epoch}/{config['num_epochs']} terminée en {elapsed:.1f}s "
f"| Loss: {train_loss:.4f}")
# Sauvegarde du meilleur modèle
if epoch % 5 == 0:
checkpoint = {
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': scheduler.state_dict(),
'train_loss': train_loss,
'config': config
}
path = f"{config['save_dir']}/checkpoint_epoch{epoch}.pth"
torch.save(checkpoint, path)
print(f"Checkpoint sauvegardé : {path}")
if __name__ == '__main__':
main()
8. Métriques : mAP et IoU
L’évaluation rigoureuse des modèles de détection nécessite des métriques spécifiques qui capturent à la fois la qualité de la localisation et la précision de la classification. Les deux métriques fondamentales sont l’IoU (Intersection over Union) et le mAP (mean Average Precision).
8.1 IoU (Intersection over Union)
L’IoU, aussi appelé indice de Jaccard, mesure le degré de chevauchement entre la boîte prédite et la boîte de vérité terrain (ground truth). Elle se calcule comme le rapport entre l’aire de l’intersection des deux boîtes et l’aire de leur union. Un IoU de 1,0 signifie une correspondance parfaite, tandis qu’un IoU de 0 indique aucun chevauchement. Conventionnellement, un seuil d’IoU de 0,5 est utilisé pour déterminer si une prédiction est un vrai positif (True Positive) ou un faux positif (False Positive).
def compute_iou(box1, box2):
"""
Calcule l'IoU entre deux boîtes englobantes.
Format: [x_min, y_min, x_max, y_max]
"""
# Coordonnées de l'intersection
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[2], box2[2])
y2 = min(box1[3], box2[3])
# Aire de l'intersection
intersection = max(0, x2 - x1) * max(0, y2 - y1)
# Aires des boîtes individuelles
area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
# IoU = Intersection / Union
union = area1 + area2 - intersection
iou = intersection / union if union > 0 else 0
return iou
def compute_iou_batch(boxes1, boxes2):
"""Calcul vectorisé de l'IoU pour des tenseurs de boîtes."""
# boxes1: [N, 4], boxes2: [M, 4] -> output: [N, M]
x1 = torch.max(boxes1[:, None, 0], boxes2[None, :, 0])
y1 = torch.max(boxes1[:, None, 1], boxes2[None, :, 1])
x2 = torch.min(boxes1[:, None, 2], boxes2[None, :, 2])
y2 = torch.min(boxes1[:, None, 3], boxes2[None, :, 3])
intersection = torch.clamp(x2 - x1, min=0) * torch.clamp(y2 - y1, min=0)
area1 = (boxes1[:, 2] - boxes1[:, 0]) * (boxes1[:, 3] - boxes1[:, 1])
area2 = (boxes2[:, 2] - boxes2[:, 0]) * (boxes2[:, 3] - boxes2[:, 1])
union = area1[:, None] + area2[None, :] - intersection
return intersection / (union + 1e-6)
8.2 mAP (mean Average Precision)
Le mAP est la métrique standard pour évaluer les détecteurs d’objets. Pour chaque classe, on calcule la courbe Precision-Recall en faisant varier le seuil de confiance. L’Average Precision (AP) est l’aire sous cette courbe, calculée par interpolation. Le mAP est la moyenne des AP sur toutes les classes. Sur COCO, on rapporte généralement le mAP@0.5 (IoU ≥ 0.5) et le mAP@0.5:0.95 (moyenne sur 10 seuils d’IoU de 0.5 à 0.95 par pas de 0.05), cette dernière étant la plus exigeante et la plus représentative des performances réelles.
from torchmetrics.detection.mean_ap import MeanAveragePrecision
# Calcul du mAP avec torchmetrics
metric = MeanAveragePrecision(
iou_type='bbox',
iou_thresholds=[0.5, 0.75], # mAP@50 et mAP@75
class_metrics=True # AP par classe
)
# Prédictions et ground truth
preds = [{
'boxes': torch.tensor([[100, 50, 300, 200], [350, 100, 500, 400]]),
'scores': torch.tensor([0.95, 0.87]),
'labels': torch.tensor([1, 2])
}]
targets = [{
'boxes': torch.tensor([[105, 48, 295, 205], [340, 90, 510, 410]]),
'labels': torch.tensor([1, 2])
}]
metric.update(preds, targets)
results = metric.compute()
print(f"mAP@0.5: {results['map_50']:.4f}")
print(f"mAP@0.75: {results['map_75']:.4f}")
print(f"mAP@0.5:0.95: {results['map']:.4f}")
print(f"AP par classe: {results['map_per_class']}")
9. Inférence en temps réel
L’inférence en temps réel est l’objectif ultime pour de nombreuses applications de détection d’objets : surveillance vidéo, conduite autonome, robotique industrielle, réalité augmentée. Atteindre des performances temps réel (typiquement ≥ 30 FPS) nécessite une optimisation minutieuse à plusieurs niveaux : choix du modèle, précision numérique, batching, et pipeline de pré/post-traitement.
import cv2
import torch
import time
from ultralytics import YOLO
class RealtimeDetector:
"""Détecteur d'objets temps réel avec YOLOv8."""
def __init__(self, model_path, conf_threshold=0.5, iou_threshold=0.45):
self.model = YOLO(model_path)
self.conf_threshold = conf_threshold
self.iou_threshold = iou_threshold
self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
# Warmup du modèle
dummy = torch.randn(1, 3, 640, 640).to(self.device)
for _ in range(3):
self.model.predict(dummy, verbose=False)
print(f"Modèle chargé sur {self.device}, warmup terminé.")
def process_video(self, source=0, save_output=None):
"""
Détection en temps réel sur un flux vidéo.
source: 0 pour webcam, ou chemin vers un fichier vidéo
"""
cap = cv2.VideoCapture(source)
if not cap.isOpened():
raise RuntimeError(f"Impossible d'ouvrir la source: {source}")
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS) or 30
writer = None
if save_output:
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
writer = cv2.VideoWriter(save_output, fourcc, fps, (width, height))
frame_count = 0
total_time = 0
try:
while True:
ret, frame = cap.read()
if not ret:
break
start = time.perf_counter()
# Inférence
results = self.model.predict(
frame,
conf=self.conf_threshold,
iou=self.iou_threshold,
device=self.device,
half=True, # FP16 pour accélérer
verbose=False
)
# Dessiner les détections
annotated = results[0].plot(
line_width=2,
font_size=12,
pil=False
)
elapsed = time.perf_counter() - start
total_time += elapsed
frame_count += 1
current_fps = 1.0 / elapsed if elapsed > 0 else 0
# Afficher les stats
cv2.putText(annotated, f"FPS: {current_fps:.1f}",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX,
1, (0, 255, 0), 2)
num_detections = len(results[0].boxes)
cv2.putText(annotated, f"Objets: {num_detections}",
(10, 70), cv2.FONT_HERSHEY_SIMPLEX,
1, (0, 255, 0), 2)
if writer:
writer.write(annotated)
cv2.imshow('Detection', annotated)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
cap.release()
if writer:
writer.release()
cv2.destroyAllWindows()
avg_fps = frame_count / total_time if total_time > 0 else 0
print(f"\nStats: {frame_count} frames, FPS moyen: {avg_fps:.1f}")
# Utilisation
detector = RealtimeDetector('yolov8m.pt', conf_threshold=0.4)
detector.process_video(source='video.mp4', save_output='output.mp4')
10. Export ONNX et déploiement
Pour déployer un modèle en production, il est souvent nécessaire de l’exporter dans un format optimisé, indépendant du framework d’entraînement. ONNX (Open Neural Network Exchange) est le standard industriel qui permet d’exécuter un modèle PyTorch dans n’importe quel runtime compatible : ONNX Runtime, TensorRT, OpenVINO, CoreML, etc. L’export ONNX est une étape critique qui peut multiplier les performances d’inférence par un facteur 2 à 10 selon la plateforme cible.
# Export ONNX avec YOLOv8
from ultralytics import YOLO
model = YOLO('runs/detect/yolov8m_custom/weights/best.pt')
# Export vers ONNX
model.export(
format='onnx',
imgsz=640,
half=False, # FP32 pour compatibilité maximale
simplify=True, # Simplifier le graphe ONNX
opset=17, # Version de l'opset ONNX
dynamic=True, # Axes dynamiques (batch size variable)
batch=1
)
# Export vers TensorRT (GPU NVIDIA)
model.export(
format='engine',
imgsz=640,
half=True, # FP16 pour TensorRT
device='0',
workspace=4 # Go de mémoire GPU pour l'optimisation
)
# Export vers CoreML (Apple Silicon)
model.export(
format='coreml',
imgsz=640,
half=True, # FP16
nms=True # Inclure NMS dans le modèle
)
# Inférence avec ONNX Runtime
import onnxruntime as ort
import numpy as np
class ONNXDetector:
"""Détecteur utilisant ONNX Runtime pour l'inférence."""
def __init__(self, model_path):
# Configurer les providers (GPU si disponible)
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
self.session = ort.InferenceSession(model_path, providers=providers)
self.input_name = self.session.get_inputs()[0].name
self.input_shape = self.session.get_inputs()[0].shape
print(f"Modèle ONNX chargé. Input: {self.input_name} {self.input_shape}")
print(f"Provider actif: {self.session.get_providers()[0]}")
def preprocess(self, image, target_size=640):
"""Prétraitement identique à l'entraînement."""
img = cv2.resize(image, (target_size, target_size))
img = img.astype(np.float32) / 255.0
img = np.transpose(img, (2, 0, 1)) # HWC -> CHW
img = np.expand_dims(img, axis=0) # Ajouter batch dim
return img
def predict(self, image, conf_threshold=0.5):
"""Exécuter l'inférence ONNX."""
input_tensor = self.preprocess(image)
outputs = self.session.run(None, {self.input_name: input_tensor})
detections = self.postprocess(outputs, conf_threshold)
return detections
def postprocess(self, outputs, conf_threshold):
"""Post-traitement: filtrage par confiance et NMS."""
predictions = outputs[0][0] # [num_predictions, 5+num_classes]
boxes = predictions[:, :4]
scores = predictions[:, 4]
class_ids = np.argmax(predictions[:, 5:], axis=1)
class_scores = np.max(predictions[:, 5:], axis=1)
final_scores = scores * class_scores
mask = final_scores > conf_threshold
return {
'boxes': boxes[mask],
'scores': final_scores[mask],
'class_ids': class_ids[mask]
}
# Utilisation
onnx_detector = ONNXDetector('best.onnx')
image = cv2.imread('test_image.jpg')
results = onnx_detector.predict(image, conf_threshold=0.5)
print(f"Détections: {len(results['boxes'])}")
Benchmarks typiques sur NVIDIA T4 avec YOLOv8m (640×640) : PyTorch FP32 = 78 FPS, ONNX Runtime FP32 = 115 FPS, TensorRT FP16 = 280 FPS. L’export TensorRT offre le meilleur ratio performance/qualité pour les déploiements GPU NVIDIA en production.
11. Conclusion et ressources
La détection d’objets avec PyTorch a considérablement évolué ces dernières années, passant de modèles complexes et lents à des architectures comme YOLOv8 et v9 qui atteignent des performances remarquables en temps réel. Les outils modernes — transfer learning, augmentation avancée, mixed precision training, export ONNX — rendent le développement de solutions de détection accessible à un nombre croissant de praticiens, même avec des ressources computationnelles limitées.
Les points clés à retenir de cet article sont les suivants. Premièrement, le transfer learning est incontournable : utilisez toujours un backbone pré-entraîné sur ImageNet comme point de départ, sauf si vous disposez de millions d’images annotées. Deuxièmement, l’augmentation de données est critique : Albumentations avec des pipelines bien conçus peut multiplier la taille effective de votre dataset et améliorer significativement la robustesse du modèle. Troisièmement, le choix de l’architecture dépend du contexte : YOLO pour le temps réel, Faster R-CNN pour la précision maximale, et les modèles transformer-based (DETR, RT-DETR) pour les approches les plus modernes. Quatrièmement, l’évaluation rigoureuse avec mAP@0.5:0.95 est essentielle pour comparer objectivement les modèles. Enfin, l’export et l’optimisation (ONNX, TensorRT) sont des étapes non négligeables qui peuvent multiplier les performances d’inférence par un facteur significatif.
Le domaine continue d’évoluer rapidement avec l’émergence des modèles de fondation en vision (SAM, DINO, Grounding DINO) qui promettent une détection d’objets plus flexible, plus généraliste et potentiellement sans entraînement spécifique (zero-shot detection). Ces avancées ouvrent la voie à des applications encore plus ambitieuses et démocratisent l’accès à la vision par ordinateur de haute qualité.
📚 Sources et références
- He, K. et al. (2016). Deep Residual Learning for Image Recognition. CVPR 2016. arXiv:1512.03385
- Jocher, G. et al. (2023). Ultralytics YOLOv8. Documentation officielle. docs.ultralytics.com
- Wang, C-Y. et al. (2024). YOLOv9: Learning What You Want to Learn Using Programmable Gradient Information. arXiv:2402.13616
- Lin, T-Y. et al. (2014). Microsoft COCO: Common Objects in Context. ECCV 2014. arXiv:1405.0312
- Tan, M. & Le, Q. (2019). EfficientNet: Rethinking Model Scaling for CNNs. ICML 2019. arXiv:1905.11946
- Buslaev, A. et al. (2020). Albumentations: Fast and Flexible Image Augmentations. Information, 11(2). DOI:10.3390/info11020125
- PyTorch Documentation. TorchVision Object Detection Finetuning Tutorial. pytorch.org/tutorials
- ONNX Runtime Documentation. Performance Tuning Guide. onnxruntime.ai/docs