Suivre des objets avec ByteTrack, BoT-SORT et TrackTrack
Attribue à chaque objet un ID stable d’une image à l’autre, choisis un tracker et règle ses seuils en fonction de ton détecteur.
Chaque décompte, durée de présence et estimation de vitesse dépend d’une question : l’ID 17 dans cette image désigne-t-il le même objet que l’ID 17 dans la précédente ? Sur le même extrait de 62 images, qui montre environ 30 piétons, et avec le même détecteur, ByteTrack a trouvé 32 ID et TrackTrack, avec sa configuration par défaut, en a trouvé 9. Le choix et le réglage du tracker font partie du modèle, et une erreur à ce niveau fausse silencieusement tous les chiffres qui en découlent.
Pratique
Ce qu’apporte un tracker#

Les étiquettes et les trajectoires de cette image sont le résultat du travail d’un tracker. Chaque ID est une piste, une hypothèse sur un objet réel, et la trajectoire correspond à son historique récent. À chaque image, le tracker prédit où chaque piste devrait se trouver à l’aide d’un filtre de Kalman, puis associe les nouvelles détections à ces prédictions en fonction du chevauchement des boîtes (et, éventuellement, de l’apparence) :
Les pistes qui ne correspondent à aucune détection pendant track_buffer images sont supprimées. C’est tout le mécanisme, et ses seuils déterminent tout ce qui suit.
Choisir un tracker#
Le mode Track propose six trackers : BoT-SORT, ByteTrack, OC-SORT, Deep OC-SORT, FastTracker et TrackTrack (le tracker par défaut si tu ne passes pas de tracker). Trois d’entre eux répondent à la plupart des besoins :
| Suiveur | Critères d’association (configuration par défaut) | À privilégier quand |
|---|---|---|
| ByteTrack | Mouvement et chevauchement des boîtes, avec une seconde passe qui récupère les boîtes à faible score de confiance | Caméras fixes, détecteurs de petite taille ou incertains, priorité au débit |
| BoT-SORT | Association de type ByteTrack avec compensation du mouvement de la caméra (gmc_method: sparseOptFlow) | Caméras mobiles ou tremblantes ; définis with_reid: True pour les foules et les longues occultations |
| TrackTrack | Coût multicritère (forme de la boîte, confiance, angle), initialisation tenant compte des pistes, compensation du mouvement de la caméra | Détections fiables issues d’un modèle robuste et bien entraîné |
L’association par apparence de BoT-SORT (réidentification) est désactivée par défaut ; avec model: auto, elle réutilise les caractéristiques du détecteur lui-même, donc son activation ajoute peu de surcharge.
Comparer les trackers sur un même extrait#
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
for tracker in ("bytetrack.yaml", "botsort.yaml", "tracktrack.yaml"):
ids, frames = set(), 0
for r in model.track(video, stream=True, tracker=tracker, classes=[0], verbose=False):
frames += 1
if r.boxes.is_track:
ids.update(r.boxes.id.int().tolist())
print(f"{tracker:16s} {frames} frames, {len(ids)} unique person IDs")bytetrack.yaml 62 frames, 32 unique person IDs
botsort.yaml 62 frames, 33 unique person IDs
tracktrack.yaml 62 frames, 9 unique person IDsL’extrait montre environ 30 personnes traversant une place, filmée d’en haut ; ByteTrack et BoT-SORT sont donc proches de la réalité. TrackTrack n’est pas défectueux : il ne crée une piste qu’au-dessus de new_track_thresh: 0.7 et n’effectue la première association qu’au-dessus de track_high_thresh: 0.6, alors que le score de confiance médian des personnes détectées par YOLO26n dans cet extrait est de 0,53 et que seulement 17 % des détections atteignent 0,7. La plupart des personnes n’obtiennent jamais d’ID. La règle à suivre : compare les seuils du tracker à la distribution des scores de confiance de ton détecteur avant de te fier à un décompte.
Utiliser les ID dans un pipeline#
from collections import defaultdict
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
counts = defaultdict(set)
for r in model.track(video, stream=True, tracker="bytetrack.yaml", verbose=False):
if not r.boxes.is_track: # no confirmed tracks in this frame yet
continue
for cls, tid in zip(r.boxes.cls.int().tolist(), r.boxes.id.int().tolist()):
counts[r.names[cls]].add(tid)
for cls, ids in counts.items():
print(f"{cls}: {len(ids)} unique objects")person: 32 unique objectsUn ensemble par classe comptabilise chaque ID une seule fois, quel que soit le nombre d’images où il apparaît ; le résultat représente donc des objets, et non des détections. stream=True renvoie les résultats image par image au lieu de garder toute la vidéo en mémoire. Le comptage par région, les cartes thermiques et le calcul de distance sont de simples couches ajoutées à cette boucle.
Suivre les images que tu lis toi-même#
Si tu passes un chemin ou une URL, l’état du tracker est géré automatiquement. Si les images proviennent de ton propre décodeur, d’une webcam ou d’une caméra RTSP, passe persist=True pour que chaque appel poursuive la même séquence (la documentation du mode Predict répertorie tous les types de sources) :
import cv2
from ultralytics import YOLO
from ultralytics.utils.downloads import safe_download
model = YOLO("yolo26n.pt")
video = safe_download("https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4")
cap = cv2.VideoCapture(str(video)) # or 0 for a webcam, or an rtsp:// URL
seen = set()
while True:
ok, frame = cap.read()
if not ok:
break
r = model.track(frame, persist=True, tracker="bytetrack.yaml", classes=[0], verbose=False)[0]
if r.boxes.is_track:
seen.update(r.boxes.id.int().tolist()) # your logic: dwell time, trajectories, alerts
cap.release()
print(f"{len(seen)} unique IDs")32 unique IDsLe résultat est identique à celui obtenu en passant le chemin, car persist=True a conservé un seul tracker actif pendant les 62 appels.
Régler un tracker en fonction de ton détecteur#
Copie une configuration fournie avec le paquet, abaisse les seuils qui ne conviennent pas à ton détecteur et passe le nouveau fichier :
from pathlib import Path
from ultralytics import YOLO
from ultralytics.utils import ROOT
cfg = (ROOT / "cfg/trackers/tracktrack.yaml").read_text() # packaged defaults
for old, new in (
("track_high_thresh: 0.6", "track_high_thresh: 0.3"),
("track_low_thresh: 0.25", "track_low_thresh: 0.1"),
("new_track_thresh: 0.7", "new_track_thresh: 0.35"),
):
cfg = cfg.replace(old, new)
Path("tracktrack_tuned.yaml").write_text(cfg)
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
for tracker in ("tracktrack.yaml", "tracktrack_tuned.yaml"):
ids = set()
for r in model.track(video, stream=True, tracker=tracker, classes=[0], verbose=False):
if r.boxes.is_track:
ids.update(r.boxes.id.int().tolist())
print(f"{tracker:22s} {len(ids)} unique person IDs")tracktrack.yaml 9 unique person IDs
tracktrack_tuned.yaml 27 unique person IDsEn réglant les seuils juste en dessous du score de confiance médian du détecteur, TrackTrack est passé de 9 à 27 ID, un résultat proche du décompte manuel. Les paramètres communs à tous les trackers :
| Paramètre | Valeur par défaut de ByteTrack | Augmente-le pour | Abaisse-le pour |
|---|---|---|---|
track_high_thresh | 0.25 | Ignorer les détections faibles lors de la première association | Suivre les détections d’un détecteur peu fiable |
new_track_thresh | 0.25 | Éliminer les pistes fausses de courte durée | Créer des pistes pour les objets peu visibles |
track_buffer | 30 images | Mieux résister aux occultations prolongées | Libérer plus tôt les ID des objets qui ont quitté la scène |
match_thresh | 0.8 | Effectuer des associations plus souples (mouvements rapides, boîtes instables) | Effectuer des associations plus strictes (foules) |
Pièges#
Symptôme : beaucoup moins d'ID que d'objets, surtout avec un petit modèle. Solution : affiche la confiance médiane des détections (voir la solution) et règle new_track_thresh et track_high_thresh juste en dessous.
Symptôme : les ID se réinitialisent à chaque image quand tu appelles toi-même model.track(frame), ou passent d'un objet à l'autre lorsque deux caméras alimentent un même modèle. Solution : transmets persist=True pour les images consécutives d'un même flux, et attribue une instance de modèle à chaque caméra (ou utilise une source .streams, qui conserve un suivi par flux).
Symptôme : l'ID d'une voiture garée change toutes les quelques images. Solution : le suivi démarre de nouvelles pistes au lieu de faire correspondre les anciennes. Vérifie d'abord la confiance du détecteur sur cet objet, puis augmente match_thresh et track_buffer.
Afficher la solution
Mesure d’abord les scores de confiance du détecteur, puis règle les seuils juste en dessous :
import torch
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
video = "https://github.com/ultralytics/assets/releases/download/v0.0.0/solutions_ci_demo.mp4"
conf = torch.cat([r.boxes.conf for r in model.predict(video, stream=True, classes=[0], verbose=False)])
print(f"median {conf.median():.2f}, share >= 0.7: {(conf >= 0.7).float().mean():.0%}")median 0.53, share >= 0.7: 17%Pour l’extrait de la place, le tableau indique : ByteTrack 32, BoT-SORT 33, TrackTrack 9, TrackTrack réglé 27, décompte manuel d’environ 30. Seules 17 % des détections dépassent le seuil new_track_thresh: 0.7 de TrackTrack ; l’abaisser à 0,35 (avec track_high_thresh: 0.3) est donc la bonne correction. Pour cette caméra fixe en plongée, ByteTrack sans réglage particulier est le choix le plus simple.