Aller au contenu principal

Module 10 — Projet : détection sur un jeu personnalisé

Les neuf modules précédents ont posé les briques. Ce module les assemble en un pipeline complet, du jeu brut à l'export d'un modèle utilisable. Le fil rouge du carrefour prend ici sa forme finale : 3 000 images annotées à la main, un YOLOv8n affiné, une évaluation avec pycocotools, une analyse d'erreurs et un modèle ONNX exportable dans une API de comptage.

L'organisation du jeu

Les images sortent d'une caméra 1080p à 25 images par seconde, filmée pendant sept jours. Après déduplication perceptuelle (module 9), il reste 3 200 images distinctes. Le split respecte la règle temporelle :

SplitPériodeImages
Entraînementjours 1 à 52 400
Validationjour 6 matinée400
Testjour 7 en entier400

Six classes sont annotées : voiture, camion, moto, bus, pieton, velo. Le format retenu est YOLO — une ligne par objet, coordonnées normalisées.

donnees/
├── images/
│ ├── train/ (2400 fichiers .jpg)
│ ├── val/ (400)
│ └── test/ (400)
├── labels/
│ ├── train/ (2400 fichiers .txt)
│ ├── val/ (400)
│ └── test/ (400)
└── carrefour.yaml

Le fichier carrefour.yaml déclare le jeu à Ultralytics :

path: donnees
train: images/train
val: images/val
test: images/test

names:
0: voiture
1: camion
2: moto
3: bus
4: pieton
5: velo

Affinage YOLOv8n

yolov8n.pt (3,2 millions de paramètres) est le compromis raisonnable pour un flux temps réel sur un portable. On part de ses poids COCO et on n'entraîne que les têtes plus quelques couches finales.

from ultralytics import YOLO

modele = YOLO("yolov8n.pt")

modele.train(
data="donnees/carrefour.yaml",
epochs=60,
imgsz=640,
batch=16,
lr0=0.001, # petit taux, on affine
freeze=10, # gele les 10 premieres couches
patience=15, # arret anticipe
project="runs/carrefour",
name="v1",
)

Sur une carte graphique de bureau, l'entraînement dure trois à quatre heures. Ultralytics écrit ses journaux dans runs/carrefour/v1/ : matrices de confusion, courbes de perte et un fichier best.pt qui garde les meilleurs poids selon la mAP@0,5.

Évaluation officielle avec pycocotools

Ultralytics affiche déjà la mAP, mais l'évaluation de référence passe par pycocotools. Cela demande de convertir les prédictions au format COCO.

import json
from ultralytics import YOLO

modele = YOLO("runs/carrefour/v1/weights/best.pt")
predictions = modele.val(data="donnees/carrefour.yaml", split="test",
save_json=True)

# Ultralytics ecrit predictions.json au format COCO
chemin = "runs/carrefour/v1/predictions.json"

from pycocotools.coco import COCO
from pycocotools.cocoeval import COCOeval

verite = COCO("donnees/annotations_test.json")
pred = verite.loadRes(chemin)
evaluateur = COCOeval(verite, pred, iouType="bbox")
evaluateur.evaluate()
evaluateur.accumulate()
evaluateur.summarize()

Sortie typique à l'issue d'un premier tour :

AP @[ IoU=0.50:0.95 | area=   all | maxDets=100 ] = 0.548
AP @[ IoU=0.50 | area= all | maxDets=100 ] = 0.751
AP @[ IoU=0.75 | area= all | maxDets=100 ] = 0.593
AP @[ IoU=0.50:0.95 | area= small | maxDets=100 ] = 0.221
AP @[ IoU=0.50:0.95 | area=medium | maxDets=100 ] = 0.586
AP @[ IoU=0.50:0.95 | area= large | maxDets=100 ] = 0.702

La ligne à lire en priorité est AP small, à 0,22. Le modèle rate massivement les piétons et vélos lointains. Le module 5 avait annoncé ce diagnostic ; sur un projet réel, il s'agit maintenant d'y remédier.

Analyse d'erreurs par classe et par taille

Passer de la mAP globale à la mAP par classe et par taille se fait en quelques lignes. pycocotools expose la matrice evaluateur.eval avec les scores par tranche d'IoU, taille et classe.

import numpy as np

# eval["precision"] a la forme (10, 101, C, 4, 3)
# T ious x R rappels x C classes x A aires x M maxDets
precision = evaluateur.eval["precision"]

# mAP par classe, IoU 0.5:0.95, toutes tailles, 100 detections
map_par_classe = precision[:, :, :, 0, 2].mean(axis=(0, 1))
for i, nom in enumerate(["voiture", "camion", "moto", "bus", "pieton", "velo"]):
print(f"{nom}: {map_par_classe[i]:.3f}")

Résultat typique :

ClassemAP@0,5:0,95
voiture0,74
bus0,68
camion0,55
moto0,42
pieton0,31
velo0,22

Trois observations exploitables :

  • Vélo est de loin la classe la plus faible, cohérent avec un échantillon rare dans le jeu (seulement 180 vélos sur 3 200 images).
  • Piéton souffre à la fois de l'échelle petite et d'un désaccord inter-annotateur documenté (piétons cachés partiellement).
  • Voiture et bus sont exploitables tels quels.

Cycle d'amélioration

Trois actions se justifient à ce stade :

  1. Annoter 800 images supplémentaires en priorité sur les scènes piétonnières et vélos, en évitant les images voisines pour ne pas gonfler artificiellement la mAP.
  2. Entraîner à 960 pixels au lieu de 640 pour donner plus de résolution aux petits objets. Le prix : entraînement 2,3 fois plus long, inférence 1,6 fois plus lente.
  3. Ajouter un échantillonnage pondéré pour compenser la rareté du vélo : dupliquer les images qui en contiennent au moment du chargement, sans toucher au jeu de test.

Après un deuxième cycle, la mAP globale monte typiquement de 0,55 à 0,63, avec une nette amélioration sur piéton (0,45) et vélo (0,38). L'histoire du projet devient une trace utile — nombre d'images annotées, mAP à chaque étape, temps d'inférence — bien plus parlante qu'un chiffre isolé de fin.

Export pour la production

Une fois le modèle satisfaisant, l'export découple entraînement et service.

modele = YOLO("runs/carrefour/v1/weights/best.pt")
modele.export(format="onnx", imgsz=640, half=True, dynamic=True)

Le fichier best.onnx produit se charge dans onnxruntime ou TensorRT sans dépendance à Ultralytics. C'est cet artefact qui va dans un conteneur Docker de production, pas les scripts d'entraînement. Un modèle ONNX quantifié en INT8 fait tourner YOLOv8n à 400 images par seconde sur un serveur avec un T4, pour un coût mensuel inférieur à 40 euros.

Pipeline complet en production

Le carrefour tourne en quatre étapes :

  1. Décodage vidéo (FFmpeg) : 25 images par seconde.
  2. Inférence YOLOv8n ONNX : 5 millisecondes par image.
  3. Suivi ByteTrack (module 8) avec identifiants stables : 3 millisecondes.
  4. Comptage par ligne virtuelle, écrit dans une base : 1 milliseconde.

Total : 9 millisecondes par image, très en dessous des 40 disponibles. La marge sert à absorber les pics et à afficher les résultats.

Ne pas oublier le test hors distribution

La mAP@0,5:0,95 de 0,63 mesurée sur le split test reste locale au carrefour d'origine. Testez le modèle sur au moins une caméra différente avant de généraliser : c'est là que la précision réelle se révèle, et souvent que 15 à 25 % de mAP s'évaporent. Prévoyez un ré-entraînement par site plutôt qu'un modèle universel.

Documenter le modèle comme un artefact

Chaque poids exporté mérite une carte de modèle : jeu source, période d'acquisition, mAP par classe, latence mesurée, classes qui échouent le plus, images typiques d'échec. Ces informations font la différence entre un modèle qu'on met en production sans crainte et un modèle qu'on redoute de toucher six mois plus tard.

En résumé

  • Un projet de détection réel s'organise en trois blocs : jeu propre (split temporel, déduplication), modèle affiné à partir d'un préentraîné COCO, évaluation officielle par pycocotools.
  • La mAP globale masque toujours les classes faibles ; l'analyse par classe et par taille guide les décisions d'annotation et d'échelle d'entrée bien mieux qu'un chiffre unique.
  • L'export ONNX découple le service de l'entraînement ; un modèle quantifié tourne à des centaines d'images par seconde pour un coût de serveur négligeable.
  • Le test hors distribution — un autre carrefour, une autre caméra — révèle systématiquement une chute de mAP. C'est le meilleur baromètre de la généralisation réelle.

Module suivant : le récapitulatif du cours et l'examen final de 40 questions.