| name | object-tracking |
| description | Suivi d'objets (Object Tracking) — SORT, DeepSORT, ByteTrack, BoT-SORT, OC-SORT, MOT, tracking multi-caméra, Kalman, ré-identification. En français. |
Object Tracking — Suivi d'Objets
Tracking = assigner des ID uniques à des objets détectés à travers les frames vidéo. Problèmes : occlusions, réapparitions, mouvements rapides, objets identiques.
1. Taxonomie du Tracking
Tracking d'Objets
├── Tracking par Détection (Tracking-by-Detection)
│ ├── SORT (Kalman + IoU, 2016)
│ ├── DeepSORT (SORT + ReID, 2017)
│ ├── ByteTrack (BYTE association, 2022)
│ ├── BoT-SORT (SORT + Camera Motion, 2022)
│ ├── OC-SORT (Observation-Centric, 2023)
│ └── StrongSORT (DeepSORT + améliorations, 2023)
│
├── Tracking One-Shot (Joint Detection & Tracking)
│ ├── FairMOT (CentreNet + ReID)
│ ├── TrackFormer (Transformer)
│ ├── MOTR (DETR-based)
│ └── DanceTrack (Danse)
│
├── Tracking Visuel (Single Object)
│ ├── KCF, CSRT, MOSSE (OpenCV classique)
│ └── SiamRPN, TransT (Deep Learning)
│
└── Multi-Camera Tracking
├── Cross-camera ReID
└── Global ID Fusion
2. Filtre de Kalman (Cœur du Tracking)
import numpy as np
from filterpy.kalman import KalmanFilter
class KalmanBoxTracker:
"""
Filtre de Kalman 8D pour un objet.
État : [x, y, s, r, x', y', s']
- x, y : centre de la boîte
- s : surface (w * h)
- r : ratio hauteur/largeur
- x', y', s' : vitesses
"""
def __init__(self, bbox):
self.kf = KalmanFilter(dim_x=7, dim_z=4)
w = bbox[2] - bbox[0]
h = bbox[3] - bbox[1]
self.kf.F = np.array([
[1, 0, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 1, 0],
[0, 0, 1, 0, 0, 0, 1],
[0, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 1],
])
self.kf.H = np.array([
[1, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 0, 0],
])
self.kf.R[2:, 2:] *= 10.
self.kf.P[4:, 4:] *= 1000.
self.kf.P *= 10.
self.kf.Q[-1, -1] *= 0.01
self.kf.Q[4:, 4:] *= 0.01
self.kf.x[:4] = self._bbox_to_z(bbox)
self.time_since_update = 0
self.hits = 0
self.id = KalmanBoxTracker._count
KalmanBoxTracker._count += 1
def update(self, bbox):
self.time_since_update = 0
self.hits += 1
self.kf.update(self._bbox_to_z(bbox))
def predict(self):
if (self.kf.x[6] + self.kf.x[2]) <= 0:
self.kf.x[6] *= 0.0
self.kf.predict()
self.time_since_update += 1
return self._x_to_bbox()
def _bbox_to_z(self, bbox):
w = bbox[2] - bbox[0]
h = bbox[3] - bbox[1]
return np.array([bbox[0] + w/2, bbox[1] + h/2, w * h, w / h]).reshape(4, 1)
def _x_to_bbox(self):
x, y, s, r = self.kf.x[:4].flatten()
w = np.sqrt(s * r)
h = s / w
return [x - w/2, y - h/2, x + w/2, y + h/2]
KalmanBoxTracker._count = 0
3. SORT (Simple Online Realtime Tracking)
from collections import defaultdict
import numpy as np
from scipy.optimize import linear_sum_assignment
class Sort:
"""SORT : Simple Online and Realtime Tracking"""
def __init__(self, max_age=30, min_hits=3, iou_threshold=0.3):
self.max_age = max_age
self.min_hits = min_hits
self.iou_threshold = iou_threshold
self.trackers = []
self.frame_count = 0
def update(self, detections):
"""
detections : liste de [x1, y1, x2, y2, score]
Retourne : [x1, y1, x2, y2, track_id]
"""
self.frame_count += 1
for tracker in self.trackers:
tracker.predict()
matched, unmatched_dets, unmatched_trks = self._associate(detections)
for match in matched:
track_idx, det_idx = match
self.trackers[track_idx].update(detections[det_idx])
for det_idx unmatched_dets:
tracker = KalmanBoxTracker(detections[det_idx])
.trackers.append(tracker)
active_trackers = []
results = []
tracker .trackers:
tracker.time_since_update > .max_age:
tracker.hits < .min_hits .frame_count > .min_hits:
active_trackers.append(tracker)
bbox = tracker.get_state()
results.append([*bbox, tracker.])
.trackers = active_trackers
results
():
.trackers:
[], (((detections))), []
iou_matrix = np.zeros(((.trackers), (detections)))
t, tracker (.trackers):
d, det (detections):
iou_matrix[t, d] = ._iou(tracker.get_state(), det)
row_ind, col_ind = linear_sum_assignment(-iou_matrix)
matched = []
unmatched_dets = (((detections)))
unmatched_trks = (((.trackers)))
t, d (row_ind, col_ind):
iou_matrix[t, d] < .iou_threshold:
matched.append((t, d))
unmatched_dets.remove(d)
unmatched_trks.remove(t)
matched, unmatched_dets, unmatched_trks
():
x1 = (bbox1[], bbox2[])
y1 = (bbox1[], bbox2[])
x2 = (bbox1[], bbox2[])
y2 = (bbox1[], bbox2[])
inter = (, x2 - x1) * (, y2 - y1)
area1 = (bbox1[] - bbox1[]) * (bbox1[] - bbox1[])
area2 = (bbox2[] - bbox2[]) * (bbox2[] - bbox2[])
inter / (area1 + area2 - inter + )
4. DeepSORT (SORT + Ré-Identification)
import torch
import torch.nn as nn
import torchvision.transforms as T
from PIL import Image
class ReIDNet(nn.Module):
"""Extracteur de features pour ré-identification"""
def __init__(self, feature_dim=128):
super().__init__()
from torchvision.models import resnet50
backbone = resnet50(pretrained=True)
self.backbone = nn.Sequential(*list(backbone.children())[:-2])
self.gap = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Linear(2048, feature_dim)
self.bn = nn.BatchNorm1d(feature_dim)
def forward(self, x):
x = self.backbone(x)
x = self.gap(x).flatten(1)
x = self.fc(x)
x = self.bn(x)
return nn.functional.normalize(x, dim=1)
class DeepSort:
def __init__(self, reid_model_path, max_dist=0.2, max_age=30, nn_budget=100):
self.reid = ReIDNet()
.reid.load_state_dict(torch.load(reid_model_path))
.reid.().cuda()
.max_dist = max_dist
.max_age = max_age
.nn_budget = nn_budget
.trackers = []
.frame_count =
.transform = T.Compose([
T.Resize((, )),
T.ToTensor(),
T.Normalize([, , ], [, , ]),
])
():
x1, y1, x2, y2 = (, bbox)
crop = frame[y1:y2, x1:x2]
crop.size == :
crop = Image.fromarray(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))
crop = .transform(crop).unsqueeze().cuda()
torch.no_grad():
features = .reid(crop)
features[].cpu().numpy()
():
cost_matrix = np.zeros(((.trackers), (detections)))
t, tracker (.trackers):
d, feat (features):
cos_dist = np.dot(tracker.features, feat) / (
np.linalg.norm(tracker.features) * np.linalg.norm(feat))
cost_matrix[t, d] = - cos_dist
row_ind, col_ind = linear_sum_assignment(cost_matrix)
matched = []
unmatched_dets = []
t, d (row_ind, col_ind):
cost_matrix[t, d] > .max_dist:
unmatched_dets.append(d)
:
matched.append((t, d))
matched, unmatched_dets, []
5. ByteTrack (BYTE Association)
class ByteTrack:
"""
ByteTrack : associe les détections faible confiance avec les trackers.
Principe : utiliser TOUTES les détections (high + low confidence)
en deux tours d'association.
"""
def __init__(self, track_thresh=0.5, match_thresh=0.8, track_buffer=30):
self.track_thresh = track_thresh
self.match_thresh = match_thresh
self.track_buffer = track_buffer
self.tracks = []
self.frame_id = 0
def update(self, detections):
"""
detections : [x1, y1, x2, y2, score, class_id]
"""
self.frame_id += 1
activated = []
refined = []
lost = []
removed = []
high_dets = [d for d in detections if d[4] >= self.track_thresh]
low_dets = [d for d in detections if d[4] < self.track_thresh]
matches, unmatched_high, unmatched_tracks = self._associate(high_dets)
matches2, unmatched_low, _ = self._associate(low_dets, unmatched_tracks)
idx unmatched_high:
track = ._create_track(high_dets[idx])
activated.append(track)
idx unmatched_tracks:
track = .tracks[idx]
track.is_activated:
track.mark_lost()
lost.append(track)
:
track.mark_removed()
removed.append(track)
activated + refined, lost, removed
6. BoT-SORT (avec Compensation de Mouvement Caméra)
import cv2
class BotSort:
"""
BoT-SORT : Ajoute la compensation de mouvement caméra
(ECC ou ORB) entre les frames.
"""
def __init__(self, use_camera_motion=True):
self.use_camera_motion = use_camera_motion
self.prev_frame = None
self.ecc = cv2.findTransformECC
def compensate_motion(self, curr_frame, prev_frame):
if prev_frame is None:
return np.eye(2, 3)
prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
curr_gray = cv2.cvtColor(curr_frame, cv2.COLOR_BGR2GRAY)
warp_matrix = np.eye(2, 3, dtype=np.float32)
criteria = (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 50, 1e-4)
try:
_, warp_matrix = cv2.findTransformECC(
prev_gray, curr_gray, warp_matrix,
cv2.MOTION_EUCLIDEAN, criteria
)
except:
pass
return warp_matrix
def warp_bbox(self, bbox, homography):
"""Appliquer la transformation à la boîte prédite"""
x1, y1, x2, y2 = bbox
corners = np.array([[x1, y1, 1],
[x2, y1, ],
[x1, y2, ],
[x2, y2, ]]).T
warped = homography @ corners[:, :]
[warped[].(), warped[].(),
warped[].(), warped[].()]
7. Évaluation (MOT Metrics)
from motmetrics import metrics, io
Benchmarks
| Tracker | MOTA | IDF1 | HOTA | FPS | Année |
|---|
| SORT | 59.8 | 53.8 | 34.0 | 600+ | 2016 |
| DeepSORT | 61.4 | 62.2 | 37.6 | 40 | 2017 |
| FairMOT | 73.7 | 72.3 | 48.8 | 25 | 2021 |
| ByteTrack | 80.3 | 77.3 | 54.5 | 30 | 2022 |
| BoT-SORT | 80.5 | 78.6 | 55.0 | 25 | 2022 |
| OC-SORT | 78.0 | 77.5 | 52.4 | 30 | 2023 |
| StrongSORT | 80.9 | 80.3 | 57.0 | 20 | 2023 |
8. Pipeline Complet Tracking + Détection YOLO
import cv2
import numpy as np
from ultralytics import YOLO
class TrackingPipeline:
def __init__(self, detector="yolo11x.pt", tracker="bytetrack.yaml"):
self.detector = YOLO(detector)
self.tracker_config = tracker
self.track_history = {}
def process_frame(self, frame, detections_only=False):
if detections_only:
results = self.detector(frame, conf=0.25)
else:
results = self.detector.track(
frame, conf=0.25, persist=True, tracker=self.tracker_config
)
tracks = []
for result in results:
if result.boxes is None:
continue
for box in result.boxes:
track = {
"bbox": box.xyxy[0].tolist(),
"conf": float(box.conf[0]),
"class": int(box.cls[0]),
"label": result.names[int(box.cls[])],
}
box. :
track[] = (box.[])
tracks.append(track)
track tracks:
track:
tid = track[]
cx = (track[][] + track[][]) /
cy = (track[][] + track[][]) /
tid .track_history:
.track_history[tid] = []
.track_history[tid].append((cx, cy))
(.track_history[tid]) > :
.track_history[tid].pop()
tracks, results[].plot() results frame
():
cap = cv2.VideoCapture(video_path)
out =
output_path:
w = (cap.get())
h = (cap.get())
fourcc = cv2.VideoWriter_fourcc(*)
out = cv2.VideoWriter(output_path, fourcc,
(cap.get()), (w, h))
cap.isOpened():
ret, frame = cap.read()
ret:
tracks, annotated = .process_frame(frame)
out:
out.write(annotated)
cv2.imshow(, annotated)
cv2.waitKey() & == ():
cap.release()
out:
out.release()
cv2.destroyAllWindows()
9. Multi-Camera Tracking
class MultiCameraTracker:
"""
Fusionne les pistes de plusieurs caméras.
Utilise la ré-identification pour associer les IDs entre caméras.
"""
def __init__(self, num_cameras, reid_model):
self.cameras = [TrackingPipeline() for _ in range(num_cameras)]
self.reid = reid_model
self.global_id_map = {}
def associate_across_cameras(self, tracks_per_camera):
"""
Associe les pistes entre caméras via ReID.
"""
global_tracks = []
for cam_id, tracks in enumerate(tracks_per_camera):
for track in tracks:
if track["id"] in self.global_id_map:
track["global_id"] = self.global_id_map[track["id"]]
else:
matched = self.find_reid_match(track["features"], global_tracks)
if matched is not None:
track["global_id"] = matched
else:
track["global_id"] = len(self.global_id_map)
.global_id_map[track[]] = track[]
global_tracks.append(track)
global_tracks
Références