Module 8 — Suivi multi-objets dans une vidéo
Les modules 2 à 7 ont produit des boîtes et des masques image par image. Sur le carrefour, un compteur qui applique un détecteur à 25 images par seconde et additionne les boîtes compte chaque voiture 500 fois en 20 secondes. Il faut donc relier les détections successives, savoir que la boîte de l'image et celle de sont le même véhicule, et lui assigner un identifiant stable. C'est le suivi multi-objets.
Le problème d'association
À chaque image, on a :
- Une liste de pistes actives — objets déjà suivis, avec un identifiant.
- Une liste de détections nouvelles produites par le module 3.
Le suivi consiste à apparier ces deux listes : chaque détection reçoit soit l'identifiant d'une piste existante, soit un nouvel identifiant. La matrice de coûts la plus simple utilise l'IoU entre la boîte prédite par la piste et la boîte de la détection.
L'appariement optimal se résout en par l'algorithme hongrois
(scipy.optimize.linear_sum_assignment), qui minimise la somme des coûts
sous la contrainte qu'une piste et une détection ne s'apparient qu'une seule
fois.
import numpy as np
from scipy.optimize import linear_sum_assignment
def iou(a, b):
x1 = max(a[0], b[0]); y1 = max(a[1], b[1])
x2 = min(a[2], b[2]); y2 = min(a[3], b[3])
inter = max(0, x2 - x1) * max(0, y2 - y1)
union = (a[2]-a[0])*(a[3]-a[1]) + (b[2]-b[0])*(b[3]-b[1]) - inter
return inter / union if union > 0 else 0.0
def apparier(pistes, detections, seuil=0.3):
if not pistes or not detections:
return [], list(range(len(pistes))), list(range(len(detections)))
cout = np.zeros((len(pistes), len(detections)))
for i, p in enumerate(pistes):
for j, d in enumerate(detections):
cout[i, j] = 1 - iou(p, d)
lignes, colonnes = linear_sum_assignment(cout)
paires, pistes_libres, detections_libres = [], [], []
for i, j in zip(lignes, colonnes):
if 1 - cout[i, j] >= seuil:
paires.append((i, j))
pistes_libres = [i for i in range(len(pistes))
if i not in [p for p, _ in paires]]
detections_libres = [j for j in range(len(detections))
if j not in [d for _, d in paires]]
return paires, pistes_libres, detections_libres
Le seuil d'IoU est le premier réglage : trop bas, deux véhicules proches peuvent se voler l'identité ; trop haut, le moindre saut de boîte casse la piste.
Le filtre de Kalman prédit la boîte suivante
Une simple association par IoU échoue dès que la caméra vibre ou que le détecteur rate une image. La piste avance de plusieurs pixels en une image ; si la nouvelle détection est décalée de 30 pixels, l'IoU chute.
Le filtre de Kalman modélise la piste comme un état à vitesse constante :
Position, échelle, rapport d'aspect et leurs vitesses. À chaque image, le filtre prédit le nouvel état, puis corrige avec la mesure quand elle existe. Cela donne trois avantages majeurs :
- La boîte prédite est plus proche de la réalité qu'une simple reconduction, ce qui rend l'IoU pertinent.
- La piste peut survivre à quelques images manquées, où aucun détecteur ne l'a repérée.
- Les vitesses estimées deviennent une information exploitable — vitesse moyenne au carrefour, direction dominante.
Le prix à payer est un léger biais d'inertie : sur un changement brutal de direction, la prédiction est en retard d'une ou deux images.
SORT en trois étapes
SORT (Simple Online and Realtime Tracker, 2016) combine ces briques en un algorithme minimal :
- Détecter les objets à l'image avec YOLO ou Faster R-CNN.
- Prédire l'état de chaque piste active avec un Kalman à vitesse constante.
- Apparier pistes prédites et détections par IoU + algorithme hongrois.
Les pistes non appariées pendant images (par défaut ) sont supprimées. Les détections non appariées créent une nouvelle piste. C'est tout.
SORT tourne à plus de 200 images par seconde sur un processeur ordinaire, en plus du détecteur. Sa faiblesse est l'échange d'identifiants (ID switch) lors d'occlusions : quand un piéton passe derrière un poteau, sa piste disparaît puis un nouvel identifiant se crée à la sortie.
ByteTrack : sauver les détections faibles
ByteTrack (2022) part d'une observation simple : les détections à score faible ne sont pas toutes des faux positifs. Beaucoup sont des objets partiellement occultés qui, malgré leur score de 0,35, correspondent à une vraie piste.
L'algorithme fait deux passes d'association :
- Première passe avec les détections à score élevé (au-dessus de 0,5).
- Deuxième passe, sur les pistes non appariées à la première, avec les détections à score faible (entre 0,1 et 0,5).
Le résultat pratique : la stabilité des identifiants s'améliore sensiblement sur les scènes encombrées, sans changer le d étecteur ni ajouter de caractéristiques d'apparence. Sur MOT17, ByteTrack dépasse DeepSORT à un coût de calcul inférieur.
Comptage par ligne virtuelle
Une fois les identifiants stables, le comptage devient simple. On trace une ligne virtuelle au milieu du carrefour et on compte les pistes qui la traversent.
def franchissement(piste_precedente, piste_courante, y_ligne):
"""Vrai si la piste franchit la ligne horizontale y_ligne."""
y_avant = (piste_precedente[1] + piste_precedente[3]) / 2
y_apres = (piste_courante[1] + piste_courante[3]) / 2
return (y_avant - y_ligne) * (y_apres - y_ligne) < 0
class Compteur:
def __init__(self, y_ligne):
self.y_ligne = y_ligne
self.deja_comptes = set()
self.total = 0
def mettre_a_jour(self, pistes_precedentes, pistes_courantes):
for pid, boite in pistes_courantes.items():
if pid in self.deja_comptes:
continue
if pid in pistes_precedentes and franchissement(
pistes_precedentes[pid], boite, self.y_ligne
):
self.deja_comptes.add(pid)
self.total += 1
Le deja_comptes empêche qu'un véhicule à cheval sur la ligne compte deux
fois quand sa boîte oscille. Sans un identifiant stable, un tel compteur
comptera cent fois le même véhicule à l'arrêt sur la ligne.
MOTA, IDF1 et les métriques du suivi
Le suivi introduit ses propres métriques, qui vont au-delà de la mAP :
- MOTA (Multi-Object Tracking Accuracy) : agrège FP, FN et changements d'identifiants sur toute la vidéo. Un MOTA élevé signale peu d'erreurs par image, mais tolère les changements d'identifiants.
- IDF1 : F1 score des identifiants correctement suivis. Sensible aux échanges d'identité, il complète MOTA sur les scènes à long terme.
- HOTA (Higher Order Tracking Accuracy) : sépare la qualité de détection et la qualité d'association, plus fine que MOTA.
Sur un système de comptage, l'IDF1 est souvent plus important que le MOTA : un identifiant qui saute produit un double comptage, alors qu'un faux positif isolé s'auto-corrige à l'image suivante.
Au carrefour, une moto qui passe à 50 km/h parcourt 20 pixels par image à 25 images par seconde sur une caméra 4K. Si sa boîte est petite, l'IoU entre deux images consécutives peut chuter à 0,15, sous le seuil standard. Utilisez la boîte prédite par le Kalman pour l'association, pas la boîte brute de l'image précédente ; c'est tout l'intérêt du filtre.
YOLOv8 embarque un suivi ByteTrack activable par un simple tracker="bytetrack.yaml".
Cela évite d'écrire soi-même le pipeline pour un premier prototype, tout en
gardant la possibilité de brancher un tracker personnalisé plus tard.
En résumé
- Le suivi transforme des détections image par image en pistes avec un identifiant stable, résolu par appariement optimal (algorithme hongrois) sur une matrice de coûts d'IoU.
- Un filtre de Kalman prédit la boîte suivante à partir de la vitesse estimée, ce qui rend l'IoU d'association pertinent et permet aux pistes de survivre à quelques images manquées.
- SORT est le pipeline minimal (détection + Kalman + association) ; ByteTrack ajoute une deuxième passe sur les détections à score faible et gagne en stabilité d'identifiants sur les scènes encombrées.
- Un compteur par ligne virtuelle additionne les identifiants qui la
franchissent, à condition d'utiliser un ensemble
deja_comptespour éviter qu'un véhicule oscillant sur la ligne compte plusieurs fois.
Module suivant : l'annotation, l'augmentation et la qualité des jeux — tout ce qui se passe avant l'entraînement et qui détermine la moitié de la précision finale.