Skip to content

Releases: roboflow/trackers

v2.5.0.post: re-release pkg

Choose a tag to compare

@Borda Borda released this 09 Jul 13:16

Full Changelog: 2.5.0...2.5.0.post0

v2.5.0: Pluggable IoU variants + C-BIoU tracker

Choose a tag to compare

@Borda Borda released this 24 Jun 18:30
1bf611d

📋 Summary

trackers 2.5.0 makes the association step configurable: all four trackers now accept an iou= parameter and ship five built-in IoU variants. The new CBIoUTracker (C-BIoU, WACV 2023) extends the built-in algorithm roster with cascaded buffered matching for fast-moving or small objects. The Tuner gets four new parameters that round out the tuning workflow, and a correctness fix in HOTA evaluation brings numbers in line with TrackEval.

Track IDs are now allocated per-tracker-instance, so multi-camera and class-specific workflows no longer risk duplicate IDs across independent trackers.

ciou.botsort-14s.mp4

✨ Spotlights

🎯 Pluggable IoU variants

All four trackers now accept an iou= parameter that takes any BaseIoU subclass. Five built-in variants:

Variant Score range Best for
IoU [0, 1] Default — strong baseline
GIoU [-1, 1] Boxes that frequently lose overlap (occlusion)
DIoU [-1, 1] Fast-moving objects; centre-distance signal
CIoU [-1, 1] Same as DIoU + aspect-ratio consistency
BIoU [0, 1] Small or very fast objects (buffered boxes)
from trackers import ByteTrackTracker
from trackers.utils.iou import GIoU

tracker = ByteTrackTracker(
    iou=GIoU(),
    minimum_iou_threshold=0.1,  # re-tune for the new score range
)
detections = tracker.update(detections)

Subclass BaseIoU and override _compute to plug in a fully custom metric. The normalize_for_fusion hook handles signed-range variants correctly in BoT-SORT's score-fusion step.

🆕 CBIoUTracker

CBIoUTracker (Yang et al., WACV 2023) expands boxes by a configurable margin before computing IoU and runs two cascade passes with independently tunable buffer ratios:

from trackers import CBIoUTracker

tracker = CBIoUTracker(
    buffer_ratio_first=0.1,  # first pass: small buffer (high-conf dets vs tracks)
    buffer_ratio_second=0.3,  # second pass: larger buffer (confirmed tracks vs low-conf)
)
detections = tracker.update(detections)

Benchmark scores on val sets (default parameters):

Algorithm MOT17 HOTA SportsMOT HOTA SoccerNet HOTA DanceTrack HOTA
SORT 58.4 70.8 81.6 47.2
ByteTrack 60.1 73.0 84.0 53.3
OC-SORT 61.9 71.7 78.4 54.1
BoT-SORT 63.7 73.8 84.5 57.8
C-BIoU 63.0 73.1 82.6 56.7

C-BIoU keeps the computational simplicity of ByteTrack while it can exceed its performance depending on the video characteristics.

See tracker comparison for all metrics, tuned numbers and methodology.

⚙️ Tuner enhancements

Four new Tuner parameters:

from trackers.tune import Tuner
from trackers import BoTSORTTracker

tuner = Tuner(
    tracker_class=BoTSORTTracker,
    gt_dir="data/gt/",
    detections_dir="data/det/",
    images_dir="data/img/",  # new: frames for CMC-enabled trackers
    fixed_params={"enable_cmc": True},  # new: held constant across all trials
    enqueue_defaults=True,  # new: baseline trial before Optuna samples
    seed=42,  # new: reproducible TPE sampling
)
result = tuner.tune()

Migration guide

Breaking: tracklet class-level ID counters removed

If you subclass BaseTracker and called a tracklet's class-level get_next_tracker_id():

# Before (v2.4.0)
tracker_id = SORTTracklet.get_next_tracker_id()

# After (v2.5.0)
tracker_id = self._allocate_tracker_id()  # in your BaseTracker subclass

Users of the public tracker.update() API are not affected.

Deprecations (removed in v3.0)

# SORTTracker.trackers → .tracks
for t in tracker.tracks:  # was: tracker.trackers

# CMC import path
from trackers.utils.cmc import CMC, CMCConfig, CMCMethod  # was: trackers.core.botsort.cmc
from trackers import CMC  # also works

# CMC.apply_to_xyxy → CMC.warp_xyxy_corners
warped = cmc.warp_xyxy_corners(H, boxes)  # was: apply_to_xyxy

# BoTSORTTracker.apply_cmc_batch → CMC.apply_batch
cmc.apply_batch(H, tracker.tracks)  # was: tracker.apply_cmc_batch(H)

# CMCTMethod → CMCMethod
from trackers.utils.cmc import CMCMethod  # was: CMCTMethod

Notable changes

🚀 Added

  • Pluggable IoU variantsiou= parameter on SORTTracker, ByteTrackTracker, OCSORTTracker, BoTSORTTracker; five built-in variants plus BaseIoU ABC for custom metrics. (#403)
  • CBIoUTracker — Cascaded-Buffered IoU tracker (Yang et al., WACV 2023). (#417)
  • Tuner paramsenqueue_defaults, fixed_params, images_dir, seed. (#427)
  • py.typed marker — PEP 561 compliance; downstream type checkers now recognise the package as typed.

⚠️ Breaking Changes

  • Tracklet class-level ID counters removed*Tracklet.get_next_tracker_id() class methods removed from all tracklet subclasses. Use self._allocate_tracker_id() (from BaseTracker) instead. Only affects custom BaseTracker subclassers. (#419)

🌱 Changed

  • CMC, CMCConfig, CMCMethod moved to trackers.utils.cmc, re-exported from top-level trackers; old trackers.core.botsort.cmc path kept as deprecated shim. (#414)
  • CMC.apply_batch homogeneity guard — raises TypeError on mixed state-estimator tracklet lists. (#414)
  • OC-SORT OCR step always uses standard IoU regardless of configured iou= variant, matching the paper. (#403)

🗑️ Deprecated

  • SORTTracker.trackers.tracks (FutureWarning, removed in v3.0). (#460)
  • trackers.core.botsort.cmc import path → trackers.utils.cmc (DeprecationWarning, removed in v3.0). (#414)
  • BoTSORTTracker.apply_cmc_batchCMC.apply_batch(H, tracker.tracks) (removed in v3.0). (#414)
  • CMCTMethodCMCMethod (removed in v3.0). (#414)
  • CMC.apply_to_xyxyCMC.warp_xyxy_corners (removed in v3.0). (#414)

🔧 Fixed

  • BoT-SORT score fusion — signed IoU variants (GIoU/DIoU/CIoU) now normalised before fusion via normalize_for_fusion, preventing track-ranking inversion. (#403)
  • Non-finite box coordinatesBaseIoU.compute raises ValueError with a clear message for NaN/inf inputs instead of propagating into SciPy's linear_sum_assignment. (#403)
  • ByteTrack confidence=None — detections without confidence now treated as high-confidence (SORT-like); previously produced no tracks. (#415)
  • HOTA evaluation accuracy — per-frame alpha loop vectorized; MOT distractor class filtering now matches TrackEval, correcting reported HOTA on MOT17. (#462, #466)
  • Tracker instances no longer share ID counters — resetting one tracker no longer affects another's ID allocator. (#419)

🏆 Contributors

  • Alexander Bodner (@AlexBodner, LinkedIn) — C-BIoU tracker, IoU variants, tuner enhancements, DanceTrack benchmark results
  • Tomasz Stańczyk (@tstanczyk95) (LinkedIn) — C-BIoU and IoU variants - expertise sharing and supervision
  • Christoph Deil (@cdeil, LinkedIn) — per-instance ID allocators, ByteTrack confidence fix
  • Manan Gupta (@manan152003, LinkedIn) — SORTTracker deprecation docs
  • Ruben Haisma (@RubenHaisma, LinkedIn) — HOTA vectorization, MOT distractor class fix
  • Jirka Borovec (@Borda, LinkedIn) — review, docs, test infrastructure

Full changelog: 2.4.0...v2.5.0

v2.4.0: BoT-SORT & hyperparameter tuning

Choose a tag to compare

@Borda Borda released this 07 May 05:00

📋 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=True

CLI: 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_objects property on BaseTracker — returns every confirmed alive track (within lost_track_buffer) with Kalman-predicted boxes, not just tracks matched on the current frame. confidence and class_id are None; guard before using supervision annotators. (#373)
  • Tuner class (trackers.tune.Tuner) — Optuna-based hyperparameter optimisation with HOTA / MOTA / IDF1 objectives over MOT ground-truth and pre-computed detections. (#301)
  • trackers tune CLI subcommand — wires Tuner into the CLI. (#374)
  • load_mot_file is now public — exported from trackers.io.mot for custom evaluation and tuning scripts. (#301, #374)
  • frame parameter on BaseTracker.update()update(detections, frame=None). Required by BoT-SORT with CMC enabled; SORT/ByteTrack/OC-SORT emit UserWarning if frame is provided but not used. (#386)
  • Swappable Kalman state estimatorsBaseStateEstimator, XCYCWHStateEstimator, XCYCSRStateEstimator, XYXYStateEstimator in trackers.utils.state_representations. (#310)
  • search_space ClassVar on every tracker — declarative hyperparameter spaces consumed by Tuner. (#301)
  • TrackletProtocol structural type — formalises the tracklet contract used by BaseTracker.tracks.
  • Modern Python 3.10+ type hints across the public surface. (#302)

⚠️ Impactful Changes

  • SORTTracker.update() no longer assigns tracker_id on the input object — previously wrote tracker_id onto the caller's sv.Detections in-place; now returns a fresh indexed copy. Code reading detections.tracker_id without using the return value will get None. (#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_updates instead of total number_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__ for evaluate_mot_sequence to avoid circular imports.
  • For unmatched tracks, time_since_update now increments inside predict() rather than via an explicit tracklet.update(None) call. Custom OCSORTTracklet subclasses that overrode update() to handle None detections must move that logic into predict(). (#383)

🔧 Fixed

  • ByteTrack: unmatched tracks now expire correctlytime_since_update advances on unmatched tracks and _get_alive_tracklets respects lost_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.

Read more

Trackers 2.3.0

Choose a tag to compare

@SkalskiP SkalskiP released this 16 Mar 14:18
32d5882

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 download CLI command and download_dataset Python 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 ./datasets
from 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-ids flag to trackers track CLI 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 --source optional in trackers track when --detections is provided and no visual output is requested, enabling frameless tracking for evaluation workflows. (#322)

  • Optimized xcycsr_to_xyxy and xyxy_to_xcycsr bounding 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 with id < 0 are 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

Choose a tag to compare

@SkalskiP SkalskiP released this 18 Feb 18:17
5f05879

Changelog

🚀 Added

  • Added camera motion compensation for stable trajectory visualization. (#263)
trackers-2.2.0-promo.mp4
  • Added trackers track CLI 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 eval CLI 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 IDSW
    Sequence                        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.

Choose a tag to compare

@SkalskiP SkalskiP released this 28 Jan 10:36
e4cc623

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)