Releases: roboflow/trackers
v2.4.0: BoT-SORT & hyperparameter tuning
📋 Summary
trackers 2.4.0 ships the fourth tracker — BoT-SORT, built around camera motion compensation (CMC) — alongside a first-class hyperparameter tuning workflow, a unified Kalman implementation shared by all trackers, and a tracked_objects property on every tracker that exposes alive tracks even through occlusions.
All existing tracker.update(detections) calls continue to work without changes. One true breaking change: SORTTracker.update() no longer assigns tracker_id on the input object — read it from the returned value. Spawn order is now deterministic. See the Migration guide and Behaviour shifts below.
botsort_demo_sportsmot_03_train_v_2j7kLB-vEEk_c005_h264.mp4
✨ Spotlights
🎯 BoT-SORT tracker
BoT-SORT is designed for footage where the camera itself moves — handheld, drone, sport. Camera motion compensation (CMC) is what makes BoT-SORT distinct: it estimates the inter-frame homography and corrects Kalman predictions before association, so stationary subjects stay stationary in track coordinates even when the camera pans. CMC is on by default (enable_cmc=True); set enable_cmc=False only when the camera is fixed, though at that point BoT-SORT reduces to a ByteTrack variant with a different Kalman state estimator. Four CMC backends: sparseOptFlow (default), orb, sift, ecc. (#386)
| Algorithm | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | DanceTrack val HOTA |
|---|---|---|---|---|
| SORT | 58.4 | 70.8 | 81.6 | 45.0 |
| ByteTrack | 60.1 | 73.0 | 84.0 | 50.2 |
| OC-SORT | 61.9 | 71.7 | 78.4 | 51.8 |
| BoT-SORT | 63.7 | 73.8 | 84.5 | 50.5 |
MOT17 and SportsMOT use YOLOX detections. SoccerNet and DanceTrack use oracle (ground-truth) detections.
Note: Pass the raw video frame as the second argument to BoTSORTTracker.update() when CMC is enabled (the default). Without it, CMC silently no-ops and Kalman predictions may diverge. Existing calls to SORT, ByteTrack, and OC-SORT without frame are unaffected.
from trackers import BoTSORTTracker
tracker = BoTSORTTracker(
track_activation_threshold=0.7,
high_conf_det_threshold=0.6,
enable_cmc=True,
cmc_method="sparseOptFlow", # "orb" | "sift" | "sparseOptFlow" | "ecc"
)
for frame in frames:
detections = model(frame)
tracked = tracker.update(detections, frame) # frame required when enable_cmc=TrueCLI: trackers track --tracker botsort ... — discoverable automatically.
⚙️ Hyperparameter tuning
Each tracker now declares a search_space ClassVar. Tuner uses Optuna — a Bayesian hyperparameter optimisation framework — to sample from that space, runs the tracker over MOT sequences with pre-computed detections, evaluates with HOTA / MOTA / IDF1, and returns the best parameter set. (#301, #374)
from trackers.tune import Tuner
tuner = Tuner(
tracker_id="bytetrack",
gt_dir="data/gt/",
detections_dir="data/det/",
objective="HOTA",
n_trials=100,
)
best_params = tuner.run()trackers tune --tracker bytetrack \
--gt-dir data/gt/ --detections-dir data/det/ \
--objective HOTA --n-trials 200👁️ tracked_objects — every confirmed track, including between detections
tracker.update(detections) returns only the tracks matched to a detection on the current frame. The new tracked_objects property returns every confirmed track still alive in the buffer, including those not matched on this frame — subjects temporarily occluded or skipped by the detector. Boxes come from the Kalman state (get_state_bbox()), not from a detection, so position may drift slightly for unmatched tracks.
Tracks stay in the result until time_since_update exceeds lost_track_buffer frames (scaled by frame_rate). After that they are pruned and will not appear in tracked_objects.
confidence and class_id are None on this result — sv.LabelAnnotator and other annotators that read those fields will raise TypeError unless guarded. (#373)
tracked = tracker.update(detections, frame) # matched this frame only
alive = tracker.tracked_objects # all confirmed alive tracks
# guard before using supervision annotators:
if alive.class_id is not None:
frame = label_annotator.annotate(frame, alive, ...)🔄 Migration guide
✏️ SORTTracker.update() no longer assigns tracker_id on the input object
SORTTracker previously wrote tracker_id directly onto the caller's sv.Detections object and returned that same instance. It now returns a fresh indexed copy — matching ByteTrack and OC-SORT. Code that read detections.tracker_id after calling tracker.update(detections) without using the return value will now get None. (#360)
# Before (v2.3.0) — tracker_id written back to input
tracker.update(detections)
print(detections.tracker_id) # worked (aliased)
# After (v2.4.0) — read from returned value
tracked = tracker.update(detections)
print(tracked.tracker_id)🔀 Behaviour shifts
These changes affect reproducibility or numerical outputs but do not break correct usage of the public API.
🔢 Spawn order is now deterministic
Track IDs assigned to detections that spawn in the same frame are now sorted rather than dependent on CPython set iteration order — reproducible across machines and Python versions for the first time. One-time impact: re-record any raw ID baselines compared against v2.3.0. (#361)
📝 Notable changes
🚀 Added
- BoT-SORT tracker (
BoTSORTTracker) — camera-motion-aware tracker with CMC on by default (enable_cmc=True), configurable backends (sparseOptFlow,orb,sift,ecc), and three-stage score-fused association. (#386) tracked_objectsproperty onBaseTracker— returns every confirmed alive track (withinlost_track_buffer) with Kalman-predicted boxes, not just tracks matched on the current frame.confidenceandclass_idareNone; guard before using supervision annotators. (#373)Tunerclass (trackers.tune.Tuner) — Optuna-based hyperparameter optimisation with HOTA / MOTA / IDF1 objectives over MOT ground-truth and pre-computed detections. (#301)trackers tuneCLI subcommand — wiresTunerinto the CLI. (#374)load_mot_fileis now public — exported fromtrackers.io.motfor custom evaluation and tuning scripts. (#301, #374)frameparameter onBaseTracker.update()—update(detections, frame=None). Required by BoT-SORT with CMC enabled; SORT/ByteTrack/OC-SORT emitUserWarningifframeis provided but not used. (#386)- Swappable Kalman state estimators —
BaseStateEstimator,XCYCWHStateEstimator,XCYCSRStateEstimator,XYXYStateEstimatorintrackers.utils.state_representations. (#310) search_spaceClassVar on every tracker — declarative hyperparameter spaces consumed byTuner. (#301)TrackletProtocolstructural type — formalises the tracklet contract used byBaseTracker.tracks.- Modern Python 3.10+ type hints across the public surface. (#302)
⚠️ Impactful Changes
SORTTracker.update()no longer assignstracker_idon the input object — previously wrotetracker_idonto the caller'ssv.Detectionsin-place; now returns a fresh indexed copy. Code readingdetections.tracker_idwithout using the return value will getNone. (#360)
🌱 Changed
- Kalman filter refactored out of tracklet classes — all trackers share one implementation via
BaseStateEstimator. Tracklet classes handle association and lifecycle only. (#310) - ByteTrack tracklets now count
number_of_successful_consecutive_updatesinstead of totalnumber_of_updates, matching the original paper; public API and numerical outputs unchanged. (#310, #376) - Spawn order is now deterministic across CPython versions — IDs reproducible across machines for the first time; re-record baselines from v2.3.0. (#361)
- Eval submodule uses lazy
__getattr__forevaluate_mot_sequenceto avoid circular imports. - For unmatched tracks,
time_since_updatenow increments insidepredict()rather than via an explicittracklet.update(None)call. CustomOCSORTTrackletsubclasses that overrodeupdate()to handleNonedetections must move that logic intopredict(). (#383)
🔧 Fixed
- ByteTrack: unmatched tracks now expire correctly —
time_since_updateadvances on unmatched tracks and_get_alive_trackletsrespectslost_track_buffer. (#376) - Evaluation distractor filtering corrected on benchmark comparison numbers.
- ByteTrack documentation correction. (#371)
🏆 Contributors
This release is brought to you by five community contributors and the maintainer team.
- Tomasz Stańczyk (@tstanczyk95) (LinkedIn) — original BoT-SORT implementation and supervision integration
- Alexander Bodner (@AlexBodner) (LinkedIn) — BoT-SORT refactoring, Kalman filter unification, OC-SORT tracklet adaptation
- Omkar Kabde (@omkar-334) (LinkedIn) —
Tunerclass,tuneCLI,tracked_objects, ByteTrack fix, type hints - Christoph Deil (@cdeil) (LinkedIn) — deterministic spawn order, SORT input-mutation fix
- Piotr Skalski (@SkalskiP) (LinkedIn) — BoT-SORT branch integration, docs rewrite, comparison page expansion
- Jirka Borovec (@Borda) ([LinkedIn](https://www.linkedin.com/in/jirka-borove...
Trackers 2.3.0
Changelog
🚀 Added
- Added
OCSORTTracker, a clean re-implementation of OC-SORT. OC-SORT shifts to an observation-centric paradigm, using real detections to correct Kalman filter errors accumulated during occlusions. It introduces Observation-Centric Re-Update (ORU) for state recovery, Observation-Centric Momentum (OCM) for direction-consistency-weighted association, and Observation-Centric Recovery (OCR) for second-stage heuristic matching. OC-SORT achieves the highest HOTA on MOT17 and DanceTrack with default parameters. (#207)
| Algorithm | Description | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | DanceTrack HOTA |
|---|---|---|---|---|---|
| SORT | Kalman filter + Hungarian matching baseline. | 58.4 | 70.9 | 81.6 | 45.0 |
| ByteTrack | Two-stage association using high and low confidence detections. | 60.1 | 73.0 | 84.0 | 50.2 |
| OC-SORT | Observation-centric recovery for lost tracks. | 61.9 | 71.7 | 78.4 | 51.8 |
import cv2
import supervision as sv
from inference import get_model
from trackers import OCSORTTracker
model = get_model("rfdetr-medium")
tracker = OCSORTTracker()
box_annotator = sv.BoxAnnotator()
label_annotator = sv.LabelAnnotator()
cap = cv2.VideoCapture("<SOURCE_VIDEO_PATH>")
if not cap.isOpened():
raise RuntimeError("Failed to open video source")
while True:
ret, frame = cap.read()
if not ret:
break
result = model.infer(frame)[0]
detections = sv.Detections.from_inference(result)
detections = tracker.update(detections)
frame = box_annotator.annotate(frame, detections)
frame = label_annotator.annotate(frame, detections, labels=detections.tracker_id)
cv2.imshow("RF-DETR + OC-SORT", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
cap.release()
cv2.destroyAllWindows()trackers-2.3.0-promo.mp4
- Added
trackers downloadCLI command anddownload_datasetPython API. Download benchmark datasets directly from the command line or from code. Supports MOT17 and SportsMOT with split and asset filtering. (#262)
# List available datasets
trackers download --list
# Download full dataset
trackers download mot17
# Download specific split and asset type
trackers download mot17 --split train --annotations-only
# Custom output directory
trackers download sportsmot --split val -o ./datasetsfrom trackers import download_dataset, Dataset, DatasetSplit, DatasetAsset
download_dataset(
dataset=Dataset.MOT17,
split=[DatasetSplit.VAL],
asset=[DatasetAsset.ANNOTATIONS, DatasetAsset.DETECTIONS],
output_dir="./data",
)| Dataset | Description | Splits | Assets | License |
|---|---|---|---|---|
mot17 |
Pedestrian tracking with crowded scenes and frequent occlusions. | train, val, test |
frames, annotations, detections |
CC BY-NC-SA 3.0 |
sportsmot |
Sports broadcast tracking with fast motion and similar-looking targets. | train, val, test |
frames, annotations |
CC BY 4.0 |
MOT17_MOT17-02-DPM.mp4
SportsMOT_v_-6Os86HzwCs_c001.mp4
- Added
--track-idsflag totrackers trackCLI command. Filter displayed tracks by track ID to focus on specific objects in a scene. (#280)
trackers track --source video.mp4 --output output.mp4 \
--model rfdetr-medium \
--tracker bytetrack \
--track-ids 1,2🌱 Changed
-
Made
--sourceoptional intrackers trackwhen--detectionsis provided and no visual output is requested, enabling frameless tracking for evaluation workflows. (#322) -
Optimized
xcycsr_to_xyxyandxyxy_to_xcycsrbounding box converters for the single-box hot path, reducing per-call overhead in inner tracking loops. (#296)
🛠️ Fix
- Fixed a bug in MOT evaluation where ground-truth entries with
conf=0(distractors) were not filtered, causing artificially low scores on MOT17. Tracker entries withid < 0are now also excluded. Results now match TrackEval exactly. (#322)
🏆 Contributors
@JVSCHANDRADITHYA (Chandradithya Janaswami), @salmanmkc (Salman Chishti), @AlexBodner (Alexander Bodner), @Borda (Jirka Borovec), @SkalskiP (Piotr Skalski)
Trackers 2.2.0
Changelog
🚀 Added
- Added camera motion compensation for stable trajectory visualization. (#263)
trackers-2.2.0-promo.mp4
-
Added
trackers trackCLI command. Full tracking pipeline from the command line. Point it at a video, webcam, RTSP stream, or image directory. (#242, #230, #252, #243)trackers track --source video.mp4 --output output.mp4 \ --model rfdetr-medium \ --model.confidence 0.3 \ --classes person \ --show-labels --show-trajectories -
Added
trackers evalCLI command. Evaluate tracker predictions against ground truth using standard MOT metrics. (#210, #211, #212, #214, #215, #223, #224, #226, #250)trackers eval \ --gt-dir data/gt \ --tracker-dir data/trackers \ --metrics CLEAR HOTA Identity \ --columns MOTA HOTA IDF1 IDSWSequence MOTA HOTA IDF1 IDSW ---------------------------------------------------------- MOT17-02-FRCNN 75.600 62.300 72.100 42 MOT17-04-FRCNN 78.200 65.100 74.800 31 ---------------------------------------------------------- COMBINED 75.033 62.400 72.033 73
-
Added Trackers Playground on Hugging Face Spaces. Interactive Gradio demo with model and tracker selection, COCO class filtering, visualization flags, and cached examples. (#249)
-
Added interactive CLI command builder to the docs. Generate trackers track commands with interactive controls. (#242)
🏆 Contributors
@omkar-334 (Omkar Kabde), @Aaryan2304 (Aaryan Kurade), @juan-cobos (Juan Cobos Álvarez), @Borda (Jirka Borovec), @SkalskiP (Piotr Skalski)
Trackers 2.1.0.
Changelog
Warning
Starting with version 2.1.0, Trackers package drops support for Python 3.9. If your environment still relies on Python 3.9, stay on Trackers 2.0.x or upgrade your Python runtime to 3.10 or newer.
Warning
Starting with version 2.1.0, the Trackers package drops support for DeepSORTTracker and ReIDModel. We plan to bring back improved ReID support in future releases.
🚀 Added
Added support for ByteTrack, a fast tracking by detection algorithm focused on stable identities under occlusion. We evaluated both SORT and ByteTrack implementations on three standard multiple object tracking benchmarks, MOT17, SportsMOT, and SoccerNet Tracking.
| Algorithm | Trackers API | MOT17 HOTA | MOT17 IDF1 | MOT17 MOTA | SportsMOT HOTA | SoccerNet HOTA |
|---|---|---|---|---|---|---|
| SORT | SORTTracker |
58.4 | 69.9 | 67.2 | 70.9 | 81.6 |
| ByteTrack | ByteTrackTracker |
60.1 | 73.2 | 74.1 | 73.0 | 84.0 |
import cv2
import supervision as sv
from rfdetr import RFDETRMedium
from trackers import ByteTrack
tracker = ByteTrack()
model = RFDETRMedium()
box_annotator = sv.BoxAnnotator()
label_annotator = sv.LabelAnnotator()
video_capture = cv2.VideoCapture("<SOURCE_VIDEO_PATH>")
if not video_capture.isOpened():
raise RuntimeError("Failed to open video source")
while True:
success, frame_bgr = video_capture.read()
if not success:
break
frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
detections = model.predict(frame_rgb)
detections = tracker.update(detections)
annotated_frame = box_annotator.annotate(frame_bgr, detections)
annotated_frame = label_annotator.annotate(annotated_frame, detections, labels=detections.tracker_id)
cv2.imshow("RF-DETR + ByteTrack", annotated_frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
video_capture.release()
cv2.destroyAllWindows()rf-detr-1.4.0-and-trackers-2.1.0-promo.mp4
🏆 Contributors
@tstanczyk95 (Tomasz Stańczyk), @AlexBodner (Alexander Bodner), @Borda (Jirka Borovec), @SkalskiP (Piotr Skalski)