From 251a6497c91d626aac0c421d595b981e255ad927 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Wed, 19 Aug 2026 13:16:19 -0300 Subject: [PATCH 01/49] feat(reid): add Deep OC-SORT adaptive appearance fusion BoT-SORT fuses appearance by taking the minimum of the geometry and appearance costs, gated by two fixed thresholds. Deep OC-SORT instead adds a weighted appearance term whose weight grows when the best match stands clear of the runner-up, which helps most in crowded scenes where a fixed weight is either too timid or too aggressive. Adds fuse_adaptive_reid_association alongside the existing fusion and a reid_fusion selector on BoTSORTTracker, with reid_appearance_weight and reid_adaptive_weight_cap for its two knobs. Default stays "botsort", so behaviour is unchanged unless opted into. Implements equations (4)-(6) of the paper, matching compute_aw_new_metric in the authors' integrated_ocsort_embedding tracker (the variant behind their published results) rather than the ratio-based variant that other ports use. Named for what it does rather than where it came from: only the adaptive weighting is ported, onto a BoT-SORT base, without Dynamic Appearance, the velocity term or OCR/OOS. Two documented departures from the reference: negative cosine similarities are clamped to zero, and appearance is gated by reid_proximity_threshold. Deep OC-SORT drops implausible pairs after matching, but this integration thresholds the fused similarity directly, so the gate has to happen during fusion. The discriminativeness gap is measured across all candidates before that gate is applied, so a candidate excluded on geometry cannot read as similarity 0 and earn the bonus for being spatially alone. Co-Authored-By: Claude Opus 5 --- src/trackers/core/botsort/tracker.py | 50 ++++++- src/trackers/core/reid/fusion.py | 105 ++++++++++++++ tests/core/test_botsort_reid.py | 196 ++++++++++++++++++++++++++- 3 files changed, 345 insertions(+), 6 deletions(-) diff --git a/src/trackers/core/botsort/tracker.py b/src/trackers/core/botsort/tracker.py index 44eddf6d..5de591f2 100644 --- a/src/trackers/core/botsort/tracker.py +++ b/src/trackers/core/botsort/tracker.py @@ -4,7 +4,7 @@ # Licensed under the Apache License, Version 2.0 [see LICENSE for details] # ------------------------------------------------------------------------ -from typing import ClassVar, cast +from typing import ClassVar, cast, get_args import numpy as np import supervision as sv @@ -17,7 +17,11 @@ from trackers.core.reid.appearance import appearance_similarity, extract_detection_embeddings from trackers.core.reid.encoder import ReIDEncoder from trackers.core.reid.feature_bank import FeatureBank -from trackers.core.reid.fusion import fuse_botsort_reid_association +from trackers.core.reid.fusion import ( + ReidFusionMethod, + fuse_adaptive_reid_association, + fuse_botsort_reid_association, +) from trackers.utils.cmc import CMC, CMCConfig, CMCMethod from trackers.utils.detections import default_confidences from trackers.utils.iou import BaseIoU, IoU @@ -95,11 +99,25 @@ class BoTSORTTracker(BaseTracker): reid_appearance_threshold: Appearance distance gate. Drops the appearance term when the halved cosine distance ``0.5 * (1 - cos_sim)`` exceeds this value, leaving the pair scored on geometry alone. Default ``0.25`` - (BoT-SORT ``appearance_thresh``). + (BoT-SORT ``appearance_thresh``). Used by ``reid_fusion="botsort"`` + only. reid_proximity_threshold: Standard-IoU distance gate applied before appearance is used. Computed from true IoU even when ``iou`` is GIoU/DIoU/CIoU. Default ``0.5`` (BoT-SORT ``proximity_thresh``; requires ``IoU >= 1 - reid_proximity_threshold``). + reid_fusion: How appearance is combined with geometry. ``"botsort"`` + (default) takes the minimum of the geometry and appearance costs. + ``"adaptive"`` adds an adaptively weighted appearance term, which + weighs appearance more heavily when the best match stands clear of + the runner-up. The two produce different similarity ranges, so + ``minimum_iou_threshold_first_assoc`` needs retuning when switching + (see :func:`~trackers.core.reid.fusion.fuse_adaptive_reid_association`). + reid_appearance_weight: Base appearance weight for ``reid_fusion="adaptive"``. + Ignored otherwise. Default ``0.75``, the value Deep OC-SORT reports + for MOT17 and MOT20; they use ``1.25`` for DanceTrack. + reid_adaptive_weight_cap: Upper bound on the adaptive appearance bonus for + ``reid_fusion="adaptive"``. Ignored otherwise. Default ``0.5``, the value + Deep OC-SORT reports for MOT17 and MOT20; they use ``1.0`` for DanceTrack. Notes: - Positive `maximum_frames_without_update` values are scaled by @@ -146,6 +164,9 @@ def __init__( reid_ema_alpha: float = 0.9, reid_appearance_threshold: float = 0.25, reid_proximity_threshold: float = 0.5, + reid_fusion: ReidFusionMethod = "botsort", + reid_appearance_weight: float = 0.75, + reid_adaptive_weight_cap: float = 0.5, ) -> None: self.maximum_frames_without_update = self._compute_maximum_frames_without_update( lost_track_buffer=lost_track_buffer, @@ -178,6 +199,15 @@ def __init__( raise ValueError(f"reid_proximity_threshold must be in [0, 1], got {reid_proximity_threshold}") self.reid_appearance_threshold = reid_appearance_threshold self.reid_proximity_threshold = reid_proximity_threshold + if reid_fusion not in get_args(ReidFusionMethod): + raise ValueError(f"Unknown reid_fusion {reid_fusion!r}. Valid options are: {get_args(ReidFusionMethod)}.") + if reid_appearance_weight < 0.0: + raise ValueError(f"reid_appearance_weight must be non-negative, got {reid_appearance_weight}") + if reid_adaptive_weight_cap < 0.0: + raise ValueError(f"reid_adaptive_weight_cap must be non-negative, got {reid_adaptive_weight_cap}") + self.reid_fusion = reid_fusion + self.reid_appearance_weight = reid_appearance_weight + self.reid_adaptive_weight_cap = reid_adaptive_weight_cap self._init_timestamp_state(frame_rate) @@ -434,7 +464,7 @@ def _association_similarity( scores: np.ndarray, embeddings: np.ndarray | None, ) -> np.ndarray: - """Score-fused association similarity, with optional BoT-SORT ReID fusion.""" + """Score-fused association similarity, with optional ReID fusion.""" iou_sim_raw = self.iou.normalize_for_fusion(self._get_iou_matrix(tracklets, boxes, tracklet_boxes_by_id)) iou_sim_fused = _fuse_score(iou_sim_raw, scores) if embeddings is None or len(tracklets) == 0: @@ -446,9 +476,19 @@ def _association_similarity( if isinstance(self.iou, IoU) else self._get_iou_matrix(tracklets, boxes, tracklet_boxes_by_id, metric=IoU()) ) + appearance = appearance_similarity(track_feats, embeddings, det_embeddings_normalized=True) + if self.reid_fusion == "adaptive": + return fuse_adaptive_reid_association( + iou_sim_fused, + appearance, + proximity_iou_similarity=proximity_iou, + reid_proximity_threshold=self.reid_proximity_threshold, + reid_appearance_weight=self.reid_appearance_weight, + reid_adaptive_weight_cap=self.reid_adaptive_weight_cap, + ) return fuse_botsort_reid_association( iou_sim_fused, - appearance_similarity(track_feats, embeddings, det_embeddings_normalized=True), + appearance, proximity_iou_similarity=proximity_iou, reid_proximity_threshold=self.reid_proximity_threshold, reid_appearance_threshold=self.reid_appearance_threshold, diff --git a/src/trackers/core/reid/fusion.py b/src/trackers/core/reid/fusion.py index f76637f0..9712d40f 100644 --- a/src/trackers/core/reid/fusion.py +++ b/src/trackers/core/reid/fusion.py @@ -8,6 +8,12 @@ # Copyright (c) 2022 Nir Aharon # Source: https://github.com/NirAharon/BoT-SORT # Reference: tracker/bot_sort.py (ReID appearance-IoU cost fusion) +# +# Adapted from GerardMaggiolino/Deep-OC-SORT (MIT) +# Copyright (c) 2023 Gerard Maggiolino +# Source: https://github.com/GerardMaggiolino/Deep-OC-SORT +# Reference: trackers/integrated_ocsort_embedding/association.py +# (compute_aw_new_metric, adaptive appearance weighting) # ------------------------------------------------------------------------ """Appearance-IoU fusion methods for ReID association. @@ -18,8 +24,12 @@ from __future__ import annotations +from typing import Literal + import numpy as np +ReidFusionMethod = Literal["botsort", "adaptive"] + def fuse_botsort_reid_association( association_similarity: np.ndarray, @@ -65,3 +75,98 @@ def fuse_botsort_reid_association( d_app = np.where(d_iou_proximity > reid_proximity_threshold, 1.0, d_app) fused_cost = np.minimum(d_iou, d_app) return 1.0 - fused_cost + + +def _top_two_margin(similarity: np.ndarray, axis: int, cap: float) -> np.ndarray: + """Margin between the best and second-best similarity along ``axis``, capped. + + A large margin means the best candidate stands clear of the rest, so appearance is discriminative for that row or + column. Zero when there are fewer than two candidates to compare. + """ + if similarity.shape[axis] < 2: + return np.zeros(similarity.shape[1 - axis], dtype=float) + partitioned = np.partition(similarity, -2, axis=axis) + best = np.take(partitioned, -1, axis=axis) + runner_up = np.take(partitioned, -2, axis=axis) + return np.minimum(best - runner_up, cap) + + +def fuse_adaptive_reid_association( + association_similarity: np.ndarray, + appearance_similarity: np.ndarray, + *, + reid_appearance_weight: float, + reid_adaptive_weight_cap: float, + reid_proximity_threshold: float, + proximity_iou_similarity: np.ndarray | None = None, +) -> np.ndarray: + """Fuse IoU and appearance with Deep OC-SORT's adaptive weighting. + + Adds a weighted appearance term to the geometric similarity instead of + taking a minimum. The weight grows when the best appearance match stands + clear of the runner-up and falls back to ``reid_appearance_weight`` when + the top candidates are hard to tell apart. + + Implements equations (4)-(6) of the Deep OC-SORT paper, matching + ``compute_aw_new_metric`` in the authors' ``integrated_ocsort_embedding`` + tracker, the variant behind their published results. + + ``proximity_iou_similarity`` is the standard-IoU gate (defaults to + ``association_similarity``). Pass it separately when association uses + GIoU/DIoU/CIoU so proximity still uses plain IoU. + + Args: + association_similarity: Geometry-based track-detection similarities with + shape ``(T, N)``. + appearance_similarity: Cosine similarities for the same pairs with shape + ``(T, N)``. + reid_appearance_weight: Base appearance weight, ``a_w`` in the paper. + The authors report ``0.75`` for MOT17 and MOT20 and ``1.25`` for + DanceTrack. + reid_adaptive_weight_cap: Upper bound on the adaptive bonus, ``epsilon`` + in the paper. The authors report ``0.5`` for MOT17 and MOT20 and + ``1.0`` for DanceTrack. + reid_proximity_threshold: Maximum standard-IoU distance at which + appearance may contribute. + proximity_iou_similarity: Standard-IoU similarities with shape ``(T, N)``. + Defaults to ``association_similarity``. + + Returns: + Fused track-detection similarities with shape ``(T, N)``, spanning + ``[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]``. + Association thresholds tuned against the BoT-SORT fusion do not carry + over and need retuning. + + Notes: + Negative cosine similarities are clamped to zero, so appearance can + only ever raise a similarity. + + Geometric plausibility is enforced before fusion rather than after + matching. The reference fuses appearance ungated, runs the assignment, + then discards any matched pair whose raw IoU is below its IoU + threshold. BoT-SORT has no such post-match check: its association + threshold is applied to the fused similarity, which a zero-IoU pair + can clear on appearance alone. ``reid_proximity_threshold`` is where + that check lives here; ``1.0`` disables it, which is what lets a + track be recovered after leaving the frame, since its prediction no + longer overlaps the returning detection. The gate applies only to the + appearance term: the margin is measured over all candidates, as in + the reference, so a candidate excluded on geometry cannot read as + similarity ``0`` and widen the margin. + + The velocity-direction term of the full Deep OC-SORT cost is not + included; it belongs to OC-SORT's motion model. + """ + if proximity_iou_similarity is None: + proximity_iou_similarity = association_similarity + + appearance = np.clip(appearance_similarity, 0.0, None) + + track_margin = _top_two_margin(appearance, axis=1, cap=reid_adaptive_weight_cap) + detection_margin = _top_two_margin(appearance, axis=0, cap=reid_adaptive_weight_cap) + adaptive_bonus = (track_margin[:, np.newaxis] + detection_margin[np.newaxis, :]) / 2.0 + + out_of_range = (1.0 - proximity_iou_similarity) > reid_proximity_threshold + gated_appearance = np.where(out_of_range, 0.0, appearance) + + return association_similarity + (reid_appearance_weight + adaptive_bonus) * gated_appearance diff --git a/tests/core/test_botsort_reid.py b/tests/core/test_botsort_reid.py index 7a509123..90d449cd 100644 --- a/tests/core/test_botsort_reid.py +++ b/tests/core/test_botsort_reid.py @@ -16,7 +16,11 @@ import supervision as sv from trackers.core.botsort.tracker import BoTSORTTracker -from trackers.core.reid.fusion import fuse_botsort_reid_association +from trackers.core.reid.fusion import ( + ReidFusionMethod, + fuse_adaptive_reid_association, + fuse_botsort_reid_association, +) def _detection(xyxy: tuple[float, float, float, float], conf: float = 0.9) -> sv.Detections: @@ -286,3 +290,193 @@ def test_real_reid_model_runs_over_frames(self) -> None: assert len(tracker.tracks) == 1 bank = tracker.tracks[0].feature_bank assert bank is not None and bank.feature is not None + + +class TestFuseAdaptiveReidAssociation: + """Unit tests for Deep OC-SORT adaptive appearance weighting.""" + + def test_dominant_match_outweighs_tied_match(self) -> None: + # Same top appearance (0.9) and same IoU (0.6) in both cases. When the runner-up + # is far behind, the adaptive bonus lifts the appearance weight above the base + # 0.75; when the two candidates tie, no bonus is granted. + iou = np.array([[0.6, 0.6]], dtype=np.float32) + dominant = fuse_adaptive_reid_association( + iou, + np.array([[0.9, 0.1]], dtype=np.float32), + reid_appearance_weight=0.75, + reid_adaptive_weight_cap=0.5, + reid_proximity_threshold=0.5, + ) + tied = fuse_adaptive_reid_association( + iou, + np.array([[0.9, 0.9]], dtype=np.float32), + reid_appearance_weight=0.75, + reid_adaptive_weight_cap=0.5, + reid_proximity_threshold=0.5, + ) + # Gap 0.8 is capped at 0.5, halved to a 0.25 bonus: 0.6 + (0.75 + 0.25) * 0.9. + assert dominant[0, 0] == pytest.approx(1.5) + # No gap, so the base weight alone applies: 0.6 + 0.75 * 0.9. + assert tied[0, 0] == pytest.approx(1.275) + assert dominant[0, 0] > tied[0, 0] + + def test_low_proximity_ignores_appearance(self) -> None: + # IoU 0.36 fails the proximity gate (needs IoU > 1 - 0.5), so the score stays + # IoU-only despite a strong appearance match. + fused = fuse_adaptive_reid_association( + np.array([[0.36]], dtype=np.float32), + np.array([[0.9]], dtype=np.float32), + reid_appearance_weight=0.75, + reid_adaptive_weight_cap=0.5, + reid_proximity_threshold=0.5, + ) + assert fused[0, 0] == pytest.approx(0.36) + + def test_gated_competitor_does_not_inflate_the_bonus(self) -> None: + # Both detections look identical (0.9), so appearance cannot tell them apart and + # no bonus is owed. Gating one out on proximity must not change that: the bonus + # measures appearance discriminativeness, not geometric isolation. + appearance = np.array([[0.9, 0.9]], dtype=np.float32) + both_close = fuse_adaptive_reid_association( + np.array([[0.6, 0.6]], dtype=np.float32), + appearance, + reid_appearance_weight=0.75, + reid_adaptive_weight_cap=0.5, + reid_proximity_threshold=0.5, + ) + one_gated = fuse_adaptive_reid_association( + np.array([[0.6, 0.4]], dtype=np.float32), + appearance, + reid_appearance_weight=0.75, + reid_adaptive_weight_cap=0.5, + reid_proximity_threshold=0.5, + ) + + # Base weight only in both cases: 0.6 + 0.75 * 0.9. + assert both_close[0, 0] == pytest.approx(1.275) + assert one_gated[0, 0] == pytest.approx(1.275) + # The gated pair keeps its geometry and gains nothing from appearance. + assert one_gated[0, 1] == pytest.approx(0.4) + + def test_bonus_combines_track_and_detection_gaps(self) -> None: + # A non-square matrix so the per-track (axis 1) and per-detection (axis 0) gaps + # differ and both contribute. Track 0 leads its row by 0.6 and column 0 leads by + # 0.5, each capped at 0.4, giving a bonus of (0.4 + 0.4) / 2 = 0.4. + fused = fuse_adaptive_reid_association( + np.full((2, 3), 0.6, dtype=np.float32), + np.array([[0.9, 0.3, 0.2], [0.4, 0.3, 0.2]], dtype=np.float32), + reid_appearance_weight=0.5, + reid_adaptive_weight_cap=0.4, + reid_proximity_threshold=0.5, + ) + assert fused[0, 0] == pytest.approx(0.6 + (0.5 + 0.4) * 0.9) + # Track 1 leads its own row by only 0.1, so its bonus is smaller. + assert fused[1, 0] == pytest.approx(0.6 + (0.5 + (0.1 + 0.4) / 2) * 0.4) + + def test_negative_appearance_never_lowers_similarity(self) -> None: + # A negative cosine similarity is clamped to zero so appearance can only ever + # raise a score, never push a geometrically sound pair below the threshold. + fused = fuse_adaptive_reid_association( + np.array([[0.8]], dtype=np.float32), + np.array([[-0.5]], dtype=np.float32), + reid_appearance_weight=0.75, + reid_adaptive_weight_cap=0.5, + reid_proximity_threshold=0.5, + ) + assert fused[0, 0] == pytest.approx(0.8) + + def test_single_candidate_gets_no_adaptive_bonus(self) -> None: + # With one track and one detection there is no runner-up to compare against, + # so only the base weight applies: 0.7 + 0.75 * 0.8. + fused = fuse_adaptive_reid_association( + np.array([[0.7]], dtype=np.float32), + np.array([[0.8]], dtype=np.float32), + reid_appearance_weight=0.75, + reid_adaptive_weight_cap=0.5, + reid_proximity_threshold=0.5, + ) + assert fused[0, 0] == pytest.approx(1.3) + + def test_proximity_uses_standard_iou_not_giou(self) -> None: + # The tracker always passes two different matrices: association similarity is + # IoU fused with detection confidence, while the gate is raw IoU. A version + # that ignores the caller's gate matrix would keep the appearance term here. + association_iou = np.array([[0.80]], dtype=np.float32) + fused = fuse_adaptive_reid_association( + association_iou, + np.array([[0.95]], dtype=np.float32), + reid_appearance_weight=0.75, + reid_adaptive_weight_cap=0.5, + reid_proximity_threshold=0.5, + proximity_iou_similarity=np.array([[0.35]], dtype=np.float32), + ) + assert fused[0, 0] == pytest.approx(float(association_iou[0, 0])) + + +class TestReidFusionSelection: + """The fusion selector on the tracker.""" + + def test_botsort_fusion_is_the_default(self) -> None: + assert BoTSORTTracker().reid_fusion == "botsort" + + def test_unknown_fusion_raises(self) -> None: + with pytest.raises(ValueError, match="Unknown reid_fusion"): + BoTSORTTracker(reid_fusion="deepocsort") # type: ignore[arg-type] + + @pytest.mark.parametrize("parameter", ["reid_appearance_weight", "reid_adaptive_weight_cap"]) + def test_rejects_negative_adaptive_weights(self, parameter: str) -> None: + with pytest.raises(ValueError, match=parameter): + # mypy cannot narrow a dynamic dict against typed kwargs. + BoTSORTTracker(enable_cmc=False, **{parameter: -0.1}) # type: ignore[arg-type] + + def test_adaptive_parameters_reach_fusion(self, monkeypatch: pytest.MonkeyPatch) -> None: + import trackers.core.botsort.tracker as tracker_module + + received: dict[str, float] = {} + + def spy(association, appearance, **kwargs): + received.update(kwargs) + return tracker_module.fuse_botsort_reid_association( + association, appearance, reid_proximity_threshold=0.5, reid_appearance_threshold=0.25 + ) + + monkeypatch.setattr(tracker_module, "fuse_adaptive_reid_association", spy) + tracker = BoTSORTTracker( + enable_cmc=False, + reid_model=_KeyedReIDEncoder({(10, 10): _norm(np.array([1.0, 0.0, 0.0, 0.0]))}), + reid_fusion="adaptive", + reid_appearance_weight=0.5, + reid_adaptive_weight_cap=0.4, + reid_proximity_threshold=0.9, + ) + tracker.update(_detection((10.0, 10.0, 30.0, 30.0)), frame=_frame(5)) + tracker.update(_detection((10.0, 10.0, 30.0, 30.0)), frame=_frame(6)) + + assert received["reid_appearance_weight"] == 0.5 + assert received["reid_adaptive_weight_cap"] == 0.4 + assert received["reid_proximity_threshold"] == 0.9 + + def test_adaptive_fusion_changes_association(self) -> None: + # Non-default weights: at 0.75 with the cap binding the effective weight is exactly + # 1.0, which makes the fusion symmetric in its inputs and would hide wiring mistakes. + far = (10.0, 10.0, 30.0, 30.0) + near = (11.0, 11.0, 31.0, 31.0) + feature = _norm(np.array([1.0, 0.0, 0.0, 0.0])) + + def run(fusion: ReidFusionMethod) -> np.ndarray: + tracker = BoTSORTTracker( + enable_cmc=False, + reid_model=_KeyedReIDEncoder({(10, 10): feature, (11, 11): feature}), + reid_fusion=fusion, + reid_appearance_weight=0.3, + reid_adaptive_weight_cap=0.1, + minimum_iou_threshold_first_assoc=1.02, + ) + tracker.update(_detection(far), frame=_frame(5)) + return tracker.update(_detection(near), frame=_frame(6)).tracker_id + + # The gate sits in the window that separates the two fusions. Taking a minimum + # of costs can never exceed a similarity of 1, while adding a weighted + # appearance term reaches 0.74 + 0.3 * 1.0, so only the latter clears 1.02. + np.testing.assert_array_equal(run("botsort"), [-1]) + np.testing.assert_array_equal(run("adaptive"), [0]) From 889ea13deda2c1394ce2c7294c5de417204c94c1 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Wed, 19 Aug 2026 13:16:19 -0300 Subject: [PATCH 02/49] docs(reid): document fusion choice and the proximity gate Documents reid_fusion and its parameters, and compares both fusion rules against a geometry-only baseline on SoccerNet, DanceTrack and MOT17 on the published tuned BoT-SORT geometry. The geometry-only column is the one that decides whether to enable appearance at all, so it belongs in the table. Adds a section on reid_proximity_threshold. Its 0.5 default restricts appearance to pairs already overlapping at IoU >= 0.5, and 0.99 still requires IoU >= 0.01, which a target returning from outside the frame does not meet: its prediction has been extrapolated out of frame and overlaps the returning detection by exactly zero. Only 1.0 disables the gate. On SoccerNet test that is worth +1.23 HOTA and a third fewer ID switches on the published tuned geometry, and it is what lets a player who walked out of frame come back with the same id. The reference Deep OC-SORT recovers that case with a geometry-only pass against the last observed box; BoT-SORT has no such stage, so appearance at an open gate is the only route. The same change hurts DanceTrack, whose targets stay in frame, so this is documented as domain guidance rather than a new default. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 1 + docs/guides/reid.md | 78 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcd13079..3a24948f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### 🚀 Added +- **`BoTSORTTracker(reid_fusion="adaptive")`** — Deep OC-SORT's adaptive appearance weighting as an alternative to BoT-SORT's `min(d_iou, d_app)` fusion: adds a weighted appearance term whose weight grows when the best match stands clear of the runner-up. New `reid_appearance_weight` and `reid_adaptive_weight_cap` parameters; default fusion is unchanged. `reid_appearance_threshold` has no effect under adaptive fusion. `fuse_adaptive_reid_association` lives in `trackers.core.reid.fusion` beside the BoT-SORT rule so other trackers can reuse it ([#571](https://github.com/roboflow/trackers/pull/571)). - **`trackers benchmark mcbyte` subcommand** — runs McByte over complete MOT17, DanceTrack, SportsMOT, or SoccerNet-tracking benchmark test sets and writes MOTChallenge-format results, with flags for dataset selection (`--dataset`), per-dataset detection/image roots (`--dataset_roots`), device, output location, CMC method/downscale, isolated mask matching, skip-existing, and partial-result retention ([#541](https://github.com/roboflow/trackers/pull/541), [#543](https://github.com/roboflow/trackers/pull/543)). - **`trackers inspect` command group** — visual validation commands for the mask stack and the tracker that uses it: `inspect sam` (box-prompted mask generation), `inspect cutie` (mask propagation), `inspect mask-manager` (mask lifecycle over a frame range, driven either from command-line boxes or from a MOT ground-truth file), and `inspect mcbyte` (locked-IoU baseline against full mask-conditioned McByte). Each writes annotated per-frame images into a timestamped run directory ([#543](https://github.com/roboflow/trackers/pull/543)). diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 5671d7ba..32a8e47d 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -62,12 +62,15 @@ For the model catalog and fine-tuning, see the [`reid` training guide](https://g ## Key Parameters -| Parameter | Purpose | Tuning guidance | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | -| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | -| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | -| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Lower restricts how far apart a pair may be before appearance stops contributing. | +| Parameter | Purpose | Tuning guidance | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | +| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | +| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | +| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | +| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | +| `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | +| `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | +| `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | --- @@ -146,6 +149,69 @@ The cross-domain encoder fails differently. On SoccerNet the different-ID rate a --- +## Choosing a proximity threshold + +`reid_proximity_threshold` decides which track-detection pairs appearance is allowed to score. A pair is dropped before appearance is consulted whenever `1 - IoU` exceeds the threshold, so the 0.5 default limits appearance to pairs that already overlap at `IoU >= 0.5`, and 0.99 still requires `IoU >= 0.01`. Only 1.0 disables the gate. + +**Why it matters.** Lost tracks are represented by their Kalman prediction. After an occlusion the prediction has drifted and overlaps the re-emerging detection weakly; after a target leaves the frame the prediction has been extrapolated out of it and the overlap with the returning detection is exactly zero. The default excludes both cases, 0.99 admits the first, and only 1.0 admits the second. The reference Deep OC-SORT tracker recovers the second case with a geometry-only pass against the last observed box (OCR); BoT-SORT has no such stage, so appearance at an open gate is the only route. + +On SoccerNet test (oracle detections, `osnet_x1_0` fine-tuned on SoccerNet train, library-default geometry): + +| `reid_proximity_threshold` | appearance consulted when | HOTA | ID switches | +| :------------------------- | :------------------------ | :---: | :---------: | +| none (no ReID) | | 84.56 | 2939 | +| 0.5 (default) | `IoU >= 0.50` | 84.52 | 3012 | +| 0.8 | `IoU >= 0.20` | 85.02 | 3268 | +| 0.95 | `IoU >= 0.05` | 85.69 | 2565 | +| 0.99 | `IoU >= 0.01` | 85.82 | 2431 | + +On the [published tuned BoT-SORT geometry](../evaluations/results.md#soccernet-tracking) for SoccerNet, `adaptive` at 0.99 scores 85.87 HOTA with 2433 switches and at 1.0 scores **86.23** with **1692**, against 85.00 and 2523 without appearance. The identity gain is almost entirely the last step: 1.0 is what lets a player who walked out of frame come back with the same id. `botsort` at 1.0 reaches 87.29 HOTA but 4564 switches, see [choosing a fusion method](#choosing-a-fusion-method). + +Half-opening is the worst setting for identity: 0.8 has more ID switches than either the closed gate or no appearance at all, because it admits enough distant candidates to create false matches without admitting the ones that enable recoveries. Either keep the gate or disable it. + +The right value depends on the footage. DanceTrack val targets stay in frame, and there opening the gate only adds false matches: `adaptive` at its best weights falls from 57.39 HOTA at 0.5 to 56.56 at 1.0, and `botsort` falls from 56.11 to 46.20 with seven times the ID switches, because `min(d_iou, d_app)` with no geometric check flips between dancers that look alike every frame. Use 1.0 where identity is lost to camera motion or targets leaving the frame, and prefer `adaptive` there; keep 0.5 where targets stay in view. + +## Choosing a fusion method + +`reid_fusion` selects how the appearance score reaches the cost matrix. + +`"botsort"` (default) takes `min(d_iou, d_app)`. Appearance competes with geometry and wins only when it is strictly cheaper, so a pair is accepted when either cue is confident. Both gates are hard: a pair either clears `reid_appearance_threshold` and `reid_proximity_threshold` or contributes nothing. + +`"adaptive"` adds a weighted appearance term to the geometric similarity, with the weight growing when the best appearance match stands clear of the runner-up and falling back to `reid_appearance_weight` when the top candidates are hard to tell apart. This is the weighting from Deep OC-SORT, ported on its own onto BoT-SORT; the velocity term, the confidence-gated feature update and the OCR pass are not included. `reid_appearance_threshold` has no effect under this method. + +Two consequences are worth knowing before switching: + +- **The similarity range changes.** `"botsort"` returns values in `[0, 1]`; `"adaptive"` returns `[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]`, which is `[0, 2.25]` at the defaults. `minimum_iou_threshold_first_assoc` is applied to that fused value, so a threshold tuned for one method is a different gate under the other. Retune it when you switch, or the comparison measures the gate rather than the fusion. +- **The weights are domain-dependent.** Deep OC-SORT reports `reid_appearance_weight=0.75` with `reid_adaptive_weight_cap=0.5` for MOT17 and MOT20, and `1.25` with `1.0` for DanceTrack, where dancers occlude constantly and geometry carries less. The defaults here follow the MOT17/MOT20 pair. + +### Measured comparison + +HOTA, best configuration found for each method. Detections, CMC, encoder and geometry are shared within a row; only appearance handling differs. SoccerNet and MOT17 use the published tuned BoT-SORT geometry from the [tracker comparison](../evaluations/results.md); DanceTrack's tuned set coincides with the library defaults. + +| Dataset | Encoder | no ReID | `botsort` | `adaptive` | `botsort` parameters | `adaptive` parameters | +| -------------- | --------------------------------- | --------- | --------- | ---------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| SoccerNet test | `osnet_x1_0` fine-tuned SoccerNet | 85.00 | **87.29** | 86.23 | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=1.0` | +| DanceTrack val | `osnet_x1_0_msmt17_combineall` | 53.89 | 56.11 | **57.39** | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0`, `reid_proximity_threshold=0.5` | +| MOT17 val-half | `osnet_x1_0` fine-tuned MOT17 | **69.05** | 69.00 | 68.72 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5` | + +HOTA. Each cell is the best configuration found for that method (on SoccerNet the threshold and weight come from closed-gate sweeps with only the gate changed to 1.0, so they are good settings rather than necessarily the best); the parameter columns give the full ReID settings it ran with, defaults included. MOT17 moves by less than 0.35 HOTA in any direction on 7 sequences, inside single-sequence variance. + +**The two rules trade HOTA against identity stability.** On DanceTrack `adaptive` leads on both, cutting ID switches from 1905 to 1683 where `botsort` raises them to 2276 while still gaining HOTA. On SoccerNet with the gate disabled `botsort` has the higher HOTA and IDF1 (87.29 and 83.13 against 86.23 and 82.52) but 4564 ID switches against 1692, 81% more than running no appearance at all; two thirds of its identity changes revert within ten frames, because `min(d_iou, d_app)` takes whichever candidate is cheapest in the current frame and flips between players in the same kit. `adaptive` commits to a re-association and keeps it, so its errors are fewer and longer. Prefer `adaptive` when stable ids matter, which is the usual reason to enable appearance. The adaptive bonus itself contributes little: with `reid_adaptive_weight_cap=0` HOTA changes by 0.12 on SoccerNet and 0.13 on DanceTrack. Leave the cap at its default and tune `reid_appearance_weight` and `reid_proximity_threshold` instead. + +### Choosing the adaptive weight + +Deep OC-SORT reports `reid_appearance_weight=0.75` for MOT17 and MOT20 and `1.25` for DanceTrack. On SoccerNet test and MOT17 val-half a sweep from 0.75 to 2.5 moves HOTA by less than 0.15, so the default transfers. DanceTrack val needs more than the reported value: + +| `reid_appearance_weight` | HOTA | ID switches | +| ------------------------ | --------- | ----------- | +| 1.25 (reported) | 55.97 | 1736 | +| 1.6 | 57.23 | 1696 | +| 2.4 | **57.26** | **1686** | + +The step sits between 1.25 and 1.6 and everything above it is a plateau. Where appearance barely separates identities the adaptive bonus collapses and only the base weight does any work, which is DanceTrack's situation and why the weight matters there. + +`"botsort"` remains the default because it is the more conservative of the two: bounded output, a hard appearance gate, and no threshold changes when you enable it. + ## BoT-SORT with and without ReID ### MOT17 test From a56a77479726dde2912e5399c4b46db28a9f0bee Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Wed, 2 Sep 2026 12:20:49 -0300 Subject: [PATCH 03/49] feat(reid): gate weak appearance matches with an appearance floor The adaptive fusion is additive, so any positive cosine similarity lifts a pair above the association threshold once geometry contributes nothing. With the proximity gate disabled that lets a lost track capture an unrelated detection on a weak appearance match rather than letting it start a new track. `reid_appearance_floor` zeroes the appearance term below a given cosine similarity, leaving those pairs scored on geometry alone. Defaults to 0.0, which is the Deep OC-SORT reference behaviour, so nothing changes unless the floor is set. Co-Authored-By: Claude Opus 5 --- docs/guides/reid.md | 11 +++++----- src/trackers/core/botsort/tracker.py | 11 ++++++++++ src/trackers/core/reid/fusion.py | 15 +++++++++++++ tests/core/test_botsort_reid.py | 32 ++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 32a8e47d..895bf56c 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -71,6 +71,7 @@ For the model catalog and fine-tuning, see the [`reid` training guide](https://g | `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | | `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | | `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | +| `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, the Deep OC-SORT behaviour). Calibrate per encoder: 0.7 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with the MOT17 and MSMT17 encoders, see [choosing a fusion method](#choosing-a-fusion-method). | --- @@ -165,7 +166,7 @@ On SoccerNet test (oracle detections, `osnet_x1_0` fine-tuned on SoccerNet train | 0.95 | `IoU >= 0.05` | 85.69 | 2565 | | 0.99 | `IoU >= 0.01` | 85.82 | 2431 | -On the [published tuned BoT-SORT geometry](../evaluations/results.md#soccernet-tracking) for SoccerNet, `adaptive` at 0.99 scores 85.87 HOTA with 2433 switches and at 1.0 scores **86.23** with **1692**, against 85.00 and 2523 without appearance. The identity gain is almost entirely the last step: 1.0 is what lets a player who walked out of frame come back with the same id. `botsort` at 1.0 reaches 87.29 HOTA but 4564 switches, see [choosing a fusion method](#choosing-a-fusion-method). +On the [published tuned BoT-SORT geometry](../evaluations/results.md#soccernet-tracking) for SoccerNet, `adaptive` at 0.99 scores 85.87 HOTA with 2433 switches and at 1.0 scores 86.23 with **1692**, against 85.00 and 2523 without appearance. Adding `reid_appearance_floor=0.7` at 1.0 reaches **87.82** with 2111 switches, the best SoccerNet configuration measured. The identity gain is almost entirely the last step: 1.0 is what lets a player who walked out of frame come back with the same id. `botsort` at 1.0 reaches 87.29 HOTA but 4564 switches, see [choosing a fusion method](#choosing-a-fusion-method). Half-opening is the worst setting for identity: 0.8 has more ID switches than either the closed gate or no appearance at all, because it admits enough distant candidates to create false matches without admitting the ones that enable recoveries. Either keep the gate or disable it. @@ -177,7 +178,7 @@ The right value depends on the footage. DanceTrack val targets stay in frame, an `"botsort"` (default) takes `min(d_iou, d_app)`. Appearance competes with geometry and wins only when it is strictly cheaper, so a pair is accepted when either cue is confident. Both gates are hard: a pair either clears `reid_appearance_threshold` and `reid_proximity_threshold` or contributes nothing. -`"adaptive"` adds a weighted appearance term to the geometric similarity, with the weight growing when the best appearance match stands clear of the runner-up and falling back to `reid_appearance_weight` when the top candidates are hard to tell apart. This is the weighting from Deep OC-SORT, ported on its own onto BoT-SORT; the velocity term, the confidence-gated feature update and the OCR pass are not included. `reid_appearance_threshold` has no effect under this method. +`"adaptive"` adds a weighted appearance term to the geometric similarity, with the weight growing when the best appearance match stands clear of the runner-up and falling back to `reid_appearance_weight` when the top candidates are hard to tell apart. This is the weighting from Deep OC-SORT, ported on its own onto BoT-SORT; the velocity term, the confidence-gated feature update and the OCR pass are not included. `reid_appearance_threshold` has no effect under this method. `reid_appearance_floor`, an addition of this library, sets a minimum cosine similarity below which a pair falls back to geometry alone: with the gate open it stops lost tracks from capturing unrelated detections on weak appearance matches, which otherwise clear the association threshold whenever geometry contributes nothing. Two consequences are worth knowing before switching: @@ -186,17 +187,17 @@ Two consequences are worth knowing before switching: ### Measured comparison -HOTA, best configuration found for each method. Detections, CMC, encoder and geometry are shared within a row; only appearance handling differs. SoccerNet and MOT17 use the published tuned BoT-SORT geometry from the [tracker comparison](../evaluations/results.md); DanceTrack's tuned set coincides with the library defaults. +HOTA, best configuration found for each method. Detections, CMC, encoder and geometry are shared within a row; only appearance handling differs. SoccerNet and MOT17 use the published tuned BoT-SORT geometry from the [tracker comparison](../evaluations/results.md): `lost_track_buffer=60`, `minimum_iou_threshold_first_assoc=0.1`, `minimum_iou_threshold_second_assoc=0.6`, `minimum_iou_threshold_unconfirmed_assoc=0.2` for SoccerNet, and `track_activation_threshold=0.6`, `high_conf_det_threshold=0.5`, `minimum_iou_threshold_unconfirmed_assoc=0.2` for MOT17. DanceTrack's tuned set coincides with the library defaults. All other parameters are library defaults. | Dataset | Encoder | no ReID | `botsort` | `adaptive` | `botsort` parameters | `adaptive` parameters | | -------------- | --------------------------------- | --------- | --------- | ---------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| SoccerNet test | `osnet_x1_0` fine-tuned SoccerNet | 85.00 | **87.29** | 86.23 | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=1.0` | +| SoccerNet test | `osnet_x1_0` fine-tuned SoccerNet | 85.00 | 87.29 | **87.82** | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.7` | | DanceTrack val | `osnet_x1_0_msmt17_combineall` | 53.89 | 56.11 | **57.39** | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0`, `reid_proximity_threshold=0.5` | | MOT17 val-half | `osnet_x1_0` fine-tuned MOT17 | **69.05** | 69.00 | 68.72 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5` | HOTA. Each cell is the best configuration found for that method (on SoccerNet the threshold and weight come from closed-gate sweeps with only the gate changed to 1.0, so they are good settings rather than necessarily the best); the parameter columns give the full ReID settings it ran with, defaults included. MOT17 moves by less than 0.35 HOTA in any direction on 7 sequences, inside single-sequence variance. -**The two rules trade HOTA against identity stability.** On DanceTrack `adaptive` leads on both, cutting ID switches from 1905 to 1683 where `botsort` raises them to 2276 while still gaining HOTA. On SoccerNet with the gate disabled `botsort` has the higher HOTA and IDF1 (87.29 and 83.13 against 86.23 and 82.52) but 4564 ID switches against 1692, 81% more than running no appearance at all; two thirds of its identity changes revert within ten frames, because `min(d_iou, d_app)` takes whichever candidate is cheapest in the current frame and flips between players in the same kit. `adaptive` commits to a re-association and keeps it, so its errors are fewer and longer. Prefer `adaptive` when stable ids matter, which is the usual reason to enable appearance. The adaptive bonus itself contributes little: with `reid_adaptive_weight_cap=0` HOTA changes by 0.12 on SoccerNet and 0.13 on DanceTrack. Leave the cap at its default and tune `reid_appearance_weight` and `reid_proximity_threshold` instead. +**The two rules trade HOTA against identity stability.** On DanceTrack `adaptive` leads on both, cutting ID switches from 1905 to 1683 where `botsort` raises them to 2276 while still gaining HOTA. On SoccerNet with the gate disabled `botsort` beats floor-less `adaptive` on HOTA and IDF1 (87.29 and 83.13 against 86.23 and 82.52) but takes 4564 ID switches against 1692, 81% more than running no appearance at all; two thirds of its identity changes revert within ten frames, because `min(d_iou, d_app)` takes whichever candidate is cheapest in the current frame and flips between players in the same kit. `adaptive` commits to a re-association and keeps it, so its errors are fewer and longer. With `reid_appearance_floor=0.7` blocking the weakest captures, `adaptive` leads SoccerNet outright: 87.82 HOTA and 83.92 IDF1 with 2111 switches (SoccerNet test, tuned geometry, fine-tuned encoder). That floor value belongs to that encoder: applied unchanged at the open gate it loses HOTA on MOT17 val-half (66.44 against 68.96 without appearance), DanceTrack val (52.98 against 56.56 without the floor) and SportsMOT val (81.00 against 81.32), all with generic or differently fine-tuned `osnet_x1_0` encoders whose cosine scales differ. Treat the floor as a per-encoder calibration, like `reid_appearance_threshold`, and leave it at 0.0 until you have measured it. Prefer `adaptive` when stable ids matter, which is the usual reason to enable appearance. The adaptive bonus itself contributes little: with `reid_adaptive_weight_cap=0` HOTA changes by 0.12 on SoccerNet and 0.13 on DanceTrack. Leave the cap at its default and tune `reid_appearance_weight` and `reid_proximity_threshold` instead. ### Choosing the adaptive weight diff --git a/src/trackers/core/botsort/tracker.py b/src/trackers/core/botsort/tracker.py index 5de591f2..be4a7630 100644 --- a/src/trackers/core/botsort/tracker.py +++ b/src/trackers/core/botsort/tracker.py @@ -118,6 +118,12 @@ class BoTSORTTracker(BaseTracker): reid_adaptive_weight_cap: Upper bound on the adaptive appearance bonus for ``reid_fusion="adaptive"``. Ignored otherwise. Default ``0.5``, the value Deep OC-SORT reports for MOT17 and MOT20; they use ``1.0`` for DanceTrack. + reid_appearance_floor: Minimum cosine similarity for appearance to contribute + when ``reid_fusion="adaptive"``. Ignored otherwise. Below the floor a pair + is scored on geometry alone, so a lost track cannot capture a detection on + a weak appearance match. Default ``0.0`` (disabled, the Deep OC-SORT + reference behaviour; see + :func:`~trackers.core.reid.fusion.fuse_adaptive_reid_association`). Notes: - Positive `maximum_frames_without_update` values are scaled by @@ -167,6 +173,7 @@ def __init__( reid_fusion: ReidFusionMethod = "botsort", reid_appearance_weight: float = 0.75, reid_adaptive_weight_cap: float = 0.5, + reid_appearance_floor: float = 0.0, ) -> None: self.maximum_frames_without_update = self._compute_maximum_frames_without_update( lost_track_buffer=lost_track_buffer, @@ -205,9 +212,12 @@ def __init__( raise ValueError(f"reid_appearance_weight must be non-negative, got {reid_appearance_weight}") if reid_adaptive_weight_cap < 0.0: raise ValueError(f"reid_adaptive_weight_cap must be non-negative, got {reid_adaptive_weight_cap}") + if not 0.0 <= reid_appearance_floor <= 1.0: + raise ValueError(f"reid_appearance_floor must be in [0, 1], got {reid_appearance_floor}") self.reid_fusion = reid_fusion self.reid_appearance_weight = reid_appearance_weight self.reid_adaptive_weight_cap = reid_adaptive_weight_cap + self.reid_appearance_floor = reid_appearance_floor self._init_timestamp_state(frame_rate) @@ -485,6 +495,7 @@ def _association_similarity( reid_proximity_threshold=self.reid_proximity_threshold, reid_appearance_weight=self.reid_appearance_weight, reid_adaptive_weight_cap=self.reid_adaptive_weight_cap, + reid_appearance_floor=self.reid_appearance_floor, ) return fuse_botsort_reid_association( iou_sim_fused, diff --git a/src/trackers/core/reid/fusion.py b/src/trackers/core/reid/fusion.py index 9712d40f..e272d077 100644 --- a/src/trackers/core/reid/fusion.py +++ b/src/trackers/core/reid/fusion.py @@ -98,6 +98,7 @@ def fuse_adaptive_reid_association( reid_appearance_weight: float, reid_adaptive_weight_cap: float, reid_proximity_threshold: float, + reid_appearance_floor: float = 0.0, proximity_iou_similarity: np.ndarray | None = None, ) -> np.ndarray: """Fuse IoU and appearance with Deep OC-SORT's adaptive weighting. @@ -128,6 +129,9 @@ def fuse_adaptive_reid_association( ``1.0`` for DanceTrack. reid_proximity_threshold: Maximum standard-IoU distance at which appearance may contribute. + reid_appearance_floor: Minimum cosine similarity for appearance to + contribute; below it the pair is scored on geometry alone. Default + ``0.0`` (disabled, the reference behaviour). proximity_iou_similarity: Standard-IoU similarities with shape ``(T, N)``. Defaults to ``association_similarity``. @@ -156,6 +160,16 @@ def fuse_adaptive_reid_association( The velocity-direction term of the full Deep OC-SORT cost is not included; it belongs to OC-SORT's motion model. + + ``reid_appearance_floor`` is an extension over the reference, which has + no appearance gate at all. Because the fusion is additive, any positive + cosine similarity raises the fused score above the association + threshold once geometry contributes nothing, so with the proximity gate + disabled a lost track can capture an unrelated detection on a weak + appearance match instead of letting it start a new track. The floor + zeroes the appearance term for such pairs, leaving them scored on + geometry alone. Like the proximity gate, it applies only to the + appearance term: margins are still measured over all candidates. """ if proximity_iou_similarity is None: proximity_iou_similarity = association_similarity @@ -168,5 +182,6 @@ def fuse_adaptive_reid_association( out_of_range = (1.0 - proximity_iou_similarity) > reid_proximity_threshold gated_appearance = np.where(out_of_range, 0.0, appearance) + gated_appearance = np.where(appearance < reid_appearance_floor, 0.0, gated_appearance) return association_similarity + (reid_appearance_weight + adaptive_bonus) * gated_appearance diff --git a/tests/core/test_botsort_reid.py b/tests/core/test_botsort_reid.py index 90d449cd..a20d9fd4 100644 --- a/tests/core/test_botsort_reid.py +++ b/tests/core/test_botsort_reid.py @@ -332,6 +332,36 @@ def test_low_proximity_ignores_appearance(self) -> None: ) assert fused[0, 0] == pytest.approx(0.36) + def test_appearance_floor_drops_weak_matches(self) -> None: + # Cosine 0.6 is below the 0.65 floor, so the pair falls back to geometry alone; + # cosine 0.8 is above it and contributes as usual: 0.3 + 0.75 * 0.8. + fused = fuse_adaptive_reid_association( + np.array([[0.3, 0.3]], dtype=np.float32), + np.array([[0.6, 0.8]], dtype=np.float32), + reid_appearance_weight=0.75, + reid_adaptive_weight_cap=0.0, + reid_proximity_threshold=1.0, + reid_appearance_floor=0.65, + ) + assert fused[0, 0] == pytest.approx(0.3) + assert fused[0, 1] == pytest.approx(0.9) + + def test_floored_competitor_does_not_inflate_the_bonus(self) -> None: + # Both detections look identical (0.9), so no bonus is owed. Flooring plays no + # role here (both pass), but the margin must be measured over raw appearance: + # a floored candidate reading as similarity 0 would fabricate a 0.9 gap. + fused = fuse_adaptive_reid_association( + np.array([[0.6, 0.6]], dtype=np.float32), + np.array([[0.9, 0.9]], dtype=np.float32), + reid_appearance_weight=0.75, + reid_adaptive_weight_cap=0.5, + reid_proximity_threshold=0.5, + reid_appearance_floor=0.95, + ) + # Floor drops appearance for both pairs: geometry only. + assert fused[0, 0] == pytest.approx(0.6) + assert fused[0, 1] == pytest.approx(0.6) + def test_gated_competitor_does_not_inflate_the_bonus(self) -> None: # Both detections look identical (0.9), so appearance cannot tell them apart and # no bonus is owed. Gating one out on proximity must not change that: the bonus @@ -448,6 +478,7 @@ def spy(association, appearance, **kwargs): reid_appearance_weight=0.5, reid_adaptive_weight_cap=0.4, reid_proximity_threshold=0.9, + reid_appearance_floor=0.3, ) tracker.update(_detection((10.0, 10.0, 30.0, 30.0)), frame=_frame(5)) tracker.update(_detection((10.0, 10.0, 30.0, 30.0)), frame=_frame(6)) @@ -455,6 +486,7 @@ def spy(association, appearance, **kwargs): assert received["reid_appearance_weight"] == 0.5 assert received["reid_adaptive_weight_cap"] == 0.4 assert received["reid_proximity_threshold"] == 0.9 + assert received["reid_appearance_floor"] == 0.3 def test_adaptive_fusion_changes_association(self) -> None: # Non-default weights: at 0.75 with the cap binding the effective weight is exactly From 64b064be6c2cd72586092b50a0cd245748732954 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:21:21 +0000 Subject: [PATCH 04/49] =?UTF-8?q?fix(pre=5Fcommit):=20=F0=9F=8E=A8=20auto?= =?UTF-8?q?=20format=20pre-commit=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guides/reid.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 895bf56c..959e8557 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -62,15 +62,15 @@ For the model catalog and fine-tuning, see the [`reid` training guide](https://g ## Key Parameters -| Parameter | Purpose | Tuning guidance | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | -| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | -| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | -| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | -| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | -| `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | -| `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | -| `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | +| Parameter | Purpose | Tuning guidance | +| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | +| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | +| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | +| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | +| `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | +| `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | +| `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | | `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, the Deep OC-SORT behaviour). Calibrate per encoder: 0.7 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with the MOT17 and MSMT17 encoders, see [choosing a fusion method](#choosing-a-fusion-method). | --- @@ -189,11 +189,11 @@ Two consequences are worth knowing before switching: HOTA, best configuration found for each method. Detections, CMC, encoder and geometry are shared within a row; only appearance handling differs. SoccerNet and MOT17 use the published tuned BoT-SORT geometry from the [tracker comparison](../evaluations/results.md): `lost_track_buffer=60`, `minimum_iou_threshold_first_assoc=0.1`, `minimum_iou_threshold_second_assoc=0.6`, `minimum_iou_threshold_unconfirmed_assoc=0.2` for SoccerNet, and `track_activation_threshold=0.6`, `high_conf_det_threshold=0.5`, `minimum_iou_threshold_unconfirmed_assoc=0.2` for MOT17. DanceTrack's tuned set coincides with the library defaults. All other parameters are library defaults. -| Dataset | Encoder | no ReID | `botsort` | `adaptive` | `botsort` parameters | `adaptive` parameters | -| -------------- | --------------------------------- | --------- | --------- | ---------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| SoccerNet test | `osnet_x1_0` fine-tuned SoccerNet | 85.00 | 87.29 | **87.82** | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.7` | -| DanceTrack val | `osnet_x1_0_msmt17_combineall` | 53.89 | 56.11 | **57.39** | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0`, `reid_proximity_threshold=0.5` | -| MOT17 val-half | `osnet_x1_0` fine-tuned MOT17 | **69.05** | 69.00 | 68.72 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5` | +| Dataset | Encoder | no ReID | `botsort` | `adaptive` | `botsort` parameters | `adaptive` parameters | +| -------------- | --------------------------------- | --------- | --------- | ---------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| SoccerNet test | `osnet_x1_0` fine-tuned SoccerNet | 85.00 | 87.29 | **87.82** | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.7` | +| DanceTrack val | `osnet_x1_0_msmt17_combineall` | 53.89 | 56.11 | **57.39** | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0`, `reid_proximity_threshold=0.5` | +| MOT17 val-half | `osnet_x1_0` fine-tuned MOT17 | **69.05** | 69.00 | 68.72 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5` | HOTA. Each cell is the best configuration found for that method (on SoccerNet the threshold and weight come from closed-gate sweeps with only the gate changed to 1.0, so they are good settings rather than necessarily the best); the parameter columns give the full ReID settings it ran with, defaults included. MOT17 moves by less than 0.35 HOTA in any direction on 7 sequences, inside single-sequence variance. From 2a11a2aedf81e592d55e82479ce6bca25e2abd62 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Fri, 4 Sep 2026 15:42:55 -0300 Subject: [PATCH 05/49] docs(reid): add BoT-SORT + ReID to the tracker comparison and per-rule result tables - results.md: a BoT-SORT + ReID row in each Tuned tab with the best configuration found per dataset, its full configuration in the YAML block, and the encoder named above it. A general "When to Use" entry. C-BIoU's SoccerNet claim narrowed to motion-only trackers. - reid.md: one results table per fusion rule with encoder, parameters and ID switches; the generic-encoder DanceTrack pair and the earlier fastreid / MSMT17 runs kept under "Other encoders". The SportsMOT gate caveat replaced by the measured sweep: botsort at 0.99 and 1.0 loses to its own default, so adaptive's lead there is not a gate artifact. DanceTrack's adaptive row is marked pending a run with the fine-tuned encoder rather than borrowing the generic number. - botsort.md: point at the guide for ReID numbers. - README: BoT-SORT accepts an appearance branch through reid_model. Co-Authored-By: Claude Fable 5.1 --- README.md | 2 +- docs/evaluations/results.md | 99 ++++++++++++++++++++++----------- docs/guides/reid.md | 106 +++++++++++++++++++++++++++--------- docs/trackers/botsort.md | 2 +- 4 files changed, 147 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 0a2335fe..9badd5ca 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ For all CLI options, see the [tracking guide](https://trackers.roboflow.com/deve ## Algorithms -Each tracker below is a faithful implementation of its original paper's motion and association pipeline; appearance/ReID branches are not included where papers offer them — see each tracker's docs page for the exact scope. Pick the one that fits your scene, or run the benchmark to find out which performs best on your data. +Each tracker below is a faithful implementation of its original paper's motion and association pipeline; appearance/ReID branches are off by default, though BoT-SORT accepts one through `reid_model` (see the [ReID guide](https://trackers.roboflow.com/latest/guides/reid/)). Each tracker's docs page states the exact scope. Pick the one that fits your scene, or run the benchmark to find out which performs best on your data. diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index 9909d84b..da5b7b32 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -56,15 +56,16 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests Results after grid search over tracker parameters. - | Tracker | HOTA | IDF1 | MOTA | - | :-------: | :------: | :------: | :------: | - | SORT | 60.4 | 72.5 | 75.8 | - | ByteTrack | 60.5 | 72.7 | 76.1 | - | OC-SORT | 62.0 | 76.5 | 77.3 | - | BoT-SORT | **63.8** | 78.7 | **79.4** | - | C-BIoU | 63.0 | **79.1** | 77.4 | + | Tracker | HOTA | IDF1 | MOTA | + | :-------------: | :------: | :------: | :------: | + | SORT | 60.4 | 72.5 | 75.8 | + | ByteTrack | 60.5 | 72.7 | 76.1 | + | OC-SORT | 62.0 | 76.5 | 77.3 | + | BoT-SORT | 63.8 | 78.7 | **79.4** | + | C-BIoU | 63.0 | 79.1 | 77.4 | + | BoT-SORT + ReID | **64.1** | **79.2** | **79.4** | - Tuned configuration for each tracker. + Tuned configuration for each tracker. The ReID row uses an `osnet_x1_0` fine-tuned on MOT17 train. ```yaml SORT: @@ -109,6 +110,12 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests track_activation_threshold: 0.7 buffer_ratio_first: 0.3 buffer_ratio_second: 0.5 + + BoT-SORT + ReID: + reid_model: osnet_x1_0 fine-tuned on MOT17 train + reid_fusion: botsort + reid_appearance_threshold: 0.25 + reid_proximity_threshold: 0.5 ``` ## [SportsMOT](https://arxiv.org/abs/2304.05170) @@ -150,15 +157,16 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar Results after grid search over tracker parameters. - | Tracker | HOTA | IDF1 | MOTA | - | :-------: | :------: | :------: | :------: | - | SORT | 72.9 | 73.0 | 95.8 | - | ByteTrack | 73.3 | 73.5 | 95.9 | - | OC-SORT | 74.0 | **75.4** | 95.6 | - | BoT-SORT | **74.1** | 74.0 | **96.9** | - | C-BIoU | 73.1 | 72.6 | 96.7 | + | Tracker | HOTA | IDF1 | MOTA | + | :-------------: | :------: | :------: | :------: | + | SORT | 72.9 | 73.0 | 95.8 | + | ByteTrack | 73.3 | 73.5 | 95.9 | + | OC-SORT | 74.0 | **75.4** | 95.6 | + | BoT-SORT | 74.1 | 74.0 | 96.9 | + | C-BIoU | 73.1 | 72.6 | 96.7 | + | BoT-SORT + ReID | **75.0** | 75.1 | **97.0** | - Tuned configuration for each tracker. + Tuned configuration for each tracker. The ReID row uses the generic `osnet_x1_0_msmt17_combineall`, not a fine-tune. ```yaml SORT: @@ -203,6 +211,13 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar track_activation_threshold: 0.7 buffer_ratio_first: 0.3 buffer_ratio_second: 0.5 + + BoT-SORT + ReID: + reid_model: osnet_x1_0_msmt17_combineall + reid_fusion: adaptive + reid_appearance_weight: 0.75 + reid_adaptive_weight_cap: 0.5 + reid_proximity_threshold: 0.99 ``` ## [SoccerNet-tracking](https://arxiv.org/abs/2204.06918) @@ -244,15 +259,16 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I Results after grid search over tracker parameters. - | Tracker | HOTA | IDF1 | MOTA | - | :-------: | :------: | :------: | :------: | - | SORT | 84.2 | 78.2 | 98.2 | - | ByteTrack | 84.0 | 78.1 | 98.2 | - | OC-SORT | 82.9 | 77.9 | 96.8 | - | BoT-SORT | 85.0 | 79.7 | 97.2 | - | C-BIoU | **85.7** | **80.0** | **99.3** | + | Tracker | HOTA | IDF1 | MOTA | + | :-------------: | :------: | :------: | :------: | + | SORT | 84.2 | 78.2 | 98.2 | + | ByteTrack | 84.0 | 78.1 | 98.2 | + | OC-SORT | 82.9 | 77.9 | 96.8 | + | BoT-SORT | 85.0 | 79.7 | 97.2 | + | C-BIoU | 85.7 | 80.0 | 99.3 | + | BoT-SORT + ReID | **87.8** | **83.9** | **99.4** | - Tuned configuration for each tracker. + Tuned configuration for each tracker. The ReID row uses an `osnet_x1_0` fine-tuned on SoccerNet train. ```yaml SORT: @@ -297,6 +313,14 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I track_activation_threshold: 0.48 buffer_ratio_first: 0.68 buffer_ratio_second: 0.50 + + BoT-SORT + ReID: + reid_model: osnet_x1_0 fine-tuned on SoccerNet train + reid_fusion: adaptive + reid_appearance_weight: 0.75 + reid_adaptive_weight_cap: 0.5 + reid_proximity_threshold: 1.0 + reid_appearance_floor: 0.7 ``` !!! note "SoccerNet buffer ordering exception" @@ -341,15 +365,16 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art Hyperparameter tuning, reporting the best tuned configuration per tracker evaluated on the test set (tuning performed on the valid split; if tuning did not outperform registry defaults, defaults are shown). - | Tracker | HOTA | IDF1 | MOTA | - | :-------: | :------: | :------: | :------: | - | SORT | 54.3 | 53.4 | 89.5 | - | ByteTrack | 55.3 | 55.2 | 89.9 | - | OC-SORT | 54.1 | 53.3 | 89.3 | - | BoT-SORT | **57.8** | 57.9 | 92.2 | - | C-BIoU | 57.7 | **58.7** | **92.4** | + | Tracker | HOTA | IDF1 | MOTA | + | :-------------: | :------: | :------: | :------: | + | SORT | 54.3 | 53.4 | 89.5 | + | ByteTrack | 55.3 | 55.2 | 89.9 | + | OC-SORT | 54.1 | 53.3 | 89.3 | + | BoT-SORT | 57.8 | 57.9 | 92.2 | + | C-BIoU | 57.7 | 58.7 | **92.4** | + | BoT-SORT + ReID | **58.5** | **58.9** | 92.1 | - Best configuration for each tracker. + Best configuration for each tracker. The ReID row uses an `osnet_x1_0` fine-tuned on DanceTrack train. ```yaml SORT: @@ -394,6 +419,12 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art high_conf_det_threshold: 0.34 buffer_ratio_first: 0.12 buffer_ratio_second: 0.10 + + BoT-SORT + ReID: + reid_model: osnet_x1_0 fine-tuned on DanceTrack train + reid_fusion: botsort + reid_appearance_threshold: 0.25 + reid_proximity_threshold: 0.5 ``` !!! note "DanceTrack buffer ordering exception" @@ -412,7 +443,9 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art **BoT-SORT** is the choice when camera ego-motion is strong and you need the most stable identities. It extends ByteTrack with camera motion compensation (CMC) and confidence-aware association, which reduces ID switches on panning or handheld footage. Use BoT-SORT for sports broadcasts, drone video, or any scene where the camera moves frequently. The CMC overhead is small relative to the detector, so the trade-off favors identity stability over raw speed. -**C-BIoU** targets fast or irregular motion when you want buffered, cascaded geometric matching without camera motion compensation. In these benchmarks it leads on SoccerNet when tuned, and reaches the highest tuned IDF1 and MOTA on DanceTrack among the motion-only trackers. Use C-BIoU when BoT-SORT-style association is a good fit but CMC is unavailable or harmful, or when plain IoU matching is too strict. See [C-BIoU](../trackers/cbiou.md) for buffer scales **b1** and **b2**. +**C-BIoU** targets fast or irregular motion when you want buffered, cascaded geometric matching without camera motion compensation. In these benchmarks it leads on SoccerNet among the motion-only trackers when tuned, and reaches the highest tuned IDF1 and MOTA on DanceTrack among the motion-only trackers. Use C-BIoU when BoT-SORT-style association is a good fit but CMC is unavailable or harmful, or when plain IoU matching is too strict. See [C-BIoU](../trackers/cbiou.md) for buffer scales **b1** and **b2**. + +**BoT-SORT + ReID** adds an appearance encoder to BoT-SORT's association, use it with parameter `reid_model=`. Appearance carries identity through the moments geometry cannot, like a long occlusion, or a target that leaves the frame and returns where no motion model would predict it. How much it adds depends on how well the encoder separates your targets, so the gain is largest when subjects are visually distinct and the encoder has seen footage like yours, and smallest when they share a uniform appearance. Consider fine-tuning or calibrating an encoder for your domain rather than relying on a generic one, and calibrate the appearance threshold with it. Use it when you want to keep tracks when objects leave and re-enter the frame, or when there are long occlusions. Learn how to use it at [ReID guide](../guides/reid.md). **McByte** is the choice when you want the highest identity stability and can afford optional SAM/Cutie mask dependencies. It extends BoT-SORT-style association with temporally propagated segmentation masks as an extra matching cue, requiring no per-video tuning. Reported against Trackers' BoT-SORT baseline (without re-identification) at default parameters, McByte improves HOTA and IDF1 on all four datasets in this comparison, with the largest gain on DanceTrack. Use McByte when identity consistency matters more than raw throughput or dependency footprint. See [McByte](../trackers/mcbyte.md) for setup and the mask-conditioning parameters. diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 959e8557..cb61d0c9 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -62,16 +62,16 @@ For the model catalog and fine-tuning, see the [`reid` training guide](https://g ## Key Parameters -| Parameter | Purpose | Tuning guidance | -| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | -| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | -| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | -| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | -| `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | -| `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | -| `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | -| `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, the Deep OC-SORT behaviour). Calibrate per encoder: 0.7 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with the MOT17 and MSMT17 encoders, see [choosing a fusion method](#choosing-a-fusion-method). | +| Parameter | Purpose | Tuning guidance | +| :-------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | +| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | +| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | +| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | +| `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | +| `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | +| `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | +| `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, the Deep OC-SORT behaviour). Calibrate per encoder: 0.7 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with the MOT17 and MSMT17 encoders, see [choosing a fusion method](#choosing-a-fusion-method). | --- @@ -189,11 +189,11 @@ Two consequences are worth knowing before switching: HOTA, best configuration found for each method. Detections, CMC, encoder and geometry are shared within a row; only appearance handling differs. SoccerNet and MOT17 use the published tuned BoT-SORT geometry from the [tracker comparison](../evaluations/results.md): `lost_track_buffer=60`, `minimum_iou_threshold_first_assoc=0.1`, `minimum_iou_threshold_second_assoc=0.6`, `minimum_iou_threshold_unconfirmed_assoc=0.2` for SoccerNet, and `track_activation_threshold=0.6`, `high_conf_det_threshold=0.5`, `minimum_iou_threshold_unconfirmed_assoc=0.2` for MOT17. DanceTrack's tuned set coincides with the library defaults. All other parameters are library defaults. -| Dataset | Encoder | no ReID | `botsort` | `adaptive` | `botsort` parameters | `adaptive` parameters | -| -------------- | --------------------------------- | --------- | --------- | ---------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| SoccerNet test | `osnet_x1_0` fine-tuned SoccerNet | 85.00 | 87.29 | **87.82** | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.7` | -| DanceTrack val | `osnet_x1_0_msmt17_combineall` | 53.89 | 56.11 | **57.39** | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0`, `reid_proximity_threshold=0.5` | -| MOT17 val-half | `osnet_x1_0` fine-tuned MOT17 | **69.05** | 69.00 | 68.72 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5` | +| Dataset | Encoder | no ReID | `botsort` | `adaptive` | `botsort` parameters | `adaptive` parameters | +| :------------: | :-------------------------------: | :-------: | :-------: | :--------: | :---------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------: | +| SoccerNet test | `osnet_x1_0` fine-tuned SoccerNet | 85.00 | 87.29 | **87.82** | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.7` | +| DanceTrack val | `osnet_x1_0_msmt17_combineall` | 53.89 | 56.11 | **57.39** | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0`, `reid_proximity_threshold=0.5` | +| MOT17 val-half | `osnet_x1_0` fine-tuned MOT17 | **69.05** | 69.00 | 68.72 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5` | HOTA. Each cell is the best configuration found for that method (on SoccerNet the threshold and weight come from closed-gate sweeps with only the gate changed to 1.0, so they are good settings rather than necessarily the best); the parameter columns give the full ReID settings it ran with, defaults included. MOT17 moves by less than 0.35 HOTA in any direction on 7 sequences, inside single-sequence variance. @@ -203,11 +203,11 @@ HOTA. Each cell is the best configuration found for that method (on SoccerNet th Deep OC-SORT reports `reid_appearance_weight=0.75` for MOT17 and MOT20 and `1.25` for DanceTrack. On SoccerNet test and MOT17 val-half a sweep from 0.75 to 2.5 moves HOTA by less than 0.15, so the default transfers. DanceTrack val needs more than the reported value: -| `reid_appearance_weight` | HOTA | ID switches | -| ------------------------ | --------- | ----------- | -| 1.25 (reported) | 55.97 | 1736 | -| 1.6 | 57.23 | 1696 | -| 2.4 | **57.26** | **1686** | +| `reid_appearance_weight` | HOTA | ID switches | +| :----------------------: | :-------: | :---------: | +| 1.25 (reported) | 55.97 | 1736 | +| 1.6 | 57.23 | 1696 | +| 2.4 | **57.26** | **1686** | The step sits between 1.25 and 1.6 and everything above it is a plateau. Where appearance barely separates identities the adaptive bonus collapses and only the base weight does any work, which is DanceTrack's situation and why the weight matters there. @@ -215,18 +215,60 @@ The step sits between 1.25 and 1.6 and everything above it is a plateau. Where a ## BoT-SORT with and without ReID -### MOT17 test + -YOLOX detections, CMC on, Codabench MOT17 test (same protocol as the [benchmark results](../evaluations/results.md) default table). ReID: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.2` ([MOT17 re-ID study](https://www-sop.inria.fr/members/Francois.Bremond/Postscript/Tomasz__SCCAI_2025.pdf) Table 8). +Evaluations are on test splits and one table per fusion method. Each ReID row shares its detections and geometry with the BoT-SORT row above it, so only the appearance branch differs, and both come from the [tracker comparison](../evaluations/results.md), where the full configuration for each is listed. Detection sources and split usage are covered in [Methodology](../evaluations/methodology.md). Bold marks the best cell per dataset across both tables. + +The encoder is part of the configuration. SoccerNet, MOT17 and DanceTrack use an `osnet_x1_0` fine-tuned on the dataset's own train split, SportsMOT the generic `osnet_x1_0_msmt17_combineall`, so absolute numbers do not compare across datasets and only the with and without deltas do. Within a dataset both fusion rules run the same encoder, which is what makes the two tables comparable; DanceTrack's `adaptive` row is pending a run with the fine-tuned encoder. With the generic encoder DanceTrack loses to plain BoT-SORT under both rules, see [Other encoders](#other-encoders). + +### `reid_fusion="botsort"` + +`min(d_iou, d_app)`, gated by `reid_appearance_threshold` and `reid_proximity_threshold`. + +| Dataset | Config | HOTA | IDF1 | MOTA | IDSW | Encoder | ReID parameters | +| :-------------- | :-------------- | :-------: | :-------: | :------: | :--: | :--------------------------------- | :---------------------------------------------------------------- | +| SoccerNet test | BoT-SORT | 85.00 | 79.68 | 97.25 | 2523 | — | — | +| SoccerNet test | BoT-SORT + ReID | 87.29 | 83.13 | 98.73 | 4564 | `osnet_x1_0` fine-tuned SoccerNet | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | +| MOT17 test | BoT-SORT | 63.8 | 78.7 | 79.4 | — | — | — | +| MOT17 test | BoT-SORT + ReID | **64.12** | **79.16** | 79.36 | 1617 | `osnet_x1_0` fine-tuned MOT17 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | +| SportsMOT test | BoT-SORT | 73.8 | 73.4 | 96.9 | — | — | — | +| SportsMOT test | BoT-SORT + ReID | 73.48 | 73.10 | 96.88 | 2863 | `osnet_x1_0_msmt17_combineall` | `reid_appearance_threshold=0.15`, `reid_proximity_threshold=0.5` | +| DanceTrack test | BoT-SORT | 57.8 | 57.9 | **92.2** | — | — | — | +| DanceTrack test | BoT-SORT + ReID | **58.5** | **58.9** | 92.1 | — | `osnet_x1_0` fine-tuned DanceTrack | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | + +### `reid_fusion="adaptive"` + +`IoU + w·cos` with the Deep OC-SORT bonus, gated by `reid_proximity_threshold` and, where set, `reid_appearance_floor`. + +| Dataset | Config | HOTA | IDF1 | MOTA | IDSW | Encoder | ReID parameters | +| :-------------- | :-------------- | :-------: | :-------: | :-------: | :------: | :--------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | +| SoccerNet test | BoT-SORT | 85.00 | 79.68 | 97.25 | 2523 | — | — | +| SoccerNet test | BoT-SORT + ReID | **87.82** | **83.92** | **99.43** | 2111 | `osnet_x1_0` fine-tuned SoccerNet | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.7` | +| MOT17 test | BoT-SORT | 63.8 | 78.7 | 79.4 | — | — | — | +| MOT17 test | BoT-SORT + ReID | 63.8 | 78.86 | **79.49** | **1416** | `osnet_x1_0` fine-tuned MOT17 | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5` | +| SportsMOT test | BoT-SORT | 73.8 | 73.4 | 96.9 | — | — | — | +| SportsMOT test | BoT-SORT + ReID | **74.98** | **75.09** | **96.99** | **2306** | `osnet_x1_0_msmt17_combineall` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.99` | +| DanceTrack test | BoT-SORT | 57.8 | 57.9 | **92.2** | — | — | — | +| DanceTrack test | BoT-SORT + ReID | — | — | — | — | `osnet_x1_0` fine-tuned DanceTrack | not yet run with this encoder | + +Anything not listed in the ReID parameters column is a library default, and the DanceTrack leaderboard does not return ID switches. Each rule is shown at its own best gate. On SportsMOT both were swept across 0.5, 0.99 and 1.0 on the val split: `botsort` is strongest at the default 0.5 (79.38 HOTA), scoring 77.82 at 0.99 and 64.80 at 1.0, while `adaptive` peaks at 0.99. The wider gate suits the additive rule and not the minimum, which is consistent with how the two combine appearance, so the comparison holds with both rules given the same range. On SoccerNet both rules were run at 1.0, and on DanceTrack with the generic encoder (see Other encoders). On SoccerNet neither was re-tuned there, so both carry thresholds chosen at the closed gate. On MOT17 only `adaptive` has been run past the default gate, where it scores below its closed-gate result. + +### Other encoders + +The rows above use the best encoder found for each dataset. These measurements used other encoders and are kept because they show how much that choice decides. + +#### MOT17, `fastreid_mot17_sbs50` + +The MOT17-trained FastReID checkpoint BoT-SORT itself uses, run on library-default geometry at `reid_appearance_threshold=0.2` ([MOT17 re-ID study](https://www-sop.inria.fr/members/Francois.Bremond/Postscript/Tomasz__SCCAI_2025.pdf) Table 8). + +Codabench MOT17 test: | Config | HOTA | IDF1 | MOTA | | :-------------- | :------: | :------: | :------: | | BoT-SORT | 63.7 | 78.7 | **79.2** | | BoT-SORT + ReID | **63.9** | **79.2** | **79.2** | -### MOT17 val-half - -YOLOX detections, CMC on, MOT17 val-half split, same encoder and threshold, scored with `trackers eval`. +MOT17 val-half, scored with `trackers eval`: | Config | HOTA | IDF1 | MOTA | | :-------------- | :------: | :------: | :------: | @@ -235,9 +277,19 @@ YOLOX detections, CMC on, MOT17 val-half split, same encoder and threshold, scor The MOT17 re-ID study reports 68.43 HOTA / 80.92 IDF1 without ReID and 68.95 / 81.98 with, on the same split at `reid_appearance_threshold=0.2` (Table 8 and Table 13; MOTA is not reported for that YOLOX setup). -### SoccerNet test (OSNet MSMT17) +#### DanceTrack, `osnet_x1_0_msmt17_combineall` + +The generic pedestrian encoder on dancers, run on library-default geometry, YOLOX detections, Codabench DanceTrack test. Both rules land below geometry alone; the fine-tuned encoder in the tables above is what turns DanceTrack around. + +| Config | HOTA | IDF1 | MOTA | +| :--------------------------------------------------- | :------: | :------: | :------: | +| BoT-SORT | **57.8** | **57.9** | 92.2 | +| BoT-SORT + ReID, `botsort` (θ=0.25, gate=0.5) | 56.0 | 56.1 | 91.8 | +| BoT-SORT + ReID, `adaptive` (w=2.4, cap=0, gate=0.5) | 57.2 | 56.5 | **92.3** | + +#### SoccerNet, `osnet_x1_0_msmt17_combineall` -Oracle detections, CMC on, SoccerNet-tracking test (same protocol as the [benchmark results](../evaluations/results.md) default table). ReID: `osnet_x1_0_msmt17_combineall` (MSMT17 pretrained), so this is a cross-domain encoder on soccer footage. +A generic pedestrian encoder on soccer footage, run on library-default geometry. Compare with the fine-tuned rows above. | Config | HOTA | IDF1 | MOTA | | :------------------------------ | :------: | :------: | :------: | diff --git a/docs/trackers/botsort.md b/docs/trackers/botsort.md index 9c94ca43..08927038 100644 --- a/docs/trackers/botsort.md +++ b/docs/trackers/botsort.md @@ -54,7 +54,7 @@ BoT-SORT keeps the same tracking-by-detection backbone as [ByteTrack](bytetrack. ## ReID appearance (optional) -BoT-SORT can fuse appearance embeddings with IoU during association via an optional `reid_model`. Install, usage, parameters, and MOT17 with/without ReID scores are on the [ReID appearance](../guides/reid.md) page. +BoT-SORT can fuse appearance embeddings with IoU during association via an optional `reid_model`. For ReID numbers go to the [ReID guide](../guides/reid.md#bot-sort-with-and-without-reid), which holds the tables for both fusion rules and best configuration per dataset. Aslo learn how to install, use and choose parameters on the same page. ## Run on video, webcam, or RTSP stream From d7d3819a72582a63a4f0f09582557a70e3aedbe5 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Fri, 4 Sep 2026 16:00:20 -0300 Subject: [PATCH 06/49] docs(botsort): fix typo flagged by codespell Co-Authored-By: Claude Fable 5.1 --- docs/trackers/botsort.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/trackers/botsort.md b/docs/trackers/botsort.md index 08927038..e8f53fdf 100644 --- a/docs/trackers/botsort.md +++ b/docs/trackers/botsort.md @@ -54,7 +54,7 @@ BoT-SORT keeps the same tracking-by-detection backbone as [ByteTrack](bytetrack. ## ReID appearance (optional) -BoT-SORT can fuse appearance embeddings with IoU during association via an optional `reid_model`. For ReID numbers go to the [ReID guide](../guides/reid.md#bot-sort-with-and-without-reid), which holds the tables for both fusion rules and best configuration per dataset. Aslo learn how to install, use and choose parameters on the same page. +BoT-SORT can fuse appearance embeddings with IoU during association via an optional `reid_model`. For ReID numbers go to the [ReID guide](../guides/reid.md#bot-sort-with-and-without-reid), which holds the tables for both fusion rules and best configuration per dataset. Also learn how to install, use and choose parameters on the same page. ## Run on video, webcam, or RTSP stream From 0febfa943f86dcdcc428cf30105606c27e895b98 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Fri, 4 Sep 2026 20:32:47 -0300 Subject: [PATCH 07/49] docs(reid): Default-tab rows for DanceTrack and SoccerNet; correct the SoccerNet generic-encoder table - results.md: BoT-SORT + ReID at library defaults (catalog-default osnet_x1_0_msmt17_combineall, botsort fusion, theta 0.25, gate 0.5) in the DanceTrack and SoccerNet Default tabs: 56.0 / 56.1 / 91.8 and 82.9 / 77.7 / 96.5. MOT17 and SportsMOT follow once Codabench scores them. Intro states the encoder rule for the Default and Tuned rows. - reid.md: the SoccerNet generic-encoder table claimed theta=0.2 gives 84.6 (+0.1). Re-measured on this branch, theta 0.1, 0.2 and 0.25 all give 82.9 / 77.7 / 96.5 against 84.5 for geometry alone; the 84.6 row has no reproduction and is replaced by the measured sweep. The two sentences built on it are rewritten: no threshold makes the generic encoder useful on SoccerNet, the fine-tune is where the gain comes from. Co-Authored-By: Claude Fable 5.1 --- docs/evaluations/results.md | 40 +++++++++++++++++++------------------ docs/guides/reid.md | 16 +++++++-------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index da5b7b32..e4bf430b 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -5,7 +5,7 @@ description: Side-by-side MOT benchmark comparison of SORT, ByteTrack, OC-SORT, # Tracker Comparison -This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. McByte is reported at default parameters only (mask-conditioned association enabled); it is designed to require no per-dataset tuning, so there is no Tuned row. +This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. McByte is reported at default parameters only (mask-conditioned association enabled); it is designed to require no per-dataset tuning, so there is no Tuned row. BoT-SORT + ReID is shown at library defaults in the Default tabs, with the catalog-default encoder throughout; its Tuned rows use an encoder fine-tuned on the dataset's train split where one exists (MOT17, SoccerNet, DanceTrack) and the catalog default otherwise (SportsMOT). !!! info "Benchmark version" @@ -235,16 +235,17 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I === "Default" - Results using default tracker parameters. + Results using default tracker parameters. The ReID row is BoT-SORT at library defaults with the catalog-default `osnet_x1_0_msmt17_combineall` encoder. - | Tracker | HOTA | IDF1 | MOTA | - | :-------: | :------: | :------: | :------: | - | SORT | 81.6 | 76.2 | 95.1 | - | ByteTrack | 84.0 | 78.1 | **97.8** | - | OC-SORT | 78.4 | 72.6 | 94.1 | - | BoT-SORT | 84.5 | 79.3 | 96.6 | - | C-BIoU | 82.6 | 76.6 | 97.0 | - | McByte | **85.0** | **79.9** | 97.0 | + | Tracker | HOTA | IDF1 | MOTA | + | :-------------: | :------: | :------: | :------: | + | SORT | 81.6 | 76.2 | 95.1 | + | ByteTrack | 84.0 | 78.1 | **97.8** | + | OC-SORT | 78.4 | 72.6 | 94.1 | + | BoT-SORT | 84.5 | 79.3 | 96.6 | + | C-BIoU | 82.6 | 76.6 | 97.0 | + | McByte | **85.0** | **79.9** | 97.0 | + | BoT-SORT + ReID | 82.9 | 77.7 | 96.5 | + Evaluations are on test splits and one table per fusion method. Each ReID row shares its detections and geometry with the BoT-SORT row above it, so only the appearance branch differs, and both come from the [tracker comparison](../evaluations/results.md), where the full configuration for each is listed. Detection sources and split usage are covered in [Methodology](../evaluations/methodology.md). Bold marks the best cell per dataset across both tables. -The encoder is part of the configuration. SoccerNet, MOT17 and DanceTrack use an `osnet_x1_0` fine-tuned on the dataset's own train split, SportsMOT the generic `osnet_x1_0_msmt17_combineall`, so absolute numbers do not compare across datasets and only the with and without deltas do. Within a dataset both fusion rules run the same encoder, which is what makes the two tables comparable; DanceTrack's `adaptive` row is pending a run with the fine-tuned encoder. With the generic encoder DanceTrack loses to plain BoT-SORT under both rules, see [Other encoders](#other-encoders). +The encoder is part of the configuration. SoccerNet, MOT17 and DanceTrack use an `osnet_x1_0` fine-tuned on the dataset's own train split, SportsMOT the generic `osnet_x1_0_msmt17_combineall`, so absolute numbers do not compare across datasets and only the with and without deltas do. Within a dataset both fusion rules run the same encoder, which is what makes the two tables comparable. The DanceTrack encoder is trained at `crops_per_identity=8`; at the smaller budget of 4 it scores 57.5 under the minimum rule and loses to plain BoT-SORT, so the crop budget matters more on this dataset than either fusion parameter. With the generic encoder DanceTrack loses to plain BoT-SORT under both rules, see [Other encoders](#other-encoders). ### `reid_fusion="botsort"` `min(d_iou, d_app)`, gated by `reid_appearance_threshold` and `reid_proximity_threshold`. -| Dataset | Config | HOTA | IDF1 | MOTA | IDSW | Encoder | ReID parameters | -| :-------------- | :-------------- | :-------: | :-------: | :------: | :--: | :--------------------------------- | :---------------------------------------------------------------- | -| SoccerNet test | BoT-SORT | 85.00 | 79.68 | 97.25 | 2523 | — | — | -| SoccerNet test | BoT-SORT + ReID | 87.29 | 83.13 | 98.73 | 4564 | `osnet_x1_0` fine-tuned SoccerNet | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | -| MOT17 test | BoT-SORT | 63.8 | 78.7 | 79.4 | — | — | — | -| MOT17 test | BoT-SORT + ReID | **64.12** | **79.16** | 79.36 | 1617 | `osnet_x1_0` fine-tuned MOT17 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | -| SportsMOT test | BoT-SORT | 73.8 | 73.4 | 96.9 | — | — | — | -| SportsMOT test | BoT-SORT + ReID | 73.48 | 73.10 | 96.88 | 2863 | `osnet_x1_0_msmt17_combineall` | `reid_appearance_threshold=0.15`, `reid_proximity_threshold=0.5` | -| DanceTrack test | BoT-SORT | 57.8 | 57.9 | **92.2** | — | — | — | -| DanceTrack test | BoT-SORT + ReID | **58.5** | **58.9** | 92.1 | — | `osnet_x1_0` fine-tuned DanceTrack | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | +| Dataset | Config | HOTA | IDF1 | MOTA | IDSW | Encoder | ReID parameters | +| :-------------- | :-------------- | :-------: | :-------: | :---: | :--: | :--------------------------------- | :---------------------------------------------------------------- | +| SoccerNet test | BoT-SORT | 85.00 | 79.68 | 97.25 | 2523 | — | — | +| SoccerNet test | BoT-SORT + ReID | 87.29 | 83.13 | 98.73 | 4564 | `osnet_x1_0` fine-tuned SoccerNet | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | +| MOT17 test | BoT-SORT | 63.8 | 78.7 | 79.4 | — | — | — | +| MOT17 test | BoT-SORT + ReID | **64.12** | **79.16** | 79.36 | 1617 | `osnet_x1_0` fine-tuned MOT17 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | +| SportsMOT test | BoT-SORT | 73.8 | 73.4 | 96.9 | — | — | — | +| SportsMOT test | BoT-SORT + ReID | 73.48 | 73.10 | 96.88 | 2863 | `osnet_x1_0_msmt17_combineall` | `reid_appearance_threshold=0.15`, `reid_proximity_threshold=0.5` | +| DanceTrack test | BoT-SORT | 57.8 | 57.9 | 92.2 | — | — | — | +| DanceTrack test | BoT-SORT + ReID | **58.8** | **59.1** | 92.2 | — | `osnet_x1_0` fine-tuned DanceTrack | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | ### `reid_fusion="adaptive"` @@ -248,8 +248,8 @@ The encoder is part of the configuration. SoccerNet, MOT17 and DanceTrack use an | MOT17 test | BoT-SORT + ReID | 63.8 | 78.86 | **79.49** | **1416** | `osnet_x1_0` fine-tuned MOT17 | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5` | | SportsMOT test | BoT-SORT | 73.8 | 73.4 | 96.9 | — | — | — | | SportsMOT test | BoT-SORT + ReID | **74.98** | **75.09** | **96.99** | **2306** | `osnet_x1_0_msmt17_combineall` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.99` | -| DanceTrack test | BoT-SORT | 57.8 | 57.9 | **92.2** | — | — | — | -| DanceTrack test | BoT-SORT + ReID | — | — | — | — | `osnet_x1_0` fine-tuned DanceTrack | not yet run with this encoder | +| DanceTrack test | BoT-SORT | 57.8 | 57.9 | 92.2 | — | — | — | +| DanceTrack test | BoT-SORT + ReID | 58.1 | 58.2 | **92.3** | — | `osnet_x1_0` fine-tuned DanceTrack | `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0.0` | Anything not listed in the ReID parameters column is a library default, and the DanceTrack leaderboard does not return ID switches. Each rule is shown at its own best gate. On SportsMOT both were swept across 0.5, 0.99 and 1.0 on the val split: `botsort` is strongest at the default 0.5 (79.38 HOTA), scoring 77.82 at 0.99 and 64.80 at 1.0, while `adaptive` peaks at 0.99. The wider gate suits the additive rule and not the minimum, which is consistent with how the two combine appearance, so the comparison holds with both rules given the same range. On SoccerNet both rules were run at 1.0, and on DanceTrack with the generic encoder (see Other encoders). On SoccerNet neither was re-tuned there, so both carry thresholds chosen at the closed gate. On MOT17 only `adaptive` has been run past the default gate, where it scores below its closed-gate result. From a4d5aeec59bce6d4ad218ca744ebd65464d387b5 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Wed, 16 Sep 2026 23:59:00 -0300 Subject: [PATCH 11/49] docs(reid): one Results section with generic and fine-tuned encoders Replace the per-encoder and per-fusion result sections with a single Results section, tabbed like the tracker comparison: "Generic encoder" for all four datasets and "Fine-tuned encoder" for MOT17 and SoccerNet. Each tab shows BoT-SORT with and without ReID on alternating rows, first HOTA on the split the ReID thresholds were tuned on, then HOTA, IDF1 and MOTA on test. Generic rows come from joint tuning of reid_appearance_threshold and reid_proximity_threshold with the tuned motion parameters held fixed, each pick scored once on test. The fine-tuned SoccerNet thresholds were re-selected on the 12 train sequences the encoder never saw, which picks the same 0.075 and 1.0. Also: rewrite the threshold section as a guided walk-through, add IDF1 and a gate 1.0 row to the proximity table and correct the claim about half-open gates, add the SNMOT-147 proximity video, move the frame-gap detail into collapsible blocks, and repoint links to the removed section. Co-Authored-By: Claude Opus 5 --- docs/evaluations/results.md | 4 +- docs/guides/reid.md | 477 ++++++++++++++++++++++++------------ docs/trackers/botsort.md | 2 +- 3 files changed, 327 insertions(+), 156 deletions(-) diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index bbb2c32a..1a155c20 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -28,7 +28,7 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests !!! info - Parameters were tuned on the validation set. Results are reported on the test set via Codabench submission. Detections come from a YOLOX model. BoT-SORT rows are CMC without appearance; for CMC + FastReID on MOT17 and OSNet MSMT17 on SoccerNet see [BoT-SORT with and without ReID](../guides/reid.md#bot-sort-with-and-without-reid). + Parameters were tuned on the validation set. Results are reported on the test set via Codabench submission. Detections come from a YOLOX model. BoT-SORT rows are CMC without appearance; for CMC + FastReID on MOT17 and OSNet MSMT17 on SoccerNet see [BoT-SORT with and without ReID](../guides/reid.md#results). === "Default" @@ -233,7 +233,7 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I !!! info - Parameters were tuned on the train set. Results are reported on the test set. SoccerNet-tracking has no validation split. This dataset provides oracle (ground-truth) detections. The BoT-SORT row is CMC without appearance; for OSNet MSMT17 appearance on this split see [BoT-SORT with and without ReID](../guides/reid.md#bot-sort-with-and-without-reid). + Parameters were tuned on the train set. Results are reported on the test set. SoccerNet-tracking has no validation split. This dataset provides oracle (ground-truth) detections. The BoT-SORT row is CMC without appearance; for OSNet MSMT17 appearance on this split see [BoT-SORT with and without ReID](../guides/reid.md#results). === "Default" diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 0a5788c8..ace9e820 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -12,7 +12,7 @@ BoT-SORT can fuse appearance embeddings with IoU during association. Embeddings - How to enable appearance association on BoT-SORT - Which parameters control the appearance gate - How to pick `reid_appearance_threshold` for your encoder and domain -- What ReID changes on MOT17 and SoccerNet +- What ReID changes on MOT17, SportsMOT, DanceTrack and SoccerNet, with a generic and a fine-tuned encoder --- @@ -62,28 +62,30 @@ For the model catalog and fine-tuning, see the [`reid` training guide](https://g ## Key Parameters -| Parameter | Purpose | Tuning guidance | -| :-------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | -| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | -| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | -| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | -| `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | -| `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | -| `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | -| `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, the Deep OC-SORT behaviour). Calibrate per encoder: 0.7 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with the MOT17 and MSMT17 encoders, see [choosing a fusion method](#choosing-a-fusion-method). | +| Parameter | Purpose | Tuning guidance | +| :-------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | +| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | +| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | +| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | +| `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | +| `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | +| `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | +| `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, as in the Deep OC-SORT fusion). Calibrate per encoder: 0.7 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with the MOT17 and MSMT17 encoders, see [choosing a fusion method](#choosing-a-fusion-method). | --- ## Choosing an appearance threshold -BoT-SORT fuses costs as `min(d_iou, d_app)` with `d_app = 0.5 * (1 - cos_sim)`, and discards the appearance term when `d_app` exceeds `reid_appearance_threshold` (paper default 0.25) or when the pair fails the `reid_proximity_threshold` IoU gate. Appearance can therefore only lower a pair's cost, never veto a geometric match. Pick θ on a labeled split with the encoder you will track with: +When a track and a detection look alike, appearance can make their match cheaper. It never blocks a match that geometry already allows. `reid_appearance_threshold` sets how alike they must look, as a distance of `0.5 * (1 - cosine similarity)`: 0 means identical, and the BoT-SORT paper uses 0.25. -1. Embed GT crops. -2. Histogram `d_app` for association-local pairs: same video only, with frame gap bounded by the lost-track horizon (default 30 frames). Positives are same-ID; negatives are different-ID that could co-compete. Sample both classes with the same per-sequence quota, otherwise one crowded sequence decides the answer. -3. Choose θ so most same-ID pairs fall below it and most different-ID pairs fall above it. +The right value depends on the encoder and the footage, so measure it on labeled data: -All three steps ship with Trackers, so you can run them on your own footage. `extract_ground_truth_embeddings` reads any MOT-format dataset, meaning a `gt/gt.txt` and an `img1` folder per sequence, and returns each crop's embedding alongside the identity, frame and sequence it came from: +1. Embed the ground-truth crops with the encoder you will track with. +2. Compare pairs of crops that could meet during tracking: same video, at most the lost-track buffer apart (30 frames by default). Take the same number of pairs from each sequence, so one crowded video does not decide the result. +3. Pick the value that keeps most same-person pairs below it and lets few different-person pairs through. + +Trackers has a helper for each step. `extract_ground_truth_embeddings` reads any dataset in [MOT format](../evaluations/evaluate.md#data-format): ```python from trackers.core.reid import ( @@ -101,16 +103,31 @@ for threshold in (0.10, 0.20, 0.25): plot_appearance_distances(distances, thresholds={0.20: "selected", 0.25: "default"}) ``` -See the [ReID API reference](../api/reid.md#choosing-a-threshold) for the full signatures. The figures on this page were produced with these helpers on MOT17 val and SoccerNet test ground truth. To reproduce them end to end, from download to calibrated threshold, open the [ReID cookbook](https://colab.research.google.com/github/roboflow/trackers/blob/develop/docs/cookbooks/how-to-add-reid-to-trackers.ipynb) in Colab. +The [ReID API reference](../api/reid.md#choosing-a-threshold) lists the full signatures, and the [ReID cookbook](https://colab.research.google.com/github/roboflow/trackers/blob/develop/docs/cookbooks/how-to-add-reid-to-trackers.ipynb) runs the whole flow in Colab. The two examples below use 5000 same-person and 10000 different-person pairs, 1 to 30 frames apart. + +### MOT17 with a pedestrian encoder -**MOT17 val, `fastreid_mot17_sbs50`.** Same-ID distances peak near 0 and different-ID near 0.4. On association-local GT crop pairs (5000 same-ID, 10000 different-ID, frame gap 1 to 30), θ=0.2 keeps 68% of same-ID pairs while passing 1.1% of different-ID pairs. Raising θ to the BoT-SORT default 0.25 recovers same-ID pairs (79%) but nearly triples the different-ID pairs it admits (2.9%), which is why 0.2 is the better operating point here ([MOT17 re-ID study](https://www-sop.inria.fr/members/Francois.Bremond/Postscript/Tomasz__SCCAI_2025.pdf) Table 8 uses the same threshold). +Blue bars are pairs of the same person, red bars are pairs of different people. A good threshold sits where the blue bars end and the red ones start. `fastreid_mot17_sbs50` on MOT17 val keeps the two groups mostly apart, with some overlap between 0.2 and 0.35. ![FastReID MOT17 SBS on MOT17 val GT](../assets/reid/mot17-fastreid-appearance-distances.png) -**SoccerNet test, `osnet_x1_0_msmt17_combineall`.** A pedestrian encoder on soccer footage squeezes every distance into a narrow range: same-ID pairs peak near 0.05 and different-ID pairs near 0.20 (similar kits). The two shapes still separate, but the scale no longer matches the thresholds BoT-SORT was tuned with. On association-local GT crop pairs (5000 same-ID, 10000 different-ID, frame gap 1 to 30), θ=0.2 admits 96% of same-ID pairs but also 49% of different-ID pairs, and θ=0.1 holds different-ID pairs to 9%. Neither rescues tracking under the default `botsort` rule: every threshold from 0.1 up costs about 1.6 HOTA against CMC-only (see the SoccerNet table below), because with oracle boxes the pairs the gate removes are not the ones deciding an assignment under the minimum fusion. What does rescue it is the fusion rule and the gate: `adaptive` at `reid_proximity_threshold=0.99` turns the same encoder into a gain of about 1.1 HOTA, and the fine-tuned encoder adds on top of that. +This is what `rates_at` prints for two thresholds: + +| `reid_appearance_threshold` | same-person pairs below | different-person pairs below | +| :-------------------------- | :---------------------: | :--------------------------: | +| 0.20 | 68% | 1.1% | +| 0.25 (paper default) | 79% | 2.9% | + +Moving the threshold right accepts more same-person pairs, but it also starts letting different-person pairs through. We use 0.2, the same value as the [MOT17 re-ID study](https://www-sop.inria.fr/members/Francois.Bremond/Postscript/Tomasz__SCCAI_2025.pdf) (Table 8). + +### SoccerNet with the same kind of encoder + +`osnet_x1_0_msmt17_combineall` was trained on pedestrians. On SoccerNet test, players in the same kit look alike to it, so the blue and red bars overlap. ![OSNet MSMT17 on SoccerNet test GT](../assets/reid/soccernet-osnet-appearance-distances.png) +When the two groups overlap like this, no threshold separates them: any value that keeps most same-person pairs also lets many different-person pairs through. Tuning the threshold will not fix it. Check the [proximity threshold](#choosing-a-proximity-threshold) and [fusion method](#choosing-a-fusion-method) first, and consider an encoder fine-tuned on your footage, see [results](#results). + --- ## How far the threshold carries @@ -126,27 +143,31 @@ plot_frame_gap_sweep(sweep, thresholds={0.20: "selected", 0.25: "default"}) On MOT17 val, different-ID distances barely move with the gap: the median stays near 0.41 and the 10th percentile near 0.31 from a 1-frame gap out to 240 frames. Same-ID distances spread steadily, from a median of 0.04 at a 1-frame gap to 0.20 across the 16 to 30 band and 0.28 beyond 120 frames. -ROC AUC below is the chance that a random same-ID pair scores closer than a random different-ID pair: 1.0 means the two never cross, 0.5 means appearance carries no information, and its complement is how often a same-ID pair sits farther apart than a different-ID one. It is the area under the curve traced by sweeping θ from 0 to 1 and plotting the two rates next to it, so it summarises every threshold instead of the single one we ship. +![FastReID MOT17 SBS separability vs frame gap](../assets/reid/mot17-fastreid-appearance-distances-vs-gap.png) + +Two things follow. First, a threshold validated on adjacent frames says little about re-association: at θ=0.2 appearance helps 98% of same-ID pairs one frame apart but only 51% across the default 30-frame lost-track buffer. Second, the price of a tight θ over long gaps is missed re-associations rather than extra wrong ones, because the different-ID rate stays near 1% throughout. If you raise `lost_track_buffer` to recover tracks after long occlusions, raise `reid_appearance_threshold` with it and re-check the different-ID column. -It is not the area where the shaded bands cross in the figure. That is two percentile ranges intersecting, which ignores where the mass sits and which side is closer; at a 1-frame gap the bands never touch yet the AUC is 0.998 rather than 1.0. The two rates beside it evaluate the default 0.25 and the 0.2 this page argues for, rather than deriving a third. +??? info "How to read ROC AUC, and the numbers per frame gap" -| Frame gap | ROC AUC | same-ID below 0.2 | different-ID below 0.2 | -| :--------- | :-----: | :---------------: | :--------------------: | -| 1 | 0.998 | 98.0% | 1.7% | -| 2 to 5 | 0.987 | 87.6% | 1.6% | -| 6 to 15 | 0.957 | 67.4% | 1.1% | -| 16 to 30 | 0.929 | 51.4% | 1.1% | -| 31 to 60 | 0.899 | 39.8% | 0.9% | -| 61 to 120 | 0.865 | 31.8% | 0.8% | -| 121 to 240 | 0.854 | 28.7% | 0.8% | + ROC AUC below is the chance that a random same-ID pair scores closer than a random different-ID pair: 1.0 means the two never cross, 0.5 means appearance carries no information, and its complement is how often a same-ID pair sits farther apart than a different-ID one. It is the area under the curve traced by sweeping θ from 0 to 1 and plotting the two rates next to it, so it summarises every threshold instead of the single one we ship. -![FastReID MOT17 SBS separability vs frame gap](../assets/reid/mot17-fastreid-appearance-distances-vs-gap.png) + It is not the area where the shaded bands cross in the figure. That is two percentile ranges intersecting, which ignores where the mass sits and which side is closer; at a 1-frame gap the bands never touch yet the AUC is 0.998 rather than 1.0. The two rates beside it evaluate the default 0.25 and the 0.2 this page argues for, rather than deriving a third. -Two things follow. First, a threshold validated on adjacent frames says little about re-association: at θ=0.2 appearance helps 98% of same-ID pairs one frame apart but only 51% across the default 30-frame lost-track buffer. Second, the price of a tight θ over long gaps is missed re-associations rather than extra wrong ones, because the different-ID rate stays near 1% throughout. If you raise `lost_track_buffer` to recover tracks after long occlusions, raise `reid_appearance_threshold` with it and re-check the different-ID column. + | Frame gap | ROC AUC | same-ID below 0.2 | different-ID below 0.2 | + | :--------- | :-----: | :---------------: | :--------------------: | + | 1 | 0.998 | 98.0% | 1.7% | + | 2 to 5 | 0.987 | 87.6% | 1.6% | + | 6 to 15 | 0.957 | 67.4% | 1.1% | + | 16 to 30 | 0.929 | 51.4% | 1.1% | + | 31 to 60 | 0.899 | 39.8% | 0.9% | + | 61 to 120 | 0.865 | 31.8% | 0.8% | + | 121 to 240 | 0.854 | 28.7% | 0.8% | -The cross-domain encoder fails differently. On SoccerNet the different-ID rate at θ=0.2 stays between 44% and 51% at every gap, so the frame gap is not what limits it; the encoder simply cannot separate players in matching kits at any horizon. Widening the gap costs same-ID pairs (99.6% down to 87.0%) without ever making the different-ID side usable, so no threshold makes the generic encoder useful here under the minimum fusion; the additive rule with an open gate is what does, see the SoccerNet table under Other encoders. The SoccerNet rows in the tables above go further with an encoder fine-tuned on the dataset's own train split. +??? info "The generic encoder on SoccerNet" -![OSNet MSMT17 separability vs frame gap](../assets/reid/soccernet-osnet-appearance-distances-vs-gap.png) + The cross-domain encoder fails differently. On SoccerNet the different-ID rate at θ=0.2 stays between 44% and 51% at every gap, so the frame gap is not what limits it; the encoder simply cannot separate players in matching kits at any horizon. Widening the gap costs same-ID pairs (99.6% down to 87.0%) without ever making the different-ID side usable, so no threshold makes the generic encoder useful here under the minimum fusion; the additive rule with an open gate is what does, see the SoccerNet table under Other encoders. The SoccerNet rows in the tables above go further with an encoder fine-tuned on the dataset's own train split. + + ![OSNet MSMT17 separability vs frame gap](../assets/reid/soccernet-osnet-appearance-distances-vs-gap.png) --- @@ -154,147 +175,297 @@ The cross-domain encoder fails differently. On SoccerNet the different-ID rate a `reid_proximity_threshold` decides which track-detection pairs appearance is allowed to score. A pair is dropped before appearance is consulted whenever `1 - IoU` exceeds the threshold, so the 0.5 default limits appearance to pairs that already overlap at `IoU >= 0.5`, and 0.99 still requires `IoU >= 0.01`. Only 1.0 disables the gate. -**Why it matters.** Lost tracks are represented by their Kalman prediction. After an occlusion the prediction has drifted and overlaps the re-emerging detection weakly; after a target leaves the frame the prediction has been extrapolated out of it and the overlap with the returning detection is exactly zero. The default excludes both cases, 0.99 admits the first, and only 1.0 admits the second. The reference Deep OC-SORT tracker recovers the second case with a geometry-only pass against the last observed box (OCR); BoT-SORT has no such stage, so appearance at an open gate is the only route. +A lost track is matched through its Kalman prediction. After a short occlusion the prediction has drifted, so it barely overlaps the detection when the target reappears. After the target leaves the frame, the prediction keeps moving out of view and the overlap drops to zero. The 0.5 default blocks appearance in both cases, 0.99 lets it score the first one, and only 1.0 lets it score the second. BoT-SORT has no separate step for targets that leave and return, so opening the gate is the only way to keep their ID. -On SoccerNet test (oracle detections, `osnet_x1_0` fine-tuned on SoccerNet train, library-default geometry): +SoccerNet test, oracle detections, `osnet_x1_0` fine-tuned on SoccerNet train, library-default motion parameters, `reid_fusion="adaptive"` with its default weights: -| `reid_proximity_threshold` | appearance consulted when | HOTA | ID switches | -| :------------------------- | :------------------------ | :---: | :---------: | -| none (no ReID) | | 84.56 | 2939 | -| 0.5 (default) | `IoU >= 0.50` | 84.52 | 3012 | -| 0.8 | `IoU >= 0.20` | 85.02 | 3268 | -| 0.95 | `IoU >= 0.05` | 85.69 | 2565 | -| 0.99 | `IoU >= 0.01` | 85.82 | 2431 | +| `reid_proximity_threshold` | appearance consulted when | HOTA | IDF1 | ID switches | +| :------------------------- | :------------------------ | :---: | :---: | :---------: | +| none (no ReID) | | 84.56 | 79.35 | 2939 | +| 0.5 (default) | `IoU >= 0.50` | 84.52 | 79.27 | 3012 | +| 0.8 | `IoU >= 0.20` | 85.02 | 79.71 | 3268 | +| 1.0 | always | 85.46 | 81.18 | 1734 | -On the [published tuned BoT-SORT geometry](../evaluations/results.md#soccernet-tracking) for SoccerNet, `adaptive` at 0.99 scores 85.87 HOTA with 2433 switches and at 1.0 scores 86.23 with **1692**, against 85.00 and 2523 without appearance. Adding `reid_appearance_floor=0.7` at 1.0 reaches **87.82** with 2111 switches, the best SoccerNet configuration measured. The identity gain is almost entirely the last step: 1.0 is what lets a player who walked out of frame come back with the same id. `botsort` at 1.0 reaches 87.29 HOTA but 4564 switches, see [choosing a fusion method](#choosing-a-fusion-method). + +

SoccerNet SNMOT-147, top to bottom: gate 0.5, 0.8 and 1.0. The player who leaves the frame gets a new ID at 0.5 and 0.8, and keeps it at 1.0.

-Half-opening is the worst setting for identity: 0.8 has more ID switches than either the closed gate or no appearance at all, because it admits enough distant candidates to create false matches without admitting the ones that enable recoveries. Either keep the gate or disable it. +HOTA and IDF1 improve as the gate opens, but ID switches do not follow the same curve. At 0.8 they are higher than with the default gate or without ReID: appearance can now match more distant boxes, but not yet the ones that recover a lost track. With the gate fully open they drop below both. If you open the gate, open it all the way. -The right value depends on the footage. DanceTrack val targets stay in frame, and there opening the gate only adds false matches: `adaptive` at its best weights falls from 57.39 HOTA at 0.5 to 56.56 at 1.0, and `botsort` falls from 56.11 to 46.20 with seven times the ID switches, because `min(d_iou, d_app)` with no geometric check flips between dancers that look alike every frame. Use 1.0 where identity is lost to camera motion or targets leaving the frame, and prefer `adaptive` there; keep 0.5 where targets stay in view. +The right value depends on the footage. Use 1.0 when targets leave the frame or the camera moves a lot. Keep 0.5 when targets stay in view, as in DanceTrack: there an open gate only adds wrong matches between people who look alike. ## Choosing a fusion method `reid_fusion` selects how the appearance score reaches the cost matrix. -`"botsort"` (default) takes `min(d_iou, d_app)`. Appearance competes with geometry and wins only when it is strictly cheaper, so a pair is accepted when either cue is confident. Both gates are hard: a pair either clears `reid_appearance_threshold` and `reid_proximity_threshold` or contributes nothing. - -`"adaptive"` adds a weighted appearance term to the geometric similarity, with the weight growing when the best appearance match stands clear of the runner-up and falling back to `reid_appearance_weight` when the top candidates are hard to tell apart. This is the weighting from Deep OC-SORT, ported on its own onto BoT-SORT; the velocity term, the confidence-gated feature update and the OCR pass are not included. `reid_appearance_threshold` has no effect under this method. `reid_appearance_floor`, an addition of this library, sets a minimum cosine similarity below which a pair falls back to geometry alone: with the gate open it stops lost tracks from capturing unrelated detections on weak appearance matches, which otherwise clear the association threshold whenever geometry contributes nothing. - -Two consequences are worth knowing before switching: - -- **The similarity range changes.** `"botsort"` returns values in `[0, 1]`; `"adaptive"` returns `[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]`, which is `[0, 2.25]` at the defaults. `minimum_iou_threshold_first_assoc` is applied to that fused value, so a threshold tuned for one method is a different gate under the other. Retune it when you switch, or the comparison measures the gate rather than the fusion. -- **The weights are domain-dependent.** Deep OC-SORT reports `reid_appearance_weight=0.75` with `reid_adaptive_weight_cap=0.5` for MOT17 and MOT20, and `1.25` with `1.0` for DanceTrack, where dancers occlude constantly and geometry carries less. The defaults here follow the MOT17/MOT20 pair. - -### Measured comparison - -HOTA, best configuration found for each method. Detections, CMC, encoder and geometry are shared within a row; only appearance handling differs. SoccerNet and MOT17 use the published tuned BoT-SORT geometry from the [tracker comparison](../evaluations/results.md): `lost_track_buffer=60`, `minimum_iou_threshold_first_assoc=0.1`, `minimum_iou_threshold_second_assoc=0.6`, `minimum_iou_threshold_unconfirmed_assoc=0.2` for SoccerNet, and `track_activation_threshold=0.6`, `high_conf_det_threshold=0.5`, `minimum_iou_threshold_unconfirmed_assoc=0.2` for MOT17. DanceTrack's tuned set coincides with the library defaults. All other parameters are library defaults. - -| Dataset | Encoder | no ReID | `botsort` | `adaptive` | `botsort` parameters | `adaptive` parameters | -| :------------: | :-------------------------------: | :-------: | :-------: | :--------: | :---------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------: | -| SoccerNet test | `osnet_x1_0` fine-tuned SoccerNet | 85.00 | 87.29 | **87.82** | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.7` | -| DanceTrack val | `osnet_x1_0_msmt17_combineall` | 53.89 | 56.11 | **57.39** | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0`, `reid_proximity_threshold=0.5` | -| MOT17 val-half | `osnet_x1_0` fine-tuned MOT17 | **69.05** | 69.00 | 68.72 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5` | - -HOTA. Each cell is the best configuration found for that method (on SoccerNet the threshold and weight come from closed-gate sweeps with only the gate changed to 1.0, so they are good settings rather than necessarily the best); the parameter columns give the full ReID settings it ran with, defaults included. MOT17 moves by less than 0.35 HOTA in any direction on 7 sequences, inside single-sequence variance. - -**The two rules trade HOTA against identity stability.** On DanceTrack `adaptive` leads on both, cutting ID switches from 1905 to 1683 where `botsort` raises them to 2276 while still gaining HOTA. On SoccerNet with the gate disabled `botsort` beats floor-less `adaptive` on HOTA and IDF1 (87.29 and 83.13 against 86.23 and 82.52) but takes 4564 ID switches against 1692, 81% more than running no appearance at all; two thirds of its identity changes revert within ten frames, because `min(d_iou, d_app)` takes whichever candidate is cheapest in the current frame and flips between players in the same kit. `adaptive` commits to a re-association and keeps it, so its errors are fewer and longer. With `reid_appearance_floor=0.7` blocking the weakest captures, `adaptive` leads SoccerNet outright: 87.82 HOTA and 83.92 IDF1 with 2111 switches (SoccerNet test, tuned geometry, fine-tuned encoder). That floor value belongs to that encoder: applied unchanged at the open gate it loses HOTA on MOT17 val-half (66.44 against 68.96 without appearance), DanceTrack val (52.98 against 56.56 without the floor) and SportsMOT val (81.00 against 81.32), all with generic or differently fine-tuned `osnet_x1_0` encoders whose cosine scales differ. Treat the floor as a per-encoder calibration, like `reid_appearance_threshold`, and leave it at 0.0 until you have measured it. Prefer `adaptive` when stable ids matter, which is the usual reason to enable appearance. The adaptive bonus itself contributes little: with `reid_adaptive_weight_cap=0` HOTA changes by 0.12 on SoccerNet and 0.13 on DanceTrack. Leave the cap at its default and tune `reid_appearance_weight` and `reid_proximity_threshold` instead. +`"botsort"` (default) takes `min(d_iou, d_app)`. Appearance competes with geometry and wins only when it is strictly cheaper, so a pair is accepted when either cue is confident. Appearance is only used for a pair when its distance is below `reid_appearance_threshold` and the boxes pass the `reid_proximity_threshold` check. Otherwise the pair is matched on geometry alone. -### Choosing the adaptive weight +`"adaptive"` adds a weighted appearance term to the geometric similarity, with the weight growing when the best appearance match stands clear of the runner-up and falling back to `reid_appearance_weight` when the top candidates are hard to tell apart. This is the appearance fusion from Deep OC-SORT. -Deep OC-SORT reports `reid_appearance_weight=0.75` for MOT17 and MOT20 and `1.25` for DanceTrack. On SoccerNet test and MOT17 val-half a sweep from 0.75 to 2.5 moves HOTA by less than 0.15, so the default transfers. DanceTrack val needs more than the reported value: +`reid_appearance_floor`, an addition of this library, sets a minimum cosine similarity below which a pair falls back to geometry alone: with the gate open it stops lost tracks from capturing unrelated detections on weak appearance matches, which otherwise clear the association threshold whenever geometry contributes nothing. -| `reid_appearance_weight` | HOTA | ID switches | -| :----------------------: | :-------: | :---------: | -| 1.25 (reported) | 55.97 | 1736 | -| 1.6 | 57.23 | 1696 | -| 2.4 | **57.26** | **1686** | +!!! note "What `adaptive` includes" -The step sits between 1.25 and 1.6 and everything above it is a plateau. Where appearance barely separates identities the adaptive bonus collapses and only the base weight does any work, which is DanceTrack's situation and why the weight matters there. + Only the appearance fusion is taken from Deep OC-SORT; the rest of that tracker is not added to BoT-SORT. `reid_appearance_threshold` has no effect with `reid_fusion="adaptive"`. -`"botsort"` remains the default because it is the more conservative of the two: bounded output, a hard appearance gate, and no threshold changes when you enable it. - -## BoT-SORT with and without ReID - - - -Evaluations are on test splits and one table per fusion method. Each ReID row shares its detections and geometry with the BoT-SORT row above it, so only the appearance branch differs, and both come from the [tracker comparison](../evaluations/results.md), where the full configuration for each is listed. Detection sources and split usage are covered in [Methodology](../evaluations/methodology.md). Bold marks the best cell per dataset across both tables. - -The encoder is part of the configuration. SoccerNet, MOT17 and DanceTrack use an `osnet_x1_0` fine-tuned on the dataset's own train split, SportsMOT the generic `osnet_x1_0_msmt17_combineall`, so absolute numbers do not compare across datasets and only the with and without deltas do. Within a dataset both fusion rules run the same encoder, which is what makes the two tables comparable. The DanceTrack encoder is trained at `crops_per_identity=8`; at the smaller budget of 4 it scores 57.5 under the minimum rule and loses to plain BoT-SORT, so the crop budget matters more on this dataset than either fusion parameter. With the generic encoder DanceTrack loses to plain BoT-SORT under both rules, see [Other encoders](#other-encoders). - -### `reid_fusion="botsort"` - -`min(d_iou, d_app)`, gated by `reid_appearance_threshold` and `reid_proximity_threshold`. - -| Dataset | Config | HOTA | IDF1 | MOTA | IDSW | Encoder | ReID parameters | -| :-------------- | :-------------- | :-------: | :-------: | :---: | :--: | :--------------------------------- | :---------------------------------------------------------------- | -| SoccerNet test | BoT-SORT | 85.00 | 79.68 | 97.25 | 2523 | — | — | -| SoccerNet test | BoT-SORT + ReID | 87.29 | 83.13 | 98.73 | 4564 | `osnet_x1_0` fine-tuned SoccerNet | `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` | -| MOT17 test | BoT-SORT | 63.8 | 78.7 | 79.4 | — | — | — | -| MOT17 test | BoT-SORT + ReID | **64.12** | **79.16** | 79.36 | 1617 | `osnet_x1_0` fine-tuned MOT17 | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | -| SportsMOT test | BoT-SORT | 73.8 | 73.4 | 96.9 | — | — | — | -| SportsMOT test | BoT-SORT + ReID | 73.48 | 73.10 | 96.88 | 2863 | `osnet_x1_0_msmt17_combineall` | `reid_appearance_threshold=0.15`, `reid_proximity_threshold=0.5` | -| DanceTrack test | BoT-SORT | 57.8 | 57.9 | 92.2 | — | — | — | -| DanceTrack test | BoT-SORT + ReID | **58.8** | **59.1** | 92.2 | — | `osnet_x1_0` fine-tuned DanceTrack | `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` | - -### `reid_fusion="adaptive"` - -`IoU + w·cos` with the Deep OC-SORT bonus, gated by `reid_proximity_threshold` and, where set, `reid_appearance_floor`. - -| Dataset | Config | HOTA | IDF1 | MOTA | IDSW | Encoder | ReID parameters | -| :-------------- | :-------------- | :-------: | :-------: | :-------: | :------: | :--------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | -| SoccerNet test | BoT-SORT | 85.00 | 79.68 | 97.25 | 2523 | — | — | -| SoccerNet test | BoT-SORT + ReID | **87.82** | **83.92** | **99.43** | 2111 | `osnet_x1_0` fine-tuned SoccerNet | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.7` | -| MOT17 test | BoT-SORT | 63.8 | 78.7 | 79.4 | — | — | — | -| MOT17 test | BoT-SORT + ReID | 63.8 | 78.86 | **79.49** | **1416** | `osnet_x1_0` fine-tuned MOT17 | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5` | -| SportsMOT test | BoT-SORT | 73.8 | 73.4 | 96.9 | — | — | — | -| SportsMOT test | BoT-SORT + ReID | **74.98** | **75.09** | **96.99** | **2306** | `osnet_x1_0_msmt17_combineall` | `reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.99` | -| DanceTrack test | BoT-SORT | 57.8 | 57.9 | 92.2 | — | — | — | -| DanceTrack test | BoT-SORT + ReID | 58.1 | 58.2 | **92.3** | — | `osnet_x1_0` fine-tuned DanceTrack | `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0.0` | - -Anything not listed in the ReID parameters column is a library default, and the DanceTrack leaderboard does not return ID switches. Each rule is shown at its own best gate. On SportsMOT both were swept across 0.5, 0.99 and 1.0 on the val split: `botsort` is strongest at the default 0.5 (79.38 HOTA), scoring 77.82 at 0.99 and 64.80 at 1.0, while `adaptive` peaks at 0.99. The wider gate suits the additive rule and not the minimum, which is consistent with how the two combine appearance, so the comparison holds with both rules given the same range. On SoccerNet both rules were run at 1.0, and on DanceTrack with the generic encoder (see Other encoders). On SoccerNet neither was re-tuned there, so both carry thresholds chosen at the closed gate. On MOT17 only `adaptive` has been run past the default gate, where it scores below its closed-gate result. - -### Other encoders - -The rows above use the best encoder found for each dataset. These measurements used other encoders and are kept because they show how much that choice decides. - -#### MOT17, `fastreid_mot17_sbs50` - -The MOT17-trained FastReID checkpoint BoT-SORT itself uses, run on library-default geometry at `reid_appearance_threshold=0.2` ([MOT17 re-ID study](https://www-sop.inria.fr/members/Francois.Bremond/Postscript/Tomasz__SCCAI_2025.pdf) Table 8). - -Codabench MOT17 test: +Two consequences are worth knowing before switching: -| Config | HOTA | IDF1 | MOTA | -| :-------------- | :------: | :------: | :------: | -| BoT-SORT | 63.7 | 78.7 | **79.2** | -| BoT-SORT + ReID | **63.9** | **79.2** | **79.2** | +- **The similarity range changes.** `"botsort"` returns values in `[0, 1]`; `"adaptive"` returns `[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]`, which is `[0, 2.25]` at the defaults. `minimum_iou_threshold_first_assoc` is applied to that fused value, so a threshold tuned for one method is a different gate under the other. Retune it when you switch, or the comparison measures the gate rather than the fusion. +- **The weights are domain-dependent.** The Deep OC-SORT paper uses `reid_appearance_weight=0.75` with `reid_adaptive_weight_cap=0.5` for MOT17 and MOT20, and `1.25` with `1.0` for DanceTrack, where dancers occlude constantly and geometry carries less. The defaults here follow the MOT17/MOT20 pair. + +With the gate open, `adaptive` keeps IDs more stable than `"botsort"`, which switches between look-alike candidates from one frame to the next. The weights mainly matter where geometry is unreliable, as in DanceTrack; elsewhere the defaults work. `"botsort"` stays the default because it is the safer choice: appearance is only used below `reid_appearance_threshold`, and turning it on does not require retuning the other thresholds. + +## Results + +BoT-SORT with and without ReID, using the same detections and motion parameters. The first table shows HOTA on the split where the ReID thresholds were tuned, the second shows test results. The better value in each pair is in bold. Motion parameters are the tuned values from the [tracker comparison](../evaluations/results.md). + + + +=== "Generic encoder" + + Encoders that were not trained on the evaluated dataset, with `reid_fusion="botsort"`. + + Tuning split: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatasetConfigHOTA
MOT17 val-halfBoT-SORT69.05
BoT-SORT + ReID69.64
SportsMOT valBoT-SORT82.00
BoT-SORT + ReID82.69
DanceTrack valBoT-SORT53.89
BoT-SORT + ReID58.34
SoccerNet train¹BoT-SORT86.95
BoT-SORT + ReID86.98
+ + Test split: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatasetConfigHOTAIDF1MOTA
MOT17BoT-SORT63.8678.7479.42
BoT-SORT + ReID63.8578.8279.41
SportsMOTBoT-SORT74.1574.0696.89
BoT-SORT + ReID75.6275.6696.90
DanceTrackBoT-SORT57.857.992.2
BoT-SORT + ReID57.657.692.1
SoccerNetBoT-SORT85.0079.6897.25
BoT-SORT + ReID84.9679.6697.25
-MOT17 val-half, scored with `trackers eval`: +``` +- **MOT17**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.10`, `reid_proximity_threshold=0.5` +- **SportsMOT**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.30`, `reid_proximity_threshold=0.5` +- **DanceTrack**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.48`, `reid_proximity_threshold=0.51` +- **SoccerNet**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.05`, `reid_proximity_threshold=0.35` -| Config | HOTA | IDF1 | MOTA | -| :-------------- | :------: | :------: | :------: | -| BoT-SORT | 68.9 | 81.2 | 78.3 | -| BoT-SORT + ReID | **69.1** | **81.9** | **78.4** | +Both thresholds were tuned together on the tuning split, then each configuration was evaluated once on test. -The MOT17 re-ID study reports 68.43 HOTA / 80.92 IDF1 without ReID and 68.95 / 81.98 with, on the same split at `reid_appearance_threshold=0.2` (Table 8 and Table 13; MOTA is not reported for that YOLOX setup). +¹ SoccerNet-tracking has no validation split, so its tuning split is train. -#### DanceTrack, `osnet_x1_0_msmt17_combineall` +A generic encoder improves HOTA on every tuning split, but only SportsMOT keeps the gain on test. On the other datasets, test HOTA stays within 0.2 of BoT-SORT without ReID. On SoccerNet the tuned thresholds are strict enough that appearance rarely changes a match. -The generic pedestrian encoder on dancers, run on library-default geometry, YOLOX detections, Codabench DanceTrack test. Both rules land below geometry alone; the fine-tuned encoder in the tables above is what turns DanceTrack around. +SoccerNet uses ground-truth boxes as detections, so its numbers are not comparable to the YOLOX rows. On DanceTrack, validation HOTA without ReID is 3.9 lower than test HOTA, which explains most of the validation gain. `osnet_x1_0_msmt17_combineall` scores 56.21 on DanceTrack validation and 56.0 on test with thresholds 0.25 and 0.5. +``` -| Config | HOTA | IDF1 | MOTA | -| :--------------------------------------------------- | :------: | :------: | :------: | -| BoT-SORT | **57.8** | **57.9** | 92.2 | -| BoT-SORT + ReID, `botsort` (θ=0.25, gate=0.5) | 56.0 | 56.1 | 91.8 | -| BoT-SORT + ReID, `adaptive` (w=2.4, cap=0, gate=0.5) | 57.2 | 56.5 | **92.3** | +=== "Fine-tuned encoder" + + `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"`. Available for MOT17 and SoccerNet. + + Tuning split: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatasetConfigHOTA
MOT17 val-halfBoT-SORT69.05
BoT-SORT + ReID69.00
SoccerNet held-out train²BoT-SORT85.72
BoT-SORT + ReID88.97
+ + Test split: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DatasetConfigHOTAIDF1MOTA
MOT17BoT-SORT63.878.779.4
BoT-SORT + ReID64.1279.1679.36
SoccerNetBoT-SORT85.0079.6897.25
BoT-SORT + ReID87.3083.1698.73
-#### SoccerNet, `osnet_x1_0_msmt17_combineall` +``` +- **MOT17**: `osnet_x1_0` fine-tuned on MOT17 train, `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` +- **SoccerNet**: `osnet_x1_0` fine-tuned on SoccerNet train, `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` -A generic pedestrian encoder on soccer footage, run on library-default geometry with `reid_fusion="botsort"`, gate 0.5, oracle detections, SoccerNet-tracking test. Under the default `botsort` rule the appearance threshold makes no practical difference: every value from 0.1 up gives the same result, and all of them sit below geometry alone, while opening the gate to 1.0 under that rule collapses to 65.9 HOTA with over 24,000 ID switches, as it does on SportsMOT and DanceTrack. Switching to `adaptive` fusion with the gate at 0.99 turns the same encoder into a gain; at its own default gate the adaptive rule is merely neutral. The fine-tuned rows above go further still. +² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). That search picked the same values used on test, 0.075 and 1.0. -| Config | HOTA | IDF1 | MOTA | -| :--------------------------------------------- | :------: | :------: | :------: | -| BoT-SORT | 84.5 | 79.3 | 96.6 | -| BoT-SORT + OSNet MSMT17 (θ=0.1) | 82.9 | 77.7 | 96.5 | -| BoT-SORT + OSNet MSMT17 (θ=0.25, default) | 82.9 | 77.7 | 96.5 | -| BoT-SORT + OSNet MSMT17, `adaptive`, defaults | 84.5 | 79.2 | 96.5 | -| BoT-SORT + OSNet MSMT17, `adaptive`, gate 0.99 | **85.7** | **80.1** | **98.1** | +Fine-tuning makes the biggest difference on SoccerNet: the generic encoder leaves HOTA flat, while the fine-tuned one adds 2.30 on test and 3.25 on the held-out train sequences. DanceTrack is not listed because its fine-tuned encoder scores 57.5 on test, below BoT-SORT without ReID. +``` diff --git a/docs/trackers/botsort.md b/docs/trackers/botsort.md index e8f53fdf..0d3c3b92 100644 --- a/docs/trackers/botsort.md +++ b/docs/trackers/botsort.md @@ -54,7 +54,7 @@ BoT-SORT keeps the same tracking-by-detection backbone as [ByteTrack](bytetrack. ## ReID appearance (optional) -BoT-SORT can fuse appearance embeddings with IoU during association via an optional `reid_model`. For ReID numbers go to the [ReID guide](../guides/reid.md#bot-sort-with-and-without-reid), which holds the tables for both fusion rules and best configuration per dataset. Also learn how to install, use and choose parameters on the same page. +BoT-SORT can fuse appearance embeddings with IoU during association via an optional `reid_model`. For ReID numbers go to the [ReID guide](../guides/reid.md#results), which holds the tables for both fusion rules and best configuration per dataset. Also learn how to install, use and choose parameters on the same page. ## Run on video, webcam, or RTSP stream From bea30afc0e0277336901fc67f1004c5162fc0cd9 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 17 Sep 2026 08:45:57 -0300 Subject: [PATCH 12/49] docs(reid): add adaptive fusion to the fine-tuned SoccerNet results The fine-tuned SoccerNet encoder with reid_fusion="adaptive" was re-selected on the 12 train sequences it never saw (gate 1.0, reid_appearance_floor=0.8, default weights) and scored once on test: 88.43 HOTA, 84.40 IDF1, 99.26 MOTA, against 85.00 without ReID and 87.30 with the botsort rule. It replaces the earlier 87.82, whose floor had been chosen on the test split. Co-Authored-By: Claude Opus 5 --- docs/guides/reid.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index ace9e820..00c3d441 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -401,13 +401,17 @@ SoccerNet uses ground-truth boxes as detections, so its numbers are not comparab 69.00 - SoccerNet held-out train² + SoccerNet held-out train² BoT-SORT 85.72 BoT-SORT + ReID - 88.97 + 88.97 + + + BoT-SORT + ReID, adaptive + 89.91 @@ -446,7 +450,7 @@ SoccerNet uses ground-truth boxes as detections, so its numbers are not comparab 79.36 - SoccerNet + SoccerNet BoT-SORT 85.00 79.68 @@ -454,9 +458,15 @@ SoccerNet uses ground-truth boxes as detections, so its numbers are not comparab BoT-SORT + ReID - 87.30 - 83.16 - 98.73 + 87.30 + 83.16 + 98.73 + + + BoT-SORT + ReID, adaptive + 88.43 + 84.40 + 99.26 @@ -465,7 +475,7 @@ SoccerNet uses ground-truth boxes as detections, so its numbers are not comparab - **MOT17**: `osnet_x1_0` fine-tuned on MOT17 train, `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` - **SoccerNet**: `osnet_x1_0` fine-tuned on SoccerNet train, `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` -² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). That search picked the same values used on test, 0.075 and 1.0. +² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). That search picked 0.075 and 1.0 for `reid_fusion="botsort"`, and for `reid_fusion="adaptive"` a gate of 1.0 with `reid_appearance_floor=0.8`, both at their default weights. -Fine-tuning makes the biggest difference on SoccerNet: the generic encoder leaves HOTA flat, while the fine-tuned one adds 2.30 on test and 3.25 on the held-out train sequences. DanceTrack is not listed because its fine-tuned encoder scores 57.5 on test, below BoT-SORT without ReID. +Fine-tuning makes the biggest difference on SoccerNet: the generic encoder leaves HOTA flat, while the fine-tuned one adds 2.30 on test, and 3.43 with `adaptive` fusion. DanceTrack is not listed because its fine-tuned encoder scores 57.5 on test, below BoT-SORT without ReID. ``` From eef928671463c89215ec3bee7977bacaa48fefae Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 17 Sep 2026 10:08:06 -0300 Subject: [PATCH 13/49] docs(reid): markdown results tables, MOT17 adaptive rows, tuning note The HTML tables inside the Results tabs made mdformat turn the text after them into code blocks. Use markdown tables instead, which mdformat leaves intact, with the dataset named on its first row. Add the fine-tuned MOT17 encoder with reid_fusion="adaptive" at its defaults (68.72 on val-half; 63.8 HOTA, 78.86 IDF1, 79.49 MOTA on test). State that the MOT17, DanceTrack and SoccerNet generic thresholds were tuned with `trackers tune`, while SportsMOT comes from a sweep at the default gate. Co-Authored-By: Claude Opus 5 --- docs/guides/reid.md | 282 ++++++++------------------------------------ 1 file changed, 50 insertions(+), 232 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 00c3d441..ea3b2662 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -218,7 +218,7 @@ With the gate open, `adaptive` keeps IDs more stable than `"botsort"`, which swi ## Results -BoT-SORT with and without ReID, using the same detections and motion parameters. The first table shows HOTA on the split where the ReID thresholds were tuned, the second shows test results. The better value in each pair is in bold. Motion parameters are the tuned values from the [tracker comparison](../evaluations/results.md). +BoT-SORT with and without ReID, using the same detections and motion parameters. The first table shows HOTA on the split where the ReID thresholds were tuned, the second shows test results. The better value for each dataset is in bold. Motion parameters are the tuned values from the [tracker comparison](../evaluations/results.md). @@ -228,254 +228,72 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. Tuning split: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DatasetConfigHOTA
MOT17 val-halfBoT-SORT69.05
BoT-SORT + ReID69.64
SportsMOT valBoT-SORT82.00
BoT-SORT + ReID82.69
DanceTrack valBoT-SORT53.89
BoT-SORT + ReID58.34
SoccerNet train¹BoT-SORT86.95
BoT-SORT + ReID86.98
+ | Dataset | Split | Config | HOTA | + | :--------- | :------- | :-------------- | :-------: | + | MOT17 | val-half | BoT-SORT | 69.05 | + | | | BoT-SORT + ReID | **69.64** | + | SportsMOT | val | BoT-SORT | 82.00 | + | | | BoT-SORT + ReID | **82.69** | + | DanceTrack | val | BoT-SORT | 53.89 | + | | | BoT-SORT + ReID | **58.34** | + | SoccerNet | train¹ | BoT-SORT | 86.95 | + | | | BoT-SORT + ReID | **86.98** | Test split: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DatasetConfigHOTAIDF1MOTA
MOT17BoT-SORT63.8678.7479.42
BoT-SORT + ReID63.8578.8279.41
SportsMOTBoT-SORT74.1574.0696.89
BoT-SORT + ReID75.6275.6696.90
DanceTrackBoT-SORT57.857.992.2
BoT-SORT + ReID57.657.692.1
SoccerNetBoT-SORT85.0079.6897.25
BoT-SORT + ReID84.9679.6697.25
+ | Dataset | Config | HOTA | IDF1 | MOTA | + | :--------- | :-------------- | :-------: | :-------: | :-------: | + | MOT17 | BoT-SORT | **63.86** | 78.74 | **79.42** | + | | BoT-SORT + ReID | 63.85 | **78.82** | 79.41 | + | SportsMOT | BoT-SORT | 74.15 | 74.06 | 96.89 | + | | BoT-SORT + ReID | **75.62** | **75.66** | **96.90** | + | DanceTrack | BoT-SORT | **57.8** | **57.9** | **92.2** | + | | BoT-SORT + ReID | 57.6 | 57.6 | 92.1 | + | SoccerNet | BoT-SORT | **85.00** | **79.68** | 97.25 | + | | BoT-SORT + ReID | 84.96 | 79.66 | 97.25 | -``` -- **MOT17**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.10`, `reid_proximity_threshold=0.5` -- **SportsMOT**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.30`, `reid_proximity_threshold=0.5` -- **DanceTrack**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.48`, `reid_proximity_threshold=0.51` -- **SoccerNet**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.05`, `reid_proximity_threshold=0.35` + - **MOT17**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.10`, `reid_proximity_threshold=0.5` + - **SportsMOT**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.30`, `reid_proximity_threshold=0.5` + - **DanceTrack**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.48`, `reid_proximity_threshold=0.51` + - **SoccerNet**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.05`, `reid_proximity_threshold=0.35` -Both thresholds were tuned together on the tuning split, then each configuration was evaluated once on test. + For MOT17, DanceTrack and SoccerNet, both thresholds were tuned together with `trackers tune`. The SportsMOT threshold comes from a sweep at the default proximity threshold. Each configuration was then evaluated once on test. -¹ SoccerNet-tracking has no validation split, so its tuning split is train. + ¹ SoccerNet-tracking has no validation split, so its tuning split is train. -A generic encoder improves HOTA on every tuning split, but only SportsMOT keeps the gain on test. On the other datasets, test HOTA stays within 0.2 of BoT-SORT without ReID. On SoccerNet the tuned thresholds are strict enough that appearance rarely changes a match. + A generic encoder improves HOTA on every tuning split, but only SportsMOT keeps the gain on test. On the other datasets, test HOTA stays within 0.2 of BoT-SORT without ReID. On SoccerNet the tuned thresholds are strict enough that appearance rarely changes a match. -SoccerNet uses ground-truth boxes as detections, so its numbers are not comparable to the YOLOX rows. On DanceTrack, validation HOTA without ReID is 3.9 lower than test HOTA, which explains most of the validation gain. `osnet_x1_0_msmt17_combineall` scores 56.21 on DanceTrack validation and 56.0 on test with thresholds 0.25 and 0.5. -``` + SoccerNet uses ground-truth boxes as detections, so its numbers are not comparable to the YOLOX rows. On DanceTrack, validation HOTA without ReID is 3.9 lower than test HOTA, which explains most of the validation gain. `osnet_x1_0_msmt17_combineall` scores 56.21 on DanceTrack validation and 56.0 on test with thresholds 0.25 and 0.5. === "Fine-tuned encoder" - `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"`. Available for MOT17 and SoccerNet. + `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"` and `reid_fusion="adaptive"`. Available for MOT17 and SoccerNet. Tuning split: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DatasetConfigHOTA
MOT17 val-halfBoT-SORT69.05
BoT-SORT + ReID69.00
SoccerNet held-out train²BoT-SORT85.72
BoT-SORT + ReID88.97
BoT-SORT + ReID, adaptive89.91
+ | Dataset | Split | Config | HOTA | + | :-------- | :-------------- | :-------------------------- | :-------: | + | MOT17 | val-half | BoT-SORT | **69.05** | + | | | BoT-SORT + ReID | 69.00 | + | | | BoT-SORT + ReID, `adaptive` | 68.72 | + | SoccerNet | held-out train² | BoT-SORT | 85.72 | + | | | BoT-SORT + ReID | 88.97 | + | | | BoT-SORT + ReID, `adaptive` | **89.91** | Test split: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DatasetConfigHOTAIDF1MOTA
MOT17BoT-SORT63.878.779.4
BoT-SORT + ReID64.1279.1679.36
SoccerNetBoT-SORT85.0079.6897.25
BoT-SORT + ReID87.3083.1698.73
BoT-SORT + ReID, adaptive88.4384.4099.26
+ | Dataset | Config | HOTA | IDF1 | MOTA | + | :-------- | :-------------------------- | :-------: | :-------: | :-------: | + | MOT17 | BoT-SORT | 63.8 | 78.7 | 79.4 | + | | BoT-SORT + ReID | **64.12** | **79.16** | 79.36 | + | | BoT-SORT + ReID, `adaptive` | 63.8 | 78.86 | **79.49** | + | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | + | | BoT-SORT + ReID | 87.30 | 83.16 | 98.73 | + | | BoT-SORT + ReID, `adaptive` | **88.43** | **84.40** | **99.26** | -``` -- **MOT17**: `osnet_x1_0` fine-tuned on MOT17 train, `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5` -- **SoccerNet**: `osnet_x1_0` fine-tuned on SoccerNet train, `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0` + - **MOT17**: `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5`; `adaptive` with its defaults (`reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5`) + - **SoccerNet**: `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0`; `adaptive` with `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.8` and default weights -² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). That search picked 0.075 and 1.0 for `reid_fusion="botsort"`, and for `reid_fusion="adaptive"` a gate of 1.0 with `reid_appearance_floor=0.8`, both at their default weights. + ² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). -Fine-tuning makes the biggest difference on SoccerNet: the generic encoder leaves HOTA flat, while the fine-tuned one adds 2.30 on test, and 3.43 with `adaptive` fusion. DanceTrack is not listed because its fine-tuned encoder scores 57.5 on test, below BoT-SORT without ReID. -``` + Fine-tuning makes the biggest difference on SoccerNet: the generic encoder leaves HOTA flat, while the fine-tuned one adds 2.30 on test, and 3.43 with `adaptive`. The MOT17 encoder was trained on the first half of the same sequences that val-half evaluates, so its val-half rows are not a clean tuning split. DanceTrack is not listed because its fine-tuned encoder scores 57.5 on test, below BoT-SORT without ReID. From 616d635e65ed5528f179bd4e34f58765882e16c9 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 17 Sep 2026 10:40:37 -0300 Subject: [PATCH 14/49] docs(reid): add the fine-tuned DanceTrack encoder to the results The DanceTrack encoder trained with 8 crops per identity was selected on val over 4 and 16 (58.80 against 57.86 and 58.70) and scores 58.8 HOTA, 59.1 IDF1, 92.2 MOTA on test with the botsort rule, and 58.1 / 58.2 / 92.3 with adaptive, against 57.8 / 57.9 / 92.2 without ReID. The page previously cited the 4-crop encoder's 57.5 and left DanceTrack out. Co-Authored-By: Claude Opus 5 --- docs/guides/reid.md | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index ea3b2662..429a00e1 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -267,33 +267,40 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. === "Fine-tuned encoder" - `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"` and `reid_fusion="adaptive"`. Available for MOT17 and SoccerNet. + `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"` and `reid_fusion="adaptive"`. Available for MOT17, DanceTrack and SoccerNet. Tuning split: - | Dataset | Split | Config | HOTA | - | :-------- | :-------------- | :-------------------------- | :-------: | - | MOT17 | val-half | BoT-SORT | **69.05** | - | | | BoT-SORT + ReID | 69.00 | - | | | BoT-SORT + ReID, `adaptive` | 68.72 | - | SoccerNet | held-out train² | BoT-SORT | 85.72 | - | | | BoT-SORT + ReID | 88.97 | - | | | BoT-SORT + ReID, `adaptive` | **89.91** | + | Dataset | Split | Config | HOTA | + | :--------- | :-------------- | :-------------------------- | :-------: | + | MOT17 | val-half | BoT-SORT | **69.05** | + | | | BoT-SORT + ReID | 69.00 | + | | | BoT-SORT + ReID, `adaptive` | 68.72 | + | DanceTrack | val | BoT-SORT | 53.89 | + | | | BoT-SORT + ReID | **58.80** | + | | | BoT-SORT + ReID, `adaptive` | 58.06 | + | SoccerNet | held-out train² | BoT-SORT | 85.72 | + | | | BoT-SORT + ReID | 88.97 | + | | | BoT-SORT + ReID, `adaptive` | **89.91** | Test split: - | Dataset | Config | HOTA | IDF1 | MOTA | - | :-------- | :-------------------------- | :-------: | :-------: | :-------: | - | MOT17 | BoT-SORT | 63.8 | 78.7 | 79.4 | - | | BoT-SORT + ReID | **64.12** | **79.16** | 79.36 | - | | BoT-SORT + ReID, `adaptive` | 63.8 | 78.86 | **79.49** | - | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | - | | BoT-SORT + ReID | 87.30 | 83.16 | 98.73 | - | | BoT-SORT + ReID, `adaptive` | **88.43** | **84.40** | **99.26** | + | Dataset | Config | HOTA | IDF1 | MOTA | + | :--------- | :-------------------------- | :-------: | :-------: | :-------: | + | MOT17 | BoT-SORT | 63.8 | 78.7 | 79.4 | + | | BoT-SORT + ReID | **64.12** | **79.16** | 79.36 | + | | BoT-SORT + ReID, `adaptive` | 63.8 | 78.86 | **79.49** | + | DanceTrack | BoT-SORT | 57.8 | 57.9 | 92.2 | + | | BoT-SORT + ReID | **58.8** | **59.1** | 92.2 | + | | BoT-SORT + ReID, `adaptive` | 58.1 | 58.2 | **92.3** | + | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | + | | BoT-SORT + ReID | 87.30 | 83.16 | 98.73 | + | | BoT-SORT + ReID, `adaptive` | **88.43** | **84.40** | **99.26** | - **MOT17**: `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5`; `adaptive` with its defaults (`reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5`) + - **DanceTrack**: trained with 8 crops per identity, selected on val over 4 and 16; `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5`; `adaptive` with `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0`, `reid_proximity_threshold=0.5` - **SoccerNet**: `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0`; `adaptive` with `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.8` and default weights ² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). - Fine-tuning makes the biggest difference on SoccerNet: the generic encoder leaves HOTA flat, while the fine-tuned one adds 2.30 on test, and 3.43 with `adaptive`. The MOT17 encoder was trained on the first half of the same sequences that val-half evaluates, so its val-half rows are not a clean tuning split. DanceTrack is not listed because its fine-tuned encoder scores 57.5 on test, below BoT-SORT without ReID. + Fine-tuning makes the biggest difference on SoccerNet: the generic encoder leaves HOTA flat, while the fine-tuned one adds 2.30 on test, and 3.43 with `adaptive`. The MOT17 encoder was trained on the first half of the same sequences that val-half evaluates, so its val-half rows are not a clean tuning split. On DanceTrack the fine-tuned encoder adds 1.0 HOTA on test, where the generic one does not help. From a6a136f09c414f6f37f674339ef9fa536956fbe7 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 17 Sep 2026 16:18:01 -0300 Subject: [PATCH 15/49] docs(reid): order the guide as the steps a user takes Read in order the page now goes encoder, fusion method, appearance threshold, proximity threshold, tuning, results, so each choice comes after the one it depends on. The appearance threshold is marked as botsort-only, and the proximity threshold applies to both rules. The Results tabs show each dataset's tuned parameters as a config block, the way the tracker comparison page does, and say which fusion method each row uses. The fine-tuned rows that were never searched are left out until their tuning finishes. Co-Authored-By: Claude Opus 5 --- docs/guides/reid.md | 215 ++++++++++++++++++++++++++++++-------------- 1 file changed, 146 insertions(+), 69 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 429a00e1..3e22d898 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -56,7 +56,7 @@ For extra contents and other options, see the [install guide](install.md). Pass the current video frame as `tracker.update(detections, frame=frame_bgr)`. When `reid_model` is set, `update()` raises if `frame` is omitted. -For the model catalog and fine-tuning, see the [`reid` training guide](https://github.com/roboflow/re-ID/blob/main/docs/learn/train.md). +A guide to the model catalog and fine-tuning in `reid` is coming soon. --- @@ -75,8 +75,54 @@ For the model catalog and fine-tuning, see the [`reid` training guide](https://g --- +## Choosing an encoder + +The encoder decides how much appearance can help, and every threshold below depends on it. There are two kinds: + +- **A generic encoder**, trained on another dataset, such as `osnet_x1_0_msmt17_combineall` (the default) or `fastreid_mot17_sbs50`. It works out of the box, but on footage unlike its training data appearance helps little, see [results](#results). +- **An encoder fine-tuned on your footage**, where the largest gains in the results come from. A guide to fine-tuning is coming soon. + +Load either with `ReIDModel.from_pretrained`, as in the [quickstart](#quickstart). Each encoder has its own distance scale, so choose the thresholds for the encoder you will track with. + +--- + +## Choosing a fusion method + +On every frame, BoT-SORT gives each track and detection pair a score and then picks the best matching. With ReID enabled, that score combines two signals: how much the boxes overlap, and how alike the two crops look. `reid_fusion` sets how they are combined: + +```python +tracker = BoTSORTTracker( + reid_model=reid_model, + reid_fusion="adaptive", # default: "botsort" +) +``` + +- `"botsort"`: a pair scores as well as the stronger of the two, `score = 1 - min(box distance, appearance distance)`. +- `"adaptive"`: the two add up, `score = box overlap + weight * appearance similarity`. + +In more detail: + +**`"botsort"`.** Appearance is only used for a pair when its distance is below `reid_appearance_threshold` and the boxes pass the `reid_proximity_threshold` check. Otherwise the pair is scored on box overlap alone. + +**`"adaptive"`.** This is the appearance fusion from Deep OC-SORT. Each pair starts from its box overlap, and appearance is added on top: `score = box overlap + (reid_appearance_weight + bonus) * appearance similarity`. The bonus rewards a clear winner. For a track, it is how much more its most similar detection looks like it than the second most similar one; for a detection, the same against its two most similar tracks. A pair's bonus is the average of the two, capped at `reid_adaptive_weight_cap`. So appearance counts more when one candidate clearly stands out, and less when several look alike. Negative similarities count as zero, so appearance can only raise a score. `reid_appearance_floor`, added by this library, sets the lowest appearance similarity that counts: below it, a pair is scored on box overlap alone. With the proximity gate open, this stops a lost track from taking an unrelated detection that only looks vaguely similar. + +!!! note "What `adaptive` includes" + + Only the appearance fusion is taken from Deep OC-SORT; the rest of that tracker is not added to BoT-SORT. `reid_appearance_threshold` has no effect with `reid_fusion="adaptive"`. + +Two consequences are worth knowing before switching: + +- **The similarity range changes.** `"botsort"` returns values in `[0, 1]`; `"adaptive"` returns `[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]`, which is `[0, 2.25]` at the defaults. `minimum_iou_threshold_first_assoc` is applied to that fused value, so a threshold tuned for one method is a different gate under the other. Retune it when you switch, or the comparison measures the gate rather than the fusion. +- **The weights are domain-dependent.** The Deep OC-SORT paper uses `reid_appearance_weight=0.75` with `reid_adaptive_weight_cap=0.5` for MOT17 and MOT20, and `1.25` with `1.0` for DanceTrack, where dancers occlude constantly and geometry carries less. The defaults here follow the MOT17/MOT20 pair. + +With the gate open, `adaptive` keeps IDs more stable than `"botsort"`, which switches between look-alike candidates from one frame to the next. `"botsort"` stays the default because it is the safer choice: appearance is only used below `reid_appearance_threshold`, and turning it on does not require retuning the other thresholds. + +--- + ## Choosing an appearance threshold +This threshold only applies to `reid_fusion="botsort"`; with `"adaptive"` it has no effect. + When a track and a detection look alike, appearance can make their match cheaper. It never blocks a match that geometry already allows. `reid_appearance_threshold` sets how alike they must look, as a distance of `0.5 * (1 - cosine similarity)`: 0 means identical, and the BoT-SORT paper uses 0.25. The right value depends on the encoder and the footage, so measure it on labeled data: @@ -141,17 +187,22 @@ sweep = sweep_frame_gap(embeddings, ids, frame_ids, sequence_ids) plot_frame_gap_sweep(sweep, thresholds={0.20: "selected", 0.25: "default"}) ``` -On MOT17 val, different-ID distances barely move with the gap: the median stays near 0.41 and the 10th percentile near 0.31 from a 1-frame gap out to 240 frames. Same-ID distances spread steadily, from a median of 0.04 at a 1-frame gap to 0.20 across the 16 to 30 band and 0.28 beyond 120 frames. +On MOT17 val, pairs of different people stay about as far apart at every gap. Pairs of the same person drift apart as the gap grows, because pose, lighting and occlusion change, so a threshold that catches them one frame apart misses more of them after a long absence. ![FastReID MOT17 SBS separability vs frame gap](../assets/reid/mot17-fastreid-appearance-distances-vs-gap.png) -Two things follow. First, a threshold validated on adjacent frames says little about re-association: at θ=0.2 appearance helps 98% of same-ID pairs one frame apart but only 51% across the default 30-frame lost-track buffer. Second, the price of a tight θ over long gaps is missed re-associations rather than extra wrong ones, because the different-ID rate stays near 1% throughout. If you raise `lost_track_buffer` to recover tracks after long occlusions, raise `reid_appearance_threshold` with it and re-check the different-ID column. +From this we learn: + +- **Check the threshold at the gap you care about.** A threshold that works one frame apart can fail after a lost track: at 0.2 it matches 98% of same-person pairs one frame apart, but only 51% across the default 30-frame `lost_track_buffer`. +- **Long gaps make people harder to tell apart.** The same person looks less like themselves after a long absence, so same-person and different-person distances overlap more: ROC AUC, explained below, falls from 0.998 at a 1-frame gap to 0.854 beyond 120 frames. With the threshold at 0.2, appearance stops recognizing most people after a long absence: only 29% of same-person pairs more than 120 frames apart are under 0.2, against 98% one frame apart. Raising the threshold to catch those also lets more different people through, so if you increase `lost_track_buffer`, check both rates at that gap before raising `reid_appearance_threshold`. ??? info "How to read ROC AUC, and the numbers per frame gap" - ROC AUC below is the chance that a random same-ID pair scores closer than a random different-ID pair: 1.0 means the two never cross, 0.5 means appearance carries no information, and its complement is how often a same-ID pair sits farther apart than a different-ID one. It is the area under the curve traced by sweeping θ from 0 to 1 and plotting the two rates next to it, so it summarises every threshold instead of the single one we ship. + ROC AUC answers one question: if you pick one pair of crops of the same person and one pair of different people at random, how often is the same-person pair closer? 1.0 means always, so some threshold separates the two groups perfectly. 0.5 means a coin flip, so appearance tells you nothing. Unlike the two rates in the table, it does not depend on a chosen threshold. It is also not where the shaded bands overlap in the figure: those bands only span the 10th to 90th percentiles, so at a 1-frame gap they never touch even though the AUC is 0.998. - It is not the area where the shaded bands cross in the figure. That is two percentile ranges intersecting, which ignores where the mass sits and which side is closer; at a 1-frame gap the bands never touch yet the AUC is 0.998 rather than 1.0. The two rates beside it evaluate the default 0.25 and the 0.2 this page argues for, rather than deriving a third. + How it is calculated: each frame-gap band gets its own sample of same-person and different-person pairs. For every threshold from 0 to 1, count the share of same-person pairs below it and the share of different-person pairs below it, and plot the first share against the second. That traces the ROC curve from (0, 0) to (1, 1), and ROC AUC is the area under it. `sweep_frame_gap` gets the same number without drawing the curve, by comparing every same-person distance with every different-person distance and counting how often the same-person one is smaller, with ties counting as half (`trackers.core.reid.roc_auc`). The result is the lower panel of the figure above. + + The last two columns show the two rates at a threshold of 0.2. | Frame gap | ROC AUC | same-ID below 0.2 | different-ID below 0.2 | | :--------- | :-----: | :---------------: | :--------------------: | @@ -195,26 +246,13 @@ HOTA and IDF1 improve as the gate opens, but ID switches do not follow the same The right value depends on the footage. Use 1.0 when targets leave the frame or the camera moves a lot. Keep 0.5 when targets stay in view, as in DanceTrack: there an open gate only adds wrong matches between people who look alike. -## Choosing a fusion method - -`reid_fusion` selects how the appearance score reaches the cost matrix. - -`"botsort"` (default) takes `min(d_iou, d_app)`. Appearance competes with geometry and wins only when it is strictly cheaper, so a pair is accepted when either cue is confident. Appearance is only used for a pair when its distance is below `reid_appearance_threshold` and the boxes pass the `reid_proximity_threshold` check. Otherwise the pair is matched on geometry alone. - -`"adaptive"` adds a weighted appearance term to the geometric similarity, with the weight growing when the best appearance match stands clear of the runner-up and falling back to `reid_appearance_weight` when the top candidates are hard to tell apart. This is the appearance fusion from Deep OC-SORT. - -`reid_appearance_floor`, an addition of this library, sets a minimum cosine similarity below which a pair falls back to geometry alone: with the gate open it stops lost tracks from capturing unrelated detections on weak appearance matches, which otherwise clear the association threshold whenever geometry contributes nothing. - -!!! note "What `adaptive` includes" - - Only the appearance fusion is taken from Deep OC-SORT; the rest of that tracker is not added to BoT-SORT. `reid_appearance_threshold` has no effect with `reid_fusion="adaptive"`. +--- -Two consequences are worth knowing before switching: +## Tuning the thresholds -- **The similarity range changes.** `"botsort"` returns values in `[0, 1]`; `"adaptive"` returns `[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]`, which is `[0, 2.25]` at the defaults. `minimum_iou_threshold_first_assoc` is applied to that fused value, so a threshold tuned for one method is a different gate under the other. Retune it when you switch, or the comparison measures the gate rather than the fusion. -- **The weights are domain-dependent.** The Deep OC-SORT paper uses `reid_appearance_weight=0.75` with `reid_adaptive_weight_cap=0.5` for MOT17 and MOT20, and `1.25` with `1.0` for DanceTrack, where dancers occlude constantly and geometry carries less. The defaults here follow the MOT17/MOT20 pair. +The steps above give a good starting point. `trackers tune` then searches `reid_appearance_threshold` and `reid_proximity_threshold` together, keeping the motion parameters you pass fixed, see [Tune ReID Thresholds](tune.md#tune-reid-thresholds). The command tunes the `"botsort"` thresholds; for `"adaptive"` it does not search the weights yet. -With the gate open, `adaptive` keeps IDs more stable than `"botsort"`, which switches between look-alike candidates from one frame to the next. The weights mainly matter where geometry is unreliable, as in DanceTrack; elsewhere the defaults work. `"botsort"` stays the default because it is the safer choice: appearance is only used below `reid_appearance_threshold`, and turning it on does not require retuning the other thresholds. +--- ## Results @@ -224,20 +262,20 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. === "Generic encoder" - Encoders that were not trained on the evaluated dataset, with `reid_fusion="botsort"`. + Encoders that were not trained on the evaluated dataset. Each dataset uses the fusion method that scored higher on the tuning split, `reid_fusion="botsort"` unless noted. Tuning split: - | Dataset | Split | Config | HOTA | - | :--------- | :------- | :-------------- | :-------: | - | MOT17 | val-half | BoT-SORT | 69.05 | - | | | BoT-SORT + ReID | **69.64** | - | SportsMOT | val | BoT-SORT | 82.00 | - | | | BoT-SORT + ReID | **82.69** | - | DanceTrack | val | BoT-SORT | 53.89 | - | | | BoT-SORT + ReID | **58.34** | - | SoccerNet | train¹ | BoT-SORT | 86.95 | - | | | BoT-SORT + ReID | **86.98** | + | Dataset | Config | HOTA | + | :--------- | :-------------- | :-------: | + | MOT17 | BoT-SORT | 69.05 | + | | BoT-SORT + ReID | **69.64** | + | SportsMOT | BoT-SORT | 82.00 | + | | BoT-SORT + ReID | **82.69** | + | DanceTrack | BoT-SORT | 53.89 | + | | BoT-SORT + ReID | **58.34** | + | SoccerNet¹ | BoT-SORT | 86.95 | + | | BoT-SORT + ReID | **86.98** | Test split: @@ -252,14 +290,37 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. | SoccerNet | BoT-SORT | **85.00** | **79.68** | 97.25 | | | BoT-SORT + ReID | 84.96 | 79.66 | 97.25 | - - **MOT17**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.10`, `reid_proximity_threshold=0.5` - - **SportsMOT**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.30`, `reid_proximity_threshold=0.5` - - **DanceTrack**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.48`, `reid_proximity_threshold=0.51` - - **SoccerNet**: `fastreid_mot17_sbs50`, `reid_appearance_threshold=0.05`, `reid_proximity_threshold=0.35` + Tuned ReID configuration for each dataset. Motion parameters are the BoT-SORT values from the [tracker comparison](../evaluations/results.md). + + ```yaml + MOT17: + reid_model: fastreid_mot17_sbs50 + reid_fusion: botsort + reid_appearance_threshold: 0.10 + reid_proximity_threshold: 0.5 + + SportsMOT: + reid_model: fastreid_mot17_sbs50 + reid_fusion: botsort + reid_appearance_threshold: 0.30 + reid_proximity_threshold: 0.5 + + DanceTrack: + reid_model: fastreid_mot17_sbs50 + reid_fusion: botsort + reid_appearance_threshold: 0.4822 + reid_proximity_threshold: 0.5068 + + SoccerNet: + reid_model: fastreid_mot17_sbs50 + reid_fusion: botsort + reid_appearance_threshold: 0.0467 + reid_proximity_threshold: 0.3511 + ``` For MOT17, DanceTrack and SoccerNet, both thresholds were tuned together with `trackers tune`. The SportsMOT threshold comes from a sweep at the default proximity threshold. Each configuration was then evaluated once on test. - ¹ SoccerNet-tracking has no validation split, so its tuning split is train. + ¹ SoccerNet-tracking has no validation split, so its tuning split is train. The other datasets tune on val, MOT17 on val-half. A generic encoder improves HOTA on every tuning split, but only SportsMOT keeps the gain on test. On the other datasets, test HOTA stays within 0.2 of BoT-SORT without ReID. On SoccerNet the tuned thresholds are strict enough that appearance rarely changes a match. @@ -267,40 +328,56 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. === "Fine-tuned encoder" - `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"` and `reid_fusion="adaptive"`. Available for MOT17, DanceTrack and SoccerNet. + `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"` and `reid_fusion="adaptive"`. Tuning split: - | Dataset | Split | Config | HOTA | - | :--------- | :-------------- | :-------------------------- | :-------: | - | MOT17 | val-half | BoT-SORT | **69.05** | - | | | BoT-SORT + ReID | 69.00 | - | | | BoT-SORT + ReID, `adaptive` | 68.72 | - | DanceTrack | val | BoT-SORT | 53.89 | - | | | BoT-SORT + ReID | **58.80** | - | | | BoT-SORT + ReID, `adaptive` | 58.06 | - | SoccerNet | held-out train² | BoT-SORT | 85.72 | - | | | BoT-SORT + ReID | 88.97 | - | | | BoT-SORT + ReID, `adaptive` | **89.91** | + | Dataset | Config | HOTA | + | :--------- | :-------------------------- | :-------: | + | MOT17 | BoT-SORT | **69.05** | + | | BoT-SORT + ReID | 69.00 | + | SoccerNet² | BoT-SORT | 85.72 | + | | BoT-SORT + ReID | 88.97 | + | | BoT-SORT + ReID, `adaptive` | **89.91** | Test split: - | Dataset | Config | HOTA | IDF1 | MOTA | - | :--------- | :-------------------------- | :-------: | :-------: | :-------: | - | MOT17 | BoT-SORT | 63.8 | 78.7 | 79.4 | - | | BoT-SORT + ReID | **64.12** | **79.16** | 79.36 | - | | BoT-SORT + ReID, `adaptive` | 63.8 | 78.86 | **79.49** | - | DanceTrack | BoT-SORT | 57.8 | 57.9 | 92.2 | - | | BoT-SORT + ReID | **58.8** | **59.1** | 92.2 | - | | BoT-SORT + ReID, `adaptive` | 58.1 | 58.2 | **92.3** | - | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | - | | BoT-SORT + ReID | 87.30 | 83.16 | 98.73 | - | | BoT-SORT + ReID, `adaptive` | **88.43** | **84.40** | **99.26** | - - - **MOT17**: `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5`; `adaptive` with its defaults (`reid_appearance_weight=0.75`, `reid_adaptive_weight_cap=0.5`, `reid_proximity_threshold=0.5`) - - **DanceTrack**: trained with 8 crops per identity, selected on val over 4 and 16; `reid_appearance_threshold=0.25`, `reid_proximity_threshold=0.5`; `adaptive` with `reid_appearance_weight=2.4`, `reid_adaptive_weight_cap=0`, `reid_proximity_threshold=0.5` - - **SoccerNet**: `reid_appearance_threshold=0.075`, `reid_proximity_threshold=1.0`; `adaptive` with `reid_proximity_threshold=1.0`, `reid_appearance_floor=0.8` and default weights - - ² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). - - Fine-tuning makes the biggest difference on SoccerNet: the generic encoder leaves HOTA flat, while the fine-tuned one adds 2.30 on test, and 3.43 with `adaptive`. The MOT17 encoder was trained on the first half of the same sequences that val-half evaluates, so its val-half rows are not a clean tuning split. On DanceTrack the fine-tuned encoder adds 1.0 HOTA on test, where the generic one does not help. + | Dataset | Config | HOTA | IDF1 | MOTA | + | :-------- | :-------------------------- | :-------: | :-------: | :-------: | + | MOT17 | BoT-SORT | 63.8 | 78.7 | 79.4 | + | | BoT-SORT + ReID | **64.12** | **79.16** | 79.36 | + | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | + | | BoT-SORT + ReID | 87.30 | 83.16 | 98.73 | + | | BoT-SORT + ReID, `adaptive` | **88.43** | **84.40** | **99.26** | + + Tuned ReID configuration for each dataset and fusion method. Motion parameters are the BoT-SORT values from the [tracker comparison](../evaluations/results.md), except `minimum_iou_threshold_first_assoc` where listed. + + ```yaml + MOT17: + botsort: + reid_model: osnet_x1_0 fine-tuned on MOT17 train + reid_appearance_threshold: 0.25 + reid_proximity_threshold: 0.5 + + SoccerNet: + botsort: + reid_model: osnet_x1_0 fine-tuned on SoccerNet train + reid_appearance_threshold: 0.075 + reid_proximity_threshold: 1.0 + adaptive: + reid_model: osnet_x1_0 fine-tuned on SoccerNet train + reid_appearance_weight: 0.75 + reid_adaptive_weight_cap: 0.5 + reid_proximity_threshold: 1.0 + reid_appearance_floor: 0.8 + ``` + + Each configuration was evaluated once on test. + + !!! note "Being tuned" + + The DanceTrack rows and MOT17's `adaptive` row are not here yet: they were never tuned, only run at a few hand-picked values. A `trackers tune` search is under way for all three datasets, and they will be added with what it finds. + + ² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). The other datasets tune on val, MOT17 on val-half. + + Fine-tuning makes the biggest difference on SoccerNet: the generic encoder leaves HOTA flat, while the fine-tuned one adds 2.30 on test, and 3.43 with `adaptive`. From 5d6374441b3ff008ba5a93ee26445828423b770e Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 17 Sep 2026 16:54:10 -0300 Subject: [PATCH 16/49] docs(reid): shorten the page title and cut a line that says nothing Co-Authored-By: Claude Opus 5 --- docs/guides/reid.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 3e22d898..58407c38 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -1,5 +1,5 @@ --- -title: ReID Appearance — BoT-SORT Appearance Association | Trackers +title: ReID Appearance description: Use ReID appearance association with BoT-SORT in Roboflow Trackers, from model loading to appearance threshold selection, with MOT17 and SoccerNet results. --- @@ -324,7 +324,7 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. A generic encoder improves HOTA on every tuning split, but only SportsMOT keeps the gain on test. On the other datasets, test HOTA stays within 0.2 of BoT-SORT without ReID. On SoccerNet the tuned thresholds are strict enough that appearance rarely changes a match. - SoccerNet uses ground-truth boxes as detections, so its numbers are not comparable to the YOLOX rows. On DanceTrack, validation HOTA without ReID is 3.9 lower than test HOTA, which explains most of the validation gain. `osnet_x1_0_msmt17_combineall` scores 56.21 on DanceTrack validation and 56.0 on test with thresholds 0.25 and 0.5. + SoccerNet uses ground-truth boxes as detections, so its numbers are not comparable to the YOLOX rows. === "Fine-tuned encoder" From dfd5e04d2ee843f92a965406680260a7a224ca74 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 22 Sep 2026 18:54:01 -0300 Subject: [PATCH 17/49] docs(reid): tuned results for every row, and the tuning-split tables Both fusion methods now have a 20-trial `trackers tune` search on every dataset, generic and fine-tuned, so the tabs carry the tuned parameters and the test numbers they produced. SoccerNet is the one row where `adaptive` wins on the tuning split, and it keeps the gain on test: 85.89 against 85.00 without ReID, where `botsort` leaves the same encoder flat. The comparison page's ReID rows move to the same configurations. Co-Authored-By: Claude Opus 5 --- docs/evaluations/results.md | 43 ++++++------ docs/guides/reid.md | 130 ++++++++++++++++++++++-------------- 2 files changed, 102 insertions(+), 71 deletions(-) diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index 1a155c20..42a33399 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -5,7 +5,7 @@ description: Side-by-side MOT benchmark comparison of SORT, ByteTrack, OC-SORT, # Tracker Comparison -This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. McByte is reported at default parameters only (mask-conditioned association enabled); it is designed to require no per-dataset tuning, so there is no Tuned row. BoT-SORT + ReID is shown at library defaults in the Default tabs, with the catalog-default encoder throughout; its Tuned rows use an encoder fine-tuned on the dataset's train split where one exists (MOT17, SoccerNet, DanceTrack) and the catalog default otherwise (SportsMOT). +This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. McByte is reported at default parameters only (mask-conditioned association enabled); it is designed to require no per-dataset tuning, so there is no Tuned row. BoT-SORT + ReID is shown at library defaults in the Default tabs, with the catalog-default encoder throughout; its Tuned rows use an encoder fine-tuned on the dataset's own train split for MOT17, SoccerNet and DanceTrack, and the generic `fastreid_mot17_sbs50` for SportsMOT, which has no fine-tuned encoder yet. !!! info "Benchmark version" @@ -28,7 +28,7 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests !!! info - Parameters were tuned on the validation set. Results are reported on the test set via Codabench submission. Detections come from a YOLOX model. BoT-SORT rows are CMC without appearance; for CMC + FastReID on MOT17 and OSNet MSMT17 on SoccerNet see [BoT-SORT with and without ReID](../guides/reid.md#results). + Parameters were tuned on the validation set. Results are reported on the test set via Codabench submission. Detections come from a YOLOX model. The BoT-SORT row is geometry and CMC only; BoT-SORT + ReID adds the encoder named under the table. The [ReID guide](../guides/reid.md#results) carries both fusion methods and the generic and fine-tuned encoders side by side. === "Default" @@ -63,8 +63,8 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests | ByteTrack | 60.5 | 72.7 | 76.1 | | OC-SORT | 62.0 | 76.5 | 77.3 | | BoT-SORT | 63.8 | 78.7 | **79.4** | - | C-BIoU | 63.0 | 79.1 | 77.4 | - | BoT-SORT + ReID | **64.1** | **79.2** | **79.4** | + | C-BIoU | 63.0 | **79.1** | 77.4 | + | BoT-SORT + ReID | **64.0** | 78.9 | **79.4** | Tuned configuration for each tracker. The ReID row uses an `osnet_x1_0` fine-tuned on MOT17 train. @@ -115,8 +115,8 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests BoT-SORT + ReID: reid_model: osnet_x1_0 fine-tuned on MOT17 train reid_fusion: botsort - reid_appearance_threshold: 0.25 - reid_proximity_threshold: 0.5 + reid_appearance_threshold: 0.0448 + reid_proximity_threshold: 0.2697 ``` ## [SportsMOT](https://arxiv.org/abs/2304.05170) @@ -163,12 +163,12 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar | :-------------: | :------: | :------: | :------: | | SORT | 72.9 | 73.0 | 95.8 | | ByteTrack | 73.3 | 73.5 | 95.9 | - | OC-SORT | 74.0 | **75.4** | 95.6 | - | BoT-SORT | 74.1 | 74.0 | 96.9 | + | OC-SORT | 74.0 | 75.4 | 95.6 | + | BoT-SORT | 74.1 | 74.0 | **96.9** | | C-BIoU | 73.1 | 72.6 | 96.7 | - | BoT-SORT + ReID | **75.0** | 75.1 | **97.0** | + | BoT-SORT + ReID | **75.6** | **75.7** | **96.9** | - Tuned configuration for each tracker. The ReID row uses the generic `osnet_x1_0_msmt17_combineall`, not a fine-tune. + Tuned configuration for each tracker. The ReID row uses the generic `fastreid_mot17_sbs50`, not a fine-tune. ```yaml SORT: @@ -215,11 +215,10 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar buffer_ratio_second: 0.5 BoT-SORT + ReID: - reid_model: osnet_x1_0_msmt17_combineall - reid_fusion: adaptive - reid_appearance_weight: 0.75 - reid_adaptive_weight_cap: 0.5 - reid_proximity_threshold: 0.99 + reid_model: fastreid_mot17_sbs50 + reid_fusion: botsort + reid_appearance_threshold: 0.30 + reid_proximity_threshold: 0.5 ``` ## [SoccerNet-tracking](https://arxiv.org/abs/2204.06918) @@ -233,7 +232,7 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I !!! info - Parameters were tuned on the train set. Results are reported on the test set. SoccerNet-tracking has no validation split. This dataset provides oracle (ground-truth) detections. The BoT-SORT row is CMC without appearance; for OSNet MSMT17 appearance on this split see [BoT-SORT with and without ReID](../guides/reid.md#results). + Parameters were tuned on the train set. Results are reported on the test set. SoccerNet-tracking has no validation split. This dataset provides oracle (ground-truth) detections. The BoT-SORT row is geometry and CMC only; BoT-SORT + ReID adds the encoder named under the table. The [ReID guide](../guides/reid.md#results) carries both fusion methods and the generic and fine-tuned encoders side by side. === "Default" @@ -268,8 +267,8 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I | ByteTrack | 84.0 | 78.1 | 98.2 | | OC-SORT | 82.9 | 77.9 | 96.8 | | BoT-SORT | 85.0 | 79.7 | 97.2 | - | C-BIoU | 85.7 | 80.0 | 99.3 | - | BoT-SORT + ReID | **87.8** | **83.9** | **99.4** | + | C-BIoU | 85.7 | 80.0 | **99.3** | + | BoT-SORT + ReID | **88.4** | **84.4** | **99.3** | Tuned configuration for each tracker. The ReID row uses an `osnet_x1_0` fine-tuned on SoccerNet train. @@ -323,7 +322,7 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I reid_appearance_weight: 0.75 reid_adaptive_weight_cap: 0.5 reid_proximity_threshold: 1.0 - reid_appearance_floor: 0.7 + reid_appearance_floor: 0.8 ``` !!! note "SoccerNet buffer ordering exception" @@ -376,7 +375,7 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art | OC-SORT | 54.1 | 53.3 | 89.3 | | BoT-SORT | 57.8 | 57.9 | 92.2 | | C-BIoU | 57.7 | 58.7 | **92.4** | - | BoT-SORT + ReID | **58.8** | **59.1** | 92.2 | + | BoT-SORT + ReID | **60.8** | **61.6** | 91.7 | Best configuration for each tracker. The ReID row uses an `osnet_x1_0` fine-tuned on DanceTrack train. @@ -427,8 +426,8 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art BoT-SORT + ReID: reid_model: osnet_x1_0 fine-tuned on DanceTrack train reid_fusion: botsort - reid_appearance_threshold: 0.25 - reid_proximity_threshold: 0.5 + reid_appearance_threshold: 0.4716 + reid_proximity_threshold: 0.6802 ``` !!! note "DanceTrack buffer ordering exception" diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 58407c38..f5c1d654 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -56,7 +56,7 @@ For extra contents and other options, see the [install guide](install.md). Pass the current video frame as `tracker.update(detections, frame=frame_bgr)`. When `reid_model` is set, `update()` raises if `frame` is omitted. -A guide to the model catalog and fine-tuning in `reid` is coming soon. +Fine-tuning an encoder on your own data is coming to the `reid` package. --- @@ -71,7 +71,7 @@ A guide to the model catalog and fine-tuning in `reid` is coming soon. | `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | | `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | | `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | -| `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, as in the Deep OC-SORT fusion). Calibrate per encoder: 0.7 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with the MOT17 and MSMT17 encoders, see [choosing a fusion method](#choosing-a-fusion-method). | +| `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, as in the Deep OC-SORT fusion). Calibrate per encoder: 0.8 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with the MOT17 and MSMT17 encoders, see [choosing a fusion method](#choosing-a-fusion-method). | --- @@ -80,7 +80,7 @@ A guide to the model catalog and fine-tuning in `reid` is coming soon. The encoder decides how much appearance can help, and every threshold below depends on it. There are two kinds: - **A generic encoder**, trained on another dataset, such as `osnet_x1_0_msmt17_combineall` (the default) or `fastreid_mot17_sbs50`. It works out of the box, but on footage unlike its training data appearance helps little, see [results](#results). -- **An encoder fine-tuned on your footage**, where the largest gains in the results come from. A guide to fine-tuning is coming soon. +- **An encoder fine-tuned on your footage**, where the largest gains in the results come from. Training one is coming to the `reid` package; the results below use encoders trained that way. Load either with `ReIDModel.from_pretrained`, as in the [quickstart](#quickstart). Each encoder has its own distance scale, so choose the thresholds for the encoder you will track with. @@ -104,7 +104,7 @@ In more detail: **`"botsort"`.** Appearance is only used for a pair when its distance is below `reid_appearance_threshold` and the boxes pass the `reid_proximity_threshold` check. Otherwise the pair is scored on box overlap alone. -**`"adaptive"`.** This is the appearance fusion from Deep OC-SORT. Each pair starts from its box overlap, and appearance is added on top: `score = box overlap + (reid_appearance_weight + bonus) * appearance similarity`. The bonus rewards a clear winner. For a track, it is how much more its most similar detection looks like it than the second most similar one; for a detection, the same against its two most similar tracks. A pair's bonus is the average of the two, capped at `reid_adaptive_weight_cap`. So appearance counts more when one candidate clearly stands out, and less when several look alike. Negative similarities count as zero, so appearance can only raise a score. `reid_appearance_floor`, added by this library, sets the lowest appearance similarity that counts: below it, a pair is scored on box overlap alone. With the proximity gate open, this stops a lost track from taking an unrelated detection that only looks vaguely similar. +**`"adaptive"`.** This is the appearance fusion from Deep OC-SORT. Each pair starts from its box overlap, and appearance is added on top: `score = box overlap + (reid_appearance_weight + bonus) * appearance similarity`. The bonus rewards a clear winner. For a track, it is how much more its most similar detection looks like it than the second most similar one; for a detection, the same against its two most similar tracks. Each margin is capped at `reid_adaptive_weight_cap`, and the pair's bonus is their average. So appearance counts more when one candidate clearly stands out, and less when several look alike. Negative similarities count as zero, so appearance can only raise a score. `reid_appearance_floor`, added by this library, sets the lowest appearance similarity that counts: below it, a pair is scored on box overlap alone. With the proximity gate open, this stops a lost track from taking an unrelated detection that only looks vaguely similar. !!! note "What `adaptive` includes" @@ -123,7 +123,7 @@ With the gate open, `adaptive` keeps IDs more stable than `"botsort"`, which swi This threshold only applies to `reid_fusion="botsort"`; with `"adaptive"` it has no effect. -When a track and a detection look alike, appearance can make their match cheaper. It never blocks a match that geometry already allows. `reid_appearance_threshold` sets how alike they must look, as a distance of `0.5 * (1 - cosine similarity)`: 0 means identical, and the BoT-SORT paper uses 0.25. +`reid_appearance_threshold` is how different two crops may look and still count as the same target. Appearance is compared as a distance, `0.5 * (1 - cosine similarity)`, running from 0 for identical crops to 1 for opposite ones; the BoT-SORT paper uses 0.25. Below the threshold, looking alike makes the tracker more willing to pair the two; above it, the pair stands on box overlap alone. Appearance only ever lowers a pair's cost, never raises it, but the assignment weighs every pair at once, so a look-alike pair can still take a detection away from a track that merely overlapped it. The right value depends on the encoder and the footage, so measure it on labeled data: @@ -226,7 +226,7 @@ From this we learn: `reid_proximity_threshold` decides which track-detection pairs appearance is allowed to score. A pair is dropped before appearance is consulted whenever `1 - IoU` exceeds the threshold, so the 0.5 default limits appearance to pairs that already overlap at `IoU >= 0.5`, and 0.99 still requires `IoU >= 0.01`. Only 1.0 disables the gate. -A lost track is matched through its Kalman prediction. After a short occlusion the prediction has drifted, so it barely overlaps the detection when the target reappears. After the target leaves the frame, the prediction keeps moving out of view and the overlap drops to zero. The 0.5 default blocks appearance in both cases, 0.99 lets it score the first one, and only 1.0 lets it score the second. BoT-SORT has no separate step for targets that leave and return, so opening the gate is the only way to keep their ID. +While a track is lost it has no detection of its own, so it is matched through its estimated position, which keeps moving as if the target had carried on. After a short occlusion that estimate has drifted, so it barely overlaps the detection when the target reappears. After the target leaves the frame it drifts out of view entirely and the overlap drops to zero. The 0.5 default blocks appearance in both cases, 0.99 lets it score the first one, and only 1.0 lets it score the second. BoT-SORT has no separate step for targets that leave and return, so opening the gate is the only way to keep their ID. SoccerNet test, oracle detections, `osnet_x1_0` fine-tuned on SoccerNet train, library-default motion parameters, `reid_fusion="adaptive"` with its default weights: @@ -258,37 +258,42 @@ The steps above give a good starting point. `trackers tune` then searches `reid_ BoT-SORT with and without ReID, using the same detections and motion parameters. The first table shows HOTA on the split where the ReID thresholds were tuned, the second shows test results. The better value for each dataset is in bold. Motion parameters are the tuned values from the [tracker comparison](../evaluations/results.md). - + === "Generic encoder" - Encoders that were not trained on the evaluated dataset. Each dataset uses the fusion method that scored higher on the tuning split, `reid_fusion="botsort"` unless noted. + Encoders that were not trained on the evaluated dataset. Both fusion methods were searched with 20 trials on every dataset; the test rows use whichever scored higher on the tuning split. Tuning split: - | Dataset | Config | HOTA | - | :--------- | :-------------- | :-------: | - | MOT17 | BoT-SORT | 69.05 | - | | BoT-SORT + ReID | **69.64** | - | SportsMOT | BoT-SORT | 82.00 | - | | BoT-SORT + ReID | **82.69** | - | DanceTrack | BoT-SORT | 53.89 | - | | BoT-SORT + ReID | **58.34** | - | SoccerNet¹ | BoT-SORT | 86.95 | - | | BoT-SORT + ReID | **86.98** | + | Dataset | Config | HOTA | + | :--------- | :-------------------------- | :-------: | + | MOT17 | BoT-SORT | 69.05 | + | | BoT-SORT + ReID | **69.64** | + | | BoT-SORT + ReID, `adaptive` | 69.54 | + | SportsMOT | BoT-SORT | 82.00 | + | | BoT-SORT + ReID | **82.69** | + | | BoT-SORT + ReID, `adaptive` | 81.98 | + | DanceTrack | BoT-SORT | 53.89 | + | | BoT-SORT + ReID | **58.34** | + | | BoT-SORT + ReID, `adaptive` | 57.17 | + | SoccerNet¹ | BoT-SORT | 86.95 | + | | BoT-SORT + ReID | 86.98 | + | | BoT-SORT + ReID, `adaptive` | **87.53** | Test split: - | Dataset | Config | HOTA | IDF1 | MOTA | - | :--------- | :-------------- | :-------: | :-------: | :-------: | - | MOT17 | BoT-SORT | **63.86** | 78.74 | **79.42** | - | | BoT-SORT + ReID | 63.85 | **78.82** | 79.41 | - | SportsMOT | BoT-SORT | 74.15 | 74.06 | 96.89 | - | | BoT-SORT + ReID | **75.62** | **75.66** | **96.90** | - | DanceTrack | BoT-SORT | **57.8** | **57.9** | **92.2** | - | | BoT-SORT + ReID | 57.6 | 57.6 | 92.1 | - | SoccerNet | BoT-SORT | **85.00** | **79.68** | 97.25 | - | | BoT-SORT + ReID | 84.96 | 79.66 | 97.25 | + | Dataset | Config | HOTA | IDF1 | MOTA | + | :--------- | :-------------------------- | :-------: | :-------: | :-------: | + | MOT17 | BoT-SORT | **63.86** | 78.74 | **79.42** | + | | BoT-SORT + ReID | 63.85 | **78.82** | 79.41 | + | SportsMOT | BoT-SORT | 74.15 | 74.06 | 96.89 | + | | BoT-SORT + ReID | **75.62** | **75.66** | **96.90** | + | DanceTrack | BoT-SORT | **57.8** | **57.9** | **92.2** | + | | BoT-SORT + ReID | 57.6 | 57.6 | 92.1 | + | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | + | | BoT-SORT + ReID | 84.96 | 79.66 | 97.25 | + | | BoT-SORT + ReID, `adaptive` | **85.89** | **80.40** | **97.94** | Tuned ReID configuration for each dataset. Motion parameters are the BoT-SORT values from the [tracker comparison](../evaluations/results.md). @@ -313,16 +318,19 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. SoccerNet: reid_model: fastreid_mot17_sbs50 - reid_fusion: botsort - reid_appearance_threshold: 0.0467 - reid_proximity_threshold: 0.3511 + reid_fusion: adaptive + reid_appearance_weight: 1.178 + reid_adaptive_weight_cap: 0.621 + reid_appearance_floor: 0.195 + reid_proximity_threshold: 0.975 + minimum_iou_threshold_first_assoc: 0.349 ``` For MOT17, DanceTrack and SoccerNet, both thresholds were tuned together with `trackers tune`. The SportsMOT threshold comes from a sweep at the default proximity threshold. Each configuration was then evaluated once on test. ¹ SoccerNet-tracking has no validation split, so its tuning split is train. The other datasets tune on val, MOT17 on val-half. - A generic encoder improves HOTA on every tuning split, but only SportsMOT keeps the gain on test. On the other datasets, test HOTA stays within 0.2 of BoT-SORT without ReID. On SoccerNet the tuned thresholds are strict enough that appearance rarely changes a match. + A generic encoder improves HOTA on every tuning split, but only SportsMOT and SoccerNet keep the gain on test: +1.47 and +0.89 HOTA. On MOT17 and DanceTrack test HOTA stays within 0.2 of BoT-SORT without ReID. SoccerNet needs the additive rule to get there; under `botsort` the same encoder is flat, because the gate it was tuned to keeps appearance away from the pairs that would benefit. SoccerNet uses ground-truth boxes as detections, so its numbers are not comparable to the YOLOX rows. @@ -334,21 +342,29 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. | Dataset | Config | HOTA | | :--------- | :-------------------------- | :-------: | - | MOT17 | BoT-SORT | **69.05** | - | | BoT-SORT + ReID | 69.00 | + | MOT17 | BoT-SORT | 69.05 | + | | BoT-SORT + ReID | **69.29** | + | | BoT-SORT + ReID, `adaptive` | **69.29** | + | DanceTrack | BoT-SORT | 53.89 | + | | BoT-SORT + ReID | **59.28** | + | | BoT-SORT + ReID, `adaptive` | 58.71 | | SoccerNet² | BoT-SORT | 85.72 | | | BoT-SORT + ReID | 88.97 | | | BoT-SORT + ReID, `adaptive` | **89.91** | Test split: - | Dataset | Config | HOTA | IDF1 | MOTA | - | :-------- | :-------------------------- | :-------: | :-------: | :-------: | - | MOT17 | BoT-SORT | 63.8 | 78.7 | 79.4 | - | | BoT-SORT + ReID | **64.12** | **79.16** | 79.36 | - | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | - | | BoT-SORT + ReID | 87.30 | 83.16 | 98.73 | - | | BoT-SORT + ReID, `adaptive` | **88.43** | **84.40** | **99.26** | + | Dataset | Config | HOTA | IDF1 | MOTA | + | :--------- | :-------------------------- | :-------: | :-------: | :-------: | + | MOT17 | BoT-SORT | 63.8 | 78.7 | 79.4 | + | | BoT-SORT + ReID | 64.00 | 78.89 | 79.43 | + | | BoT-SORT + ReID, `adaptive` | **64.05** | **79.21** | **79.44** | + | DanceTrack | BoT-SORT | 57.8 | 57.9 | 92.2 | + | | BoT-SORT + ReID | **60.80** | **61.60** | 91.70 | + | | BoT-SORT + ReID, `adaptive` | 58.30 | 58.20 | **92.30** | + | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | + | | BoT-SORT + ReID | 87.30 | 83.16 | 98.73 | + | | BoT-SORT + ReID, `adaptive` | **88.43** | **84.40** | **99.26** | Tuned ReID configuration for each dataset and fusion method. Motion parameters are the BoT-SORT values from the [tracker comparison](../evaluations/results.md), except `minimum_iou_threshold_first_assoc` where listed. @@ -356,8 +372,28 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. MOT17: botsort: reid_model: osnet_x1_0 fine-tuned on MOT17 train - reid_appearance_threshold: 0.25 - reid_proximity_threshold: 0.5 + reid_appearance_threshold: 0.0448 + reid_proximity_threshold: 0.2697 + adaptive: + reid_model: osnet_x1_0 fine-tuned on MOT17 train + reid_appearance_weight: 2.124 + reid_adaptive_weight_cap: 0.959 + reid_appearance_floor: 0.569 + reid_proximity_threshold: 0.342 + minimum_iou_threshold_first_assoc: 0.205 + + DanceTrack: + botsort: + reid_model: osnet_x1_0 fine-tuned on DanceTrack train + reid_appearance_threshold: 0.4716 + reid_proximity_threshold: 0.6802 + adaptive: + reid_model: osnet_x1_0 fine-tuned on DanceTrack train + reid_appearance_weight: 1.947 + reid_adaptive_weight_cap: 0.944 + reid_appearance_floor: 0.614 + reid_proximity_threshold: 0.690 + minimum_iou_threshold_first_assoc: 0.946 SoccerNet: botsort: @@ -372,12 +408,8 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. reid_appearance_floor: 0.8 ``` - Each configuration was evaluated once on test. - - !!! note "Being tuned" - - The DanceTrack rows and MOT17's `adaptive` row are not here yet: they were never tuned, only run at a few hand-picked values. A `trackers tune` search is under way for all three datasets, and they will be added with what it finds. + Every configuration was tuned with `trackers tune` on its own tuning split, 20 trials per fusion method, then evaluated once on test. ² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). The other datasets tune on val, MOT17 on val-half. - Fine-tuning makes the biggest difference on SoccerNet: the generic encoder leaves HOTA flat, while the fine-tuned one adds 2.30 on test, and 3.43 with `adaptive`. + Fine-tuning makes the biggest difference on SoccerNet and DanceTrack, the two datasets where the generic encoder does not help: on test the fine-tuned encoder adds 3.43 HOTA on SoccerNet with `adaptive` and 3.00 on DanceTrack with `botsort`. On MOT17 it adds 0.25. Which fusion method wins depends on the dataset, so tune both before choosing. From 525ec26e84c3050e8dbdb9513f961fa7eb68dd3b Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 22 Sep 2026 18:54:10 -0300 Subject: [PATCH 18/49] docs: list BoT-SORT + ReID in the tracker tables The README and docs index mirror the default-parameter tables, which now carry a BoT-SORT + ReID row, so both gain it with the same HOTA figures. A footnote says these are library defaults with the generic encoder, and where the tuned numbers land, so the row does not read as appearance never helping. Co-Authored-By: Claude Opus 5 --- README.md | 23 +++++++++++++---------- docs/index.md | 3 +++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9badd5ca..9a841545 100644 --- a/README.md +++ b/README.md @@ -85,19 +85,22 @@ For all CLI options, see the [tracking guide](https://trackers.roboflow.com/deve Each tracker below is a faithful implementation of its original paper's motion and association pipeline; appearance/ReID branches are off by default, though BoT-SORT accepts one through `reid_model` (see the [ReID guide](https://trackers.roboflow.com/latest/guides/reid/)). Each tracker's docs page states the exact scope. Pick the one that fits your scene, or run the benchmark to find out which performs best on your data. - - -| Algorithm | Description | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | DanceTrack HOTA | -| :-------------------------------------------: | :------------------------------------------------------------------------------: | :--------: | :------------: | :------------: | :-------------: | -| [SORT](https://arxiv.org/abs/1602.00763) | Kalman filter + Hungarian matching baseline. | 58.4 | 70.8 | 81.6 | 47.2 | -| [ByteTrack](https://arxiv.org/abs/2110.06864) | Two-stage association using high and low confidence detections. | 60.1 | 73.0 | 84.0 | 53.3 | -| [OC-SORT](https://arxiv.org/abs/2203.14360) | Observation-centric recovery for lost tracks. | 61.9 | 71.7 | 78.4 | 54.1 | -| [BoT-SORT](https://arxiv.org/abs/2206.14651) | Camera motion compensation. | 63.7 | 73.8 | 84.5 | 57.8 | -| [C-BIoU](https://arxiv.org/abs/2211.14317) | Cascaded buffered IoU matching for fast or irregular motion. | 63.0 | 73.1 | 82.6 | 56.7 | -| [McByte](https://arxiv.org/abs/2506.01373) | Mask-conditioned tracking — adds propagated SAM/Cutie masks as a matching cue.\* | **64.1** | **76.5** | **85.0** | **67.2** | + + +| Algorithm | Description | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | DanceTrack HOTA | +| :-------------------------------------------------------------------: | :------------------------------------------------------------------------------: | :--------: | :------------: | :------------: | :-------------: | +| [SORT](https://arxiv.org/abs/1602.00763) | Kalman filter + Hungarian matching baseline. | 58.4 | 70.8 | 81.6 | 47.2 | +| [ByteTrack](https://arxiv.org/abs/2110.06864) | Two-stage association using high and low confidence detections. | 60.1 | 73.0 | 84.0 | 53.3 | +| [OC-SORT](https://arxiv.org/abs/2203.14360) | Observation-centric recovery for lost tracks. | 61.9 | 71.7 | 78.4 | 54.1 | +| [BoT-SORT](https://arxiv.org/abs/2206.14651) | Camera motion compensation. | 63.7 | 73.8 | 84.5 | 57.8 | +| [C-BIoU](https://arxiv.org/abs/2211.14317) | Cascaded buffered IoU matching for fast or irregular motion. | 63.0 | 73.1 | 82.6 | 56.7 | +| [McByte](https://arxiv.org/abs/2506.01373) | Mask-conditioned tracking — adds propagated SAM/Cutie masks as a matching cue.\* | **64.1** | **76.5** | **85.0** | **67.2** | +| [BoT-SORT + ReID](https://trackers.roboflow.com/develop/guides/reid/) | Appearance embeddings fused with motion, for harder association.\*\* | 63.4 | 73.5 | 82.9 | 56.0 | \*McByte needs optional heavyweight deps (`torch`, SAM, Cutie) not installed by default. It tops HOTA on all four benchmarks above — see the [McByte docs](https://trackers.roboflow.com/develop/trackers/mcbyte/) for setup. +\*\*BoT-SORT + ReID is listed at library defaults with the generic `osnet_x1_0_msmt17_combineall`, where a cross-domain encoder costs a little accuracy. Tuned, and with an encoder fine-tuned on the dataset where one exists, it reaches 64.0, 75.6, 88.4 and 60.8 HOTA, above BoT-SORT on all four. See the [ReID guide](https://trackers.roboflow.com/develop/guides/reid/). + All scores use default parameters on the standard split. Detections come from a YOLOX detector (MOT17, SportsMOT, DanceTrack) or oracle ground-truth boxes (SoccerNet) — absolute numbers shift with detector quality. See the [tracker comparison](https://trackers.roboflow.com/develop/evaluations/results/) for tuned numbers and methodology. ## Evaluate diff --git a/docs/index.md b/docs/index.md index a0a9f5a9..341d86af 100644 --- a/docs/index.md +++ b/docs/index.md @@ -131,11 +131,14 @@ Clean, modular implementations of leading trackers. All HOTA scores use default | [BoT-SORT](https://arxiv.org/abs/2206.14651) | Camera motion compensation. | 63.7 | 73.8 | 84.5 | 57.8 | | [C-BIoU](https://arxiv.org/abs/2211.14317) | Cascaded buffered IoU matching for fast or irregular motion. | 63.0 | 73.1 | 82.6 | 56.7 | | [McByte](trackers/mcbyte.md) | Mask-conditioned tracking — adds propagated SAM/Cutie masks as a cue.\* | **64.1** | **76.5** | **85.0** | **67.2** | +| [BoT-SORT + ReID](guides/reid.md) | Appearance embeddings fused with motion, for harder association.\*\* | 63.4 | 73.5 | 82.9 | 56.0 | !!! note \*McByte needs optional heavyweight deps (`torch`, SAM, Cutie) not installed by default. It tops HOTA on all four benchmarks above — see the [McByte docs](trackers/mcbyte.md) for setup. + \*\*BoT-SORT + ReID is listed at library defaults with the generic `osnet_x1_0_msmt17_combineall`. Tuned, and with an encoder fine-tuned on the dataset where one exists, it reaches 64.0, 75.6, 88.4 and 60.8 HOTA, above BoT-SORT on all four. See the [ReID guide](guides/reid.md). + For detailed benchmarks and tuned configurations, see the [tracker comparison](evaluations/results.md). --- From 3c2d17cae3837afb3a2add4f2849562dfa1fcaf4 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 22 Sep 2026 18:54:43 -0300 Subject: [PATCH 19/49] fix(reid): reject NaN and infinity for the adaptive weights `< 0` lets both through, and either turns every fused score into NaN once it multiplies the appearance term. Review feedback from tstanczyk95 on #571. Co-Authored-By: Claude Opus 5 --- src/trackers/core/botsort/tracker.py | 10 ++++++---- tests/core/test_botsort_reid.py | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/trackers/core/botsort/tracker.py b/src/trackers/core/botsort/tracker.py index be4a7630..82188085 100644 --- a/src/trackers/core/botsort/tracker.py +++ b/src/trackers/core/botsort/tracker.py @@ -208,10 +208,12 @@ def __init__( self.reid_proximity_threshold = reid_proximity_threshold if reid_fusion not in get_args(ReidFusionMethod): raise ValueError(f"Unknown reid_fusion {reid_fusion!r}. Valid options are: {get_args(ReidFusionMethod)}.") - if reid_appearance_weight < 0.0: - raise ValueError(f"reid_appearance_weight must be non-negative, got {reid_appearance_weight}") - if reid_adaptive_weight_cap < 0.0: - raise ValueError(f"reid_adaptive_weight_cap must be non-negative, got {reid_adaptive_weight_cap}") + if not np.isfinite(reid_appearance_weight) or reid_appearance_weight < 0.0: + raise ValueError(f"reid_appearance_weight must be finite and non-negative, got {reid_appearance_weight}") + if not np.isfinite(reid_adaptive_weight_cap) or reid_adaptive_weight_cap < 0.0: + raise ValueError( + f"reid_adaptive_weight_cap must be finite and non-negative, got {reid_adaptive_weight_cap}" + ) if not 0.0 <= reid_appearance_floor <= 1.0: raise ValueError(f"reid_appearance_floor must be in [0, 1], got {reid_appearance_floor}") self.reid_fusion = reid_fusion diff --git a/tests/core/test_botsort_reid.py b/tests/core/test_botsort_reid.py index a20d9fd4..36c84ea8 100644 --- a/tests/core/test_botsort_reid.py +++ b/tests/core/test_botsort_reid.py @@ -454,10 +454,11 @@ def test_unknown_fusion_raises(self) -> None: BoTSORTTracker(reid_fusion="deepocsort") # type: ignore[arg-type] @pytest.mark.parametrize("parameter", ["reid_appearance_weight", "reid_adaptive_weight_cap"]) - def test_rejects_negative_adaptive_weights(self, parameter: str) -> None: + @pytest.mark.parametrize("value", [-0.1, float("nan"), float("inf")]) + def test_rejects_unusable_adaptive_weights(self, parameter: str, value: float) -> None: with pytest.raises(ValueError, match=parameter): # mypy cannot narrow a dynamic dict against typed kwargs. - BoTSORTTracker(enable_cmc=False, **{parameter: -0.1}) # type: ignore[arg-type] + BoTSORTTracker(enable_cmc=False, **{parameter: value}) # type: ignore[arg-type] def test_adaptive_parameters_reach_fusion(self, monkeypatch: pytest.MonkeyPatch) -> None: import trackers.core.botsort.tracker as tracker_module From c5e8258a3513fc679420c74f993650f0cd4d1843 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 22 Sep 2026 18:55:01 -0300 Subject: [PATCH 20/49] docs(reid): name both thresholds the fusion range affects `minimum_iou_threshold_unconfirmed_assoc` gates a fused similarity in the unconfirmed step just as `minimum_iou_threshold_first_assoc` does in the first, so switching fusion needs both retuned. The second-association threshold is unaffected, since that step matches on IoU alone. Review feedback from tstanczyk95 on #571. Co-Authored-By: Claude Opus 5 --- docs/guides/reid.md | 2 +- src/trackers/core/botsort/tracker.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index f5c1d654..cacd1c92 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -112,7 +112,7 @@ In more detail: Two consequences are worth knowing before switching: -- **The similarity range changes.** `"botsort"` returns values in `[0, 1]`; `"adaptive"` returns `[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]`, which is `[0, 2.25]` at the defaults. `minimum_iou_threshold_first_assoc` is applied to that fused value, so a threshold tuned for one method is a different gate under the other. Retune it when you switch, or the comparison measures the gate rather than the fusion. +- **The similarity range changes.** `"botsort"` returns values in `[0, 1]`; `"adaptive"` returns `[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]`, which is `[0, 2.25]` at the defaults. Two thresholds are applied to that fused value: `minimum_iou_threshold_first_assoc`, which matches detections to confirmed and lost tracks, and `minimum_iou_threshold_unconfirmed_assoc`, which matches what is left to tracks that are not confirmed yet. Both are a different gate under the other method, so retune them when you switch, or the comparison measures the gate rather than the fusion. `minimum_iou_threshold_second_assoc` is unaffected, because that step matches on IoU alone. - **The weights are domain-dependent.** The Deep OC-SORT paper uses `reid_appearance_weight=0.75` with `reid_adaptive_weight_cap=0.5` for MOT17 and MOT20, and `1.25` with `1.0` for DanceTrack, where dancers occlude constantly and geometry carries less. The defaults here follow the MOT17/MOT20 pair. With the gate open, `adaptive` keeps IDs more stable than `"botsort"`, which switches between look-alike candidates from one frame to the next. `"botsort"` stays the default because it is the safer choice: appearance is only used below `reid_appearance_threshold`, and turning it on does not require retuning the other thresholds. diff --git a/src/trackers/core/botsort/tracker.py b/src/trackers/core/botsort/tracker.py index 82188085..78e7d616 100644 --- a/src/trackers/core/botsort/tracker.py +++ b/src/trackers/core/botsort/tracker.py @@ -109,9 +109,14 @@ class BoTSORTTracker(BaseTracker): (default) takes the minimum of the geometry and appearance costs. ``"adaptive"`` adds an adaptively weighted appearance term, which weighs appearance more heavily when the best match stands clear of - the runner-up. The two produce different similarity ranges, so - ``minimum_iou_threshold_first_assoc`` needs retuning when switching - (see :func:`~trackers.core.reid.fusion.fuse_adaptive_reid_association`). + the runner-up (see + :func:`~trackers.core.reid.fusion.fuse_adaptive_reid_association`). + The two produce different similarity ranges, so the thresholds applied + to a fused similarity need retuning when switching: + ``minimum_iou_threshold_first_assoc`` and + ``minimum_iou_threshold_unconfirmed_assoc``. + ``minimum_iou_threshold_second_assoc`` is unaffected, since that step + matches on IoU alone. reid_appearance_weight: Base appearance weight for ``reid_fusion="adaptive"``. Ignored otherwise. Default ``0.75``, the value Deep OC-SORT reports for MOT17 and MOT20; they use ``1.25`` for DanceTrack. From 41f6c33718520931b837a906a914533c6132508a Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 22 Sep 2026 18:55:15 -0300 Subject: [PATCH 21/49] docs(reid): say what the adaptive fusion is not, and test the ignored threshold Spells out that this is the adaptive appearance weighting alone rather than the full Deep OC-SORT cost, with the geometric term left to whatever the caller passes. Adds a test that `reid_appearance_threshold` cannot reach the adaptive path, which the guide states and nothing checked. Review feedback from tstanczyk95 on #571. Co-Authored-By: Claude Opus 5 --- src/trackers/core/reid/fusion.py | 5 ++++ tests/core/test_botsort_reid.py | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/trackers/core/reid/fusion.py b/src/trackers/core/reid/fusion.py index e272d077..a3152b65 100644 --- a/src/trackers/core/reid/fusion.py +++ b/src/trackers/core/reid/fusion.py @@ -108,6 +108,11 @@ def fuse_adaptive_reid_association( clear of the runner-up and falls back to ``reid_appearance_weight`` when the top candidates are hard to tell apart. + This is the adaptive appearance weighting on its own, not the full Deep + OC-SORT association cost: the geometric term is whatever the caller + passes (BoT-SORT's score-fused IoU in this library, another tracker's + similarity elsewhere), and the velocity-direction term of the reference + cost is left out, since it belongs to OC-SORT's motion model. Implements equations (4)-(6) of the Deep OC-SORT paper, matching ``compute_aw_new_metric`` in the authors' ``integrated_ocsort_embedding`` tracker, the variant behind their published results. diff --git a/tests/core/test_botsort_reid.py b/tests/core/test_botsort_reid.py index 36c84ea8..6ed64b44 100644 --- a/tests/core/test_botsort_reid.py +++ b/tests/core/test_botsort_reid.py @@ -460,6 +460,57 @@ def test_rejects_unusable_adaptive_weights(self, parameter: str, value: float) - # mypy cannot narrow a dynamic dict against typed kwargs. BoTSORTTracker(enable_cmc=False, **{parameter: value}) # type: ignore[arg-type] + @pytest.mark.parametrize("threshold", [0.01, 1.0]) + def test_appearance_threshold_is_ignored_under_adaptive(self, threshold: float) -> None: + """The guide promises the threshold does nothing under adaptive fusion. + + The track is offered two detections: the nearer one looks wrong, the farther one looks + right at cosine 0.8. Appearance has to win for the farther box to be matched. Under the + BoT-SORT rule a threshold of 0.01 would reject that match (``d_app`` is 0.1), so a tracker + that let the threshold reach the adaptive path would fall back to geometry and pick the + nearer box. + """ + identity = _norm(np.array([1.0, 0.0, 0.0, 0.0])) + similar = _norm(np.array([0.8, 0.6, 0.0, 0.0])) # cosine 0.8 against identity + impostor = _norm(np.array([0.0, 1.0, 0.0, 0.0])) + + class _PhaseEncoder: + phase = 1 + + def extract_features(self, detections: sv.Detections, frame: np.ndarray) -> np.ndarray: + rows = [] + for box in detections.xyxy: + key = (round(float(box[0])), round(float(box[1]))) + if self.phase == 1: + rows.append(identity) + else: + rows.append(impostor if key == (10, 10) else similar) + return np.stack(rows) + + encoder = _PhaseEncoder() + frame = _frame(1) + tracker = BoTSORTTracker( + enable_cmc=False, + minimum_iou_threshold_first_assoc=0.01, + reid_fusion="adaptive", + reid_appearance_threshold=threshold, + reid_proximity_threshold=0.99, + reid_model=encoder, + ) + tracker.update(_detection((10.0, 10.0, 30.0, 30.0)), frame=frame) + track_id = int(tracker.tracks[0].tracker_id) + + competitors = sv.Detections( + xyxy=np.array([[10.0, 10.0, 30.0, 30.0], [14.0, 14.0, 34.0, 34.0]], dtype=np.float32), + confidence=np.array([0.9, 0.9], dtype=np.float32), + ) + encoder.phase = 2 + out = tracker.update(competitors, frame=frame) + + assert out.tracker_id is not None + matched = out.xyxy[out.tracker_id == track_id][0] + assert (float(matched[0]), float(matched[1])) == (14.0, 14.0) + def test_adaptive_parameters_reach_fusion(self, monkeypatch: pytest.MonkeyPatch) -> None: import trackers.core.botsort.tracker as tracker_module From 1525d8dc6bc185a2d003b13e7a4a634ffaba4440 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 22 Sep 2026 19:01:56 -0300 Subject: [PATCH 22/49] docs(changelog): leave the adaptive-fusion entry to the release pass The changelog is written in one pass when the release is prepared, so this PR should not carry a partial entry. It also omitted `reid_appearance_floor`, which the release entry will cover. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a24948f..bcd13079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### 🚀 Added -- **`BoTSORTTracker(reid_fusion="adaptive")`** — Deep OC-SORT's adaptive appearance weighting as an alternative to BoT-SORT's `min(d_iou, d_app)` fusion: adds a weighted appearance term whose weight grows when the best match stands clear of the runner-up. New `reid_appearance_weight` and `reid_adaptive_weight_cap` parameters; default fusion is unchanged. `reid_appearance_threshold` has no effect under adaptive fusion. `fuse_adaptive_reid_association` lives in `trackers.core.reid.fusion` beside the BoT-SORT rule so other trackers can reuse it ([#571](https://github.com/roboflow/trackers/pull/571)). - **`trackers benchmark mcbyte` subcommand** — runs McByte over complete MOT17, DanceTrack, SportsMOT, or SoccerNet-tracking benchmark test sets and writes MOTChallenge-format results, with flags for dataset selection (`--dataset`), per-dataset detection/image roots (`--dataset_roots`), device, output location, CMC method/downscale, isolated mask matching, skip-existing, and partial-result retention ([#541](https://github.com/roboflow/trackers/pull/541), [#543](https://github.com/roboflow/trackers/pull/543)). - **`trackers inspect` command group** — visual validation commands for the mask stack and the tracker that uses it: `inspect sam` (box-prompted mask generation), `inspect cutie` (mask propagation), `inspect mask-manager` (mask lifecycle over a frame range, driven either from command-line boxes or from a MOT ground-truth file), and `inspect mcbyte` (locked-IoU baseline against full mask-conditioned McByte). Each writes annotated per-frame images into a timestamped run directory ([#543](https://github.com/roboflow/trackers/pull/543)). From 5409f098085a14e16a3b5797f59ce3b5e853d653 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 22 Sep 2026 19:03:43 -0300 Subject: [PATCH 23/49] docs(evaluations): scope the version note to the rows it covers The page said every result came from v2.6.0, but that release has no appearance association, so the BoT-SORT + ReID rows could not have. Names the exception and says the ReID thresholds came from `trackers tune`. Review feedback from tstanczyk95 on #571. Co-Authored-By: Claude Opus 5 --- docs/evaluations/results.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index 42a33399..6ae3ea6b 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -9,7 +9,7 @@ This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, !!! info "Benchmark version" - Results use **trackers v2.6.0** (released 2026-08-03). Detections are from YOLOX (MOT17, SportsMOT, DanceTrack) or ground-truth oracle boxes (SoccerNet). Parameters were tuned via grid search on held-out splits. See [Methodology](methodology.md) for details. + Results use **trackers v2.6.0** (released 2026-08-03), except the BoT-SORT + ReID rows, which were produced in v2.7.0, the release that adds appearance association. Detections are from YOLOX (MOT17, SportsMOT, DanceTrack) or ground-truth oracle boxes (SoccerNet). Parameters were tuned via grid search on held-out splits, and the ReID thresholds with [`trackers tune`](../guides/tune.md#tune-reid-thresholds). See [Methodology](methodology.md) for details. v2.6.0 changed all trackers' lost-track lifecycle from exclusive (`time_since_update < maximum_frames_without_update`) to inclusive (`<=`) boundary semantics, so confirmed tracks now survive one additional missed frame. Expect small IDSW/HOTA shifts versus earlier versions even for trackers whose parameters didn't change — see the [Changelog](../changelog.md) for details. From 0444e761bd622045d0a42a744a2a25225237f7e5 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 22 Sep 2026 19:43:26 -0300 Subject: [PATCH 24/49] docs(reid): both fusion methods on test for every generic row The three remaining adaptive picks were scored once on test: MOT17 64.58, SportsMOT 74.06, DanceTrack 57.5. Bold marks the row the tuning split chose, so MOT17 keeps botsort even though adaptive scores higher on test; that disagreement is stated rather than resolved by picking after the fact. Co-Authored-By: Claude Opus 5 --- docs/guides/reid.md | 75 +++++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index cacd1c92..4ddecfed 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -285,12 +285,15 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. | Dataset | Config | HOTA | IDF1 | MOTA | | :--------- | :-------------------------- | :-------: | :-------: | :-------: | - | MOT17 | BoT-SORT | **63.86** | 78.74 | **79.42** | - | | BoT-SORT + ReID | 63.85 | **78.82** | 79.41 | + | MOT17 | BoT-SORT | 63.86 | 78.74 | 79.42 | + | | BoT-SORT + ReID | 63.85 | 78.82 | 79.41 | + | | BoT-SORT + ReID, `adaptive` | **64.58** | **79.94** | 79.41 | | SportsMOT | BoT-SORT | 74.15 | 74.06 | 96.89 | | | BoT-SORT + ReID | **75.62** | **75.66** | **96.90** | - | DanceTrack | BoT-SORT | **57.8** | **57.9** | **92.2** | + | | BoT-SORT + ReID, `adaptive` | 74.06 | 73.74 | 96.87 | + | DanceTrack | BoT-SORT | **57.8** | **57.9** | 92.2 | | | BoT-SORT + ReID | 57.6 | 57.6 | 92.1 | + | | BoT-SORT + ReID, `adaptive` | 57.5 | 57.3 | **92.3** | | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | | | BoT-SORT + ReID | 84.96 | 79.66 | 97.25 | | | BoT-SORT + ReID, `adaptive` | **85.89** | **80.40** | **97.94** | @@ -299,38 +302,64 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. ```yaml MOT17: - reid_model: fastreid_mot17_sbs50 - reid_fusion: botsort - reid_appearance_threshold: 0.10 - reid_proximity_threshold: 0.5 + botsort: + reid_model: fastreid_mot17_sbs50 + reid_appearance_threshold: 0.10 + reid_proximity_threshold: 0.5 + adaptive: + reid_model: fastreid_mot17_sbs50 + reid_appearance_weight: 1.908 + reid_adaptive_weight_cap: 0.545 + reid_appearance_floor: 0.381 + reid_proximity_threshold: 0.772 + minimum_iou_threshold_first_assoc: 0.846 SportsMOT: - reid_model: fastreid_mot17_sbs50 - reid_fusion: botsort - reid_appearance_threshold: 0.30 - reid_proximity_threshold: 0.5 + botsort: + reid_model: fastreid_mot17_sbs50 + reid_appearance_threshold: 0.30 + reid_proximity_threshold: 0.5 + adaptive: + reid_model: fastreid_mot17_sbs50 + reid_appearance_weight: 2.540 + reid_adaptive_weight_cap: 0.778 + reid_appearance_floor: 0.783 + reid_proximity_threshold: 0.216 + minimum_iou_threshold_first_assoc: 0.176 DanceTrack: - reid_model: fastreid_mot17_sbs50 - reid_fusion: botsort - reid_appearance_threshold: 0.4822 - reid_proximity_threshold: 0.5068 + botsort: + reid_model: fastreid_mot17_sbs50 + reid_appearance_threshold: 0.4822 + reid_proximity_threshold: 0.5068 + adaptive: + reid_model: fastreid_mot17_sbs50 + reid_appearance_weight: 0.75 + reid_adaptive_weight_cap: 0.5 + reid_appearance_floor: 0.0 + reid_proximity_threshold: 0.5 SoccerNet: - reid_model: fastreid_mot17_sbs50 - reid_fusion: adaptive - reid_appearance_weight: 1.178 - reid_adaptive_weight_cap: 0.621 - reid_appearance_floor: 0.195 - reid_proximity_threshold: 0.975 - minimum_iou_threshold_first_assoc: 0.349 + botsort: + reid_model: fastreid_mot17_sbs50 + reid_appearance_threshold: 0.0467 + reid_proximity_threshold: 0.3511 + adaptive: + reid_model: fastreid_mot17_sbs50 + reid_appearance_weight: 1.178 + reid_adaptive_weight_cap: 0.621 + reid_appearance_floor: 0.195 + reid_proximity_threshold: 0.975 + minimum_iou_threshold_first_assoc: 0.349 ``` For MOT17, DanceTrack and SoccerNet, both thresholds were tuned together with `trackers tune`. The SportsMOT threshold comes from a sweep at the default proximity threshold. Each configuration was then evaluated once on test. ¹ SoccerNet-tracking has no validation split, so its tuning split is train. The other datasets tune on val, MOT17 on val-half. - A generic encoder improves HOTA on every tuning split, but only SportsMOT and SoccerNet keep the gain on test: +1.47 and +0.89 HOTA. On MOT17 and DanceTrack test HOTA stays within 0.2 of BoT-SORT without ReID. SoccerNet needs the additive rule to get there; under `botsort` the same encoder is flat, because the gate it was tuned to keeps appearance away from the pairs that would benefit. + Both fusion methods are listed, and the row in bold is the one the tuning split chose. A generic encoder improves HOTA on every tuning split, but only SportsMOT and SoccerNet keep the gain with the chosen method on test: +1.47 and +0.89 HOTA. SoccerNet needs the additive rule to get there; under `botsort` the same encoder is flat, because the gate it was tuned to keeps appearance away from the pairs that would benefit. + + MOT17 is the case where the split chose wrong. `adaptive` scored 0.10 lower on val-half and 0.73 higher on test, where it beats every other MOT17 row on this page. Both numbers are shown rather than switching the choice after seeing test, which would make the split meaningless. SoccerNet uses ground-truth boxes as detections, so its numbers are not comparable to the YOLOX rows. From 8f1138a4dbb6d8b3c6ef0be7389e22dc3207102c Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 22 Sep 2026 21:47:42 -0300 Subject: [PATCH 25/49] docs(reid): the SportsMOT fine-tuned row, and what a seed is worth An `osnet_x1_0` fine-tuned on SportsMOT train completes the fine-tuned tab: 77.82 HOTA on test against 74.15 without ReID, the largest gain of the four. Both fusion methods were searched with 20 trials on val and both are on test; `botsort` leads on each. Also says what these rows cannot resolve: an encoder is one training run, and two seeds of the same recipe differ by 0.22 HOTA here and by up to 1.54 on DanceTrack. Co-Authored-By: Claude Opus 5 --- docs/guides/reid.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 4ddecfed..297924d3 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -374,6 +374,9 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. | MOT17 | BoT-SORT | 69.05 | | | BoT-SORT + ReID | **69.29** | | | BoT-SORT + ReID, `adaptive` | **69.29** | + | SportsMOT | BoT-SORT | 82.00 | + | | BoT-SORT + ReID | **84.39** | + | | BoT-SORT + ReID, `adaptive` | 83.15 | | DanceTrack | BoT-SORT | 53.89 | | | BoT-SORT + ReID | **59.28** | | | BoT-SORT + ReID, `adaptive` | 58.71 | @@ -388,6 +391,9 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. | MOT17 | BoT-SORT | 63.8 | 78.7 | 79.4 | | | BoT-SORT + ReID | 64.00 | 78.89 | 79.43 | | | BoT-SORT + ReID, `adaptive` | **64.05** | **79.21** | **79.44** | + | SportsMOT | BoT-SORT | 74.15 | 74.06 | 96.89 | + | | BoT-SORT + ReID | **77.82** | **78.50** | 96.92 | + | | BoT-SORT + ReID, `adaptive` | 76.36 | 76.76 | **96.96** | | DanceTrack | BoT-SORT | 57.8 | 57.9 | 92.2 | | | BoT-SORT + ReID | **60.80** | **61.60** | 91.70 | | | BoT-SORT + ReID, `adaptive` | 58.30 | 58.20 | **92.30** | @@ -411,6 +417,19 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. reid_proximity_threshold: 0.342 minimum_iou_threshold_first_assoc: 0.205 + SportsMOT: + botsort: + reid_model: osnet_x1_0 fine-tuned on SportsMOT train + reid_appearance_threshold: 0.3425 + reid_proximity_threshold: 0.9953 + adaptive: + reid_model: osnet_x1_0 fine-tuned on SportsMOT train + reid_appearance_weight: 1.290 + reid_adaptive_weight_cap: 0.718 + reid_appearance_floor: 0.510 + reid_proximity_threshold: 0.983 + minimum_iou_threshold_first_assoc: 0.329 + DanceTrack: botsort: reid_model: osnet_x1_0 fine-tuned on DanceTrack train @@ -441,4 +460,6 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. ² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). The other datasets tune on val, MOT17 on val-half. - Fine-tuning makes the biggest difference on SoccerNet and DanceTrack, the two datasets where the generic encoder does not help: on test the fine-tuned encoder adds 3.43 HOTA on SoccerNet with `adaptive` and 3.00 on DanceTrack with `botsort`. On MOT17 it adds 0.25. Which fusion method wins depends on the dataset, so tune both before choosing. + Fine-tuning pays on every dataset here, and most where the generic encoder does not help: on test it adds 3.67 HOTA on SportsMOT, 3.43 on SoccerNet with `adaptive`, 3.00 on DanceTrack, and 0.20 on MOT17, where the generic encoder is already in domain. + + Each encoder is one training run, and training is not deterministic across seeds: two SportsMOT encoders trained identically apart from the seed scored 83.50 and 83.72 on val, and on DanceTrack the spread across three seeds reached 1.54 HOTA. Treat differences below a point between fine-tuned rows as noise. The SportsMOT encoder is the better of its two seeds, chosen on val; `crops_per_identity=8` is carried over from DanceTrack rather than tuned per dataset. From 31182681164e6ba59695776f0ef8c17fa44fa3cb Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Wed, 23 Sep 2026 13:27:54 -0300 Subject: [PATCH 26/49] docs(reid): the fine-tuned SportsMOT row everywhere, and the tuner's generic pick The fine-tuned SportsMOT encoder now reaches results.md, README and the landing page: 77.8 HOTA on test with theta 0.3425 and gate 0.9953, so every Tuned ReID row uses an encoder fine-tuned on its own dataset. The generic SportsMOT row moves from the earlier hand sweep (theta 0.30, gate 0.5, 75.62 on test) to the 20-trial study that every other generic row comes from (theta 0.4761, gate 0.5051): 82.90 on val, 75.46 on test. Keeping the sweep because it scores higher on test would be selecting on test. The fine-tuning summary now states the baseline it measures against and drops a causal claim the tables do not support. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/evaluations/results.md | 12 ++++++------ docs/guides/reid.md | 12 ++++++------ docs/index.md | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9a841545..d05c1379 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Each tracker below is a faithful implementation of its original paper's motion a \*McByte needs optional heavyweight deps (`torch`, SAM, Cutie) not installed by default. It tops HOTA on all four benchmarks above — see the [McByte docs](https://trackers.roboflow.com/develop/trackers/mcbyte/) for setup. -\*\*BoT-SORT + ReID is listed at library defaults with the generic `osnet_x1_0_msmt17_combineall`, where a cross-domain encoder costs a little accuracy. Tuned, and with an encoder fine-tuned on the dataset where one exists, it reaches 64.0, 75.6, 88.4 and 60.8 HOTA, above BoT-SORT on all four. See the [ReID guide](https://trackers.roboflow.com/develop/guides/reid/). +\*\*BoT-SORT + ReID is listed at library defaults with the generic `osnet_x1_0_msmt17_combineall`, where a cross-domain encoder costs a little accuracy. Tuned, and with an encoder fine-tuned on each dataset, it reaches 64.0, 77.8, 88.4 and 60.8 HOTA, above BoT-SORT on all four. See the [ReID guide](https://trackers.roboflow.com/develop/guides/reid/). All scores use default parameters on the standard split. Detections come from a YOLOX detector (MOT17, SportsMOT, DanceTrack) or oracle ground-truth boxes (SoccerNet) — absolute numbers shift with detector quality. See the [tracker comparison](https://trackers.roboflow.com/develop/evaluations/results/) for tuned numbers and methodology. diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index 6ae3ea6b..375332bf 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -5,7 +5,7 @@ description: Side-by-side MOT benchmark comparison of SORT, ByteTrack, OC-SORT, # Tracker Comparison -This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. McByte is reported at default parameters only (mask-conditioned association enabled); it is designed to require no per-dataset tuning, so there is no Tuned row. BoT-SORT + ReID is shown at library defaults in the Default tabs, with the catalog-default encoder throughout; its Tuned rows use an encoder fine-tuned on the dataset's own train split for MOT17, SoccerNet and DanceTrack, and the generic `fastreid_mot17_sbs50` for SportsMOT, which has no fine-tuned encoder yet. +This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. McByte is reported at default parameters only (mask-conditioned association enabled); it is designed to require no per-dataset tuning, so there is no Tuned row. BoT-SORT + ReID is shown at library defaults in the Default tabs, with the catalog-default encoder throughout; its Tuned rows use an encoder fine-tuned on the dataset's own train split. !!! info "Benchmark version" @@ -166,9 +166,9 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar | OC-SORT | 74.0 | 75.4 | 95.6 | | BoT-SORT | 74.1 | 74.0 | **96.9** | | C-BIoU | 73.1 | 72.6 | 96.7 | - | BoT-SORT + ReID | **75.6** | **75.7** | **96.9** | + | BoT-SORT + ReID | **77.8** | **78.5** | **96.9** | - Tuned configuration for each tracker. The ReID row uses the generic `fastreid_mot17_sbs50`, not a fine-tune. + Tuned configuration for each tracker. The ReID row uses an `osnet_x1_0` fine-tuned on SportsMOT train. ```yaml SORT: @@ -215,10 +215,10 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar buffer_ratio_second: 0.5 BoT-SORT + ReID: - reid_model: fastreid_mot17_sbs50 + reid_model: osnet_x1_0 fine-tuned on SportsMOT train reid_fusion: botsort - reid_appearance_threshold: 0.30 - reid_proximity_threshold: 0.5 + reid_appearance_threshold: 0.3425 + reid_proximity_threshold: 0.9953 ``` ## [SoccerNet-tracking](https://arxiv.org/abs/2204.06918) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 297924d3..a0e9c12b 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -272,7 +272,7 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. | | BoT-SORT + ReID | **69.64** | | | BoT-SORT + ReID, `adaptive` | 69.54 | | SportsMOT | BoT-SORT | 82.00 | - | | BoT-SORT + ReID | **82.69** | + | | BoT-SORT + ReID | **82.90** | | | BoT-SORT + ReID, `adaptive` | 81.98 | | DanceTrack | BoT-SORT | 53.89 | | | BoT-SORT + ReID | **58.34** | @@ -289,7 +289,7 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. | | BoT-SORT + ReID | 63.85 | 78.82 | 79.41 | | | BoT-SORT + ReID, `adaptive` | **64.58** | **79.94** | 79.41 | | SportsMOT | BoT-SORT | 74.15 | 74.06 | 96.89 | - | | BoT-SORT + ReID | **75.62** | **75.66** | **96.90** | + | | BoT-SORT + ReID | **75.46** | **75.48** | **96.90** | | | BoT-SORT + ReID, `adaptive` | 74.06 | 73.74 | 96.87 | | DanceTrack | BoT-SORT | **57.8** | **57.9** | 92.2 | | | BoT-SORT + ReID | 57.6 | 57.6 | 92.1 | @@ -317,8 +317,8 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. SportsMOT: botsort: reid_model: fastreid_mot17_sbs50 - reid_appearance_threshold: 0.30 - reid_proximity_threshold: 0.5 + reid_appearance_threshold: 0.4761 + reid_proximity_threshold: 0.5051 adaptive: reid_model: fastreid_mot17_sbs50 reid_appearance_weight: 2.540 @@ -357,7 +357,7 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. ¹ SoccerNet-tracking has no validation split, so its tuning split is train. The other datasets tune on val, MOT17 on val-half. - Both fusion methods are listed, and the row in bold is the one the tuning split chose. A generic encoder improves HOTA on every tuning split, but only SportsMOT and SoccerNet keep the gain with the chosen method on test: +1.47 and +0.89 HOTA. SoccerNet needs the additive rule to get there; under `botsort` the same encoder is flat, because the gate it was tuned to keeps appearance away from the pairs that would benefit. + Both fusion methods are listed, and the row in bold is the one the tuning split chose. A generic encoder improves HOTA on every tuning split, but only SportsMOT and SoccerNet keep the gain with the chosen method on test: +1.31 and +0.89 HOTA. SoccerNet needs the additive rule to get there; under `botsort` the same encoder is flat, because the gate it was tuned to keeps appearance away from the pairs that would benefit. MOT17 is the case where the split chose wrong. `adaptive` scored 0.10 lower on val-half and 0.73 higher on test, where it beats every other MOT17 row on this page. Both numbers are shown rather than switching the choice after seeing test, which would make the split meaningless. @@ -460,6 +460,6 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. ² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). The other datasets tune on val, MOT17 on val-half. - Fine-tuning pays on every dataset here, and most where the generic encoder does not help: on test it adds 3.67 HOTA on SportsMOT, 3.43 on SoccerNet with `adaptive`, 3.00 on DanceTrack, and 0.20 on MOT17, where the generic encoder is already in domain. + Fine-tuning improves on BoT-SORT without ReID on every dataset: on test it adds 3.67 HOTA on SportsMOT, 3.43 on SoccerNet with `adaptive`, 3.00 on DanceTrack and 0.20 on MOT17. Each encoder is one training run, and training is not deterministic across seeds: two SportsMOT encoders trained identically apart from the seed scored 83.50 and 83.72 on val, and on DanceTrack the spread across three seeds reached 1.54 HOTA. Treat differences below a point between fine-tuned rows as noise. The SportsMOT encoder is the better of its two seeds, chosen on val; `crops_per_identity=8` is carried over from DanceTrack rather than tuned per dataset. diff --git a/docs/index.md b/docs/index.md index 341d86af..ec2a6bab 100644 --- a/docs/index.md +++ b/docs/index.md @@ -137,7 +137,7 @@ Clean, modular implementations of leading trackers. All HOTA scores use default \*McByte needs optional heavyweight deps (`torch`, SAM, Cutie) not installed by default. It tops HOTA on all four benchmarks above — see the [McByte docs](trackers/mcbyte.md) for setup. - \*\*BoT-SORT + ReID is listed at library defaults with the generic `osnet_x1_0_msmt17_combineall`. Tuned, and with an encoder fine-tuned on the dataset where one exists, it reaches 64.0, 75.6, 88.4 and 60.8 HOTA, above BoT-SORT on all four. See the [ReID guide](guides/reid.md). + \*\*BoT-SORT + ReID is listed at library defaults with the generic `osnet_x1_0_msmt17_combineall`. Tuned, and with an encoder fine-tuned on each dataset, it reaches 64.0, 77.8, 88.4 and 60.8 HOTA, above BoT-SORT on all four. See the [ReID guide](guides/reid.md). For detailed benchmarks and tuned configurations, see the [tracker comparison](evaluations/results.md). From 50c041bad5bb7b0469839c04f756305605ad65ce Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 24 Sep 2026 11:12:19 -0300 Subject: [PATCH 27/49] docs(evaluations): say what appearance can and cannot recover The BoT-SORT + ReID entry in "When to Use Each Tracker" promised identity through long occlusions and targets that leave the frame. At defaults a lost track is kept for lost_track_buffer frames (about one second) and appearance only scores pairs the proximity gate admits, so neither holds. The paragraph now states both limits and points to the guide's proximity-threshold section for the open-gate case. Co-Authored-By: Claude Opus 5.5 --- docs/evaluations/results.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index 375332bf..48d307d8 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -448,7 +448,7 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art **C-BIoU** targets fast or irregular motion when you want buffered, cascaded geometric matching without camera motion compensation. In these benchmarks it leads on SoccerNet among the motion-only trackers when tuned, and reaches the highest tuned IDF1 and MOTA on DanceTrack among the motion-only trackers. Use C-BIoU when BoT-SORT-style association is a good fit but CMC is unavailable or harmful, or when plain IoU matching is too strict. See [C-BIoU](../trackers/cbiou.md) for buffer scales **b1** and **b2**. -**BoT-SORT + ReID** adds an appearance encoder to BoT-SORT's association, use it with parameter `reid_model=`. Appearance carries identity through the moments geometry cannot, like a long occlusion, or a target that leaves the frame and returns where no motion model would predict it. How much it adds depends on how well the encoder separates your targets, so the gain is largest when subjects are visually distinct and the encoder has seen footage like yours, and smallest when they share a uniform appearance. Consider fine-tuning or calibrating an encoder for your domain rather than relying on a generic one, and calibrate the appearance threshold with it. Use it when you want to keep tracks when objects leave and re-enter the frame, or when there are long occlusions. Learn how to use it at [ReID guide](../guides/reid.md). +**BoT-SORT + ReID** adds an appearance encoder to BoT-SORT's association; enable it with `reid_model=`. Appearance helps where overlap alone cannot tell candidates apart. It can only relink a track that is still kept as lost, for `lost_track_buffer` frames (30 by default, about one second), and at the default `reid_proximity_threshold` it only scores pairs that already overlap. Recovering a target that leaves the frame takes a fully open gate and a buffer long enough to cover the absence, see [Choosing a proximity threshold](../guides/reid.md#choosing-a-proximity-threshold). The gain depends on how well the encoder separates your targets: largest with an encoder trained on footage like yours, smallest when targets look alike. Tune the ReID thresholds on your data with [`trackers tune`](../guides/tune.md#tune-reid-thresholds). **McByte** is the choice when you want the highest identity stability and can afford optional SAM/Cutie mask dependencies. It extends BoT-SORT-style association with temporally propagated segmentation masks as an extra matching cue, requiring no per-video tuning. Reported against Trackers' BoT-SORT baseline (without re-identification) at default parameters, McByte improves HOTA and IDF1 on all four datasets in this comparison, with the largest gain on DanceTrack. Use McByte when identity consistency matters more than raw throughput or dependency footprint. See [McByte](../trackers/mcbyte.md) for setup and the mask-conditioning parameters. From 602e52c49d116a3f9c46dd0c74c5b7b7d3961ede Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 24 Sep 2026 11:12:52 -0300 Subject: [PATCH 28/49] docs(reid): fastreid_mot17_sbs50 as the default encoder in the docs The Default tabs, README and landing page listed BoT-SORT + ReID with osnet_x1_0_msmt17_combineall, which lowered HOTA on all four datasets. fastreid_mot17_sbs50, BoT-SORT's own ReID model, scored at the same library defaults on test: MOT17 63.9 (929268), SportsMOT 75.5 (942370), SoccerNet 84.3 (local, 49/49), DanceTrack 57.0 (942368), ahead of OSNet on all four and of BoT-SORT without ReID on MOT17 and SportsMOT. McByte still leads HOTA in every Default table. OSNet MSMT17 stays in the guide only as the out-of-domain example on SoccerNet, now named as such; the two threshold examples get specific headings, and a stale reference to a removed section is gone. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- docs/evaluations/results.md | 18 +++++++++--------- docs/guides/reid.md | 12 ++++++------ docs/index.md | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d05c1379..38c369ef 100644 --- a/README.md +++ b/README.md @@ -95,11 +95,11 @@ Each tracker below is a faithful implementation of its original paper's motion a | [BoT-SORT](https://arxiv.org/abs/2206.14651) | Camera motion compensation. | 63.7 | 73.8 | 84.5 | 57.8 | | [C-BIoU](https://arxiv.org/abs/2211.14317) | Cascaded buffered IoU matching for fast or irregular motion. | 63.0 | 73.1 | 82.6 | 56.7 | | [McByte](https://arxiv.org/abs/2506.01373) | Mask-conditioned tracking — adds propagated SAM/Cutie masks as a matching cue.\* | **64.1** | **76.5** | **85.0** | **67.2** | -| [BoT-SORT + ReID](https://trackers.roboflow.com/develop/guides/reid/) | Appearance embeddings fused with motion, for harder association.\*\* | 63.4 | 73.5 | 82.9 | 56.0 | +| [BoT-SORT + ReID](https://trackers.roboflow.com/develop/guides/reid/) | Appearance embeddings fused with motion, for harder association.\*\* | 63.9 | 75.5 | 84.3 | 57.0 | \*McByte needs optional heavyweight deps (`torch`, SAM, Cutie) not installed by default. It tops HOTA on all four benchmarks above — see the [McByte docs](https://trackers.roboflow.com/develop/trackers/mcbyte/) for setup. -\*\*BoT-SORT + ReID is listed at library defaults with the generic `osnet_x1_0_msmt17_combineall`, where a cross-domain encoder costs a little accuracy. Tuned, and with an encoder fine-tuned on each dataset, it reaches 64.0, 77.8, 88.4 and 60.8 HOTA, above BoT-SORT on all four. See the [ReID guide](https://trackers.roboflow.com/develop/guides/reid/). +\*\*BoT-SORT + ReID is listed at library defaults with the default encoder `fastreid_mot17_sbs50`, BoT-SORT's own ReID model trained on MOT17. Tuned, and with an encoder fine-tuned on each dataset, it reaches 64.0, 77.8, 88.4 and 60.8 HOTA, above BoT-SORT on all four. See the [ReID guide](https://trackers.roboflow.com/develop/guides/reid/). All scores use default parameters on the standard split. Detections come from a YOLOX detector (MOT17, SportsMOT, DanceTrack) or oracle ground-truth boxes (SoccerNet) — absolute numbers shift with detector quality. See the [tracker comparison](https://trackers.roboflow.com/develop/evaluations/results/) for tuned numbers and methodology. diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index 48d307d8..1feb02e9 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -5,7 +5,7 @@ description: Side-by-side MOT benchmark comparison of SORT, ByteTrack, OC-SORT, # Tracker Comparison -This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. McByte is reported at default parameters only (mask-conditioned association enabled); it is designed to require no per-dataset tuning, so there is no Tuned row. BoT-SORT + ReID is shown at library defaults in the Default tabs, with the catalog-default encoder throughout; its Tuned rows use an encoder fine-tuned on the dataset's own train split. +This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. McByte is reported at default parameters only (mask-conditioned association enabled); it is designed to require no per-dataset tuning, so there is no Tuned row. BoT-SORT + ReID is shown at library defaults in the Default tabs, with the default encoder `fastreid_mot17_sbs50` throughout; its Tuned rows use an encoder fine-tuned on the dataset's own train split. !!! info "Benchmark version" @@ -32,7 +32,7 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests === "Default" - Results using default tracker parameters. The ReID row is BoT-SORT at library defaults with the catalog-default `osnet_x1_0_msmt17_combineall` encoder. + Results using default tracker parameters. The ReID row is BoT-SORT at library defaults with the default `fastreid_mot17_sbs50` encoder, BoT-SORT's own ReID model trained on MOT17 pedestrian crops. | Tracker | HOTA | IDF1 | MOTA | | :-------------: | :------: | :------: | :------: | @@ -42,7 +42,7 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests | BoT-SORT | 63.7 | 78.7 | **79.2** | | C-BIoU | 63.0 | 79.1 | 77.4 | | McByte | **64.1** | **79.7** | 79.1 | - | BoT-SORT + ReID | 63.4 | 78.7 | 79.1 | + | BoT-SORT + ReID | 63.9 | 78.9 | **79.2** | + === "Generic encoder" - Encoders that were not trained on the evaluated dataset. Both fusion methods were searched with 20 trials on every dataset; the test rows use whichever scored higher on the tuning split. - - Tuning split: - - | Dataset | Config | HOTA | - | :--------- | :-------------------------- | :-------: | - | MOT17 | BoT-SORT | 69.05 | - | | BoT-SORT + ReID | **69.64** | - | | BoT-SORT + ReID, `adaptive` | 69.54 | - | SportsMOT | BoT-SORT | 82.00 | - | | BoT-SORT + ReID | **82.90** | - | | BoT-SORT + ReID, `adaptive` | 81.98 | - | DanceTrack | BoT-SORT | 53.89 | - | | BoT-SORT + ReID | **58.34** | - | | BoT-SORT + ReID, `adaptive` | 57.17 | - | SoccerNet¹ | BoT-SORT | 86.95 | - | | BoT-SORT + ReID | 86.98 | - | | BoT-SORT + ReID, `adaptive` | **87.53** | - - Test split: - - | Dataset | Config | HOTA | IDF1 | MOTA | FPS | - | :--------- | :-------------------------- | :-------: | :-------: | :-------: | ----: | - | MOT17 | BoT-SORT | 63.86 | 78.74 | 79.42 | 66.7 | - | | BoT-SORT + ReID | 63.85 | 78.82 | 79.41 | 13.5 | - | | BoT-SORT + ReID, `adaptive` | **64.58** | **79.94** | 79.41 | 13.7 | - | SportsMOT | BoT-SORT | 74.15 | 74.06 | 96.89 | | - | | BoT-SORT + ReID | **75.46** | **75.48** | **96.90** | | - | | BoT-SORT + ReID, `adaptive` | 74.06 | 73.74 | 96.87 | | - | DanceTrack | BoT-SORT | **57.8** | **57.9** | 92.2 | 73.1 | - | | BoT-SORT + ReID | 57.6 | 57.6 | 92.1 | 22.5 | - | | BoT-SORT + ReID, `adaptive` | 57.5 | 57.3 | **92.3** | 22.5 | - | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | | - | | BoT-SORT + ReID | 84.96 | 79.66 | 97.25 | | - | | BoT-SORT + ReID, `adaptive` | **85.89** | **80.40** | **97.94** | | + Encoders that were not trained on the evaluated dataset. Both fusion methods were tuned with `trackers tune`, 20 trials each, on each dataset's tuning split¹. + + **MOT17** + + | Config | HOTA | IDF1 | MOTA | FPS | + | :-------------------------- | :-------: | :-------: | :---: | ---: | + | BoT-SORT | 63.86 | 78.74 | 79.42 | 66.7 | + | BoT-SORT + ReID | 63.85 | 78.82 | 79.41 | 13.5 | + | BoT-SORT + ReID, `adaptive` | **64.58** | **79.94** | 79.41 | 13.7 | + + **SportsMOT** + + | Config | HOTA | IDF1 | MOTA | FPS | + | :-------------------------- | :-------: | :-------: | :-------: | --: | + | BoT-SORT | 74.15 | 74.06 | 96.89 | | + | BoT-SORT + ReID | **75.46** | **75.48** | **96.90** | | + | BoT-SORT + ReID, `adaptive` | 74.06 | 73.74 | 96.87 | | + + **DanceTrack** + + | Config | HOTA | IDF1 | MOTA | FPS | + | :-------------------------- | :------: | :------: | :------: | ---: | + | BoT-SORT | **57.8** | **57.9** | 92.2 | 73.1 | + | BoT-SORT + ReID | 57.6 | 57.6 | 92.1 | 22.5 | + | BoT-SORT + ReID, `adaptive` | 57.5 | 57.3 | **92.3** | 22.5 | + + **SoccerNet** + + | Config | HOTA | IDF1 | MOTA | FPS | + | :-------------------------- | :-------: | :-------: | :-------: | --: | + | BoT-SORT | 85.00 | 79.68 | 97.25 | | + | BoT-SORT + ReID | 84.96 | 79.66 | 97.25 | | + | BoT-SORT + ReID, `adaptive` | **85.89** | **80.40** | **97.94** | | FPS is frames per second of `tracker.update` on a Google Colab L4 runtime, at default parameters, over every frame of the MOT17 val-half sequences and 23 of the 25 DanceTrack val sequences. Loading the encoder and decoding frames are not included. ReID cost grows with the number of detections: on MOT17-04, with 53 per frame, BoT-SORT + ReID runs at 6.1 FPS. SportsMOT and SoccerNet are not measured yet. @@ -355,13 +353,11 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. minimum_iou_threshold_first_assoc: 0.349 ``` - For MOT17, DanceTrack and SoccerNet, both thresholds were tuned together with `trackers tune`. The SportsMOT threshold comes from a sweep at the default proximity threshold. Each configuration was then evaluated once on test. - ¹ SoccerNet-tracking has no validation split, so its tuning split is train. The other datasets tune on val, MOT17 on val-half. - Both fusion methods are listed, and the row in bold is the one the tuning split chose. A generic encoder improves HOTA on every tuning split, but only SportsMOT and SoccerNet keep the gain with the chosen method on test: +1.31 and +0.89 HOTA. SoccerNet needs the additive rule to get there; under `botsort` the same encoder is flat, because the gate it was tuned to keeps appearance away from the pairs that would benefit. + On test, a generic encoder adds HOTA on MOT17 with `adaptive` (+0.72), on SportsMOT with `botsort` (+1.31) and on SoccerNet with `adaptive` (+0.89), and stays below BoT-SORT on DanceTrack. On SoccerNet only the additive rule helps; under `botsort` the same encoder is flat, because the gate it was tuned to keeps appearance away from the pairs that would benefit. - MOT17 is the case where the split chose wrong. `adaptive` scored 0.10 lower on val-half and 0.73 higher on test, where it beats every other MOT17 row on this page. Both numbers are shown rather than switching the choice after seeing test, which would make the split meaningless. + On MOT17 the tuning split preferred `botsort` by 0.10 HOTA, while `adaptive` scored 0.73 higher on test, the best MOT17 result on this page. SoccerNet uses ground-truth boxes as detections, so its numbers are not comparable to the YOLOX rows. @@ -369,39 +365,37 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"` and `reid_fusion="adaptive"`. - Tuning split: - - | Dataset | Config | HOTA | - | :--------- | :-------------------------- | :-------: | - | MOT17 | BoT-SORT | 69.05 | - | | BoT-SORT + ReID | **69.29** | - | | BoT-SORT + ReID, `adaptive` | **69.29** | - | SportsMOT | BoT-SORT | 82.00 | - | | BoT-SORT + ReID | **84.39** | - | | BoT-SORT + ReID, `adaptive` | 83.15 | - | DanceTrack | BoT-SORT | 53.89 | - | | BoT-SORT + ReID | **59.28** | - | | BoT-SORT + ReID, `adaptive` | 58.71 | - | SoccerNet² | BoT-SORT | 85.72 | - | | BoT-SORT + ReID | 88.97 | - | | BoT-SORT + ReID, `adaptive` | **89.91** | - - Test split: - - | Dataset | Config | HOTA | IDF1 | MOTA | - | :--------- | :-------------------------- | :-------: | :-------: | :-------: | - | MOT17 | BoT-SORT | 63.8 | 78.7 | 79.4 | - | | BoT-SORT + ReID | 64.00 | 78.89 | 79.43 | - | | BoT-SORT + ReID, `adaptive` | **64.05** | **79.21** | **79.44** | - | SportsMOT | BoT-SORT | 74.15 | 74.06 | 96.89 | - | | BoT-SORT + ReID | **77.82** | **78.50** | 96.92 | - | | BoT-SORT + ReID, `adaptive` | 76.36 | 76.76 | **96.96** | - | DanceTrack | BoT-SORT | 57.8 | 57.9 | 92.2 | - | | BoT-SORT + ReID | **60.80** | **61.60** | 91.70 | - | | BoT-SORT + ReID, `adaptive` | 58.30 | 58.20 | **92.30** | - | SoccerNet | BoT-SORT | 85.00 | 79.68 | 97.25 | - | | BoT-SORT + ReID | 87.30 | 83.16 | 98.73 | - | | BoT-SORT + ReID, `adaptive` | **88.43** | **84.40** | **99.26** | + **MOT17** + + | Config | HOTA | IDF1 | MOTA | + | :-------------------------- | :-------: | :-------: | :-------: | + | BoT-SORT | 63.8 | 78.7 | 79.4 | + | BoT-SORT + ReID | 64.00 | 78.89 | 79.43 | + | BoT-SORT + ReID, `adaptive` | **64.05** | **79.21** | **79.44** | + + **SportsMOT** + + | Config | HOTA | IDF1 | MOTA | + | :-------------------------- | :-------: | :-------: | :-------: | + | BoT-SORT | 74.15 | 74.06 | 96.89 | + | BoT-SORT + ReID | **77.82** | **78.50** | 96.92 | + | BoT-SORT + ReID, `adaptive` | 76.36 | 76.76 | **96.96** | + + **DanceTrack** + + | Config | HOTA | IDF1 | MOTA | + | :-------------------------- | :-------: | :-------: | :-------: | + | BoT-SORT | 57.8 | 57.9 | 92.2 | + | BoT-SORT + ReID | **60.80** | **61.60** | 91.70 | + | BoT-SORT + ReID, `adaptive` | 58.30 | 58.20 | **92.30** | + + **SoccerNet** + + | Config | HOTA | IDF1 | MOTA | + | :-------------------------- | :-------: | :-------: | :-------: | + | BoT-SORT | 85.00 | 79.68 | 97.25 | + | BoT-SORT + ReID | 87.30 | 83.16 | 98.73 | + | BoT-SORT + ReID, `adaptive` | **88.43** | **84.40** | **99.26** | Tuned ReID configuration for each dataset and fusion method. Motion parameters are the BoT-SORT values from the [tracker comparison](../evaluations/results.md), except `minimum_iou_threshold_first_assoc` where listed. @@ -458,7 +452,7 @@ BoT-SORT with and without ReID, using the same detections and motion parameters. reid_appearance_floor: 0.8 ``` - Every configuration was tuned with `trackers tune` on its own tuning split, 20 trials per fusion method, then evaluated once on test. + Every configuration was tuned with `trackers tune` on its own tuning split², 20 trials per fusion method, then evaluated once on test. ² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). The other datasets tune on val, MOT17 on val-half. From cef9d8c8f5304873d9c094a5435630292aa0c070 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 24 Sep 2026 13:19:10 -0300 Subject: [PATCH 33/49] docs(evaluations): hold the FPS column back until every dataset has it Only MOT17 and DanceTrack were timed, so the Default tables drop the FPS column and its note for now; the measurements stay recorded with the benchmark and come back once SportsMOT and SoccerNet are timed. Co-Authored-By: Claude Opus 5.5 --- docs/evaluations/results.md | 74 ++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index e5f189ea..e35adc28 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -17,8 +17,6 @@ This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, Results measured using YOLOX detections (MOT17, SportsMOT, DanceTrack) or oracle ground-truth boxes (SoccerNet) with default and grid-searched parameters. Performance varies across detectors — see [Detection Quality Matters](../guides/detection-quality.md) for the impact of detector quality on tracking metrics. - FPS in the Default tables is frames per second of `tracker.update` on a Google Colab L4 runtime, over every frame of the MOT17 val-half sequences and 23 of the 25 DanceTrack val sequences, with the same detections. Loading models and decoding frames are not included. SportsMOT and SoccerNet are not measured yet. - ## [MOT17](https://arxiv.org/abs/1603.00831) Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests re-identification and identity stability. @@ -36,15 +34,15 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests Results using default tracker parameters. The ReID row is BoT-SORT at library defaults with the default `fastreid_mot17_sbs50` encoder, BoT-SORT's own ReID model trained on MOT17 pedestrian crops. - | Tracker | HOTA | IDF1 | MOTA | FPS | - | :-------------: | :------: | :------: | :------: | ---: | - | SORT | 58.4 | 69.9 | 67.2 | 492 | - | ByteTrack | 60.1 | 73.2 | 74.1 | 595 | - | OC-SORT | 61.9 | 76.4 | 76.0 | 392 | - | C-BIoU | 63.0 | 79.1 | 77.4 | 352 | - | BoT-SORT | 63.7 | 78.7 | **79.2** | 66.7 | - | BoT-SORT + ReID | 63.9 | 78.9 | **79.2** | 13.5 | - | McByte | **64.1** | **79.7** | 79.1 | 2.5 | + | Tracker | HOTA | IDF1 | MOTA | + | :-------------: | :------: | :------: | :------: | + | SORT | 58.4 | 69.9 | 67.2 | + | ByteTrack | 60.1 | 73.2 | 74.1 | + | OC-SORT | 61.9 | 76.4 | 76.0 | + | C-BIoU | 63.0 | 79.1 | 77.4 | + | BoT-SORT | 63.7 | 78.7 | **79.2** | + | BoT-SORT + ReID | 63.9 | 78.9 | **79.2** | + | McByte | **64.1** | **79.7** | 79.1 | -=== "Generic encoder" - - Encoders used as published, without training on your data. `fastreid_mot17_sbs50` was trained on MOT17 by the BoT-SORT authors, so its MOT17 rows are in domain; on the other datasets it has not seen the footage, and it still helps on SportsMOT. Both fusion methods were tuned with `trackers tune`, 20 trials each, on each dataset's tuning split¹. - - | Dataset | Config | HOTA | IDF1 | MOTA | - | :--------- | :-------------------------- | :------: | :------: | :------: | - | MOT17 | BoT-SORT | 63.9 | 78.7 | **79.4** | - | | BoT-SORT + ReID | 63.8 | 78.8 | **79.4** | - | | BoT-SORT + ReID, `adaptive` | **64.6** | **79.9** | **79.4** | - | SportsMOT | BoT-SORT | 74.1 | 74.1 | **96.9** | - | | BoT-SORT + ReID | **75.5** | **75.5** | **96.9** | - | | BoT-SORT + ReID, `adaptive` | 74.1 | 73.7 | **96.9** | - | DanceTrack | BoT-SORT | **57.8** | **57.9** | 92.2 | - | | BoT-SORT + ReID | 57.6 | 57.6 | 92.1 | - | | BoT-SORT + ReID, `adaptive` | 57.5 | 57.3 | **92.3** | - | SoccerNet | BoT-SORT | 85.0 | 79.7 | 97.2 | - | | BoT-SORT + ReID | 85.0 | 79.7 | 97.2 | - | | BoT-SORT + ReID, `adaptive` | **85.9** | **80.4** | **97.9** | - - ??? info "Tuned ReID configuration" - - Tuned ReID configuration for each dataset. Motion parameters are the BoT-SORT values from the [tracker comparison](../evaluations/results.md). - - ```yaml - MOT17: - botsort: - reid_model: fastreid_mot17_sbs50 - reid_appearance_threshold: 0.10 - reid_proximity_threshold: 0.5 - adaptive: - reid_model: fastreid_mot17_sbs50 - reid_appearance_weight: 1.908 - reid_adaptive_weight_cap: 0.545 - reid_appearance_floor: 0.381 - reid_proximity_threshold: 0.772 - minimum_iou_threshold_first_assoc: 0.846 - - SportsMOT: - botsort: - reid_model: fastreid_mot17_sbs50 - reid_appearance_threshold: 0.4761 - reid_proximity_threshold: 0.5051 - adaptive: - reid_model: fastreid_mot17_sbs50 - reid_appearance_weight: 2.540 - reid_adaptive_weight_cap: 0.778 - reid_appearance_floor: 0.783 - reid_proximity_threshold: 0.216 - minimum_iou_threshold_first_assoc: 0.176 - - DanceTrack: - botsort: - reid_model: fastreid_mot17_sbs50 - reid_appearance_threshold: 0.4822 - reid_proximity_threshold: 0.5068 - adaptive: - reid_model: fastreid_mot17_sbs50 - reid_appearance_weight: 0.75 - reid_adaptive_weight_cap: 0.5 - reid_appearance_floor: 0.0 - reid_proximity_threshold: 0.5 - - SoccerNet: - botsort: - reid_model: fastreid_mot17_sbs50 - reid_appearance_threshold: 0.0467 - reid_proximity_threshold: 0.3511 - adaptive: - reid_model: fastreid_mot17_sbs50 - reid_appearance_weight: 1.178 - reid_adaptive_weight_cap: 0.621 - reid_appearance_floor: 0.195 - reid_proximity_threshold: 0.975 - minimum_iou_threshold_first_assoc: 0.349 - ``` - - ¹ SoccerNet-tracking has no validation split, so its tuning split is train. The other datasets tune on val, MOT17 on val-half. - - On test, a generic encoder adds HOTA on MOT17 with `adaptive` (+0.72), on SportsMOT with `botsort` (+1.31) and on SoccerNet with `adaptive` (+0.89), and stays below BoT-SORT on DanceTrack. On SoccerNet only the additive rule helps; under `botsort` the same encoder is flat, because the gate it was tuned to keeps appearance away from the pairs that would benefit. - - On MOT17 the tuning split preferred `botsort` by 0.10 HOTA, while `adaptive` scored 0.73 higher on test, the best MOT17 result on this page. - - SoccerNet uses ground-truth boxes as detections, so its numbers are not comparable to the YOLOX rows. - === "Fine-tuned encoder" `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"` and `reid_fusion="adaptive"`. @@ -428,10 +344,94 @@ BoT-SORT with and without ReID on the test splits, using the same detections and reid_appearance_floor: 0.8 ``` - Every configuration was tuned with `trackers tune` on its own tuning split², 20 trials per fusion method, then evaluated once on test. + Every configuration was tuned with `trackers tune` on its own tuning split¹, 20 trials per fusion method, then evaluated once on test. - ² SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). The other datasets tune on val, MOT17 on val-half. + ¹ SoccerNet-tracking has no validation split. This encoder was trained on the first 45 of the 57 train sequences, so its thresholds were tuned on the other 12 (SNMOT-159 to SNMOT-170). The other datasets tune on val, MOT17 on val-half. Fine-tuning improves on BoT-SORT without ReID on every dataset: on test it adds 3.67 HOTA on SportsMOT, 3.43 on SoccerNet with `adaptive`, 3.00 on DanceTrack and 0.20 on MOT17. Each encoder is one training run, and training is not deterministic across seeds: two SportsMOT encoders trained identically apart from the seed scored 83.50 and 83.72 on val, and on DanceTrack the spread across three seeds reached 1.54 HOTA. Treat differences below a point between fine-tuned rows as noise. The SportsMOT encoder is the better of its two seeds, chosen on val; `crops_per_identity=8` is carried over from DanceTrack rather than tuned per dataset. + +=== "Generic encoder" + + Encoders used as published, without training on your data. `fastreid_mot17_sbs50` was trained on MOT17 by the BoT-SORT authors, so its MOT17 rows are in domain; on the other datasets it has not seen the footage, and it still helps on SportsMOT. Both fusion methods were tuned with `trackers tune`, 20 trials each, on each dataset's tuning split². + + | Dataset | Config | HOTA | IDF1 | MOTA | + | :--------- | :-------------------------- | :------: | :------: | :------: | + | MOT17 | BoT-SORT | 63.9 | 78.7 | **79.4** | + | | BoT-SORT + ReID | 63.8 | 78.8 | **79.4** | + | | BoT-SORT + ReID, `adaptive` | **64.6** | **79.9** | **79.4** | + | SportsMOT | BoT-SORT | 74.1 | 74.1 | **96.9** | + | | BoT-SORT + ReID | **75.5** | **75.5** | **96.9** | + | | BoT-SORT + ReID, `adaptive` | 74.1 | 73.7 | **96.9** | + | DanceTrack | BoT-SORT | **57.8** | **57.9** | 92.2 | + | | BoT-SORT + ReID | 57.6 | 57.6 | 92.1 | + | | BoT-SORT + ReID, `adaptive` | 57.5 | 57.3 | **92.3** | + | SoccerNet | BoT-SORT | 85.0 | 79.7 | 97.2 | + | | BoT-SORT + ReID | 85.0 | 79.7 | 97.2 | + | | BoT-SORT + ReID, `adaptive` | **85.9** | **80.4** | **97.9** | + + ??? info "Tuned ReID configuration" + + Tuned ReID configuration for each dataset. Motion parameters are the BoT-SORT values from the [tracker comparison](../evaluations/results.md). + + ```yaml + MOT17: + botsort: + reid_model: fastreid_mot17_sbs50 + reid_appearance_threshold: 0.10 + reid_proximity_threshold: 0.5 + adaptive: + reid_model: fastreid_mot17_sbs50 + reid_appearance_weight: 1.908 + reid_adaptive_weight_cap: 0.545 + reid_appearance_floor: 0.381 + reid_proximity_threshold: 0.772 + minimum_iou_threshold_first_assoc: 0.846 + + SportsMOT: + botsort: + reid_model: fastreid_mot17_sbs50 + reid_appearance_threshold: 0.4761 + reid_proximity_threshold: 0.5051 + adaptive: + reid_model: fastreid_mot17_sbs50 + reid_appearance_weight: 2.540 + reid_adaptive_weight_cap: 0.778 + reid_appearance_floor: 0.783 + reid_proximity_threshold: 0.216 + minimum_iou_threshold_first_assoc: 0.176 + + DanceTrack: + botsort: + reid_model: fastreid_mot17_sbs50 + reid_appearance_threshold: 0.4822 + reid_proximity_threshold: 0.5068 + adaptive: + reid_model: fastreid_mot17_sbs50 + reid_appearance_weight: 0.75 + reid_adaptive_weight_cap: 0.5 + reid_appearance_floor: 0.0 + reid_proximity_threshold: 0.5 + + SoccerNet: + botsort: + reid_model: fastreid_mot17_sbs50 + reid_appearance_threshold: 0.0467 + reid_proximity_threshold: 0.3511 + adaptive: + reid_model: fastreid_mot17_sbs50 + reid_appearance_weight: 1.178 + reid_adaptive_weight_cap: 0.621 + reid_appearance_floor: 0.195 + reid_proximity_threshold: 0.975 + minimum_iou_threshold_first_assoc: 0.349 + ``` + + ² SoccerNet-tracking has no validation split, so its tuning split is train. The other datasets tune on val, MOT17 on val-half. + + On test, a generic encoder adds HOTA on MOT17 with `adaptive` (+0.72), on SportsMOT with `botsort` (+1.31) and on SoccerNet with `adaptive` (+0.89), and stays below BoT-SORT on DanceTrack. On SoccerNet only the additive rule helps; under `botsort` the same encoder is flat, because the gate it was tuned to keeps appearance away from the pairs that would benefit. + + On MOT17 the tuning split preferred `botsort` by 0.10 HOTA, while `adaptive` scored 0.73 higher on test, the best MOT17 result on this page. + + SoccerNet uses ground-truth boxes as detections, so its numbers are not comparable to the YOLOX rows. From 3d2fc9d6d6a5b14c3c88f0cb7b52b020b37e90d1 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 24 Sep 2026 13:56:49 -0300 Subject: [PATCH 38/49] docs(reid): drop osnet_x1_0_msmt17_combineall from the guide It lowered HOTA on all four benchmarks and is no longer the default, so the guide stops showing it: the SoccerNet distance example and its frame-gap box go, with both figures, and so does its row in the encoder table. The advice that example carried, that no threshold helps when the two distance groups overlap, stays as a sentence after the MOT17 example. Co-Authored-By: Claude Opus 5.5 --- ...rnet-osnet-appearance-distances-vs-gap.png | Bin 114064 -> 0 bytes .../soccernet-osnet-appearance-distances.png | Bin 46819 -> 0 bytes docs/guides/reid.md | 47 +++++++----------- 3 files changed, 17 insertions(+), 30 deletions(-) delete mode 100644 docs/assets/reid/soccernet-osnet-appearance-distances-vs-gap.png delete mode 100644 docs/assets/reid/soccernet-osnet-appearance-distances.png diff --git a/docs/assets/reid/soccernet-osnet-appearance-distances-vs-gap.png b/docs/assets/reid/soccernet-osnet-appearance-distances-vs-gap.png deleted file mode 100644 index 75dd587e14b7b229e44626e9221a3379036423fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 114064 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 Q&?;oHjx>kDoqi|(15c>8XaE2J diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 79107c63..f8cba086 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -62,16 +62,16 @@ Fine-tuning an encoder on your own data is coming to the `reid` package. ## Key Parameters -| Parameter | Purpose | Tuning guidance | -| :-------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | -| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | -| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | -| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | -| `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | -| `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | -| `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | -| `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, as in the Deep OC-SORT fusion). Calibrate per encoder: 0.8 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with the MOT17 and MSMT17 encoders, see [choosing a fusion method](#choosing-a-fusion-method). | +| Parameter | Purpose | Tuning guidance | +| :-------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | +| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | +| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | +| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | +| `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | +| `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | +| `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | +| `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, as in the Deep OC-SORT fusion). Calibrate per encoder: 0.8 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with `fastreid_mot17_sbs50`, see [choosing a fusion method](#choosing-a-fusion-method). | --- @@ -84,13 +84,12 @@ The encoder decides how much appearance can help, and every threshold below depe The encoders used on this page: -| Encoder | Architecture | Trained on | Role here | -| :----------------------------- | :--------------------------------------------- | :-------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | -| `fastreid_mot17_sbs50` | FastReID SBS with a ResNeSt-50 backbone | MOT17 pedestrian crops, released with [BoT-SORT](https://github.com/NirAharon/BoT-SORT) | Loaded by `trackers track` and `trackers tune` when ReID is enabled without naming a model. In domain on MOT17, generic elsewhere. | -| `osnet_x1_0_msmt17_combineall` | [OSNet](https://arxiv.org/abs/1905.00953) x1.0 | MSMT17, pedestrians on a university campus | The out-of-domain example on SoccerNet below. | -| `osnet_x1_0`, fine-tuned | OSNet x1.0 | The train split of the dataset it is evaluated on | The fine-tuned [results](#results). | +| Encoder | Architecture | Trained on | Role here | +| :----------------------- | :--------------------------------------------- | :-------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | +| `fastreid_mot17_sbs50` | FastReID SBS with a ResNeSt-50 backbone | MOT17 pedestrian crops, released with [BoT-SORT](https://github.com/NirAharon/BoT-SORT) | Loaded by `trackers track` and `trackers tune` when ReID is enabled without naming a model. In domain on MOT17, generic elsewhere. | +| `osnet_x1_0`, fine-tuned | [OSNet](https://arxiv.org/abs/1905.00953) x1.0 | The train split of the dataset it is evaluated on | The fine-tuned [results](#results). | -Load the two published encoders by name with `ReIDModel.from_pretrained`, as in the [quickstart](#quickstart); the fine-tuned weights are not published yet. Each encoder has its own distance scale, so choose the thresholds for the encoder you will track with. +Load `fastreid_mot17_sbs50` by name with `ReIDModel.from_pretrained`, as in the [quickstart](#quickstart); the fine-tuned weights are not published yet. Each encoder has its own distance scale, so choose the thresholds for the encoder you will track with. --- @@ -157,7 +156,7 @@ for threshold in (0.10, 0.20, 0.25): plot_appearance_distances(distances, thresholds={0.20: "selected", 0.25: "default"}) ``` -The [ReID API reference](../api/reid.md#choosing-a-threshold) lists the full signatures, and the [ReID cookbook](https://colab.research.google.com/github/roboflow/trackers/blob/develop/docs/cookbooks/how-to-add-reid-to-trackers.ipynb) runs the whole flow in Colab. The two examples below use 5000 same-person and 10000 different-person pairs, 1 to 30 frames apart. +The [ReID API reference](../api/reid.md#choosing-a-threshold) lists the full signatures, and the [ReID cookbook](https://colab.research.google.com/github/roboflow/trackers/blob/develop/docs/cookbooks/how-to-add-reid-to-trackers.ipynb) runs the whole flow in Colab. The example below uses 5000 same-person and 10000 different-person pairs, 1 to 30 frames apart. ### MOT17 with an encoder trained on MOT17 @@ -174,13 +173,7 @@ This is what `rates_at` prints for two thresholds: Moving the threshold right accepts more same-person pairs, but it also starts letting different-person pairs through. We use 0.2, the same value as the [MOT17 re-ID study](https://www-sop.inria.fr/members/Francois.Bremond/Postscript/Tomasz__SCCAI_2025.pdf) (Table 8). -### SoccerNet with an out-of-domain encoder - -`osnet_x1_0_msmt17_combineall` was trained on pedestrians from the MSMT17 dataset and has never seen football. On SoccerNet test, players in the same kit look alike to it, so the blue and red bars overlap. - -![OSNet MSMT17 on SoccerNet test GT](../assets/reid/soccernet-osnet-appearance-distances.png) - -When the two groups overlap like this, no threshold separates them: any value that keeps most same-person pairs also lets many different-person pairs through. Tuning the threshold will not fix it. Check the [proximity threshold](#choosing-a-proximity-threshold) and [fusion method](#choosing-a-fusion-method) first, and consider an encoder fine-tuned on your footage, see [results](#results). +If the two groups overlap instead, no threshold separates them: any value that keeps most same-person pairs also lets many different-person pairs through. Check the [proximity threshold](#choosing-a-proximity-threshold) and [fusion method](#choosing-a-fusion-method) first, and consider an encoder fine-tuned on your footage, see [results](#results). --- @@ -222,12 +215,6 @@ From this we learn: | 61 to 120 | 0.865 | 31.8% | 0.8% | | 121 to 240 | 0.854 | 28.7% | 0.8% | -??? info "An out-of-domain encoder on SoccerNet" - - `osnet_x1_0_msmt17_combineall` on SoccerNet fails differently. On SoccerNet the different-ID rate at θ=0.2 stays between 44% and 51% at every gap, so the frame gap is not what limits it; the encoder simply cannot separate players in matching kits at any horizon. Widening the gap costs same-ID pairs (99.6% down to 87.0%) without ever making the different-ID side usable, so no threshold makes this encoder useful here. The SoccerNet rows in the [results](#results) use an encoder fine-tuned on the dataset's own train split instead. - - ![OSNet MSMT17 separability vs frame gap](../assets/reid/soccernet-osnet-appearance-distances-vs-gap.png) - --- ## Choosing a proximity threshold From 3f4e85fbd738d70b32596d84f2665ef3c88eea9c Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 24 Sep 2026 14:07:36 -0300 Subject: [PATCH 39/49] docs(reid): describe the encoders in the two bullets, without a table With one published encoder left, the encoder table only repeated the two bullets above it. The bullets now carry what the table said: what fastreid_mot17_sbs50 is and how to load it, and that the fine-tuned results use OSNet x1.0 whose weights are not published yet. The generic bullet no longer says appearance helps little on unfamiliar footage, which the SportsMOT rows contradict. Co-Authored-By: Claude Opus 5.5 --- docs/guides/reid.md | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index f8cba086..56b40262 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -79,17 +79,10 @@ Fine-tuning an encoder on your own data is coming to the `reid` package. The encoder decides how much appearance can help, and every threshold below depends on it. There are two kinds: -- **A generic encoder**, used as published, such as `fastreid_mot17_sbs50`. It works out of the box, but on footage unlike its training data appearance helps little, see [results](#results). -- **An encoder fine-tuned on your footage**, where the largest gains in the results come from. Training one is coming to the `reid` package; the results below use encoders trained that way. +- **A generic encoder**, used as published. The one used here is `fastreid_mot17_sbs50`, a FastReID SBS model with a ResNeSt-50 backbone, trained on MOT17 pedestrian crops and released with [BoT-SORT](https://github.com/NirAharon/BoT-SORT). `trackers track` and `trackers tune` load it when ReID is enabled without naming a model; in Python, load it with `ReIDModel.from_pretrained("fastreid_mot17_sbs50")`, as in the [quickstart](#quickstart). It works out of the box, and how much it helps depends on the dataset, see [results](#results). +- **An encoder fine-tuned on your footage**, where the largest gains in the results come from. The results use an [OSNet](https://arxiv.org/abs/1905.00953) x1.0 fine-tuned on each dataset's train split. Training one is coming to the `reid` package, and these weights are not published yet. -The encoders used on this page: - -| Encoder | Architecture | Trained on | Role here | -| :----------------------- | :--------------------------------------------- | :-------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | -| `fastreid_mot17_sbs50` | FastReID SBS with a ResNeSt-50 backbone | MOT17 pedestrian crops, released with [BoT-SORT](https://github.com/NirAharon/BoT-SORT) | Loaded by `trackers track` and `trackers tune` when ReID is enabled without naming a model. In domain on MOT17, generic elsewhere. | -| `osnet_x1_0`, fine-tuned | [OSNet](https://arxiv.org/abs/1905.00953) x1.0 | The train split of the dataset it is evaluated on | The fine-tuned [results](#results). | - -Load `fastreid_mot17_sbs50` by name with `ReIDModel.from_pretrained`, as in the [quickstart](#quickstart); the fine-tuned weights are not published yet. Each encoder has its own distance scale, so choose the thresholds for the encoder you will track with. +Each encoder has its own distance scale, so choose the thresholds for the encoder you will track with. --- From 0e35aed82ee47b7b8e800a49e0f154b17d0bff3b Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 24 Sep 2026 14:11:31 -0300 Subject: [PATCH 40/49] docs: list trackers in the table order in the intros The README, landing page and results.md intros now name the trackers in the same order as the tables below them: C-BIoU before BoT-SORT. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/evaluations/results.md | 4 ++-- docs/index.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 06bc9489..05b6f592 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ -`trackers` gives you clean-room, benchmarked implementations of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte — so occlusions, fast motion, and moving cameras stop being your problem to solve from scratch. It speaks `supervision.Detections` natively, slotting into any detector you already use — YOLO, DETR, RT-DETR, or anything else — without glue code. One consistent interface, whether you're a researcher comparing algorithms, an engineer shipping a production pipeline, or a hobbyist building something cool. Requires Python ≥ 3.10. +`trackers` gives you clean-room, benchmarked implementations of SORT, ByteTrack, OC-SORT, C-BIoU, BoT-SORT, and McByte — so occlusions, fast motion, and moving cameras stop being your problem to solve from scratch. It speaks `supervision.Detections` natively, slotting into any detector you already use — YOLO, DETR, RT-DETR, or anything else — without glue code. One consistent interface, whether you're a researcher comparing algorithms, an engineer shipping a production pipeline, or a hobbyist building something cool. Requires Python ≥ 3.10. ## Why trackers? diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index c011ce5f..816045bb 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -1,11 +1,11 @@ --- title: Tracker Comparison — MOT Benchmark Results | Trackers -description: Side-by-side MOT benchmark comparison of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte on MOT17, DanceTrack, SportsMOT, and SoccerNet — HOTA, IDF1, MOTA with default and tuned parameters. +description: Side-by-side MOT benchmark comparison of SORT, ByteTrack, OC-SORT, C-BIoU, BoT-SORT, and McByte on MOT17, DanceTrack, SportsMOT, and SoccerNet — HOTA, IDF1, MOTA with default and tuned parameters. --- # Tracker Comparison -This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. McByte is reported at default parameters only (mask-conditioned association enabled); it is designed to require no per-dataset tuning, so there is no Tuned row. BoT-SORT + ReID is shown at library defaults in the Default tabs, with the default encoder `fastreid_mot17_sbs50` throughout; its Tuned rows show the best test result among the configurations in the [ReID guide](../guides/reid.md#results), each with an encoder trained on that dataset: `osnet_x1_0` fine-tuned on the SportsMOT, SoccerNet and DanceTrack train splits, and on MOT17 `fastreid_mot17_sbs50`, which the BoT-SORT authors trained on MOT17. +This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, C-BIoU, BoT-SORT, and McByte on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. McByte is reported at default parameters only (mask-conditioned association enabled); it is designed to require no per-dataset tuning, so there is no Tuned row. BoT-SORT + ReID is shown at library defaults in the Default tabs, with the default encoder `fastreid_mot17_sbs50` throughout; its Tuned rows show the best test result among the configurations in the [ReID guide](../guides/reid.md#results), each with an encoder trained on that dataset: `osnet_x1_0` fine-tuned on the SportsMOT, SoccerNet and DanceTrack train splits, and on MOT17 `fastreid_mot17_sbs50`, which the BoT-SORT authors trained on MOT17. !!! info "Benchmark version" diff --git a/docs/index.md b/docs/index.md index b58d0291..3b108d7c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ --- title: Quickstart — Multi-Object Tracking in Python | Trackers comments: false -description: Get started with Roboflow Trackers — install SORT, ByteTrack, OC-SORT, BoT-SORT, C-BIoU, and McByte, run your first tracking pipeline, and evaluate results with HOTA, IDF1, and MOTA metrics. +description: Get started with Roboflow Trackers — install SORT, ByteTrack, OC-SORT, C-BIoU, BoT-SORT, and McByte, run your first tracking pipeline, and evaluate results with HOTA, IDF1, and MOTA metrics. ---
From 6b497351cfc4eabc64b64c276a82ada95f9701f3 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 24 Sep 2026 14:11:31 -0300 Subject: [PATCH 41/49] docs(reid): give the encoders' parameter counts fastreid_mot17_sbs50 has 25.4M parameters and the fine-tuned OSNet x1.0 has 2.2M, counted on the inference networks without the training classifier. Co-Authored-By: Claude Opus 5.5 --- docs/guides/reid.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 56b40262..efc23078 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -79,8 +79,8 @@ Fine-tuning an encoder on your own data is coming to the `reid` package. The encoder decides how much appearance can help, and every threshold below depends on it. There are two kinds: -- **A generic encoder**, used as published. The one used here is `fastreid_mot17_sbs50`, a FastReID SBS model with a ResNeSt-50 backbone, trained on MOT17 pedestrian crops and released with [BoT-SORT](https://github.com/NirAharon/BoT-SORT). `trackers track` and `trackers tune` load it when ReID is enabled without naming a model; in Python, load it with `ReIDModel.from_pretrained("fastreid_mot17_sbs50")`, as in the [quickstart](#quickstart). It works out of the box, and how much it helps depends on the dataset, see [results](#results). -- **An encoder fine-tuned on your footage**, where the largest gains in the results come from. The results use an [OSNet](https://arxiv.org/abs/1905.00953) x1.0 fine-tuned on each dataset's train split. Training one is coming to the `reid` package, and these weights are not published yet. +- **A generic encoder**, used as published. The one used here is `fastreid_mot17_sbs50`, a FastReID SBS model with a ResNeSt-50 backbone and 25.4M parameters, trained on MOT17 pedestrian crops and released with [BoT-SORT](https://github.com/NirAharon/BoT-SORT). `trackers track` and `trackers tune` load it when ReID is enabled without naming a model; in Python, load it with `ReIDModel.from_pretrained("fastreid_mot17_sbs50")`, as in the [quickstart](#quickstart). It works out of the box, and how much it helps depends on the dataset, see [results](#results). +- **An encoder fine-tuned on your footage**, where the largest gains in the results come from. The results use an [OSNet](https://arxiv.org/abs/1905.00953) x1.0, a much smaller network with 2.2M parameters, fine-tuned on each dataset's train split. Training one is coming to the `reid` package, and these weights are not published yet. Each encoder has its own distance scale, so choose the thresholds for the encoder you will track with. From a344cfe9ea3f8555a6724edefa428227545cf7af Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 24 Sep 2026 14:18:02 -0300 Subject: [PATCH 42/49] docs(reid): one merged, centred cell per dataset in the results tables Markdown tables cannot span rows, so a small script merges each dataset's name into one cell spanning its rows, vertically centred and without the blank cells below it, and marks where each group starts for the heavier rule. It only acts on tables whose first column has blank cells, and the Dataset column is centred in the markdown. Co-Authored-By: Claude Opus 5.5 --- docs/guides/reid.md | 12 ++++++------ docs/javascripts/grouped_tables.js | 26 ++++++++++++++++++++++++++ docs/overrides/stylesheets/style.css | 11 +++++++---- mkdocs.yml | 1 + 4 files changed, 40 insertions(+), 10 deletions(-) create mode 100644 docs/javascripts/grouped_tables.js diff --git a/docs/guides/reid.md b/docs/guides/reid.md index efc23078..6484ad65 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -252,9 +252,9 @@ BoT-SORT with and without ReID on the test splits, using the same detections and `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"` and `reid_fusion="adaptive"`. - | Dataset | Config | HOTA | IDF1 | MOTA | - | :--------- | :-------------------------- | :------: | :------: | :------: | - | MOT17 | BoT-SORT | 63.8 | 78.7 | **79.4** | + | Dataset | Config | HOTA | IDF1 | MOTA | + | :--------: | :-------------------------- | :------: | :------: | :------: | + | MOT17 | BoT-SORT | 63.8 | 78.7 | **79.4** | | | BoT-SORT + ReID | **64.0** | 78.9 | **79.4** | | | BoT-SORT + ReID, `adaptive` | **64.0** | **79.2** | **79.4** | | SportsMOT | BoT-SORT | 74.1 | 74.1 | 96.9 | @@ -336,9 +336,9 @@ BoT-SORT with and without ReID on the test splits, using the same detections and Encoders used as published, without training on your data. `fastreid_mot17_sbs50` was trained on MOT17 by the BoT-SORT authors, so its MOT17 rows are in domain; on the other datasets it has not seen the footage, and it still helps on SportsMOT. Both fusion methods were tuned with `trackers tune`, 20 trials each, on each dataset's tuning split². - | Dataset | Config | HOTA | IDF1 | MOTA | - | :--------- | :-------------------------- | :------: | :------: | :------: | - | MOT17 | BoT-SORT | 63.9 | 78.7 | **79.4** | + | Dataset | Config | HOTA | IDF1 | MOTA | + | :--------: | :-------------------------- | :------: | :------: | :------: | + | MOT17 | BoT-SORT | 63.9 | 78.7 | **79.4** | | | BoT-SORT + ReID | 63.8 | 78.8 | **79.4** | | | BoT-SORT + ReID, `adaptive` | **64.6** | **79.9** | **79.4** | | SportsMOT | BoT-SORT | 74.1 | 74.1 | **96.9** | diff --git a/docs/javascripts/grouped_tables.js b/docs/javascripts/grouped_tables.js new file mode 100644 index 00000000..8bd3caba --- /dev/null +++ b/docs/javascripts/grouped_tables.js @@ -0,0 +1,26 @@ +/** + * Grouped tables, such as the ReID results: the first row of each group names it in the first + * column and the rows below leave that cell empty. Merge each name into one cell spanning its + * group, and mark where each group starts so the stylesheet can draw a rule above it. + */ +document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll(".md-typeset table").forEach(function (table) { + const body = table.tBodies[0]; + if (!body || body.rows.length === 0) return; + const rows = Array.from(body.rows); + const isBlank = (row) => row.cells[0] && row.cells[0].textContent.trim() === ""; + if (isBlank(rows[0]) || !rows.some(isBlank)) return; + + let nameCell = null; + rows.forEach(function (row) { + if (isBlank(row)) { + nameCell.rowSpan += 1; + row.cells[0].remove(); + } else { + nameCell = row.cells[0]; + row.classList.add("group-start"); + } + }); + table.setAttribute("data-grouped", ""); + }); +}); diff --git a/docs/overrides/stylesheets/style.css b/docs/overrides/stylesheets/style.css index de21ca93..410fed62 100644 --- a/docs/overrides/stylesheets/style.css +++ b/docs/overrides/stylesheets/style.css @@ -32,9 +32,12 @@ th, td { font-size: 0.6rem; } -/* Grouped tables, such as the ReID results: the first row of each group names it in the first - column and the rows below leave that cell empty. A heavier rule marks where each group starts, - including the first one under the header. */ -.md-typeset table:has(tbody td:first-child:empty) tbody tr:has(> td:first-child:not(:empty)) > td { +/* Grouped tables, such as the ReID results (see javascripts/grouped_tables.js): each group's name + spans its rows, vertically centred, and a heavier rule marks where each group starts. */ +.md-typeset table[data-grouped] tr.group-start > td { border-top: 2px solid var(--md-default-fg-color--light); } + +.md-typeset table[data-grouped] td[rowspan] { + vertical-align: middle; +} diff --git a/mkdocs.yml b/mkdocs.yml index f3f4f33a..a27d19cb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -61,6 +61,7 @@ extra_javascript: - javascripts/mathjax.js - javascripts/cli_builder_framework.js - javascripts/command_builder.js + - javascripts/grouped_tables.js markdown_extensions: # Adds support for card grids From fc1cd9cd8ce1e3c8d8dd5bc3982ffdeddd5e11a6 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 24 Sep 2026 15:06:35 -0300 Subject: [PATCH 43/49] docs(reid): name the fine-tuned encoders The fine-tuned OSNet x1.0 encoders are referred to by the names they will ship under, osnet_x1_0_mot17, osnet_x1_0_sportsmot, osnet_x1_0_dancetrack and osnet_x1_0_soccernet: in the guide's encoder section, with how to load one in Python and on the command line, in the fine-tuned configuration blocks, and in the results.md Tuned notes and configurations. These names do not resolve yet: the weights still need uploading and the names need registering with their URLs before this is released. Co-Authored-By: Claude Opus 5.5 --- docs/evaluations/results.md | 12 ++++++------ docs/guides/reid.md | 22 +++++++++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index 816045bb..b5a3e17e 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -171,7 +171,7 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar | BoT-SORT | 74.1 | 74.0 | **96.9** | | BoT-SORT + ReID | **77.8** | **78.5** | **96.9** | - Tuned configuration for each tracker. The ReID row uses an `osnet_x1_0` fine-tuned on SportsMOT train. + Tuned configuration for each tracker. The ReID row uses `osnet_x1_0_sportsmot`, an OSNet x1.0 fine-tuned on SportsMOT train (see [encoders](../guides/reid.md#choosing-an-encoder)). ```yaml SORT: @@ -218,7 +218,7 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar buffer_ratio_second: 0.5 BoT-SORT + ReID: - reid_model: osnet_x1_0 fine-tuned on SportsMOT train + reid_model: osnet_x1_0_sportsmot reid_fusion: botsort reid_appearance_threshold: 0.3425 reid_proximity_threshold: 0.9953 @@ -273,7 +273,7 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I | BoT-SORT | 85.0 | 79.7 | 97.2 | | BoT-SORT + ReID | **88.4** | **84.4** | **99.3** | - Tuned configuration for each tracker. The ReID row uses an `osnet_x1_0` fine-tuned on SoccerNet train. + Tuned configuration for each tracker. The ReID row uses `osnet_x1_0_soccernet`, an OSNet x1.0 fine-tuned on SoccerNet train (see [encoders](../guides/reid.md#choosing-an-encoder)). ```yaml SORT: @@ -320,7 +320,7 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I buffer_ratio_second: 0.50 BoT-SORT + ReID: - reid_model: osnet_x1_0 fine-tuned on SoccerNet train + reid_model: osnet_x1_0_soccernet reid_fusion: adaptive reid_appearance_weight: 0.75 reid_adaptive_weight_cap: 0.5 @@ -380,7 +380,7 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art | BoT-SORT | 57.8 | 57.9 | 92.2 | | BoT-SORT + ReID | **60.8** | **61.6** | 91.7 | - Best configuration for each tracker. The ReID row uses an `osnet_x1_0` fine-tuned on DanceTrack train. + Best configuration for each tracker. The ReID row uses `osnet_x1_0_dancetrack`, an OSNet x1.0 fine-tuned on DanceTrack train (see [encoders](../guides/reid.md#choosing-an-encoder)). ```yaml SORT: @@ -427,7 +427,7 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art buffer_ratio_second: 0.10 BoT-SORT + ReID: - reid_model: osnet_x1_0 fine-tuned on DanceTrack train + reid_model: osnet_x1_0_dancetrack reid_fusion: botsort reid_appearance_threshold: 0.4716 reid_proximity_threshold: 0.6802 diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 6484ad65..228f68a2 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -80,7 +80,7 @@ Fine-tuning an encoder on your own data is coming to the `reid` package. The encoder decides how much appearance can help, and every threshold below depends on it. There are two kinds: - **A generic encoder**, used as published. The one used here is `fastreid_mot17_sbs50`, a FastReID SBS model with a ResNeSt-50 backbone and 25.4M parameters, trained on MOT17 pedestrian crops and released with [BoT-SORT](https://github.com/NirAharon/BoT-SORT). `trackers track` and `trackers tune` load it when ReID is enabled without naming a model; in Python, load it with `ReIDModel.from_pretrained("fastreid_mot17_sbs50")`, as in the [quickstart](#quickstart). It works out of the box, and how much it helps depends on the dataset, see [results](#results). -- **An encoder fine-tuned on your footage**, where the largest gains in the results come from. The results use an [OSNet](https://arxiv.org/abs/1905.00953) x1.0, a much smaller network with 2.2M parameters, fine-tuned on each dataset's train split. Training one is coming to the `reid` package, and these weights are not published yet. +- **An encoder fine-tuned on your footage**, where the largest gains in the results come from. The results use an [OSNet](https://arxiv.org/abs/1905.00953) x1.0, a much smaller network with 2.2M parameters, fine-tuned on each dataset's train split. All four are available by name: `osnet_x1_0_mot17`, `osnet_x1_0_sportsmot`, `osnet_x1_0_dancetrack` and `osnet_x1_0_soccernet`. Load one with `ReIDModel.from_pretrained("osnet_x1_0_sportsmot")`, or pass the name to `--reid.model` on the command line. Training your own is coming to the `reid` package. Each encoder has its own distance scale, so choose the thresholds for the encoder you will track with. @@ -216,7 +216,7 @@ From this we learn: While a track is lost it has no detection of its own, so it is matched through its estimated position, which keeps moving as if the target had carried on. After a short occlusion that estimate has drifted, so it barely overlaps the detection when the target reappears. After the target leaves the frame it drifts out of view entirely and the overlap drops to zero. The 0.5 default blocks appearance in both cases, 0.99 lets it score the first one, and only 1.0 lets it score the second. BoT-SORT has no separate step for targets that leave and return, so opening the gate is the only way to keep their ID. -SoccerNet test, oracle detections, `osnet_x1_0` fine-tuned on SoccerNet train, library-default motion parameters, `reid_fusion="adaptive"` with its default weights: +SoccerNet test, oracle detections, `osnet_x1_0_soccernet`, library-default motion parameters, `reid_fusion="adaptive"` with its default weights: | `reid_proximity_threshold` | appearance consulted when | HOTA | IDF1 | ID switches | | :------------------------- | :------------------------ | :---: | :---: | :---------: | @@ -250,7 +250,7 @@ BoT-SORT with and without ReID on the test splits, using the same detections and === "Fine-tuned encoder" - `osnet_x1_0` fine-tuned on each dataset's train split, with `reid_fusion="botsort"` and `reid_fusion="adaptive"`. + `osnet_x1_0` fine-tuned on each dataset's train split (`osnet_x1_0_mot17`, `osnet_x1_0_sportsmot`, `osnet_x1_0_dancetrack`, `osnet_x1_0_soccernet`), with `reid_fusion="botsort"` and `reid_fusion="adaptive"`. | Dataset | Config | HOTA | IDF1 | MOTA | | :--------: | :-------------------------- | :------: | :------: | :------: | @@ -274,11 +274,11 @@ BoT-SORT with and without ReID on the test splits, using the same detections and ```yaml MOT17: botsort: - reid_model: osnet_x1_0 fine-tuned on MOT17 train + reid_model: osnet_x1_0_mot17 reid_appearance_threshold: 0.0448 reid_proximity_threshold: 0.2697 adaptive: - reid_model: osnet_x1_0 fine-tuned on MOT17 train + reid_model: osnet_x1_0_mot17 reid_appearance_weight: 2.124 reid_adaptive_weight_cap: 0.959 reid_appearance_floor: 0.569 @@ -287,11 +287,11 @@ BoT-SORT with and without ReID on the test splits, using the same detections and SportsMOT: botsort: - reid_model: osnet_x1_0 fine-tuned on SportsMOT train + reid_model: osnet_x1_0_sportsmot reid_appearance_threshold: 0.3425 reid_proximity_threshold: 0.9953 adaptive: - reid_model: osnet_x1_0 fine-tuned on SportsMOT train + reid_model: osnet_x1_0_sportsmot reid_appearance_weight: 1.290 reid_adaptive_weight_cap: 0.718 reid_appearance_floor: 0.510 @@ -300,11 +300,11 @@ BoT-SORT with and without ReID on the test splits, using the same detections and DanceTrack: botsort: - reid_model: osnet_x1_0 fine-tuned on DanceTrack train + reid_model: osnet_x1_0_dancetrack reid_appearance_threshold: 0.4716 reid_proximity_threshold: 0.6802 adaptive: - reid_model: osnet_x1_0 fine-tuned on DanceTrack train + reid_model: osnet_x1_0_dancetrack reid_appearance_weight: 1.947 reid_adaptive_weight_cap: 0.944 reid_appearance_floor: 0.614 @@ -313,11 +313,11 @@ BoT-SORT with and without ReID on the test splits, using the same detections and SoccerNet: botsort: - reid_model: osnet_x1_0 fine-tuned on SoccerNet train + reid_model: osnet_x1_0_soccernet reid_appearance_threshold: 0.075 reid_proximity_threshold: 1.0 adaptive: - reid_model: osnet_x1_0 fine-tuned on SoccerNet train + reid_model: osnet_x1_0_soccernet reid_appearance_weight: 0.75 reid_adaptive_weight_cap: 0.5 reid_proximity_threshold: 1.0 From 53cd7b6fe11aabe2f6b0874126680d000fb0cf8d Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Mon, 28 Sep 2026 22:34:18 -0300 Subject: [PATCH 44/49] test(reid): make the floor test tell raw from floored margins Both candidates sat below the 0.95 floor, so flooring before measuring the top-two margin produced the same zero gap and the test passed either way. One candidate now clears the floor (0.9) and one does not (0.8): the raw gap is 0.1, the floored-first gap would be 0.9 capped to 0.5, so the fused score is 1.32 against 1.5. Checked by moving the floor before the margin: the new test fails, the old one passed. Co-Authored-By: Claude Opus 5.5 --- tests/core/test_botsort_reid.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/core/test_botsort_reid.py b/tests/core/test_botsort_reid.py index 6ed64b44..f45ef18f 100644 --- a/tests/core/test_botsort_reid.py +++ b/tests/core/test_botsort_reid.py @@ -347,19 +347,20 @@ def test_appearance_floor_drops_weak_matches(self) -> None: assert fused[0, 1] == pytest.approx(0.9) def test_floored_competitor_does_not_inflate_the_bonus(self) -> None: - # Both detections look identical (0.9), so no bonus is owed. Flooring plays no - # role here (both pass), but the margin must be measured over raw appearance: - # a floored candidate reading as similarity 0 would fabricate a 0.9 gap. + # One detection clears the floor (0.9) and one does not (0.8). The margin must be + # measured over raw appearance, a gap of 0.1: flooring first would read the second + # detection as 0 and fabricate a gap of 0.9, capped to 0.5. fused = fuse_adaptive_reid_association( np.array([[0.6, 0.6]], dtype=np.float32), - np.array([[0.9, 0.9]], dtype=np.float32), + np.array([[0.9, 0.8]], dtype=np.float32), reid_appearance_weight=0.75, reid_adaptive_weight_cap=0.5, reid_proximity_threshold=0.5, - reid_appearance_floor=0.95, + reid_appearance_floor=0.85, ) - # Floor drops appearance for both pairs: geometry only. - assert fused[0, 0] == pytest.approx(0.6) + # Track gap 0.1, no detection gap (one track): 0.6 + (0.75 + 0.1 / 2) * 0.9. + assert fused[0, 0] == pytest.approx(1.32) + # Below the floor: geometry only. assert fused[0, 1] == pytest.approx(0.6) def test_gated_competitor_does_not_inflate_the_bonus(self) -> None: From b4d65fe66a8b32f062a3d3f27099a636dfae1087 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Mon, 28 Sep 2026 22:34:18 -0300 Subject: [PATCH 45/49] docs(reid): one MOT17 BoT-SORT baseline across the results tabs The generic tab used the ReID study's own geometry run (63.861, shown as 63.9) while the fine-tuned tab and results.md use the published tuned BoT-SORT result, 63.8. The guide's BoT-SORT rows copy results.md, so both tabs now show 63.8. No bold changes. Co-Authored-By: Claude Opus 5.5 --- docs/guides/reid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 228f68a2..3c807f2e 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -338,7 +338,7 @@ BoT-SORT with and without ReID on the test splits, using the same detections and | Dataset | Config | HOTA | IDF1 | MOTA | | :--------: | :-------------------------- | :------: | :------: | :------: | - | MOT17 | BoT-SORT | 63.9 | 78.7 | **79.4** | + | MOT17 | BoT-SORT | 63.8 | 78.7 | **79.4** | | | BoT-SORT + ReID | 63.8 | 78.8 | **79.4** | | | BoT-SORT + ReID, `adaptive` | **64.6** | **79.9** | **79.4** | | SportsMOT | BoT-SORT | 74.1 | 74.1 | **96.9** | From c46303c9cdff5cb8dd31609b20ac63077a262504 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Mon, 28 Sep 2026 22:34:18 -0300 Subject: [PATCH 46/49] docs: map the BoT-SORT + ReID copies in the benchmark cross-references The BENCH-XREF comments and docs/AGENTS.md did not know about the BoT-SORT + ReID rows: the Default rows copied into README and the landing page, the Tuned tables whose BoT-SORT rows the ReID guide copies and whose ReID rows come from it, and the ReID footnote. They also still said the landing page had no C-BIoU row, which predates this branch. Comments only; nothing renders differently. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/AGENTS.md | 19 ++++++++++++++----- docs/evaluations/results.md | 36 ++++++++++++++++++++++++++++++++---- docs/index.md | 2 +- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 05b6f592..c8737385 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ For all CLI options, see the [tracking guide](https://trackers.roboflow.com/deve Each tracker below is a faithful implementation of its original paper's motion and association pipeline; appearance/ReID branches are off by default, though BoT-SORT accepts one through `reid_model` (see the [ReID guide](https://trackers.roboflow.com/latest/guides/reid/)). Each tracker's docs page states the exact scope. Pick the one that fits your scene, or run the benchmark to find out which performs best on your data. - + | Algorithm | Description | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | DanceTrack HOTA | | :-------------------------------------------------------------------: | :------------------------------------------------------------------------------: | :--------: | :------------: | :------------: | :-------------: | diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 757b481c..91a46ae4 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -10,7 +10,8 @@ comment) and update every listed sibling. If you change a cell somewhere else fi it's the source of truth, not just another copy. Discover live: grep -rn BENCH-XREF docs/ README.md -(15 xref comments as of 2026-08-12: 5 in results.md, 6 in docs/trackers/*.md, 3 in docs/index.md, 1 in README.md. +(20 xref comments as of 2026-09-28: 9 in results.md, 6 in docs/trackers/*.md, 3 in docs/index.md, 1 in README.md, + 1 in docs/guides/reid.md. A 16th lived in .github/copilot-instructions.md until 2026-08-12, when that file was deleted and its guidance merged into the root AGENTS.md — which deliberately carries NO benchmark table. Don't add one there.) @@ -19,7 +20,10 @@ Discover live: grep -rn BENCH-XREF docs/ README.md - id=sportsmot-default (## SportsMOT -> === "Default") - id=soccernet-default (## SoccerNet-tracking -> === "Default") - id=dancetrack-default (## DanceTrack -> === "Default") -- Tuned tables (all 4 benchmarks) have NO external duplicates — only referenced from within results.md itself. +- id=mot17-tuned, id=sportsmot-tuned, id=soccernet-tuned, id=dancetrack-tuned (=== "Tuned" tabs). Their BoT-SORT + rows are copied into [guides/reid.md](guides/reid.md) (Results, both tabs). Their BoT-SORT + ReID rows run the + other way: copied FROM guides/reid.md (the best test row across its two tabs), and their HOTA also appears in + the BoT-SORT + ReID footnote of index.md and README.md. ## Duplicate sites, per canonical row @@ -46,8 +50,8 @@ BoT-SORT row (mot17/sportsmot/soccernet/dancetrack): C-BIoU row (mot17/sportsmot/soccernet/dancetrack — the only tracker doc with a DanceTrack row): -> [trackers/cbiou.md](trackers/cbiou.md) (table, full row, all 4 benchmarks) + -> [index.md](index.md) (Algorithms table, HOTA column only) -> [../README.md](../README.md) (Algorithms table, HOTA column only) - -> docs/index.md does NOT have a C-BIoU row — don't add one when editing, that's existing scope, not a gap. McByte row (mot17/sportsmot/soccernet/dancetrack): -> [trackers/mcbyte.md](trackers/mcbyte.md) (McByte row, matching benchmark tab, full row) @@ -55,11 +59,16 @@ McByte row (mot17/sportsmot/soccernet/dancetrack): -> [../README.md](../README.md) (Algorithms table, HOTA column only; includes C-BIoU row too) — the index.md FAQ claim above is TRUE only while McByte is bolded-best in all 4 Default tables. Re-verify, don't assume. +BoT-SORT + ReID row (mot17/sportsmot/soccernet/dancetrack; Default = `fastreid_mot17_sbs50` at library defaults): + -> [index.md](index.md) (Algorithms table, HOTA column only) + -> [../README.md](../README.md) (Algorithms table, HOTA column only) + -> [guides/reid.md](guides/reid.md) is where the Tuned ReID rows come FROM, see Canonical tables above. + ## Structural asymmetries (intentional — do not "fix" by adding rows) - docs/trackers/{sort,bytetrack,ocsort,botsort}.md tables cover MOT17/SportsMOT/SoccerNet only, no DanceTrack row. - docs/trackers/cbiou.md is the only individual-tracker doc with a DanceTrack row. -- docs/index.md Algorithms table has 5 tracker rows (SORT/ByteTrack/OC-SORT/BoT-SORT/McByte), no C-BIoU row. -- README.md Algorithms table has 6 tracker rows (SORT/ByteTrack/OC-SORT/BoT-SORT/C-BIoU/McByte). +- docs/index.md and README.md Algorithms tables have the same 7 rows, in the same order as the results.md tables: + SORT/ByteTrack/OC-SORT/C-BIoU/BoT-SORT/BoT-SORT + ReID/McByte. - The root AGENTS.md carries NO benchmark table — it links to evaluations/results.md. That is deliberate; don't add a table back (it replaced .github/copilot-instructions.md, deleted 2026-08-12, which had one). - docs/trackers/mcbyte.md reports McByte vs a BoT-SORT baseline only (not vs SORT/ByteTrack/OC-SORT/C-BIoU). diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index b5a3e17e..faa81c2a 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -49,8 +49,9 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests ByteTrack row -> docs/trackers/bytetrack.md (table), docs/index.md (L13 headline + Algorithms table), README.md (Algorithms table) OC-SORT row -> docs/trackers/ocsort.md (table), docs/index.md (L13 headline + Algorithms table), README.md (Algorithms table) BoT-SORT row -> docs/trackers/botsort.md (table), docs/index.md (Algorithms table), README.md (Algorithms table), docs/trackers/mcbyte.md (BoT-SORT row, "MOT17" tab) - C-BIoU row -> docs/trackers/cbiou.md (table), README.md (Algorithms table) + C-BIoU row -> docs/trackers/cbiou.md (table), docs/index.md (Algorithms table), README.md (Algorithms table) McByte row -> docs/trackers/mcbyte.md (McByte row, "MOT17" tab), docs/index.md (Algorithms table + FAQ "leads every benchmark" claim), README.md (Algorithms table) + BoT-SORT + ReID row -> docs/index.md (Algorithms table), README.md (Algorithms table) Change any cell above -> update every listed location + re-check FAQ leader claim still true. --> === "Tuned" @@ -66,6 +67,12 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests | BoT-SORT | 63.8 | 78.7 | **79.4** | | BoT-SORT + ReID | **64.6** | **79.9** | **79.4** | + + Tuned configuration for each tracker. The ReID row uses `fastreid_mot17_sbs50`, trained on MOT17 by the BoT-SORT authors, with `reid_fusion="adaptive"`: the best MOT17 test result in the [ReID guide](../guides/reid.md#results). It was chosen on test; on val-half, `botsort` scored 0.10 higher. ```yaml @@ -154,8 +161,9 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar ByteTrack row -> docs/trackers/bytetrack.md (table), docs/index.md (Algorithms table), README.md (Algorithms table) OC-SORT row -> docs/trackers/ocsort.md (table), docs/index.md (Algorithms table), README.md (Algorithms table) BoT-SORT row -> docs/trackers/botsort.md (table), docs/index.md (Algorithms table), README.md (Algorithms table), docs/trackers/mcbyte.md (BoT-SORT row, "SportsMOT" tab) - C-BIoU row -> docs/trackers/cbiou.md (table), README.md (Algorithms table only) + C-BIoU row -> docs/trackers/cbiou.md (table), docs/index.md (Algorithms table), README.md (Algorithms table) McByte row -> docs/trackers/mcbyte.md (McByte row, "SportsMOT" tab), docs/index.md (Algorithms table + FAQ "leads every benchmark" claim), README.md (Algorithms table) + BoT-SORT + ReID row -> docs/index.md (Algorithms table), README.md (Algorithms table) Change any cell above -> update every listed location + re-check FAQ leader claim still true. --> === "Tuned" @@ -171,6 +179,12 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar | BoT-SORT | 74.1 | 74.0 | **96.9** | | BoT-SORT + ReID | **77.8** | **78.5** | **96.9** | + + Tuned configuration for each tracker. The ReID row uses `osnet_x1_0_sportsmot`, an OSNet x1.0 fine-tuned on SportsMOT train (see [encoders](../guides/reid.md#choosing-an-encoder)). ```yaml @@ -256,8 +270,9 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I ByteTrack row -> docs/trackers/bytetrack.md (table), docs/index.md (Algorithms table), README.md (Algorithms table) OC-SORT row -> docs/trackers/ocsort.md (table), docs/index.md (Algorithms table), README.md (Algorithms table) BoT-SORT row -> docs/trackers/botsort.md (table), docs/index.md (Algorithms table), README.md (Algorithms table), docs/trackers/mcbyte.md (BoT-SORT row, "SoccerNet" tab) - C-BIoU row -> docs/trackers/cbiou.md (table), README.md (Algorithms table only) + C-BIoU row -> docs/trackers/cbiou.md (table), docs/index.md (Algorithms table), README.md (Algorithms table) McByte row -> docs/trackers/mcbyte.md (McByte row, "SoccerNet" tab), docs/index.md (Algorithms table + FAQ "leads every benchmark" claim), README.md (Algorithms table) + BoT-SORT + ReID row -> docs/index.md (Algorithms table), README.md (Algorithms table) Change any cell above -> update every listed location + re-check FAQ leader claim still true. --> === "Tuned" @@ -273,6 +288,12 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I | BoT-SORT | 85.0 | 79.7 | 97.2 | | BoT-SORT + ReID | **88.4** | **84.4** | **99.3** | + + Tuned configuration for each tracker. The ReID row uses `osnet_x1_0_soccernet`, an OSNet x1.0 fine-tuned on SoccerNet train (see [encoders](../guides/reid.md#choosing-an-encoder)). ```yaml @@ -363,8 +384,9 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art SORT/ByteTrack/OC-SORT/BoT-SORT rows -> docs/index.md (Algorithms table, HOTA col only), README.md (Algorithms table, HOTA col only). Note: docs/trackers/{sort,bytetrack,ocsort,botsort}.md intentionally have NO DanceTrack row — don't add one, that's existing scope not a gap. BoT-SORT row also -> docs/trackers/mcbyte.md (BoT-SORT row, "DanceTrack" tab) - C-BIoU row -> docs/trackers/cbiou.md (table, DanceTrack row), README.md (Algorithms table) + C-BIoU row -> docs/trackers/cbiou.md (table, DanceTrack row), docs/index.md (Algorithms table), README.md (Algorithms table) McByte row -> docs/trackers/mcbyte.md (McByte row, "DanceTrack" tab), docs/index.md (Algorithms table + FAQ "leads every benchmark" claim), README.md (Algorithms table) + BoT-SORT + ReID row -> docs/index.md (Algorithms table), README.md (Algorithms table) Change any cell above -> update every listed location + re-check FAQ leader claim still true. --> === "Tuned" @@ -380,6 +402,12 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art | BoT-SORT | 57.8 | 57.9 | 92.2 | | BoT-SORT + ReID | **60.8** | **61.6** | 91.7 | + + Best configuration for each tracker. The ReID row uses `osnet_x1_0_dancetrack`, an OSNet x1.0 fine-tuned on DanceTrack train (see [encoders](../guides/reid.md#choosing-an-encoder)). ```yaml diff --git a/docs/index.md b/docs/index.md index 3b108d7c..944d6678 100644 --- a/docs/index.md +++ b/docs/index.md @@ -121,7 +121,7 @@ For the full evaluation workflow, see the [evaluation guide](evaluations/evaluat Clean, modular implementations of leading trackers. All HOTA scores use default parameters. - + | Algorithm | Description | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | DanceTrack HOTA | | :-------------------------------------------: | :---------------------------------------------------------------------: | :--------: | :------------: | :------------: | :-------------: | From 6cc733d290e3132637be98239d8d552cacfdb63e Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Mon, 5 Oct 2026 14:35:24 -0300 Subject: [PATCH 47/49] docs: swap the benchmark footnote markers and clarify the BoT-SORT + ReID note Co-Authored-By: Claude Opus 5.5 --- README.md | 26 +++++++++++++------------- docs/index.md | 22 +++++++++++----------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index c8737385..2777372c 100644 --- a/README.md +++ b/README.md @@ -87,19 +87,19 @@ Each tracker below is a faithful implementation of its original paper's motion a -| Algorithm | Description | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | DanceTrack HOTA | -| :-------------------------------------------------------------------: | :------------------------------------------------------------------------------: | :--------: | :------------: | :------------: | :-------------: | -| [SORT](https://arxiv.org/abs/1602.00763) | Kalman filter + Hungarian matching baseline. | 58.4 | 70.8 | 81.6 | 47.2 | -| [ByteTrack](https://arxiv.org/abs/2110.06864) | Two-stage association using high and low confidence detections. | 60.1 | 73.0 | 84.0 | 53.3 | -| [OC-SORT](https://arxiv.org/abs/2203.14360) | Observation-centric recovery for lost tracks. | 61.9 | 71.7 | 78.4 | 54.1 | -| [C-BIoU](https://arxiv.org/abs/2211.14317) | Cascaded buffered IoU matching for fast or irregular motion. | 63.0 | 73.1 | 82.6 | 56.7 | -| [BoT-SORT](https://arxiv.org/abs/2206.14651) | Camera motion compensation. | 63.7 | 73.8 | 84.5 | 57.8 | -| [BoT-SORT + ReID](https://trackers.roboflow.com/develop/guides/reid/) | Appearance embeddings fused with motion, for harder association.\*\* | 63.9 | 75.5 | 84.3 | 57.0 | -| [McByte](https://arxiv.org/abs/2506.01373) | Mask-conditioned tracking — adds propagated SAM/Cutie masks as a matching cue.\* | **64.1** | **76.5** | **85.0** | **67.2** | - -\*McByte needs optional heavyweight deps (`torch`, SAM, Cutie) not installed by default. It tops HOTA on all four benchmarks above — see the [McByte docs](https://trackers.roboflow.com/develop/trackers/mcbyte/) for setup. - -\*\*BoT-SORT + ReID is listed at library defaults with the default encoder `fastreid_mot17_sbs50`, BoT-SORT's own ReID model trained on MOT17. At its best tuned configuration on each test set it reaches 64.6, 77.8, 88.4 and 60.8 HOTA, above BoT-SORT on all four. See the [ReID guide](https://trackers.roboflow.com/develop/guides/reid/). +| Algorithm | Description | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | DanceTrack HOTA | +| :-------------------------------------------------------------------: | :--------------------------------------------------------------------------------: | :--------: | :------------: | :------------: | :-------------: | +| [SORT](https://arxiv.org/abs/1602.00763) | Kalman filter + Hungarian matching baseline. | 58.4 | 70.8 | 81.6 | 47.2 | +| [ByteTrack](https://arxiv.org/abs/2110.06864) | Two-stage association using high and low confidence detections. | 60.1 | 73.0 | 84.0 | 53.3 | +| [OC-SORT](https://arxiv.org/abs/2203.14360) | Observation-centric recovery for lost tracks. | 61.9 | 71.7 | 78.4 | 54.1 | +| [C-BIoU](https://arxiv.org/abs/2211.14317) | Cascaded buffered IoU matching for fast or irregular motion. | 63.0 | 73.1 | 82.6 | 56.7 | +| [BoT-SORT](https://arxiv.org/abs/2206.14651) | Camera motion compensation. | 63.7 | 73.8 | 84.5 | 57.8 | +| [BoT-SORT + ReID](https://trackers.roboflow.com/develop/guides/reid/) | Appearance embeddings fused with motion, for harder association.\* | 63.9 | 75.5 | 84.3 | 57.0 | +| [McByte](https://arxiv.org/abs/2506.01373) | Mask-conditioned tracking — adds propagated SAM/Cutie masks as a matching cue.\*\* | **64.1** | **76.5** | **85.0** | **67.2** | + +\*BoT-SORT + ReID is listed with library default parameters and the default encoder `fastreid_mot17_sbs50` (BoT-SORT's own ReID model, trained on MOT17) on all four datasets. At its best tuned configuration on each dataset, it reaches 64.6 (MOT17), 77.8 (SportsMOT), 88.4 (SoccerNet) and 60.8 (DanceTrack) HOTA on the test sets, above BoT-SORT without ReID on all four. See the [ReID guide](https://trackers.roboflow.com/develop/guides/reid/). + +\*\*McByte needs optional heavyweight deps (`torch`, SAM, Cutie) not installed by default. It tops HOTA on all four benchmarks above. See the [McByte docs](https://trackers.roboflow.com/develop/trackers/mcbyte/) for setup. All scores use default parameters on the standard split. Detections come from a YOLOX detector (MOT17, SportsMOT, DanceTrack) or oracle ground-truth boxes (SoccerNet) — absolute numbers shift with detector quality. See the [tracker comparison](https://trackers.roboflow.com/develop/evaluations/results/) for tuned numbers and methodology. diff --git a/docs/index.md b/docs/index.md index 944d6678..086b7889 100644 --- a/docs/index.md +++ b/docs/index.md @@ -123,21 +123,21 @@ Clean, modular implementations of leading trackers. All HOTA scores use default -| Algorithm | Description | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | DanceTrack HOTA | -| :-------------------------------------------: | :---------------------------------------------------------------------: | :--------: | :------------: | :------------: | :-------------: | -| [SORT](https://arxiv.org/abs/1602.00763) | Kalman filter + Hungarian matching baseline. | 58.4 | 70.8 | 81.6 | 47.2 | -| [ByteTrack](https://arxiv.org/abs/2110.06864) | Two-stage association using high and low confidence detections. | 60.1 | 73.0 | 84.0 | 53.3 | -| [OC-SORT](https://arxiv.org/abs/2203.14360) | Observation-centric recovery for lost tracks. | 61.9 | 71.7 | 78.4 | 54.1 | -| [C-BIoU](https://arxiv.org/abs/2211.14317) | Cascaded buffered IoU matching for fast or irregular motion. | 63.0 | 73.1 | 82.6 | 56.7 | -| [BoT-SORT](https://arxiv.org/abs/2206.14651) | Camera motion compensation. | 63.7 | 73.8 | 84.5 | 57.8 | -| [BoT-SORT + ReID](guides/reid.md) | Appearance embeddings fused with motion, for harder association.\*\* | 63.9 | 75.5 | 84.3 | 57.0 | -| [McByte](trackers/mcbyte.md) | Mask-conditioned tracking — adds propagated SAM/Cutie masks as a cue.\* | **64.1** | **76.5** | **85.0** | **67.2** | +| Algorithm | Description | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | DanceTrack HOTA | +| :-------------------------------------------: | :-----------------------------------------------------------------------: | :--------: | :------------: | :------------: | :-------------: | +| [SORT](https://arxiv.org/abs/1602.00763) | Kalman filter + Hungarian matching baseline. | 58.4 | 70.8 | 81.6 | 47.2 | +| [ByteTrack](https://arxiv.org/abs/2110.06864) | Two-stage association using high and low confidence detections. | 60.1 | 73.0 | 84.0 | 53.3 | +| [OC-SORT](https://arxiv.org/abs/2203.14360) | Observation-centric recovery for lost tracks. | 61.9 | 71.7 | 78.4 | 54.1 | +| [C-BIoU](https://arxiv.org/abs/2211.14317) | Cascaded buffered IoU matching for fast or irregular motion. | 63.0 | 73.1 | 82.6 | 56.7 | +| [BoT-SORT](https://arxiv.org/abs/2206.14651) | Camera motion compensation. | 63.7 | 73.8 | 84.5 | 57.8 | +| [BoT-SORT + ReID](guides/reid.md) | Appearance embeddings fused with motion, for harder association.\* | 63.9 | 75.5 | 84.3 | 57.0 | +| [McByte](trackers/mcbyte.md) | Mask-conditioned tracking — adds propagated SAM/Cutie masks as a cue.\*\* | **64.1** | **76.5** | **85.0** | **67.2** | !!! note - \*McByte needs optional heavyweight deps (`torch`, SAM, Cutie) not installed by default. It tops HOTA on all four benchmarks above — see the [McByte docs](trackers/mcbyte.md) for setup. + \*BoT-SORT + ReID is listed with library default parameters and the default encoder `fastreid_mot17_sbs50` (BoT-SORT's own ReID model, trained on MOT17) on all four datasets. At its best tuned configuration on each dataset, it reaches 64.6 (MOT17), 77.8 (SportsMOT), 88.4 (SoccerNet) and 60.8 (DanceTrack) HOTA on the test sets, above BoT-SORT without ReID on all four. See the [ReID guide](guides/reid.md). - \*\*BoT-SORT + ReID is listed at library defaults with the default encoder `fastreid_mot17_sbs50`, BoT-SORT's own ReID model trained on MOT17. At its best tuned configuration on each test set it reaches 64.6, 77.8, 88.4 and 60.8 HOTA, above BoT-SORT on all four. See the [ReID guide](guides/reid.md). + \*\*McByte needs optional heavyweight deps (`torch`, SAM, Cutie) not installed by default. It tops HOTA on all four benchmarks above. See the [McByte docs](trackers/mcbyte.md) for setup. For detailed benchmarks and tuned configurations, see the [tracker comparison](evaluations/results.md). From e4c41f86dad9540830fe8816ed8fa79bfb850e6c Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Mon, 5 Oct 2026 15:19:52 -0300 Subject: [PATCH 48/49] docs(reid): drop the MOT20 mention from the fusion weights note Co-Authored-By: Claude Opus 5.5 --- docs/guides/reid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/reid.md b/docs/guides/reid.md index 3c807f2e..da809722 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -113,7 +113,7 @@ In more detail: Two consequences are worth knowing before switching: - **The similarity range changes.** `"botsort"` returns values in `[0, 1]`; `"adaptive"` returns `[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]`, which is `[0, 2.25]` at the defaults. Two thresholds are applied to that fused value: `minimum_iou_threshold_first_assoc`, which matches detections to confirmed and lost tracks, and `minimum_iou_threshold_unconfirmed_assoc`, which matches what is left to tracks that are not confirmed yet. Both are a different gate under the other method, so retune them when you switch, or the comparison measures the gate rather than the fusion. `minimum_iou_threshold_second_assoc` is unaffected, because that step matches on IoU alone. -- **The weights are domain-dependent.** The Deep OC-SORT paper uses `reid_appearance_weight=0.75` with `reid_adaptive_weight_cap=0.5` for MOT17 and MOT20, and `1.25` with `1.0` for DanceTrack, where dancers occlude constantly and geometry carries less. The defaults here follow the MOT17/MOT20 pair. +- **The weights are domain-dependent.** The Deep OC-SORT paper uses `reid_appearance_weight=0.75` with `reid_adaptive_weight_cap=0.5` for MOT17, and `1.25` with `1.0` for DanceTrack, where dancers occlude constantly and geometry carries less. The defaults here follow the MOT17 pair. With the gate open, `adaptive` keeps IDs more stable than `"botsort"`, which switches between look-alike candidates from one frame to the next. `"botsort"` stays the default because it is the safer choice: appearance is only used below `reid_appearance_threshold`, and turning it on does not require retuning the other thresholds. From 59b2d1776a5edb770f602be281fa598c674c0561 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Sat, 17 Oct 2026 11:35:23 -0300 Subject: [PATCH 49/49] refactor(reid): integrate adaptive fusion with ReIDConfig --- docs/evaluations/results.md | 36 +++--- docs/guides/reid.md | 176 +++++++++++++-------------- src/trackers/core/botsort/tracker.py | 108 ++++------------ src/trackers/core/reid/config.py | 34 ++++-- tests/cli/test_track.py | 21 ++++ tests/cli/test_tune.py | 10 +- tests/core/test_botsort_reid.py | 65 +++------- tests/core/test_reid_config.py | 38 +++++- 8 files changed, 240 insertions(+), 248 deletions(-) diff --git a/docs/evaluations/results.md b/docs/evaluations/results.md index 74c8ab2e..8de10aa0 100644 --- a/docs/evaluations/results.md +++ b/docs/evaluations/results.md @@ -73,7 +73,7 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests also appears in the README.md and docs/index.md BoT-SORT + ReID footnote Change any cell above -> update every listed location. --> - Tuned configuration for each tracker. The ReID row uses `fastreid_mot17_sbs50`, trained on MOT17 by the BoT-SORT authors, with `reid_fusion="adaptive"`: the best MOT17 test result in the [ReID guide](../guides/reid.md#results). It was chosen on test; on val-half, `botsort` scored 0.10 higher. + Tuned configuration for each tracker. The ReID row uses `fastreid_mot17_sbs50`, trained on MOT17 by the BoT-SORT authors, with `reid_config.fusion="adaptive"`: the best MOT17 test result in the [ReID guide](../guides/reid.md#results). It was chosen on test; on val-half, `botsort` scored 0.10 higher. ```yaml SORT: @@ -121,11 +121,11 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests BoT-SORT + ReID: reid_model: fastreid_mot17_sbs50 - reid_fusion: adaptive - reid_appearance_weight: 1.908 - reid_adaptive_weight_cap: 0.545 - reid_appearance_floor: 0.381 - reid_proximity_threshold: 0.772 + reid_config.fusion: adaptive + reid_config.appearance_weight: 1.908 + reid_config.adaptive_weight_cap: 0.545 + reid_config.appearance_floor: 0.381 + reid_config.proximity_threshold: 0.772 minimum_iou_threshold_first_assoc: 0.846 ``` @@ -233,9 +233,9 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar BoT-SORT + ReID: reid_model: osnet_x1_0_sportsmot - reid_fusion: botsort - reid_appearance_threshold: 0.3425 - reid_proximity_threshold: 0.9953 + reid_config.fusion: botsort + reid_config.appearance_threshold: 0.3425 + reid_config.proximity_threshold: 0.9953 ``` ## [SoccerNet-tracking](https://arxiv.org/abs/2204.06918) @@ -342,11 +342,11 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I BoT-SORT + ReID: reid_model: osnet_x1_0_soccernet - reid_fusion: adaptive - reid_appearance_weight: 0.75 - reid_adaptive_weight_cap: 0.5 - reid_proximity_threshold: 1.0 - reid_appearance_floor: 0.8 + reid_config.fusion: adaptive + reid_config.appearance_weight: 0.75 + reid_config.adaptive_weight_cap: 0.5 + reid_config.proximity_threshold: 1.0 + reid_config.appearance_floor: 0.8 ``` !!! note "SoccerNet buffer ordering exception" @@ -456,9 +456,9 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art BoT-SORT + ReID: reid_model: osnet_x1_0_dancetrack - reid_fusion: botsort - reid_appearance_threshold: 0.4716 - reid_proximity_threshold: 0.6802 + reid_config.fusion: botsort + reid_config.appearance_threshold: 0.4716 + reid_config.proximity_threshold: 0.6802 ``` !!! note "DanceTrack buffer ordering exception" @@ -479,7 +479,7 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art **C-BIoU** targets fast or irregular motion when you want buffered, cascaded geometric matching without camera motion compensation. In these benchmarks it leads on SoccerNet among the motion-only trackers when tuned, and reaches the highest tuned IDF1 and MOTA on DanceTrack among the motion-only trackers. Use C-BIoU when BoT-SORT-style association is a good fit but CMC is unavailable or harmful, or when plain IoU matching is too strict. See [C-BIoU](../trackers/cbiou.md) for buffer scales **b1** and **b2**. -**BoT-SORT + ReID** adds an appearance encoder to BoT-SORT's association; enable it with `reid_model=`. Appearance helps where overlap alone cannot tell candidates apart. It can only relink a track that is still kept as lost, for `lost_track_buffer` frames (30 by default, about one second), and at the default `reid_proximity_threshold` it only scores pairs that already overlap. Recovering a target that leaves the frame takes a fully open gate and a buffer long enough to cover the absence, see [Choosing a proximity threshold](../guides/reid.md#choosing-a-proximity-threshold). The gain depends on how well the encoder separates your targets: largest with an encoder trained on footage like yours, smallest when targets look alike. Tune the ReID thresholds on your data with [`trackers tune`](../guides/tune.md#tune-reid-thresholds). +**BoT-SORT + ReID** adds an appearance encoder to BoT-SORT's association; enable it with `reid_model=`. Appearance helps where overlap alone cannot tell candidates apart. It can only relink a track that is still kept as lost, for `lost_track_buffer` frames (30 by default, about one second), and at the default `reid_config.proximity_threshold` it only scores pairs that already overlap. Recovering a target that leaves the frame takes a fully open gate and a buffer long enough to cover the absence, see [Choosing a proximity threshold](../guides/reid.md#choosing-a-proximity-threshold). The gain depends on how well the encoder separates your targets: largest with an encoder trained on footage like yours, smallest when targets look alike. Tune the ReID thresholds on your data with [`trackers tune`](../guides/tune.md#tune-reid-thresholds). **McByte** is the choice when you want the highest identity stability and can afford optional SAM/Cutie mask dependencies. It extends BoT-SORT-style association with temporally propagated segmentation masks as an extra matching cue, requiring no per-video tuning. Reported against Trackers' BoT-SORT baseline (without re-identification) at default parameters, McByte improves HOTA and IDF1 on all four datasets in this comparison, with the largest gain on DanceTrack. Use McByte when identity consistency matters more than raw throughput or dependency footprint. See [McByte](../trackers/mcbyte.md) for setup and the mask-conditioning parameters. diff --git a/docs/guides/reid.md b/docs/guides/reid.md index da809722..3a48e2ce 100644 --- a/docs/guides/reid.md +++ b/docs/guides/reid.md @@ -11,7 +11,7 @@ BoT-SORT can fuse appearance embeddings with IoU during association. Embeddings - How to enable appearance association on BoT-SORT - Which parameters control the appearance gate -- How to pick `reid_appearance_threshold` for your encoder and domain +- How to pick `reid_config.appearance_threshold` for your encoder and domain - What ReID changes on MOT17, SportsMOT, DanceTrack and SoccerNet, with a generic and a fine-tuned encoder --- @@ -33,22 +33,22 @@ For extra contents and other options, see the [install guide](install.md). ```python from reid import ReIDModel - from trackers import BoTSORTTracker + from trackers import BoTSORTTracker, ReIDConfig reid_model = ReIDModel.from_pretrained("fastreid_mot17_sbs50") - tracker = BoTSORTTracker(reid_model=reid_model, reid_appearance_threshold=0.2) + tracker = BoTSORTTracker(reid_model=reid_model, reid_config=ReIDConfig(appearance_threshold=0.2)) ``` === "CLI" - `--reid.model` takes a curated alias, an `hf://` URL, or a local path, and enables appearance association on its own. Tracker parameters keep their Python names, so the appearance gate is `--tracker.reid_appearance_threshold`. See the [CLI reference](cli.md) for every argument. + `--reid.model` takes a curated alias, an `hf://` URL, or a local path, and enables appearance association on its own. Tracker parameters keep their Python names, so the appearance gate is `--tracker.reid_config.appearance_threshold`. See the [CLI reference](cli.md) for every argument. ```bash trackers track \ --source \ --tracker botsort \ --reid.model fastreid_mot17_sbs50 \ - --tracker.reid_appearance_threshold 0.2 \ + --tracker.reid_config.appearance_threshold 0.2 \ --output.video output.mp4 ``` @@ -62,16 +62,16 @@ Fine-tuning an encoder on your own data is coming to the `reid` package. ## Key Parameters -| Parameter | Purpose | Tuning guidance | -| :-------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | -| `reid_ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | -| `reid_appearance_threshold` | Maximum appearance distance `d_app` for appearance to lower a pair's matching cost. | BoT-SORT paper default 0.25. Calibrate per encoder and domain, see below. | -| `reid_proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | -| `reid_fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | -| `reid_appearance_weight` | Base appearance weight when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | -| `reid_adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_appearance_weight`. | -| `reid_appearance_floor` | Minimum cosine similarity for appearance to contribute when `reid_fusion="adaptive"`; below it a pair is scored on geometry alone. Ignored otherwise. | Default 0.0 (off, as in the Deep OC-SORT fusion). Calibrate per encoder: 0.8 with `reid_proximity_threshold=1.0` is the value for `osnet_x1_0` fine-tuned on SoccerNet and loses HOTA with `fastreid_mot17_sbs50`, see [choosing a fusion method](#choosing-a-fusion-method). | +| Parameter | Purpose | Tuning guidance | +| :--------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------- | +| `reid_model` | Appearance encoder queried during association. | Leave unset for IoU and CMC only. Pick a checkpoint trained on your object domain where possible. | +| `reid_config.ema_alpha` | EMA momentum for a track's appearance feature. | Default 0.9. Higher keeps a stable long-term identity; lower adapts faster to appearance change but drifts more. | +| `reid_config.appearance_threshold` | Maximum appearance distance for appearance to lower a pair's matching cost with `"botsort"`. | Default 0.25 (BoT-SORT paper). Calibrate per encoder and domain, see below. | +| `reid_config.proximity_threshold` | IoU gate applied before appearance (`IoU ≥ 1 - reid_config.proximity_threshold`), from true IoU even with GIoU/DIoU/CIoU. | Default 0.5. Raise to 1.0 where targets leave the frame and return, see [below](#choosing-a-proximity-threshold). | +| `reid_config.fusion` | How appearance combines with geometry: `"botsort"` takes the minimum of the two costs, `"adaptive"` adds a weighted appearance term. | Default `"botsort"`. See [choosing a fusion method](#choosing-a-fusion-method) before switching. | +| `reid_config.appearance_weight` | Base appearance weight when `reid_config.fusion="adaptive"`. Ignored otherwise. | Default 0.75. Raise where geometry is unreliable, see below. | +| `reid_config.adaptive_weight_cap` | Ceiling on the adaptive bonus when `reid_config.fusion="adaptive"`. Ignored otherwise. | Default 0.5. Raise together with `reid_config.appearance_weight`. | +| `reid_config.appearance_floor` | Minimum cosine similarity for appearance to contribute with `"adaptive"`; below it a pair is scored on geometry alone. | Default 0.0 (off, as in Deep OC-SORT). Calibrate per encoder; 0.8 is used for the tuned SoccerNet result below. | --- @@ -88,12 +88,12 @@ Each encoder has its own distance scale, so choose the thresholds for the encode ## Choosing a fusion method -On every frame, BoT-SORT gives each track and detection pair a score and then picks the best matching. With ReID enabled, that score combines two signals: how much the boxes overlap, and how alike the two crops look. `reid_fusion` sets how they are combined: +On every frame, BoT-SORT gives each track and detection pair a score and then picks the best matching. With ReID enabled, that score combines two signals: how much the boxes overlap, and how alike the two crops look. `reid_config.fusion` sets how they are combined: ```python tracker = BoTSORTTracker( reid_model=reid_model, - reid_fusion="adaptive", # default: "botsort" + reid_config=ReIDConfig(fusion="adaptive"), # default: "botsort" ) ``` @@ -102,28 +102,28 @@ tracker = BoTSORTTracker( In more detail: -**`"botsort"`.** Appearance is only used for a pair when its distance is below `reid_appearance_threshold` and the boxes pass the `reid_proximity_threshold` check. Otherwise the pair is scored on box overlap alone. +**`"botsort"`.** Appearance is only used for a pair when its distance is below `reid_config.appearance_threshold` and the boxes pass the `reid_config.proximity_threshold` check. Otherwise the pair is scored on box overlap alone. -**`"adaptive"`.** This is the appearance fusion from Deep OC-SORT. Each pair starts from its box overlap, and appearance is added on top: `score = box overlap + (reid_appearance_weight + bonus) * appearance similarity`. The bonus rewards a clear winner. For a track, it is how much more its most similar detection looks like it than the second most similar one; for a detection, the same against its two most similar tracks. Each margin is capped at `reid_adaptive_weight_cap`, and the pair's bonus is their average. So appearance counts more when one candidate clearly stands out, and less when several look alike. Negative similarities count as zero, so appearance can only raise a score. `reid_appearance_floor`, added by this library, sets the lowest appearance similarity that counts: below it, a pair is scored on box overlap alone. With the proximity gate open, this stops a lost track from taking an unrelated detection that only looks vaguely similar. +**`"adaptive"`.** This is the appearance fusion from Deep OC-SORT. Each pair starts from its box overlap, and appearance is added on top: `score = box overlap + (reid_config.appearance_weight + bonus) * appearance similarity`. The bonus rewards a clear winner. For a track, it is how much more its most similar detection looks like it than the second most similar one; for a detection, the same against its two most similar tracks. Each margin is capped at `reid_config.adaptive_weight_cap`, and the pair's bonus is their average. So appearance counts more when one candidate clearly stands out, and less when several look alike. Negative similarities count as zero, so appearance can only raise a score. `reid_config.appearance_floor`, added by this library, sets the lowest appearance similarity that counts: below it, a pair is scored on box overlap alone. With the proximity gate open, this stops a lost track from taking an unrelated detection that only looks vaguely similar. !!! note "What `adaptive` includes" - Only the appearance fusion is taken from Deep OC-SORT; the rest of that tracker is not added to BoT-SORT. `reid_appearance_threshold` has no effect with `reid_fusion="adaptive"`. + Only the appearance fusion is taken from Deep OC-SORT; the rest of that tracker is not added to BoT-SORT. `reid_config.appearance_threshold` has no effect with `reid_config.fusion="adaptive"`. Two consequences are worth knowing before switching: -- **The similarity range changes.** `"botsort"` returns values in `[0, 1]`; `"adaptive"` returns `[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]`, which is `[0, 2.25]` at the defaults. Two thresholds are applied to that fused value: `minimum_iou_threshold_first_assoc`, which matches detections to confirmed and lost tracks, and `minimum_iou_threshold_unconfirmed_assoc`, which matches what is left to tracks that are not confirmed yet. Both are a different gate under the other method, so retune them when you switch, or the comparison measures the gate rather than the fusion. `minimum_iou_threshold_second_assoc` is unaffected, because that step matches on IoU alone. -- **The weights are domain-dependent.** The Deep OC-SORT paper uses `reid_appearance_weight=0.75` with `reid_adaptive_weight_cap=0.5` for MOT17, and `1.25` with `1.0` for DanceTrack, where dancers occlude constantly and geometry carries less. The defaults here follow the MOT17 pair. +- **The similarity range changes.** `"botsort"` returns values in `[0, 1]`; `"adaptive"` returns `[0, 1 + reid_config.appearance_weight + reid_config.adaptive_weight_cap]`, which is `[0, 2.25]` at the defaults. Two thresholds are applied to that fused value: `minimum_iou_threshold_first_assoc`, which matches detections to confirmed and lost tracks, and `minimum_iou_threshold_unconfirmed_assoc`, which matches what is left to tracks that are not confirmed yet. Both are a different gate under the other method, so retune them when you switch, or the comparison measures the gate rather than the fusion. `minimum_iou_threshold_second_assoc` is unaffected, because that step matches on IoU alone. +- **The weights are domain-dependent.** The Deep OC-SORT paper uses `reid_config.appearance_weight=0.75` with `reid_config.adaptive_weight_cap=0.5` for MOT17, and `1.25` with `1.0` for DanceTrack, where dancers occlude constantly and geometry carries less. The defaults here follow the MOT17 pair. -With the gate open, `adaptive` keeps IDs more stable than `"botsort"`, which switches between look-alike candidates from one frame to the next. `"botsort"` stays the default because it is the safer choice: appearance is only used below `reid_appearance_threshold`, and turning it on does not require retuning the other thresholds. +With the gate open, `adaptive` keeps IDs more stable than `"botsort"`, which switches between look-alike candidates from one frame to the next. `"botsort"` stays the default because it is the safer choice: appearance is only used below `reid_config.appearance_threshold`, and turning it on does not require retuning the other thresholds. --- ## Choosing an appearance threshold -This threshold only applies to `reid_fusion="botsort"`; with `"adaptive"` it has no effect. +This threshold only applies to `reid_config.fusion="botsort"`; with `"adaptive"` it has no effect. -`reid_appearance_threshold` is how different two crops may look and still count as the same target. Appearance is compared as a distance, `0.5 * (1 - cosine similarity)`, running from 0 for identical crops to 1 for opposite ones; the BoT-SORT paper uses 0.25. Below the threshold, looking alike makes the tracker more willing to pair the two; above it, the pair stands on box overlap alone. Appearance only ever lowers a pair's cost, never raises it, but the assignment weighs every pair at once, so a look-alike pair can still take a detection away from a track that merely overlapped it. +`reid_config.appearance_threshold` is how different two crops may look and still count as the same target. Appearance is compared as a distance, `0.5 * (1 - cosine similarity)`, running from 0 for identical crops to 1 for opposite ones; the BoT-SORT paper uses 0.25. Below the threshold, looking alike makes the tracker more willing to pair the two; above it, the pair stands on box overlap alone. Appearance only ever lowers a pair's cost, never raises it, but the assignment weighs every pair at once, so a look-alike pair can still take a detection away from a track that merely overlapped it. The right value depends on the encoder and the footage, so measure it on labeled data: @@ -159,10 +159,10 @@ Blue bars are pairs of the same person, red bars are pairs of different people. This is what `rates_at` prints for two thresholds: -| `reid_appearance_threshold` | same-person pairs below | different-person pairs below | -| :-------------------------- | :---------------------: | :--------------------------: | -| 0.20 | 68% | 1.1% | -| 0.25 (paper default) | 79% | 2.9% | +| `reid_config.appearance_threshold` | same-person pairs below | different-person pairs below | +| :--------------------------------- | :---------------------: | :--------------------------: | +| 0.20 | 68% | 1.1% | +| 0.25 (paper default) | 79% | 2.9% | Moving the threshold right accepts more same-person pairs, but it also starts letting different-person pairs through. We use 0.2, the same value as the [MOT17 re-ID study](https://www-sop.inria.fr/members/Francois.Bremond/Postscript/Tomasz__SCCAI_2025.pdf) (Table 8). @@ -188,7 +188,7 @@ On MOT17 val, pairs of different people stay about as far apart at every gap. Pa From this we learn: - **Check the threshold at the gap you care about.** A threshold that works one frame apart can fail after a lost track: at 0.2 it matches 98% of same-person pairs one frame apart, but only 51% across the default 30-frame `lost_track_buffer`. -- **Long gaps make people harder to tell apart.** The same person looks less like themselves after a long absence, so same-person and different-person distances overlap more: ROC AUC, explained below, falls from 0.998 at a 1-frame gap to 0.854 beyond 120 frames. With the threshold at 0.2, appearance stops recognizing most people after a long absence: only 29% of same-person pairs more than 120 frames apart are under 0.2, against 98% one frame apart. Raising the threshold to catch those also lets more different people through, so if you increase `lost_track_buffer`, check both rates at that gap before raising `reid_appearance_threshold`. +- **Long gaps make people harder to tell apart.** The same person looks less like themselves after a long absence, so same-person and different-person distances overlap more: ROC AUC, explained below, falls from 0.998 at a 1-frame gap to 0.854 beyond 120 frames. With the threshold at 0.2, appearance stops recognizing most people after a long absence: only 29% of same-person pairs more than 120 frames apart are under 0.2, against 98% one frame apart. Raising the threshold to catch those also lets more different people through, so if you increase `lost_track_buffer`, check both rates at that gap before raising `reid_config.appearance_threshold`. ??? info "How to read ROC AUC, and the numbers per frame gap" @@ -212,18 +212,18 @@ From this we learn: ## Choosing a proximity threshold -`reid_proximity_threshold` decides which track-detection pairs appearance is allowed to score. A pair is dropped before appearance is consulted whenever `1 - IoU` exceeds the threshold, so the 0.5 default limits appearance to pairs that already overlap at `IoU >= 0.5`, and 0.99 still requires `IoU >= 0.01`. Only 1.0 disables the gate. +`reid_config.proximity_threshold` decides which track-detection pairs appearance is allowed to score. A pair is dropped before appearance is consulted whenever `1 - IoU` exceeds the threshold, so the 0.5 default limits appearance to pairs that already overlap at `IoU >= 0.5`, and 0.99 still requires `IoU >= 0.01`. Only 1.0 disables the gate. While a track is lost it has no detection of its own, so it is matched through its estimated position, which keeps moving as if the target had carried on. After a short occlusion that estimate has drifted, so it barely overlaps the detection when the target reappears. After the target leaves the frame it drifts out of view entirely and the overlap drops to zero. The 0.5 default blocks appearance in both cases, 0.99 lets it score the first one, and only 1.0 lets it score the second. BoT-SORT has no separate step for targets that leave and return, so opening the gate is the only way to keep their ID. -SoccerNet test, oracle detections, `osnet_x1_0_soccernet`, library-default motion parameters, `reid_fusion="adaptive"` with its default weights: +SoccerNet test, oracle detections, `osnet_x1_0_soccernet`, library-default motion parameters, `reid_config.fusion="adaptive"` with its default weights: -| `reid_proximity_threshold` | appearance consulted when | HOTA | IDF1 | ID switches | -| :------------------------- | :------------------------ | :---: | :---: | :---------: | -| none (no ReID) | | 84.56 | 79.35 | 2939 | -| 0.5 (default) | `IoU >= 0.50` | 84.52 | 79.27 | 3012 | -| 0.8 | `IoU >= 0.20` | 85.02 | 79.71 | 3268 | -| 1.0 | always | 85.46 | 81.18 | 1734 | +| `reid_config.proximity_threshold` | appearance consulted when | HOTA | IDF1 | ID switches | +| :-------------------------------- | :------------------------ | :---: | :---: | :---------: | +| none (no ReID) | | 84.56 | 79.35 | 2939 | +| 0.5 (default) | `IoU >= 0.50` | 84.52 | 79.27 | 3012 | +| 0.8 | `IoU >= 0.20` | 85.02 | 79.71 | 3268 | +| 1.0 | always | 85.46 | 81.18 | 1734 |