From cb47bb2787e4ef72175670361c68bd8fb5251fc1 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 14 May 2026 13:48:57 -0300 Subject: [PATCH 01/23] feat: add CBIoUTracker (BoT-SORT with CMC disabled and BIoU association) - New `CBIoUTracker` subclass of `BoTSORTTracker` registered as "cbiou" - CMC permanently disabled; `buffer_ratio` elevated as first-class param - `update()` emits UserWarning when frame is passed (CMC off) - `search_space` covers all tunable params + buffer_ratio [0.0, 0.5] - 13 dedicated tests in tests/core/test_cbiou_tracker.py - `IOC_TRACKER_IDS` in shared_ids.py excludes CBIoU from iou= injection test - Exported from top-level trackers package Co-authored-by: Cursor --- .plans/active/plan_add-cbiou-tracker.md | 126 +++++++++++++++ src/trackers/__init__.py | 2 + src/trackers/core/cbiou/__init__.py | 9 ++ src/trackers/core/cbiou/tracker.py | 130 +++++++++++++++ tests/core/shared_ids.py | 6 +- tests/core/test_cbiou_tracker.py | 201 ++++++++++++++++++++++++ tests/core/test_registration.py | 7 + tests/core/test_trackers.py | 4 +- 8 files changed, 482 insertions(+), 3 deletions(-) create mode 100644 .plans/active/plan_add-cbiou-tracker.md create mode 100644 src/trackers/core/cbiou/__init__.py create mode 100644 src/trackers/core/cbiou/tracker.py create mode 100644 tests/core/test_cbiou_tracker.py diff --git a/.plans/active/plan_add-cbiou-tracker.md b/.plans/active/plan_add-cbiou-tracker.md new file mode 100644 index 00000000..c94b1c36 --- /dev/null +++ b/.plans/active/plan_add-cbiou-tracker.md @@ -0,0 +1,126 @@ +# Plan: Add CBIoU Tracker (BoTSORT + no CMC + BIoU association) + +## Brief + +Add `CBIoUTracker` as a thin `BoTSORTTracker` subclass with CMC permanently disabled and `BIoU` as the association metric, registering it as `"cbiou"` in the tracker registry with its own `search_space`, full test coverage, and public export — keeping all changes self-contained in a new `cbiou/` module. + +Classification : feature +Complexity : small +Affected files : 6 files across 2 modules (core + tests) +Key risks : none — BIoU already exists, BoTSORT accepts `iou=` and `enable_cmc=` +Agent review : ✓ agents ready (0 corrections incorporated) + +| # | Step | What changes | Stop condition | +|---|------|--------------|----------------| +| 1 | Create `src/trackers/core/cbiou/` module | New `__init__.py` + `tracker.py` with `CBIoUTracker` | Class instantiates cleanly | +| 2 | Export from package `__init__.py` | Add import + `__all__` entry for `CBIoUTracker` | `from trackers import CBIoUTracker` works | +| 3 | Register in shared test IDs | `ALL_TRACKER_IDS += ["cbiou"]` | Generic tracker tests include "cbiou" | +| 4 | Update `test_registration.py` explicit list | Add `CBIoUTracker` to `TestSearchSpaceValidation` loop | `search_space` validation passes | +| 5 | Add CBIoU-specific test file | `tests/core/test_cbiou_tracker.py` covering CMC-disabled, BIoU behavior, `buffer_ratio` forwarding | All tests green | + +--- + +## Full Plan + +**Classification**: feature +**Complexity**: small +**Date**: 2026-05-14 + +### Goal + +Introduce `CBIoUTracker` — a registered, tunable, fully-tested MOT tracker that is exactly BoT-SORT with CMC disabled and BIoU (Buffered IoU) as the association metric. The BIoU `buffer_ratio` is elevated to a first-class constructor parameter. CMC-related parameters (`enable_cmc`, `cmc_method`, `cmc_downscale`) are hidden from the public signature since they have no effect. The tracker is exported from the top-level `trackers` package and participates in all generic tracker contract tests via `ALL_TRACKER_IDS`. + +### Affected files + +- `src/trackers/core/cbiou/__init__.py` — new package init (empty re-export) +- `src/trackers/core/cbiou/tracker.py` — `CBIoUTracker(BoTSORTTracker)` subclass +- `src/trackers/__init__.py` — add `CBIoUTracker` import + `__all__` entry +- `tests/core/shared_ids.py` — add `"cbiou"` to `ALL_TRACKER_IDS` +- `tests/core/test_registration.py` — add `CBIoUTracker` to `TestSearchSpaceValidation` explicit tracker loop +- `tests/core/test_cbiou_tracker.py` — new CBIoU-specific tests (new file) + +### Design sketch + +```python +# src/trackers/core/cbiou/tracker.py + +class CBIoUTracker(BoTSORTTracker): + """BoT-SORT with CMC disabled and BIoU association (Buffered IoU). + + Identical to BoTSORTTracker but permanently disables camera motion + compensation and uses BIoU (Buffered IoU) for all association steps. + The buffer_ratio parameter controls how much each bounding box is + expanded before computing IoU. + """ + + tracker_id = "cbiou" + search_space: ClassVar[dict[str, dict]] = { + "lost_track_buffer": {"type": "randint", "range": [10, 91]}, + "track_activation_threshold": {"type": "uniform", "range": [0.1, 0.9]}, + "minimum_iou_threshold_first_assoc": {"type": "uniform", "range": [0.05, 0.7]}, + "minimum_iou_threshold_second_assoc": {"type": "uniform", "range": [0.05, 0.7]}, + "minimum_iou_threshold_unconfirmed_assoc": {"type": "uniform", "range": [0.05, 0.7]}, + "high_conf_det_threshold": {"type": "uniform", "range": [0.3, 0.8]}, + "minimum_consecutive_frames": {"type": "randint", "range": [1, 4]}, + "buffer_ratio": {"type": "uniform", "range": [0.0, 0.5]}, + } + + def __init__( + self, + lost_track_buffer: int = 30, + frame_rate: float = 30.0, + track_activation_threshold: float = 0.7, + minimum_consecutive_frames: int = 2, + minimum_iou_threshold_first_assoc: float = 0.2, + minimum_iou_threshold_second_assoc: float = 0.5, + minimum_iou_threshold_unconfirmed_assoc: float = 0.3, + high_conf_det_threshold: float = 0.6, + instant_first_frame_activation: bool = True, + state_estimator_class: type[BaseStateEstimator] = XCYCWHStateEstimator, + buffer_ratio: float = 0.1, + ) -> None: + super().__init__( + lost_track_buffer=lost_track_buffer, + frame_rate=frame_rate, + track_activation_threshold=track_activation_threshold, + minimum_consecutive_frames=minimum_consecutive_frames, + minimum_iou_threshold_first_assoc=minimum_iou_threshold_first_assoc, + minimum_iou_threshold_second_assoc=minimum_iou_threshold_second_assoc, + minimum_iou_threshold_unconfirmed_assoc=minimum_iou_threshold_unconfirmed_assoc, + high_conf_det_threshold=high_conf_det_threshold, + enable_cmc=False, + instant_first_frame_activation=instant_first_frame_activation, + state_estimator_class=state_estimator_class, + iou=BIoU(buffer_ratio=buffer_ratio), + ) + self.buffer_ratio = buffer_ratio +``` + +### Test coverage plan + +`tests/core/test_cbiou_tracker.py` should cover: + +1. **CMC is always off**: passing `frame=...` to `update()` triggers `UserWarning` (via `_warn_if_frame_unused` from BoTSORT's CMC-disabled path) +2. **BIoU association tolerance**: a near-miss detection (slightly outside track's predicted region) that would be missed by standard IoU *is* associated by CBIoU (buffer expansion closes the gap) +3. **buffer_ratio forwarded**: `tracker.iou.buffer_ratio == buffer_ratio` after construction +4. **buffer_ratio=0 recovers standard IoU**: behavior identical to BoTSORT(enable_cmc=False) when buffer_ratio=0 +5. **Registration**: `"cbiou"` in `BaseTracker._registered_trackers()` (covered generically via shared_ids) + +### Risks + +- None: the design reuses existing infrastructure (BoTSORT + BIoU), no new algorithms or dependencies needed. + +### Follow-up command + +/develop feature add CBIoU tracker (BoTSORT with CMC disabled and BIoU association) + +--- + +## Confidence +**Score**: 0.95 — high +**Gaps**: +- BIoU `normalize_for_fusion` returns values in [0,1] (same as IoU), so the score-fusion path in BoTSORT steps 1 & 3 remains numerically valid. +- The `_warn_if_frame_unused` warning path: BoTSORT only calls `_warn_if_frame_unused` if `enable_cmc=False` AND frame is provided; need to verify this in the existing code. (Checked: BoTSORT does NOT call `_warn_if_frame_unused` — it simply skips the CMC block. The warning test should instead assert that `frame` is ignored silently, or we add a `_warn_if_frame_unused` call in CBIoU's `update` override.) + +**Refinements**: 1 pass. +- Pass 1: Verified BIoU, BoTSORT, BaseTracker APIs against source. Confirmed no breaking changes. diff --git a/src/trackers/__init__.py b/src/trackers/__init__.py index a02c147f..c318a1fd 100644 --- a/src/trackers/__init__.py +++ b/src/trackers/__init__.py @@ -9,6 +9,7 @@ from trackers.annotators.trace import MotionAwareTraceAnnotator from trackers.core.botsort.tracker import BoTSORTTracker from trackers.core.bytetrack.tracker import ByteTrackTracker +from trackers.core.cbiou.tracker import CBIoUTracker from trackers.core.ocsort.tracker import OCSORTTracker from trackers.core.sort.tracker import SORTTracker from trackers.datasets.download import download_dataset @@ -31,6 +32,7 @@ "BaseIoU", "BoTSORTTracker", "ByteTrackTracker", + "CBIoUTracker", "CIoU", "CMCConfig", "CMCMethod", diff --git a/src/trackers/core/cbiou/__init__.py b/src/trackers/core/cbiou/__init__.py new file mode 100644 index 00000000..a926a689 --- /dev/null +++ b/src/trackers/core/cbiou/__init__.py @@ -0,0 +1,9 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from trackers.core.cbiou.tracker import CBIoUTracker + +__all__ = ["CBIoUTracker"] diff --git a/src/trackers/core/cbiou/tracker.py b/src/trackers/core/cbiou/tracker.py new file mode 100644 index 00000000..19d4228b --- /dev/null +++ b/src/trackers/core/cbiou/tracker.py @@ -0,0 +1,130 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from typing import ClassVar + +import numpy as np +import supervision as sv + +from trackers.core.botsort.tracker import BoTSORTTracker +from trackers.utils.iou import BIoU +from trackers.utils.state_representations import BaseStateEstimator, XCYCWHStateEstimator + + +class CBIoUTracker(BoTSORTTracker): + """BoT-SORT with CMC disabled and Buffered IoU (BIoU) association. + + CBIoU is identical to :class:`~trackers.core.botsort.tracker.BoTSORTTracker` + with two fixed differences: + + 1. **Camera Motion Compensation is permanently off.** This makes the + tracker faster and avoids relying on frame pixel data, which is + convenient when only detection files are available (e.g. standard + MOT benchmarks). + 2. **BIoU replaces standard IoU** for all association steps. Each + bounding box is expanded by ``buffer_ratio`` relative to its own + width/height before IoU is computed, giving the matcher more + tolerance for small localization gaps between the Kalman prediction + and the incoming detection. + + Args: + lost_track_buffer: Time buffer (in frames at 30 FPS) for keeping + lost tracks alive before deletion. Scaled by ``frame_rate``. + frame_rate: Video frame rate used to scale the lost track buffer. + track_activation_threshold: Minimum detection confidence to spawn + a new track. + minimum_consecutive_frames: Number of successful updates required + before assigning a stable track ID. + minimum_iou_threshold_first_assoc: Minimum fused similarity to + accept an association during the first association step. + minimum_iou_threshold_second_assoc: Minimum fused similarity to + accept an association during the second association step. + minimum_iou_threshold_unconfirmed_assoc: Minimum fused similarity + to accept a match between an unconfirmed track and a remaining + high-confidence detection. + high_conf_det_threshold: Confidence threshold that splits + detections into high / low confidence groups. + instant_first_frame_activation: If ``True`` (default), tracks + spawned on the very first frame receive a real tracker ID + immediately. + state_estimator_class: State estimator class for tracklets. + Defaults to ``XCYCWHStateEstimator``. + buffer_ratio: Non-negative relative margin by which each bounding + box is expanded before IoU is computed. ``0.0`` recovers + standard IoU exactly; larger values tolerate wider localization + gaps. Forwarded to :class:`~trackers.utils.iou.BIoU`. + + Notes: + - CMC parameters (``enable_cmc``, ``cmc_method``, ``cmc_downscale``) + are intentionally absent from this class's signature — CMC is + always disabled. + - Passing a ``frame`` argument to :meth:`update` emits a + ``UserWarning`` because no CMC processing takes place. + """ + + tracker_id = "cbiou" + search_space: ClassVar[dict[str, dict]] = { + "lost_track_buffer": {"type": "randint", "range": [10, 91]}, + "track_activation_threshold": {"type": "uniform", "range": [0.1, 0.9]}, + "minimum_iou_threshold_first_assoc": {"type": "uniform", "range": [0.05, 0.7]}, + "minimum_iou_threshold_second_assoc": {"type": "uniform", "range": [0.05, 0.7]}, + "minimum_iou_threshold_unconfirmed_assoc": { + "type": "uniform", + "range": [0.05, 0.7], + }, + "high_conf_det_threshold": {"type": "uniform", "range": [0.3, 0.8]}, + "minimum_consecutive_frames": {"type": "randint", "range": [1, 4]}, + "buffer_ratio": {"type": "uniform", "range": [0.0, 0.5]}, + } + + def __init__( + self, + lost_track_buffer: int = 30, + frame_rate: float = 30.0, + track_activation_threshold: float = 0.7, + minimum_consecutive_frames: int = 2, + minimum_iou_threshold_first_assoc: float = 0.2, + minimum_iou_threshold_second_assoc: float = 0.5, + minimum_iou_threshold_unconfirmed_assoc: float = 0.3, + high_conf_det_threshold: float = 0.6, + instant_first_frame_activation: bool = True, + state_estimator_class: type[BaseStateEstimator] = XCYCWHStateEstimator, + buffer_ratio: float = 0.1, + ) -> None: + super().__init__( + lost_track_buffer=lost_track_buffer, + frame_rate=frame_rate, + track_activation_threshold=track_activation_threshold, + minimum_consecutive_frames=minimum_consecutive_frames, + minimum_iou_threshold_first_assoc=minimum_iou_threshold_first_assoc, + minimum_iou_threshold_second_assoc=minimum_iou_threshold_second_assoc, + minimum_iou_threshold_unconfirmed_assoc=minimum_iou_threshold_unconfirmed_assoc, + high_conf_det_threshold=high_conf_det_threshold, + enable_cmc=False, + instant_first_frame_activation=instant_first_frame_activation, + state_estimator_class=state_estimator_class, + iou=BIoU(buffer_ratio=buffer_ratio), + ) + self.buffer_ratio = buffer_ratio + + def update( + self, + detections: sv.Detections, + frame: np.ndarray | None = None, + ) -> sv.Detections: + """Update the tracker with detections from the current frame. + + Args: + detections: Supervision detections for the current frame. + frame: Unused — CBIoU never performs CMC. Passing a non-``None`` + value emits a ``UserWarning``. + + Returns: + New ``sv.Detections`` with ``tracker_id`` assigned for each + detection. + """ + self._warn_if_frame_unused(frame) + return super().update(detections=detections, frame=None) diff --git a/tests/core/shared_ids.py b/tests/core/shared_ids.py index 8e962c8d..45eba79f 100644 --- a/tests/core/shared_ids.py +++ b/tests/core/shared_ids.py @@ -6,4 +6,8 @@ """Shared test constants for tracker IDs used across test/core files.""" -ALL_TRACKER_IDS = ["sort", "bytetrack", "ocsort", "botsort"] +ALL_TRACKER_IDS = ["sort", "bytetrack", "ocsort", "botsort", "cbiou"] + +# Trackers that accept a user-supplied ``iou=`` constructor argument. +# CBIoU is intentionally excluded: it is opinionated and always uses BIoU. +IOC_TRACKER_IDS = [tid for tid in ALL_TRACKER_IDS if tid != "cbiou"] diff --git a/tests/core/test_cbiou_tracker.py b/tests/core/test_cbiou_tracker.py new file mode 100644 index 00000000..ba81d2c3 --- /dev/null +++ b/tests/core/test_cbiou_tracker.py @@ -0,0 +1,201 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +"""CBIoU-specific tracker tests. + +Generic lifecycle / reset / tracked_objects / mutation contracts are +covered for all trackers in test_trackers.py via ALL_TRACKER_IDS. +This file covers CBIoU-specific invariants: + - CMC is always disabled (frame argument triggers UserWarning) + - buffer_ratio is correctly forwarded to BIoU + - BIoU association is more tolerant of near-miss detections than plain IoU + - buffer_ratio=0.0 produces the same results as BoTSORT(enable_cmc=False) +""" + +from __future__ import annotations + +import warnings + +import numpy as np +import pytest +import supervision as sv + +from trackers.core.botsort.tracker import BoTSORTTracker +from trackers.core.cbiou.tracker import CBIoUTracker +from trackers.utils.iou import BIoU + + +def _detection(xyxy: tuple[float, float, float, float], conf: float = 0.9) -> sv.Detections: + return sv.Detections( + xyxy=np.array([xyxy], dtype=np.float32), + confidence=np.array([conf], dtype=np.float32), + ) + + +def _make_frame(h: int = 480, w: int = 640, seed: int = 42) -> np.ndarray: + rng = np.random.default_rng(seed) + return rng.integers(0, 255, (h, w, 3), dtype=np.uint8) + + +class TestCBIoUConstruction: + def test_default_construction(self) -> None: + tracker = CBIoUTracker() + assert tracker is not None + assert isinstance(tracker.iou, BIoU) + + def test_buffer_ratio_forwarded_to_biou(self) -> None: + tracker = CBIoUTracker(buffer_ratio=0.25) + assert isinstance(tracker.iou, BIoU) + assert tracker.iou.buffer_ratio == pytest.approx(0.25) + + def test_buffer_ratio_stored_on_tracker(self) -> None: + tracker = CBIoUTracker(buffer_ratio=0.15) + assert tracker.buffer_ratio == pytest.approx(0.15) + + def test_cmc_is_disabled(self) -> None: + tracker = CBIoUTracker() + assert tracker.enable_cmc is False + assert tracker.cmc is None + + def test_tracker_id(self) -> None: + assert CBIoUTracker.tracker_id == "cbiou" + + def test_invalid_buffer_ratio_raises(self) -> None: + with pytest.raises(ValueError, match="buffer_ratio"): + CBIoUTracker(buffer_ratio=-0.01) + + +class TestCBIoUFrameWarning: + """Passing a frame to update() must emit UserWarning (CMC is disabled).""" + + def test_frame_triggers_warning(self) -> None: + tracker = CBIoUTracker() + frame = _make_frame() + det = _detection((100.0, 100.0, 200.0, 200.0)) + with pytest.warns(UserWarning): + tracker.update(det, frame=frame) + + def test_no_warning_without_frame(self) -> None: + tracker = CBIoUTracker() + det = _detection((100.0, 100.0, 200.0, 200.0)) + with warnings.catch_warnings(): + warnings.simplefilter("error") + tracker.update(det) + + +class TestCBIoUAssociationTolerance: + """BIoU should associate near-miss detections that plain IoU would miss.""" + + def test_near_miss_associated_with_buffer(self) -> None: + """ + A track initialized at box A, then a detection at box B just outside + should be associated by CBIoU (buffer expands boxes) but not by + BoTSORT with standard IoU (tight threshold). + + Box A: [0, 0, 100, 100] (100x100) + Box B: [110, 0, 210, 100] (gap of 10px = 10% of width) + With buffer_ratio=0.15 each side expands by 15px, so A becomes + [-15, -15, 115, 115] and B becomes [93.5, -15, 226.5, 115] — + they now overlap. + """ + # Frame 1: spawn a track at box A with high confidence + cbiou = CBIoUTracker( + buffer_ratio=0.15, + minimum_consecutive_frames=1, + track_activation_threshold=0.5, + minimum_iou_threshold_first_assoc=0.05, + ) + botsort = BoTSORTTracker( + enable_cmc=False, + minimum_consecutive_frames=1, + track_activation_threshold=0.5, + minimum_iou_threshold_first_assoc=0.05, + ) + + box_a = (0.0, 0.0, 100.0, 100.0) + box_b = (110.0, 0.0, 210.0, 100.0) + + for tracker in (cbiou, botsort): + tracker.update(_detection(box_a, conf=0.9)) + + # Frame 2: detection slightly outside A — CBIoU buffer closes the gap + cbiou_result = cbiou.update(_detection(box_b, conf=0.9)) + botsort_result = botsort.update(_detection(box_b, conf=0.9)) + + cbiou_ids = cbiou_result.tracker_id + botsort_ids = botsort_result.tracker_id + + # CBIoU should reuse the existing track (buffer closes the gap) + # — exactly one output detection with a confirmed (>=0) ID + assert cbiou_ids is not None and len(cbiou_ids) == 1 + assert cbiou_ids[0] >= 0, "CBIoU should have associated the near-miss detection" + + # CBIoU's confirmed ID on frame 2 must equal the one it assigned on frame 1 + cbiou_frame1 = cbiou.tracks[0].tracker_id + assert cbiou_ids[0] == cbiou_frame1, ( + "CBIoU should reuse the existing track ID, not spawn a new one" + ) + + # BoTSORT with standard IoU: boxes don't overlap, so old track goes lost + # and a new unconfirmed track is spawned (tracker_id == -1 or a fresh ID) + assert botsort_ids is not None + # The important behavioral difference: BoTSORT should NOT continue the + # original track (it can't see the gap-crossing detection as a match) + if len(botsort_ids) > 0: + botsort_frame1_track_id = next( + (t.tracker_id for t in botsort.tracks), None + ) + # Original BoTSORT track should be gone or unmatched + assert botsort_ids[0] != cbiou_frame1 or botsort_ids[0] == -1, ( + "BoTSORT standard IoU should not have matched the near-miss box" + ) + + def test_zero_buffer_behaves_like_standard_iou(self) -> None: + """With buffer_ratio=0 CBIoU produces identical results to BoTSORT(no CMC).""" + cbiou = CBIoUTracker( + buffer_ratio=0.0, + minimum_consecutive_frames=2, + track_activation_threshold=0.7, + ) + botsort = BoTSORTTracker( + enable_cmc=False, + minimum_consecutive_frames=2, + track_activation_threshold=0.7, + ) + + detections = [ + _detection((50.0, 50.0, 150.0, 150.0), conf=0.9), + _detection((55.0, 55.0, 155.0, 155.0), conf=0.9), + _detection((60.0, 60.0, 160.0, 160.0), conf=0.9), + ] + + for det in detections: + r_cbiou = cbiou.update(det) + r_botsort = botsort.update(det) + # Both should produce the same number of outputs + assert len(r_cbiou) == len(r_botsort), ( + f"CBIoU(buffer=0) and BoTSORT(no CMC) diverged: " + f"cbiou={len(r_cbiou)}, botsort={len(r_botsort)}" + ) + + +class TestCBIoUSearchSpace: + def test_buffer_ratio_in_search_space(self) -> None: + assert "buffer_ratio" in CBIoUTracker.search_space + + def test_no_cmc_params_in_search_space(self) -> None: + ss = CBIoUTracker.search_space + assert "enable_cmc" not in ss + assert "cmc_method" not in ss + assert "cmc_downscale" not in ss + + def test_search_space_buffer_ratio_range(self) -> None: + spec = CBIoUTracker.search_space["buffer_ratio"] + assert spec["type"] == "uniform" + low, high = spec["range"] + assert low >= 0.0 + assert high <= 1.0 + assert low < high diff --git a/tests/core/test_registration.py b/tests/core/test_registration.py index 4299f704..775e3ada 100644 --- a/tests/core/test_registration.py +++ b/tests/core/test_registration.py @@ -236,6 +236,7 @@ def test_tracker_is_registered(self, tracker_id: str) -> None: from trackers import ( # noqa: F401 BoTSORTTracker, ByteTrackTracker, + CBIoUTracker, OCSORTTracker, SORTTracker, ) @@ -247,6 +248,7 @@ def test_lookup_tracker(self, tracker_id: str) -> None: from trackers import ( # noqa: F401 BoTSORTTracker, ByteTrackTracker, + CBIoUTracker, OCSORTTracker, SORTTracker, ) @@ -265,6 +267,7 @@ def test_registered_trackers_returns_sorted_list(self) -> None: from trackers import ( # noqa: F401 BoTSORTTracker, ByteTrackTracker, + CBIoUTracker, OCSORTTracker, SORTTracker, ) @@ -276,6 +279,8 @@ def test_registered_trackers_returns_sorted_list(self) -> None: @pytest.mark.parametrize("tracker_id", ALL_TRACKER_IDS) def test_tracker_params_have_descriptions(self, tracker_id: str) -> None: + from trackers import CBIoUTracker # noqa: F401 + info = BaseTracker._lookup_tracker(tracker_id) assert info is not None @@ -291,6 +296,7 @@ def test_search_space_keys_match_init_params(self) -> None: from trackers import ( BoTSORTTracker, ByteTrackTracker, + CBIoUTracker, OCSORTTracker, SORTTracker, ) @@ -300,6 +306,7 @@ def test_search_space_keys_match_init_params(self) -> None: SORTTracker, OCSORTTracker, BoTSORTTracker, + CBIoUTracker, ): init_params = set(inspect.signature(tracker_cls.__init__).parameters) - {"self"} for key in tracker_cls.search_space: diff --git a/tests/core/test_trackers.py b/tests/core/test_trackers.py index 61ae2120..5f9e8864 100644 --- a/tests/core/test_trackers.py +++ b/tests/core/test_trackers.py @@ -30,7 +30,7 @@ from trackers.core.sort.tracker import SORTTracker from trackers.utils.iou import BaseIoU -from .shared_ids import ALL_TRACKER_IDS +from .shared_ids import ALL_TRACKER_IDS, IOC_TRACKER_IDS # --------------------------------------------------------------------------- # Helpers @@ -112,7 +112,7 @@ def test_tracker_update_empty_does_not_mutate_input(tracker_id: str) -> None: assert result is not dets, "update() must return a new sv.Detections instance" -@pytest.mark.parametrize("tracker_id", ALL_TRACKER_IDS) +@pytest.mark.parametrize("tracker_id", IOC_TRACKER_IDS) def test_tracker_uses_configured_iou_variant(tracker_id: str) -> None: """Trackers should use the configured IoU implementation for matching.""" tracking_iou = _TrackingIoU() From 0bcbe35009a30076535f47ae6c5be6c08e906dd4 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 14 May 2026 16:04:29 -0300 Subject: [PATCH 02/23] refactor: simplify cbiou __init__.py to header-only --- src/trackers/core/cbiou/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/trackers/core/cbiou/__init__.py b/src/trackers/core/cbiou/__init__.py index a926a689..20c42480 100644 --- a/src/trackers/core/cbiou/__init__.py +++ b/src/trackers/core/cbiou/__init__.py @@ -4,6 +4,4 @@ # Licensed under the Apache License, Version 2.0 [see LICENSE for details] # ------------------------------------------------------------------------ -from trackers.core.cbiou.tracker import CBIoUTracker -__all__ = ["CBIoUTracker"] From e1a6cf6415a5e8486bba7eac102aa224b85d5997 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 19:06:50 +0000 Subject: [PATCH 03/23] =?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 --- .plans/active/plan_add-cbiou-tracker.md | 24 ++++++++++++++---------- src/trackers/core/cbiou/__init__.py | 2 -- tests/core/test_cbiou_tracker.py | 11 +++-------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/.plans/active/plan_add-cbiou-tracker.md b/.plans/active/plan_add-cbiou-tracker.md index c94b1c36..bb0e629f 100644 --- a/.plans/active/plan_add-cbiou-tracker.md +++ b/.plans/active/plan_add-cbiou-tracker.md @@ -5,18 +5,18 @@ Add `CBIoUTracker` as a thin `BoTSORTTracker` subclass with CMC permanently disabled and `BIoU` as the association metric, registering it as `"cbiou"` in the tracker registry with its own `search_space`, full test coverage, and public export — keeping all changes self-contained in a new `cbiou/` module. Classification : feature -Complexity : small +Complexity : small Affected files : 6 files across 2 modules (core + tests) -Key risks : none — BIoU already exists, BoTSORT accepts `iou=` and `enable_cmc=` -Agent review : ✓ agents ready (0 corrections incorporated) +Key risks : none — BIoU already exists, BoTSORT accepts `iou=` and `enable_cmc=` +Agent review : ✓ agents ready (0 corrections incorporated) -| # | Step | What changes | Stop condition | -|---|------|--------------|----------------| -| 1 | Create `src/trackers/core/cbiou/` module | New `__init__.py` + `tracker.py` with `CBIoUTracker` | Class instantiates cleanly | -| 2 | Export from package `__init__.py` | Add import + `__all__` entry for `CBIoUTracker` | `from trackers import CBIoUTracker` works | -| 3 | Register in shared test IDs | `ALL_TRACKER_IDS += ["cbiou"]` | Generic tracker tests include "cbiou" | -| 4 | Update `test_registration.py` explicit list | Add `CBIoUTracker` to `TestSearchSpaceValidation` loop | `search_space` validation passes | -| 5 | Add CBIoU-specific test file | `tests/core/test_cbiou_tracker.py` covering CMC-disabled, BIoU behavior, `buffer_ratio` forwarding | All tests green | +| # | Step | What changes | Stop condition | +| --- | ------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------- | +| 1 | Create `src/trackers/core/cbiou/` module | New `__init__.py` + `tracker.py` with `CBIoUTracker` | Class instantiates cleanly | +| 2 | Export from package `__init__.py` | Add import + `__all__` entry for `CBIoUTracker` | `from trackers import CBIoUTracker` works | +| 3 | Register in shared test IDs | `ALL_TRACKER_IDS += ["cbiou"]` | Generic tracker tests include "cbiou" | +| 4 | Update `test_registration.py` explicit list | Add `CBIoUTracker` to `TestSearchSpaceValidation` loop | `search_space` validation passes | +| 5 | Add CBIoU-specific test file | `tests/core/test_cbiou_tracker.py` covering CMC-disabled, BIoU behavior, `buffer_ratio` forwarding | All tests green | --- @@ -44,6 +44,7 @@ Introduce `CBIoUTracker` — a registered, tunable, fully-tested MOT tracker tha ```python # src/trackers/core/cbiou/tracker.py + class CBIoUTracker(BoTSORTTracker): """BoT-SORT with CMC disabled and BIoU association (Buffered IoU). @@ -117,10 +118,13 @@ class CBIoUTracker(BoTSORTTracker): --- ## Confidence + **Score**: 0.95 — high **Gaps**: + - BIoU `normalize_for_fusion` returns values in [0,1] (same as IoU), so the score-fusion path in BoTSORT steps 1 & 3 remains numerically valid. - The `_warn_if_frame_unused` warning path: BoTSORT only calls `_warn_if_frame_unused` if `enable_cmc=False` AND frame is provided; need to verify this in the existing code. (Checked: BoTSORT does NOT call `_warn_if_frame_unused` — it simply skips the CMC block. The warning test should instead assert that `frame` is ignored silently, or we add a `_warn_if_frame_unused` call in CBIoU's `update` override.) **Refinements**: 1 pass. + - Pass 1: Verified BIoU, BoTSORT, BaseTracker APIs against source. Confirmed no breaking changes. diff --git a/src/trackers/core/cbiou/__init__.py b/src/trackers/core/cbiou/__init__.py index 20c42480..57226e88 100644 --- a/src/trackers/core/cbiou/__init__.py +++ b/src/trackers/core/cbiou/__init__.py @@ -3,5 +3,3 @@ # Copyright (c) 2026 Roboflow. All Rights Reserved. # Licensed under the Apache License, Version 2.0 [see LICENSE for details] # ------------------------------------------------------------------------ - - diff --git a/tests/core/test_cbiou_tracker.py b/tests/core/test_cbiou_tracker.py index ba81d2c3..c177d779 100644 --- a/tests/core/test_cbiou_tracker.py +++ b/tests/core/test_cbiou_tracker.py @@ -135,9 +135,7 @@ def test_near_miss_associated_with_buffer(self) -> None: # CBIoU's confirmed ID on frame 2 must equal the one it assigned on frame 1 cbiou_frame1 = cbiou.tracks[0].tracker_id - assert cbiou_ids[0] == cbiou_frame1, ( - "CBIoU should reuse the existing track ID, not spawn a new one" - ) + assert cbiou_ids[0] == cbiou_frame1, "CBIoU should reuse the existing track ID, not spawn a new one" # BoTSORT with standard IoU: boxes don't overlap, so old track goes lost # and a new unconfirmed track is spawned (tracker_id == -1 or a fresh ID) @@ -145,9 +143,7 @@ def test_near_miss_associated_with_buffer(self) -> None: # The important behavioral difference: BoTSORT should NOT continue the # original track (it can't see the gap-crossing detection as a match) if len(botsort_ids) > 0: - botsort_frame1_track_id = next( - (t.tracker_id for t in botsort.tracks), None - ) + botsort_frame1_track_id = next((t.tracker_id for t in botsort.tracks), None) # Original BoTSORT track should be gone or unmatched assert botsort_ids[0] != cbiou_frame1 or botsort_ids[0] == -1, ( "BoTSORT standard IoU should not have matched the near-miss box" @@ -177,8 +173,7 @@ def test_zero_buffer_behaves_like_standard_iou(self) -> None: r_botsort = botsort.update(det) # Both should produce the same number of outputs assert len(r_cbiou) == len(r_botsort), ( - f"CBIoU(buffer=0) and BoTSORT(no CMC) diverged: " - f"cbiou={len(r_cbiou)}, botsort={len(r_botsort)}" + f"CBIoU(buffer=0) and BoTSORT(no CMC) diverged: cbiou={len(r_cbiou)}, botsort={len(r_botsort)}" ) From a4ab8483bd015a99e74f622229f68b44ab03799c Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 14 May 2026 16:07:29 -0300 Subject: [PATCH 04/23] Remove plan file from version control Plans are working documents and should not be tracked in the repository. Co-authored-by: Cursor --- .plans/active/plan_add-cbiou-tracker.md | 130 ------------------------ 1 file changed, 130 deletions(-) delete mode 100644 .plans/active/plan_add-cbiou-tracker.md diff --git a/.plans/active/plan_add-cbiou-tracker.md b/.plans/active/plan_add-cbiou-tracker.md deleted file mode 100644 index bb0e629f..00000000 --- a/.plans/active/plan_add-cbiou-tracker.md +++ /dev/null @@ -1,130 +0,0 @@ -# Plan: Add CBIoU Tracker (BoTSORT + no CMC + BIoU association) - -## Brief - -Add `CBIoUTracker` as a thin `BoTSORTTracker` subclass with CMC permanently disabled and `BIoU` as the association metric, registering it as `"cbiou"` in the tracker registry with its own `search_space`, full test coverage, and public export — keeping all changes self-contained in a new `cbiou/` module. - -Classification : feature -Complexity : small -Affected files : 6 files across 2 modules (core + tests) -Key risks : none — BIoU already exists, BoTSORT accepts `iou=` and `enable_cmc=` -Agent review : ✓ agents ready (0 corrections incorporated) - -| # | Step | What changes | Stop condition | -| --- | ------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------- | -| 1 | Create `src/trackers/core/cbiou/` module | New `__init__.py` + `tracker.py` with `CBIoUTracker` | Class instantiates cleanly | -| 2 | Export from package `__init__.py` | Add import + `__all__` entry for `CBIoUTracker` | `from trackers import CBIoUTracker` works | -| 3 | Register in shared test IDs | `ALL_TRACKER_IDS += ["cbiou"]` | Generic tracker tests include "cbiou" | -| 4 | Update `test_registration.py` explicit list | Add `CBIoUTracker` to `TestSearchSpaceValidation` loop | `search_space` validation passes | -| 5 | Add CBIoU-specific test file | `tests/core/test_cbiou_tracker.py` covering CMC-disabled, BIoU behavior, `buffer_ratio` forwarding | All tests green | - ---- - -## Full Plan - -**Classification**: feature -**Complexity**: small -**Date**: 2026-05-14 - -### Goal - -Introduce `CBIoUTracker` — a registered, tunable, fully-tested MOT tracker that is exactly BoT-SORT with CMC disabled and BIoU (Buffered IoU) as the association metric. The BIoU `buffer_ratio` is elevated to a first-class constructor parameter. CMC-related parameters (`enable_cmc`, `cmc_method`, `cmc_downscale`) are hidden from the public signature since they have no effect. The tracker is exported from the top-level `trackers` package and participates in all generic tracker contract tests via `ALL_TRACKER_IDS`. - -### Affected files - -- `src/trackers/core/cbiou/__init__.py` — new package init (empty re-export) -- `src/trackers/core/cbiou/tracker.py` — `CBIoUTracker(BoTSORTTracker)` subclass -- `src/trackers/__init__.py` — add `CBIoUTracker` import + `__all__` entry -- `tests/core/shared_ids.py` — add `"cbiou"` to `ALL_TRACKER_IDS` -- `tests/core/test_registration.py` — add `CBIoUTracker` to `TestSearchSpaceValidation` explicit tracker loop -- `tests/core/test_cbiou_tracker.py` — new CBIoU-specific tests (new file) - -### Design sketch - -```python -# src/trackers/core/cbiou/tracker.py - - -class CBIoUTracker(BoTSORTTracker): - """BoT-SORT with CMC disabled and BIoU association (Buffered IoU). - - Identical to BoTSORTTracker but permanently disables camera motion - compensation and uses BIoU (Buffered IoU) for all association steps. - The buffer_ratio parameter controls how much each bounding box is - expanded before computing IoU. - """ - - tracker_id = "cbiou" - search_space: ClassVar[dict[str, dict]] = { - "lost_track_buffer": {"type": "randint", "range": [10, 91]}, - "track_activation_threshold": {"type": "uniform", "range": [0.1, 0.9]}, - "minimum_iou_threshold_first_assoc": {"type": "uniform", "range": [0.05, 0.7]}, - "minimum_iou_threshold_second_assoc": {"type": "uniform", "range": [0.05, 0.7]}, - "minimum_iou_threshold_unconfirmed_assoc": {"type": "uniform", "range": [0.05, 0.7]}, - "high_conf_det_threshold": {"type": "uniform", "range": [0.3, 0.8]}, - "minimum_consecutive_frames": {"type": "randint", "range": [1, 4]}, - "buffer_ratio": {"type": "uniform", "range": [0.0, 0.5]}, - } - - def __init__( - self, - lost_track_buffer: int = 30, - frame_rate: float = 30.0, - track_activation_threshold: float = 0.7, - minimum_consecutive_frames: int = 2, - minimum_iou_threshold_first_assoc: float = 0.2, - minimum_iou_threshold_second_assoc: float = 0.5, - minimum_iou_threshold_unconfirmed_assoc: float = 0.3, - high_conf_det_threshold: float = 0.6, - instant_first_frame_activation: bool = True, - state_estimator_class: type[BaseStateEstimator] = XCYCWHStateEstimator, - buffer_ratio: float = 0.1, - ) -> None: - super().__init__( - lost_track_buffer=lost_track_buffer, - frame_rate=frame_rate, - track_activation_threshold=track_activation_threshold, - minimum_consecutive_frames=minimum_consecutive_frames, - minimum_iou_threshold_first_assoc=minimum_iou_threshold_first_assoc, - minimum_iou_threshold_second_assoc=minimum_iou_threshold_second_assoc, - minimum_iou_threshold_unconfirmed_assoc=minimum_iou_threshold_unconfirmed_assoc, - high_conf_det_threshold=high_conf_det_threshold, - enable_cmc=False, - instant_first_frame_activation=instant_first_frame_activation, - state_estimator_class=state_estimator_class, - iou=BIoU(buffer_ratio=buffer_ratio), - ) - self.buffer_ratio = buffer_ratio -``` - -### Test coverage plan - -`tests/core/test_cbiou_tracker.py` should cover: - -1. **CMC is always off**: passing `frame=...` to `update()` triggers `UserWarning` (via `_warn_if_frame_unused` from BoTSORT's CMC-disabled path) -2. **BIoU association tolerance**: a near-miss detection (slightly outside track's predicted region) that would be missed by standard IoU *is* associated by CBIoU (buffer expansion closes the gap) -3. **buffer_ratio forwarded**: `tracker.iou.buffer_ratio == buffer_ratio` after construction -4. **buffer_ratio=0 recovers standard IoU**: behavior identical to BoTSORT(enable_cmc=False) when buffer_ratio=0 -5. **Registration**: `"cbiou"` in `BaseTracker._registered_trackers()` (covered generically via shared_ids) - -### Risks - -- None: the design reuses existing infrastructure (BoTSORT + BIoU), no new algorithms or dependencies needed. - -### Follow-up command - -/develop feature add CBIoU tracker (BoTSORT with CMC disabled and BIoU association) - ---- - -## Confidence - -**Score**: 0.95 — high -**Gaps**: - -- BIoU `normalize_for_fusion` returns values in [0,1] (same as IoU), so the score-fusion path in BoTSORT steps 1 & 3 remains numerically valid. -- The `_warn_if_frame_unused` warning path: BoTSORT only calls `_warn_if_frame_unused` if `enable_cmc=False` AND frame is provided; need to verify this in the existing code. (Checked: BoTSORT does NOT call `_warn_if_frame_unused` — it simply skips the CMC block. The warning test should instead assert that `frame` is ignored silently, or we add a `_warn_if_frame_unused` call in CBIoU's `update` override.) - -**Refinements**: 1 pass. - -- Pass 1: Verified BIoU, BoTSORT, BaseTracker APIs against source. Confirmed no breaking changes. From 7ac4b47253a5cbd2766e72e413437f7e0992865d Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Sat, 16 May 2026 13:52:31 -0300 Subject: [PATCH 05/23] updated CBioU with 2 buffer rates and docs --- docs/trackers/cbiou.md | 203 ++++++++++++++++++++++ mkdocs.yml | 1 + src/trackers/core/cbiou/tracker.py | 261 +++++++++++++++++++++++------ tests/core/test_cbiou_tracker.py | 135 +++++---------- 4 files changed, 456 insertions(+), 144 deletions(-) create mode 100644 docs/trackers/cbiou.md diff --git a/docs/trackers/cbiou.md b/docs/trackers/cbiou.md new file mode 100644 index 00000000..9acd6031 --- /dev/null +++ b/docs/trackers/cbiou.md @@ -0,0 +1,203 @@ +--- +title: C-BIoU: Cascaded-Buffered IoU Tracker | Trackers +comments: true +description: Cascaded-Buffered IoU (C-BIoU) tracker from Yang et al. (WACV 2023). Cascaded BIoU matching with buffer scales b1 and b2 (b1 < b2) for irregular motion and similar appearances. +--- + +# C-BIoU (Cascaded-Buffered IoU) + +## Overview + +**C-BIoU** implements the tracker from Yang et al., [*Hard To Track Objects with Irregular Motions and Similar Appearances? Make It Easier by Buffering the Matching Space*](https://openaccess.thecvf.com/content/WACV2023/papers/Yang_Hard_To_Track_Objects_With_Irregular_Motions_and_Similar_Appearances_WACV_2023_paper.pdf) (WACV 2023). + +The core idea is **Buffered IoU (BIoU)**: before computing overlap, each bounding box is expanded by a margin proportional to its width and height. That widens the matching space so tracks and detections can be associated even when raw boxes barely touch or miss due to fast motion or detector jitter. + +The paper uses **cascaded matching** with two buffer scales: **b1** (small, first pass) and **b2** (large, second pass on remaining unmatched pairs). You should keep **b1 < b2**. On SoccerNet, the authors report grid-searching **b1 = 0.7** and **b2 = 1.0** (with a BIoU match threshold of **0.01** on that dataset). In this library, `buffer_ratio_first` is **b1** and `buffer_ratio_second` is **b2**. + +C-BIoU follows the same tracking-by-detection backbone as [BoT-SORT](botsort.md) (Kalman prediction, two-stage high/low confidence association, unconfirmed matching) but **does not use camera motion compensation**. Only detection boxes are required, which suits MOT-benchmark and file-based workflows. + +## How does C-BIoU compare to other trackers? + +For comparisons with other trackers, plus dataset context and evaluation details, see the [tracker comparison](comparison.md) page. + +C-BIoU is aimed at sports and dance scenes with irregular motion and similar-looking objects (SoccerNet, DanceTrack, SportsMOT), where the paper reports strong gains over SORT-style baselines. + +## Algorithm + +C-BIoU keeps the [ByteTrack](bytetrack.md)-style association pipeline used in [BoT-SORT](botsort.md) but replaces plain IoU with **cascaded Buffered IoU** at each association step. + +**First association (b1).** High-confidence detections are matched to confirmed and lost tracks using BIoU with `buffer_ratio_first` (paper **b1**, small buffer). Costs are fused with detection confidence. + +**Second association (b2).** Remaining *tracked* tracks (not lost) are matched to low-confidence detections using BIoU with `buffer_ratio_second` (paper **b2**, large buffer). Score fusion is not applied here. In the paper, the large buffer is applied to **remaining unmatched track/detection pairs** after the first cascade; here it is wired to ByteTrack's low-confidence recovery stage. + +**Unconfirmed association (b1).** Leftover high-confidence detections are matched to unconfirmed tracks using the same buffer as pass 1. Unmatched unconfirmed tracks are removed. This step is from ByteTrack lifecycle logic, not the paper's two-buffer cascade. + +**Track lifecycle.** New tracks are initiated and confirmed with a conservative policy (`minimum_consecutive_frames`) to reduce one-frame false positives. Tracks that remain unmatched longer than `lost_track_buffer` are removed. + +## Key Parameters + +| Parameter | Purpose | Tuning guidance | +| ----------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `lost_track_buffer` | Frames to keep an unmatched track alive before deletion. | Higher tolerates longer occlusions but risks false re-association. 10-30 for most scenes; up to 60 for very long occlusions. | +| `track_activation_threshold` | Minimum detection confidence required to start a new track. | Higher reduces noisy track creation; lower retains harder objects. 0.5-0.9 typical depending on detector quality. This does not control low-confidence association, which still discards detections at a fixed `0.1` confidence floor. | +| `minimum_consecutive_frames` | Consecutive matches required before confirming a new track. | 1 for immediate activation; 2-3 improves robustness against flicker and false positives. | +| `minimum_iou_threshold_first_assoc` | Minimum fused BIoU similarity for the first association pass. | Paper uses very low values on some datasets (e.g. 0.01 on SoccerNet). Lower helps maintain matches under fast motion; higher is stricter. | +| `minimum_iou_threshold_second_assoc` | Minimum BIoU similarity for the second association pass. | Usually set lower than the first-pass threshold to recover weak detections without over-matching. | +| `minimum_iou_threshold_unconfirmed_assoc` | Minimum fused BIoU similarity when associating unconfirmed tracks. | Higher values make tentative tracks harder to confirm spuriously; lower values help short-lived or noisy objects survive. | +| `high_conf_det_threshold` | Confidence split between stage-1 and stage-2 detections. | 0.5-0.7 common. Higher shifts more detections to recovery stage; lower gives stage-1 broader coverage. | +| `buffer_ratio_first` | Paper **b1**, small BIoU buffer for the first association pass. | Typical range 0.1-0.7. Should be **less than** `buffer_ratio_second`. | +| `buffer_ratio_second` | Paper **b2**, large BIoU buffer for the second association pass. | Typical range 0.2-1.0. Should be **greater than** `buffer_ratio_first`. | + +!!! warning "Buffer ordering (b1 < b2)" + + Always set `buffer_ratio_first` < `buffer_ratio_second`. The cascaded matcher applies the **smaller** buffer first, then the **larger** buffer only on pairs that remain unmatched. Reversing the order (b1 ≥ b2) is not consistent with the paper and usually hurts performance. + +!!! warning "Frame input is ignored by C-BIoU" + + `CBIoUTracker.update()` accepts `frame` for API consistency with other trackers, but C-BIoU does not use image/frame pixels. + If you pass `frame` with a non-`None` value, the tracker emits a `UserWarning` and ignores it. + +## Run on video, webcam, or RTSP stream + +These examples use `opencv-python` for decoding and display. Replace ``, ``, and `` with your inputs. `` is usually 0 for the default camera. + +=== "Video" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import CBIoUTracker + + tracker = CBIoUTracker( + buffer_ratio_first=0.3, + buffer_ratio_second=0.5, + ) + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open video source") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, + detections, + labels=detections.tracker_id, + ) + + cv2.imshow("RF-DETR + C-BIoU", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` + +=== "Webcam" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import CBIoUTracker + + tracker = CBIoUTracker( + buffer_ratio_first=0.3, + buffer_ratio_second=0.5, + ) + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open webcam") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, + detections, + labels=detections.tracker_id, + ) + + cv2.imshow("RF-DETR + C-BIoU", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` + +=== "RTSP" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import CBIoUTracker + + tracker = CBIoUTracker( + buffer_ratio_first=0.3, + buffer_ratio_second=0.5, + ) + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open RTSP stream") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, + detections, + labels=detections.tracker_id, + ) + + cv2.imshow("RF-DETR + C-BIoU", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` + +For BIoU mathematics and using `BIoU(buffer_ratio=...)` on other trackers, see [IoU variants](../learn/iou.md#biou). To tune hyperparameters with Optuna, see [Hyperparameter tuning](../learn/tune.md). + +## Reference + +Yang, F., Odashima, S., Masui, S., and Jiang, S. (2023). Hard To Track Objects with Irregular Motions and Similar Appearances? Make It Easier by Buffering the Matching Space. WACV 2023. [arXiv:2211.14317](https://arxiv.org/abs/2211.14317) diff --git a/mkdocs.yml b/mkdocs.yml index 8aa8f68d..270d29f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -142,6 +142,7 @@ nav: - ByteTrack: trackers/bytetrack.md - OC-SORT: trackers/ocsort.md - BoT-SORT: trackers/botsort.md + - C-BIoU: trackers/cbiou.md - API Reference: - Trackers: api/trackers.md diff --git a/src/trackers/core/cbiou/tracker.py b/src/trackers/core/cbiou/tracker.py index 19d4228b..690563a5 100644 --- a/src/trackers/core/cbiou/tracker.py +++ b/src/trackers/core/cbiou/tracker.py @@ -4,65 +4,67 @@ # Licensed under the Apache License, Version 2.0 [see LICENSE for details] # ------------------------------------------------------------------------ -from typing import ClassVar +from typing import ClassVar, cast import numpy as np import supervision as sv from trackers.core.botsort.tracker import BoTSORTTracker +from trackers.core.botsort.tracklet import BoTSORTTracklet +from trackers.core.botsort.utils import _fuse_score, get_alive_tracklets from trackers.utils.iou import BIoU from trackers.utils.state_representations import BaseStateEstimator, XCYCWHStateEstimator class CBIoUTracker(BoTSORTTracker): - """BoT-SORT with CMC disabled and Buffered IoU (BIoU) association. + """Cascaded-Buffered IoU (C-BIoU) tracker. - CBIoU is identical to :class:`~trackers.core.botsort.tracker.BoTSORTTracker` - with two fixed differences: + Implements the matching strategy from Yang et al., *Hard To Track Objects with + Irregular Motions and Similar Appearances? Make It Easier by Buffering the + Matching Space*, WACV 2023 + ([paper](https://openaccess.thecvf.com/content/WACV2023/papers/Yang_Hard_To_Track_Objects_With_Irregular_Motions_and_Similar_Appearances_WACV_2023_paper.pdf)). - 1. **Camera Motion Compensation is permanently off.** This makes the - tracker faster and avoids relying on frame pixel data, which is - convenient when only detection files are available (e.g. standard - MOT benchmarks). - 2. **BIoU replaces standard IoU** for all association steps. Each - bounding box is expanded by ``buffer_ratio`` relative to its own - width/height before IoU is computed, giving the matcher more - tolerance for small localization gaps between the Kalman prediction - and the incoming detection. + The paper proposes **Buffered IoU (BIoU)** — expanding boxes by a proportional + margin before computing overlap — and **cascaded matching** with a small buffer + scale ``b1`` followed by a larger scale ``b2`` (typically ``b1 < b2``; e.g. + 0.7 and 1.0 on SoccerNet in the paper). + + Each association step uses its own ``buffer_ratio``: + + * ``buffer_ratio_first`` — first pass (high-confidence detections vs tracks; + paper: small ``b1``). + * ``buffer_ratio_second`` — second pass (low-confidence detections; + paper: large ``b2``). + + The ByteTrack-style unconfirmed-track step (leftover high-confidence + detections vs tentative tracks) reuses **b1** (``iou_first``); it is not a + separate paper hyperparameter. + + Camera motion compensation is not used (detection-only / MOT-file workflows). Args: - lost_track_buffer: Time buffer (in frames at 30 FPS) for keeping - lost tracks alive before deletion. Scaled by ``frame_rate``. + lost_track_buffer: Time buffer (in frames at 30 FPS) for keeping lost + tracks alive before deletion. Scaled by ``frame_rate``. frame_rate: Video frame rate used to scale the lost track buffer. - track_activation_threshold: Minimum detection confidence to spawn - a new track. + track_activation_threshold: Minimum detection confidence to spawn a + new track. minimum_consecutive_frames: Number of successful updates required before assigning a stable track ID. - minimum_iou_threshold_first_assoc: Minimum fused similarity to - accept an association during the first association step. - minimum_iou_threshold_second_assoc: Minimum fused similarity to - accept an association during the second association step. - minimum_iou_threshold_unconfirmed_assoc: Minimum fused similarity - to accept a match between an unconfirmed track and a remaining - high-confidence detection. - high_conf_det_threshold: Confidence threshold that splits - detections into high / low confidence groups. - instant_first_frame_activation: If ``True`` (default), tracks - spawned on the very first frame receive a real tracker ID - immediately. - state_estimator_class: State estimator class for tracklets. - Defaults to ``XCYCWHStateEstimator``. - buffer_ratio: Non-negative relative margin by which each bounding - box is expanded before IoU is computed. ``0.0`` recovers - standard IoU exactly; larger values tolerate wider localization - gaps. Forwarded to :class:`~trackers.utils.iou.BIoU`. - - Notes: - - CMC parameters (``enable_cmc``, ``cmc_method``, ``cmc_downscale``) - are intentionally absent from this class's signature — CMC is - always disabled. - - Passing a ``frame`` argument to :meth:`update` emits a - ``UserWarning`` because no CMC processing takes place. + minimum_iou_threshold_first_assoc: Minimum fused similarity for the + first association step. + minimum_iou_threshold_second_assoc: Minimum fused similarity for the + second association step. + minimum_iou_threshold_unconfirmed_assoc: Minimum fused similarity for + the unconfirmed association step. + high_conf_det_threshold: Confidence threshold splitting high / low + detections. + instant_first_frame_activation: If ``True``, first-frame tracks receive + a real ID immediately. + state_estimator_class: Kalman state representation for tracklets. + buffer_ratio_first: Buffer scale ``b1`` for the first BIoU pass. Should + be **less than** ``buffer_ratio_second`` (``b1 < b2``) per the paper. + buffer_ratio_second: Buffer scale ``b2`` for the second BIoU pass. Should + be **greater than** ``buffer_ratio_first``. """ tracker_id = "cbiou" @@ -76,8 +78,9 @@ class CBIoUTracker(BoTSORTTracker): "range": [0.05, 0.7], }, "high_conf_det_threshold": {"type": "uniform", "range": [0.3, 0.8]}, - "minimum_consecutive_frames": {"type": "randint", "range": [1, 4]}, - "buffer_ratio": {"type": "uniform", "range": [0.0, 0.5]}, + "minimum_consecutive_frames": {"type": "randint", "range": [1, 3]}, + "buffer_ratio_first": {"type": "uniform", "range": [0.0, 0.7]}, + "buffer_ratio_second": {"type": "uniform", "range": [0.0, 0.7]}, } def __init__( @@ -92,7 +95,8 @@ def __init__( high_conf_det_threshold: float = 0.6, instant_first_frame_activation: bool = True, state_estimator_class: type[BaseStateEstimator] = XCYCWHStateEstimator, - buffer_ratio: float = 0.1, + buffer_ratio_first: float = 0.3, + buffer_ratio_second: float = 0.5, ) -> None: super().__init__( lost_track_buffer=lost_track_buffer, @@ -106,25 +110,176 @@ def __init__( enable_cmc=False, instant_first_frame_activation=instant_first_frame_activation, state_estimator_class=state_estimator_class, - iou=BIoU(buffer_ratio=buffer_ratio), ) - self.buffer_ratio = buffer_ratio + self.iou_first = BIoU(buffer_ratio=buffer_ratio_first) + self.iou_second = BIoU(buffer_ratio=buffer_ratio_second) + self.buffer_ratio_first = buffer_ratio_first + self.buffer_ratio_second = buffer_ratio_second + + def _biou_matrix( + self, + tracklets: list[BoTSORTTracklet], + boxes: np.ndarray, + iou: BIoU, + ) -> np.ndarray: + if len(tracklets) == 0: + track_boxes = np.empty((0, 4)) + else: + track_boxes = np.array([t.get_state_bbox() for t in tracklets]) + return iou.compute(track_boxes, boxes) def update( self, detections: sv.Detections, frame: np.ndarray | None = None, ) -> sv.Detections: - """Update the tracker with detections from the current frame. + """Update the C-BIoU tracker with detections from the current frame. + + Runs the association pipeline with a distinct BIoU instance per step + (cascaded buffers per Yang et al., WACV 2023). Does not use frames or CMC. Args: detections: Supervision detections for the current frame. - frame: Unused — CBIoU never performs CMC. Passing a non-``None`` - value emits a ``UserWarning``. + frame: Unused. Emits a ``UserWarning`` if provided. Returns: - New ``sv.Detections`` with ``tracker_id`` assigned for each - detection. + Detections with ``tracker_id`` assigned. """ self._warn_if_frame_unused(frame) - return super().update(detections=detections, frame=None) + self.frame_id += 1 + + if len(self.tracks) == 0 and len(detections) == 0: + result = sv.Detections.empty() + result.tracker_id = np.array([], dtype=int) + return result + + out_det_indices: list[int] = [] + out_tracker_ids: list[int] = [] + + # Predict new locations for existing tracks + for tracker in self.tracks: + tracker.predict() + + detection_boxes = detections.xyxy + confidences = detections.confidence if detections.confidence is not None else np.ones(len(detections)) + + # Split detections into high / low / discarded by confidence + high_mask = confidences >= self.high_conf_det_threshold + low_mask = (confidences > 0.1) & (~high_mask) + high_indices = np.where(high_mask)[0] + low_indices = np.where(low_mask)[0] + high_boxes = detection_boxes[high_indices] + low_boxes = detection_boxes[low_indices] + high_scores = confidences[high_indices] + + # Split tracks into confirmed, unconfirmed, and lost. + # After predict(), time_since_update == 1 means "tracked"; > 1 means "lost". + confirmed_tracks: list[BoTSORTTracklet] = [] + unconfirmed_tracks: list[BoTSORTTracklet] = [] + lost_tracks: list[BoTSORTTracklet] = [] + for track in self.tracks: + if track.time_since_update > 1: + lost_tracks.append(track) + elif track.number_of_successful_updates >= self.minimum_consecutive_frames: + confirmed_tracks.append(track) + else: + unconfirmed_tracks.append(track) + + # Step 1: associate high-confidence detections to confirmed + lost tracks. + # Paper b1 (small buffer); BIoU fused with detection scores. + strack_pool = confirmed_tracks + lost_tracks + iou_matrix = self._biou_matrix(strack_pool, high_boxes, self.iou_first) + iou_matrix = _fuse_score(self.iou_first.normalize_for_fusion(iou_matrix), high_scores) + matched, unmatched_pool, unmatched_high = self._get_associated_indices( + iou_matrix, self.minimum_iou_threshold_first_assoc + ) + + for row, col in matched: + track = strack_pool[row] + track.update(high_boxes[col]) + if track.number_of_successful_updates >= self.minimum_consecutive_frames and track.tracker_id == -1: + track.tracker_id = BoTSORTTracklet.get_next_tracker_id() + out_det_indices.append(int(high_indices[col])) + out_tracker_ids.append(track.tracker_id) + + # Step 2: associate low-confidence detections to remaining *tracked* tracks + # only (excluding lost tracks). Paper b2 (large buffer); no score fusion. + remaining_tracked = [strack_pool[i] for i in unmatched_pool if strack_pool[i].time_since_update == 1] + iou_matrix = self._biou_matrix(remaining_tracked, low_boxes, self.iou_second) + matched, _, unmatched_low = self._get_associated_indices( + iou_matrix, self.minimum_iou_threshold_second_assoc + ) + + for row, col in matched: + track = remaining_tracked[row] + track.update(low_boxes[col]) + if track.number_of_successful_updates >= self.minimum_consecutive_frames and track.tracker_id == -1: + track.tracker_id = BoTSORTTracklet.get_next_tracker_id() + out_det_indices.append(int(low_indices[col])) + out_tracker_ids.append(track.tracker_id) + + # Unmatched low-confidence detections (output with tracker_id=-1) + for det_local_idx in sorted(unmatched_low): + out_det_indices.append(int(low_indices[det_local_idx])) + out_tracker_ids.append(-1) + + # Step 3: match unconfirmed tracks with remaining unmatched high-confidence + # detections (ByteTrack lifecycle; reuses b1 / iou_first, not a paper parameter). + # Unmatched unconfirmed tracks are removed (not kept as lost). + unmatched_high_list = sorted(unmatched_high) + unmatched_uc_indices: list[int] = list(range(len(unconfirmed_tracks))) + + if len(unconfirmed_tracks) > 0 and len(unmatched_high_list) > 0: + uh_boxes = high_boxes[unmatched_high_list] + uh_scores = high_scores[unmatched_high_list] + iou_matrix = self._biou_matrix(unconfirmed_tracks, uh_boxes, self.iou_first) + iou_matrix = _fuse_score(self.iou_first.normalize_for_fusion(iou_matrix), uh_scores) + matched_uc, unmatched_uc_indices, remaining_uh = self._get_associated_indices( + iou_matrix, self.minimum_iou_threshold_unconfirmed_assoc + ) + + for row, col in matched_uc: + track = unconfirmed_tracks[row] + orig_high_idx = unmatched_high_list[col] + track.update(high_boxes[orig_high_idx]) + if track.number_of_successful_updates >= self.minimum_consecutive_frames and track.tracker_id == -1: + track.tracker_id = BoTSORTTracklet.get_next_tracker_id() + out_det_indices.append(int(high_indices[orig_high_idx])) + out_tracker_ids.append(track.tracker_id) + + # Only remaining unmatched high-conf dets proceed to spawning + unmatched_high = [unmatched_high_list[i] for i in remaining_uh] + + # Remove unmatched unconfirmed tracks (following original ByteTrack) + if len(unmatched_uc_indices) > 0: + remove_ids = {id(unconfirmed_tracks[i]) for i in unmatched_uc_indices} + self.tracks = [t for t in self.tracks if id(t) not in remove_ids] + + # Spawn new tracks from unmatched high-confidence detections + self._spawn_new_tracks( + detection_boxes, + confidences, + unmatched_high, + high_indices, + out_det_indices, + out_tracker_ids, + is_first_frame=(self.frame_id == 1), + ) + + # Kill lost tracks + self.tracks = get_alive_tracklets( + tracklets=self.tracks, + maximum_frames_without_update=self.maximum_frames_without_update, + minimum_consecutive_frames=self.minimum_consecutive_frames, + ) + + if not out_det_indices: + result = sv.Detections.empty() + result.tracker_id = np.array([], dtype=int) + return result + + # Build final detections + idx = np.array(out_det_indices) + result = cast(sv.Detections, detections[idx]) + result.tracker_id = np.array(out_tracker_ids, dtype=int) + return result diff --git a/tests/core/test_cbiou_tracker.py b/tests/core/test_cbiou_tracker.py index c177d779..3f1d2ad0 100644 --- a/tests/core/test_cbiou_tracker.py +++ b/tests/core/test_cbiou_tracker.py @@ -6,13 +6,11 @@ """CBIoU-specific tracker tests. -Generic lifecycle / reset / tracked_objects / mutation contracts are -covered for all trackers in test_trackers.py via ALL_TRACKER_IDS. -This file covers CBIoU-specific invariants: - - CMC is always disabled (frame argument triggers UserWarning) - - buffer_ratio is correctly forwarded to BIoU - - BIoU association is more tolerant of near-miss detections than plain IoU - - buffer_ratio=0.0 produces the same results as BoTSORT(enable_cmc=False) +Generic lifecycle contracts are covered in test_trackers.py via ALL_TRACKER_IDS. +This file covers C-BIoU-specific invariants (Yang et al., WACV 2023): + - Cascaded BIoU with per-step buffer scales (b1, b2) + - CMC disabled; frame argument triggers UserWarning + - BIoU association more tolerant than standard IoU """ from __future__ import annotations @@ -44,18 +42,26 @@ class TestCBIoUConstruction: def test_default_construction(self) -> None: tracker = CBIoUTracker() assert tracker is not None - assert isinstance(tracker.iou, BIoU) - def test_buffer_ratio_forwarded_to_biou(self) -> None: - tracker = CBIoUTracker(buffer_ratio=0.25) - assert isinstance(tracker.iou, BIoU) - assert tracker.iou.buffer_ratio == pytest.approx(0.25) + def test_per_step_biou_instances(self) -> None: + tracker = CBIoUTracker( + buffer_ratio_first=0.1, + buffer_ratio_second=0.3, + ) + assert isinstance(tracker.iou_first, BIoU) + assert isinstance(tracker.iou_second, BIoU) + assert not hasattr(tracker, "iou_unconfirmed") + + def test_buffer_ratios_forwarded_to_biou(self) -> None: + tracker = CBIoUTracker( + buffer_ratio_first=0.1, + buffer_ratio_second=0.3, + ) + assert tracker.iou_first.buffer_ratio == pytest.approx(0.1) + assert tracker.iou_second.buffer_ratio == pytest.approx(0.3) - def test_buffer_ratio_stored_on_tracker(self) -> None: - tracker = CBIoUTracker(buffer_ratio=0.15) - assert tracker.buffer_ratio == pytest.approx(0.15) - def test_cmc_is_disabled(self) -> None: + def test_cmc_disabled(self) -> None: tracker = CBIoUTracker() assert tracker.enable_cmc is False assert tracker.cmc is None @@ -65,25 +71,20 @@ def test_tracker_id(self) -> None: def test_invalid_buffer_ratio_raises(self) -> None: with pytest.raises(ValueError, match="buffer_ratio"): - CBIoUTracker(buffer_ratio=-0.01) + CBIoUTracker(buffer_ratio_first=-0.01) class TestCBIoUFrameWarning: - """Passing a frame to update() must emit UserWarning (CMC is disabled).""" - def test_frame_triggers_warning(self) -> None: tracker = CBIoUTracker() - frame = _make_frame() - det = _detection((100.0, 100.0, 200.0, 200.0)) with pytest.warns(UserWarning): - tracker.update(det, frame=frame) + tracker.update(_detection((100.0, 100.0, 200.0, 200.0)), frame=_make_frame()) def test_no_warning_without_frame(self) -> None: tracker = CBIoUTracker() - det = _detection((100.0, 100.0, 200.0, 200.0)) with warnings.catch_warnings(): warnings.simplefilter("error") - tracker.update(det) + tracker.update(_detection((100.0, 100.0, 200.0, 200.0))) class TestCBIoUAssociationTolerance: @@ -103,7 +104,7 @@ def test_near_miss_associated_with_buffer(self) -> None: """ # Frame 1: spawn a track at box A with high confidence cbiou = CBIoUTracker( - buffer_ratio=0.15, + buffer_ratio_first=0.15, minimum_consecutive_frames=1, track_activation_threshold=0.5, minimum_iou_threshold_first_assoc=0.05, @@ -118,79 +119,31 @@ def test_near_miss_associated_with_buffer(self) -> None: box_a = (0.0, 0.0, 100.0, 100.0) box_b = (110.0, 0.0, 210.0, 100.0) - for tracker in (cbiou, botsort): - tracker.update(_detection(box_a, conf=0.9)) - + cbiou.update(_detection(box_a)) + botsort.update(_detection(box_a)) + # Frame 2: detection slightly outside A — CBIoU buffer closes the gap - cbiou_result = cbiou.update(_detection(box_b, conf=0.9)) - botsort_result = botsort.update(_detection(box_b, conf=0.9)) - - cbiou_ids = cbiou_result.tracker_id - botsort_ids = botsort_result.tracker_id - - # CBIoU should reuse the existing track (buffer closes the gap) - # — exactly one output detection with a confirmed (>=0) ID - assert cbiou_ids is not None and len(cbiou_ids) == 1 - assert cbiou_ids[0] >= 0, "CBIoU should have associated the near-miss detection" - - # CBIoU's confirmed ID on frame 2 must equal the one it assigned on frame 1 - cbiou_frame1 = cbiou.tracks[0].tracker_id - assert cbiou_ids[0] == cbiou_frame1, "CBIoU should reuse the existing track ID, not spawn a new one" - - # BoTSORT with standard IoU: boxes don't overlap, so old track goes lost - # and a new unconfirmed track is spawned (tracker_id == -1 or a fresh ID) - assert botsort_ids is not None - # The important behavioral difference: BoTSORT should NOT continue the - # original track (it can't see the gap-crossing detection as a match) - if len(botsort_ids) > 0: - botsort_frame1_track_id = next((t.tracker_id for t in botsort.tracks), None) - # Original BoTSORT track should be gone or unmatched - assert botsort_ids[0] != cbiou_frame1 or botsort_ids[0] == -1, ( - "BoTSORT standard IoU should not have matched the near-miss box" - ) - - def test_zero_buffer_behaves_like_standard_iou(self) -> None: - """With buffer_ratio=0 CBIoU produces identical results to BoTSORT(no CMC).""" - cbiou = CBIoUTracker( - buffer_ratio=0.0, - minimum_consecutive_frames=2, - track_activation_threshold=0.7, - ) - botsort = BoTSORTTracker( - enable_cmc=False, - minimum_consecutive_frames=2, - track_activation_threshold=0.7, - ) + cbiou_result = cbiou.update(_detection(box_b)) + botsort_result = botsort.update(_detection(box_b)) - detections = [ - _detection((50.0, 50.0, 150.0, 150.0), conf=0.9), - _detection((55.0, 55.0, 155.0, 155.0), conf=0.9), - _detection((60.0, 60.0, 160.0, 160.0), conf=0.9), - ] + assert cbiou_result.tracker_id is not None and len(cbiou_result.tracker_id) == 1 + assert cbiou_result.tracker_id[0] >= 0 + cbiou_frame1_id = cbiou.tracks[0].tracker_id + assert cbiou_result.tracker_id[0] == cbiou_frame1_id - for det in detections: - r_cbiou = cbiou.update(det) - r_botsort = botsort.update(det) - # Both should produce the same number of outputs - assert len(r_cbiou) == len(r_botsort), ( - f"CBIoU(buffer=0) and BoTSORT(no CMC) diverged: cbiou={len(r_cbiou)}, botsort={len(r_botsort)}" - ) + botsort_ids = botsort_result.tracker_id + if botsort_ids is not None and len(botsort_ids) > 0: + assert botsort_ids[0] != cbiou_frame1_id or botsort_ids[0] == -1 class TestCBIoUSearchSpace: - def test_buffer_ratio_in_search_space(self) -> None: - assert "buffer_ratio" in CBIoUTracker.search_space + def test_cascade_buffer_params_in_search_space(self) -> None: + ss = CBIoUTracker.search_space + assert "buffer_ratio_first" in ss + assert "buffer_ratio_second" in ss + assert "buffer_ratio_unconfirmed" not in ss - def test_no_cmc_params_in_search_space(self) -> None: + def test_no_cmc_in_search_space(self) -> None: ss = CBIoUTracker.search_space assert "enable_cmc" not in ss assert "cmc_method" not in ss - assert "cmc_downscale" not in ss - - def test_search_space_buffer_ratio_range(self) -> None: - spec = CBIoUTracker.search_space["buffer_ratio"] - assert spec["type"] == "uniform" - low, high = spec["range"] - assert low >= 0.0 - assert high <= 1.0 - assert low < high From dccbbafca8a90d05dd3aec663ab30ea3718f59ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 16:53:00 +0000 Subject: [PATCH 06/23] =?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/trackers/cbiou.md | 22 +++++++++++----------- src/trackers/core/cbiou/tracker.py | 6 ++---- tests/core/test_cbiou_tracker.py | 3 +-- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/docs/trackers/cbiou.md b/docs/trackers/cbiou.md index 9acd6031..0fd2de98 100644 --- a/docs/trackers/cbiou.md +++ b/docs/trackers/cbiou.md @@ -36,17 +36,17 @@ C-BIoU keeps the [ByteTrack](bytetrack.md)-style association pipeline used in [B ## Key Parameters -| Parameter | Purpose | Tuning guidance | -| ----------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `lost_track_buffer` | Frames to keep an unmatched track alive before deletion. | Higher tolerates longer occlusions but risks false re-association. 10-30 for most scenes; up to 60 for very long occlusions. | -| `track_activation_threshold` | Minimum detection confidence required to start a new track. | Higher reduces noisy track creation; lower retains harder objects. 0.5-0.9 typical depending on detector quality. This does not control low-confidence association, which still discards detections at a fixed `0.1` confidence floor. | -| `minimum_consecutive_frames` | Consecutive matches required before confirming a new track. | 1 for immediate activation; 2-3 improves robustness against flicker and false positives. | -| `minimum_iou_threshold_first_assoc` | Minimum fused BIoU similarity for the first association pass. | Paper uses very low values on some datasets (e.g. 0.01 on SoccerNet). Lower helps maintain matches under fast motion; higher is stricter. | -| `minimum_iou_threshold_second_assoc` | Minimum BIoU similarity for the second association pass. | Usually set lower than the first-pass threshold to recover weak detections without over-matching. | -| `minimum_iou_threshold_unconfirmed_assoc` | Minimum fused BIoU similarity when associating unconfirmed tracks. | Higher values make tentative tracks harder to confirm spuriously; lower values help short-lived or noisy objects survive. | -| `high_conf_det_threshold` | Confidence split between stage-1 and stage-2 detections. | 0.5-0.7 common. Higher shifts more detections to recovery stage; lower gives stage-1 broader coverage. | -| `buffer_ratio_first` | Paper **b1**, small BIoU buffer for the first association pass. | Typical range 0.1-0.7. Should be **less than** `buffer_ratio_second`. | -| `buffer_ratio_second` | Paper **b2**, large BIoU buffer for the second association pass. | Typical range 0.2-1.0. Should be **greater than** `buffer_ratio_first`. | +| Parameter | Purpose | Tuning guidance | +| ----------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `lost_track_buffer` | Frames to keep an unmatched track alive before deletion. | Higher tolerates longer occlusions but risks false re-association. 10-30 for most scenes; up to 60 for very long occlusions. | +| `track_activation_threshold` | Minimum detection confidence required to start a new track. | Higher reduces noisy track creation; lower retains harder objects. 0.5-0.9 typical depending on detector quality. This does not control low-confidence association, which still discards detections at a fixed `0.1` confidence floor. | +| `minimum_consecutive_frames` | Consecutive matches required before confirming a new track. | 1 for immediate activation; 2-3 improves robustness against flicker and false positives. | +| `minimum_iou_threshold_first_assoc` | Minimum fused BIoU similarity for the first association pass. | Paper uses very low values on some datasets (e.g. 0.01 on SoccerNet). Lower helps maintain matches under fast motion; higher is stricter. | +| `minimum_iou_threshold_second_assoc` | Minimum BIoU similarity for the second association pass. | Usually set lower than the first-pass threshold to recover weak detections without over-matching. | +| `minimum_iou_threshold_unconfirmed_assoc` | Minimum fused BIoU similarity when associating unconfirmed tracks. | Higher values make tentative tracks harder to confirm spuriously; lower values help short-lived or noisy objects survive. | +| `high_conf_det_threshold` | Confidence split between stage-1 and stage-2 detections. | 0.5-0.7 common. Higher shifts more detections to recovery stage; lower gives stage-1 broader coverage. | +| `buffer_ratio_first` | Paper **b1**, small BIoU buffer for the first association pass. | Typical range 0.1-0.7. Should be **less than** `buffer_ratio_second`. | +| `buffer_ratio_second` | Paper **b2**, large BIoU buffer for the second association pass. | Typical range 0.2-1.0. Should be **greater than** `buffer_ratio_first`. | !!! warning "Buffer ordering (b1 < b2)" diff --git a/src/trackers/core/cbiou/tracker.py b/src/trackers/core/cbiou/tracker.py index 690563a5..eef01e41 100644 --- a/src/trackers/core/cbiou/tracker.py +++ b/src/trackers/core/cbiou/tracker.py @@ -206,9 +206,7 @@ def update( # only (excluding lost tracks). Paper b2 (large buffer); no score fusion. remaining_tracked = [strack_pool[i] for i in unmatched_pool if strack_pool[i].time_since_update == 1] iou_matrix = self._biou_matrix(remaining_tracked, low_boxes, self.iou_second) - matched, _, unmatched_low = self._get_associated_indices( - iou_matrix, self.minimum_iou_threshold_second_assoc - ) + matched, _, unmatched_low = self._get_associated_indices(iou_matrix, self.minimum_iou_threshold_second_assoc) for row, col in matched: track = remaining_tracked[row] @@ -232,7 +230,7 @@ def update( if len(unconfirmed_tracks) > 0 and len(unmatched_high_list) > 0: uh_boxes = high_boxes[unmatched_high_list] uh_scores = high_scores[unmatched_high_list] - iou_matrix = self._biou_matrix(unconfirmed_tracks, uh_boxes, self.iou_first) + iou_matrix = self._biou_matrix(unconfirmed_tracks, uh_boxes, self.iou_first) iou_matrix = _fuse_score(self.iou_first.normalize_for_fusion(iou_matrix), uh_scores) matched_uc, unmatched_uc_indices, remaining_uh = self._get_associated_indices( iou_matrix, self.minimum_iou_threshold_unconfirmed_assoc diff --git a/tests/core/test_cbiou_tracker.py b/tests/core/test_cbiou_tracker.py index 3f1d2ad0..368642c5 100644 --- a/tests/core/test_cbiou_tracker.py +++ b/tests/core/test_cbiou_tracker.py @@ -60,7 +60,6 @@ def test_buffer_ratios_forwarded_to_biou(self) -> None: assert tracker.iou_first.buffer_ratio == pytest.approx(0.1) assert tracker.iou_second.buffer_ratio == pytest.approx(0.3) - def test_cmc_disabled(self) -> None: tracker = CBIoUTracker() assert tracker.enable_cmc is False @@ -121,7 +120,7 @@ def test_near_miss_associated_with_buffer(self) -> None: cbiou.update(_detection(box_a)) botsort.update(_detection(box_a)) - + # Frame 2: detection slightly outside A — CBIoU buffer closes the gap cbiou_result = cbiou.update(_detection(box_b)) botsort_result = botsort.update(_detection(box_b)) From 86c42f86d299fe2eb1ec67c1c83b462991d85224 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Mon, 18 May 2026 09:19:05 -0300 Subject: [PATCH 07/23] preliminary numbers for cbiou --- docs/trackers/cbiou.md | 23 +++++++++++ docs/trackers/comparison.md | 82 +++++++++++++++++++++++++++++++------ 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/docs/trackers/cbiou.md b/docs/trackers/cbiou.md index 9acd6031..051facb1 100644 --- a/docs/trackers/cbiou.md +++ b/docs/trackers/cbiou.md @@ -20,6 +20,29 @@ C-BIoU follows the same tracking-by-detection backbone as [BoT-SORT](botsort.md) For comparisons with other trackers, plus dataset context and evaluation details, see the [tracker comparison](comparison.md) page. +Measured with this library (YOLOX detections on MOT17 and SportsMOT test; oracle detections on SoccerNet test and DanceTrack val). Default buffers: +`buffer_ratio_first=0.3`, `buffer_ratio_second=0.5`. + +=== "Default parameters" + +| Dataset | HOTA | IDF1 | MOTA | +| :-------: | :------: | :------: | :------: | +| MOT17 | 63.0 | 79.1 | 77.4 | +| SportsMOT | 73.1 | 72.6 | 96.7 | +| SoccerNet | 82.6 | 76.6 | 97.0 | +| DanceTrack | 53.8 | 53.8 | 90.1 | + +=== "Tuned parameters" + +Tuned with Optuna (`trackers_cbiou_tuning.ipynb`): MOT17 val-half, SportsMOT val, SoccerNet train, DanceTrack train; evaluated on the splits below. + +| Dataset | HOTA | IDF1 | MOTA | +| :-------: | :------: | :------: | :------: | +| MOT17 | 63.0 | 79.1 | 77.2 | +| SportsMOT | 72.5 | 72.2 | 96.9 | +| SoccerNet | 85.5 | 79.6 | 99.3 | +| DanceTrack | 53.3 | 54.4 | 89.2 | + C-BIoU is aimed at sports and dance scenes with irregular motion and similar-looking objects (SoccerNet, DanceTrack, SportsMOT), where the paper reports strong gains over SORT-style baselines. ## Algorithm diff --git a/docs/trackers/comparison.md b/docs/trackers/comparison.md index f515ddd0..20143831 100644 --- a/docs/trackers/comparison.md +++ b/docs/trackers/comparison.md @@ -1,11 +1,11 @@ --- -title: SORT vs ByteTrack vs OC-SORT vs BoT-SORT — MOT Benchmark Comparison | Trackers -description: Side-by-side benchmark comparison of SORT, ByteTrack, OC-SORT, and BoT-SORT on MOT17, MOT20, DanceTrack, and SportsMOT — HOTA, IDF1, MOTA scores with default and tuned parameters. +title: SORT vs ByteTrack vs OC-SORT vs BoT-SORT vs C-BIoU — MOT Benchmark Comparison | Trackers +description: Side-by-side benchmark comparison of SORT, ByteTrack, OC-SORT, BoT-SORT, and C-BIoU 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, and BoT-SORT on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. +This page shows head-to-head performance of SORT, ByteTrack, OC-SORT, BoT-SORT, and C-BIoU on standard MOT benchmarks. Results are shown with default parameters and with parameter-tuned configurations found via grid search. !!! info "Benchmark version" @@ -38,7 +38,8 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests | SORT | 58.4 | 69.9 | 67.2 | | ByteTrack | 60.1 | 73.2 | 74.1 | | OC-SORT | 61.9 | 76.4 | 76.0 | - | BoT-SORT | **63.7** | **78.7** | **79.2** | + | BoT-SORT | **63.7** | 78.7 | **79.2** | + | C-BIoU | 63.0 | **79.1** | 77.4 | === "Tuned" @@ -49,7 +50,8 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests | 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** | + | BoT-SORT | **63.8** | 78.7 | **79.4** | + | C-BIoU | 63.0 | **79.1** | 77.2 | Tuned configuration for each tracker. @@ -85,6 +87,18 @@ Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests track_activation_threshold: 0.6 enable_cmc: true cmc_method: sparseOptFlow + + C-BIoU: + lost_track_buffer: 52 + minimum_consecutive_frames: 2 + minimum_iou_threshold_first_assoc: 0.26 + minimum_iou_threshold_second_assoc: 0.69 + minimum_iou_threshold_unconfirmed_assoc: 0.18 + high_conf_det_threshold: 0.57 + track_activation_threshold: 0.56 + buffer_ratio_first: 0.14 + buffer_ratio_second: 0.47 + enable_cmc: false ``` ## [SportsMOT](https://arxiv.org/abs/2304.05170) @@ -111,6 +125,7 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar | ByteTrack | 73.0 | 72.5 | 96.4 | | OC-SORT | 71.7 | 71.4 | 95.0 | | BoT-SORT | **73.8** | **73.4** | **96.9** | + | C-BIoU | 73.1 | 72.6 | 96.7 | === "Tuned" @@ -122,6 +137,7 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar | 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 | 72.5 | 72.2 | **96.9** | Tuned configuration for each tracker. @@ -157,6 +173,18 @@ Sports broadcast tracking with fast motion, camera pans, and similar-looking tar track_activation_threshold: 0.8 enable_cmc: true cmc_method: sparseOptFlow + + C-BIoU: + lost_track_buffer: 89 + minimum_consecutive_frames: 2 + minimum_iou_threshold_first_assoc: 0.16 + minimum_iou_threshold_second_assoc: 0.38 + minimum_iou_threshold_unconfirmed_assoc: 0.42 + high_conf_det_threshold: 0.57 + track_activation_threshold: 0.53 + buffer_ratio_first: 0.55 + buffer_ratio_second: 0.04 + enable_cmc: false ``` ## [SoccerNet-tracking](https://arxiv.org/abs/2204.06918) @@ -184,6 +212,7 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I | 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 | === "Tuned" @@ -191,10 +220,11 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I | Tracker | HOTA | IDF1 | MOTA | | :-------: | :------: | :------: | :------: | - | SORT | 84.2 | 78.2 | **98.2** | - | ByteTrack | 84.0 | 78.1 | **98.2** | + | 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 | + | BoT-SORT | 85.0 | **79.7** | 97.2 | + | C-BIoU | **85.5** | 79.6 | **99.3** | Tuned configuration for each tracker. @@ -230,6 +260,18 @@ Long sequences with dense interactions and partial occlusions. Tests long-term I track_activation_threshold: 0.7 enable_cmc: true cmc_method: sparseOptFlow + + C-BIoU: + lost_track_buffer: 21 + minimum_consecutive_frames: 1 + minimum_iou_threshold_first_assoc: 0.10 + minimum_iou_threshold_second_assoc: 0.42 + minimum_iou_threshold_unconfirmed_assoc: 0.47 + high_conf_det_threshold: 0.38 + track_activation_threshold: 0.26 + buffer_ratio_first: 0.70 + buffer_ratio_second: 0.12 + enable_cmc: false ``` ## [DanceTrack](https://arxiv.org/abs/2111.14690) @@ -261,8 +303,9 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art | :-------: | :------: | :------: | :------: | | SORT | 45.0 | 39.0 | 80.6 | | ByteTrack | 50.2 | 49.9 | 86.2 | - | OC-SORT | **51.8** | **50.9** | **87.3** | + | OC-SORT | 51.8 | 50.9 | 87.3 | | BoT-SORT | 50.5 | 49.2 | 85.1 | + | C-BIoU | **53.8** | **53.8** | **90.1** | === "Tuned" @@ -271,9 +314,10 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art | Tracker | HOTA | IDF1 | MOTA | | :-------: | :------: | :------: | :------: | | SORT | 50.6 | 49.6 | 84.3 | - | ByteTrack | **53.2** | **54.6** | 86.8 | - | OC-SORT | 52.0 | 51.8 | **87.2** | - | BoT-SORT | **53.5** | **54.0** | 86.5 | + | ByteTrack | 53.2 | **54.6** | 86.8 | + | OC-SORT | 52.0 | 51.8 | 87.2 | + | BoT-SORT | **53.5** | 54.0 | 86.5 | + | C-BIoU | 53.3 | 54.4 | **89.2** | Tuned configuration for each tracker. @@ -309,6 +353,18 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art track_activation_threshold: 0.7 enable_cmc: true cmc_method: sparseOptFlow + + C-BIoU: + lost_track_buffer: 42 + minimum_consecutive_frames: 2 + minimum_iou_threshold_first_assoc: 0.09 + minimum_iou_threshold_second_assoc: 0.15 + minimum_iou_threshold_unconfirmed_assoc: 0.45 + high_conf_det_threshold: 0.38 + track_activation_threshold: 0.84 + buffer_ratio_first: 0.23 + buffer_ratio_second: 0.47 + enable_cmc: false ``` ## Methodology @@ -354,6 +410,8 @@ association, which reduces ID switches on panning or handheld footage. Use BoT-S 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 irregular motion and similar appearances when you want buffered, cascaded geometric matching without camera motion compensation. It is strongest on SoccerNet and DanceTrack in these benchmarks, and reaches the highest IDF1 on MOT17 among the trackers listed here. 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](cbiou.md) for buffer scales **b1** and **b2**. + ## Metric Definitions **HOTA** (Higher Order Tracking Accuracy) — the primary benchmark metric. HOTA decomposes From 8ea2d24fd0356f0d93749003076b8b17a2722a43 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 12:19:35 +0000 Subject: [PATCH 08/23] =?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/trackers/cbiou.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/trackers/cbiou.md b/docs/trackers/cbiou.md index 791dc8e8..cdf52dd0 100644 --- a/docs/trackers/cbiou.md +++ b/docs/trackers/cbiou.md @@ -20,28 +20,28 @@ C-BIoU follows the same tracking-by-detection backbone as [BoT-SORT](botsort.md) For comparisons with other trackers, plus dataset context and evaluation details, see the [tracker comparison](comparison.md) page. -Measured with this library (YOLOX detections on MOT17 and SportsMOT test; oracle detections on SoccerNet test and DanceTrack val). Default buffers: +Measured with this library (YOLOX detections on MOT17 and SportsMOT test; oracle detections on SoccerNet test and DanceTrack val). Default buffers: `buffer_ratio_first=0.3`, `buffer_ratio_second=0.5`. === "Default parameters" -| Dataset | HOTA | IDF1 | MOTA | -| :-------: | :------: | :------: | :------: | -| MOT17 | 63.0 | 79.1 | 77.4 | -| SportsMOT | 73.1 | 72.6 | 96.7 | -| SoccerNet | 82.6 | 76.6 | 97.0 | -| DanceTrack | 53.8 | 53.8 | 90.1 | +| Dataset | HOTA | IDF1 | MOTA | +| :--------: | :--: | :--: | :--: | +| MOT17 | 63.0 | 79.1 | 77.4 | +| SportsMOT | 73.1 | 72.6 | 96.7 | +| SoccerNet | 82.6 | 76.6 | 97.0 | +| DanceTrack | 53.8 | 53.8 | 90.1 | === "Tuned parameters" Tuned with Optuna (`trackers_cbiou_tuning.ipynb`): MOT17 val-half, SportsMOT val, SoccerNet train, DanceTrack train; evaluated on the splits below. -| Dataset | HOTA | IDF1 | MOTA | -| :-------: | :------: | :------: | :------: | -| MOT17 | 63.0 | 79.1 | 77.2 | -| SportsMOT | 72.5 | 72.2 | 96.9 | -| SoccerNet | 85.5 | 79.6 | 99.3 | -| DanceTrack | 53.3 | 54.4 | 89.2 | +| Dataset | HOTA | IDF1 | MOTA | +| :--------: | :--: | :--: | :--: | +| MOT17 | 63.0 | 79.1 | 77.2 | +| SportsMOT | 72.5 | 72.2 | 96.9 | +| SoccerNet | 85.5 | 79.6 | 99.3 | +| DanceTrack | 53.3 | 54.4 | 89.2 | C-BIoU is aimed at sports and dance scenes with irregular motion and similar-looking objects (SoccerNet, DanceTrack, SportsMOT), where the paper reports strong gains over SORT-style baselines. From e4ab8c69c6d37c4ada2fdb87f047fccfc7a8ca15 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 19 May 2026 10:27:40 -0300 Subject: [PATCH 09/23] updated CBIoU numbers --- docs/trackers/cbiou.md | 63 +++++++-------------------- docs/trackers/comparison.md | 85 +++++++++++++++++++------------------ 2 files changed, 58 insertions(+), 90 deletions(-) diff --git a/docs/trackers/cbiou.md b/docs/trackers/cbiou.md index 791dc8e8..eb117499 100644 --- a/docs/trackers/cbiou.md +++ b/docs/trackers/cbiou.md @@ -1,51 +1,27 @@ --- -title: C-BIoU: Cascaded-Buffered IoU Tracker | Trackers +title: C-BIoU — Cascaded-Buffered IoU Tracker | Trackers comments: true -description: Cascaded-Buffered IoU (C-BIoU) tracker from Yang et al. (WACV 2023). Cascaded BIoU matching with buffer scales b1 and b2 (b1 < b2) for irregular motion and similar appearances. +description: "C-BIoU improves association under fast motion and similar appearances by matching with Buffered IoU instead of plain IoU, using a BoT-SORT-style pipeline without camera motion compensation." --- # C-BIoU (Cascaded-Buffered IoU) -## Overview +## What is C-BIoU? -**C-BIoU** implements the tracker from Yang et al., [*Hard To Track Objects with Irregular Motions and Similar Appearances? Make It Easier by Buffering the Matching Space*](https://openaccess.thecvf.com/content/WACV2023/papers/Yang_Hard_To_Track_Objects_With_Irregular_Motions_and_Similar_Appearances_WACV_2023_paper.pdf) (WACV 2023). - -The core idea is **Buffered IoU (BIoU)**: before computing overlap, each bounding box is expanded by a margin proportional to its width and height. That widens the matching space so tracks and detections can be associated even when raw boxes barely touch or miss due to fast motion or detector jitter. - -The paper uses **cascaded matching** with two buffer scales: **b1** (small, first pass) and **b2** (large, second pass on remaining unmatched pairs). You should keep **b1 < b2**. On SoccerNet, the authors report grid-searching **b1 = 0.7** and **b2 = 1.0** (with a BIoU match threshold of **0.01** on that dataset). In this library, `buffer_ratio_first` is **b1** and `buffer_ratio_second` is **b2**. - -C-BIoU follows the same tracking-by-detection backbone as [BoT-SORT](botsort.md) (Kalman prediction, two-stage high/low confidence association, unconfirmed matching) but **does not use camera motion compensation**. Only detection boxes are required, which suits MOT-benchmark and file-based workflows. +C-BIoU builds on the same tracking pipeline as [ByteTrack](bytetrack.md) and [BoT-SORT](botsort.md) but replaces plain IoU with **Buffered IoU (BIoU)**, expanding boxes before overlap is computed so tracks and detections can still match when motion is fast or boxes barely align. It runs two association passes with a small buffer first and a larger buffer second (`buffer_ratio_first` and `buffer_ratio_second`), and does not use camera motion compensation, so only bounding boxes are required. C-BIoU is a strong fit for sports and dance footage where objects move irregularly and look alike. ## How does C-BIoU compare to other trackers? -For comparisons with other trackers, plus dataset context and evaluation details, see the [tracker comparison](comparison.md) page. - -Measured with this library (YOLOX detections on MOT17 and SportsMOT test; oracle detections on SoccerNet test and DanceTrack val). Default buffers: -`buffer_ratio_first=0.3`, `buffer_ratio_second=0.5`. - -=== "Default parameters" - -| Dataset | HOTA | IDF1 | MOTA | -| :-------: | :------: | :------: | :------: | -| MOT17 | 63.0 | 79.1 | 77.4 | -| SportsMOT | 73.1 | 72.6 | 96.7 | -| SoccerNet | 82.6 | 76.6 | 97.0 | -| DanceTrack | 53.8 | 53.8 | 90.1 | - -=== "Tuned parameters" - -Tuned with Optuna (`trackers_cbiou_tuning.ipynb`): MOT17 val-half, SportsMOT val, SoccerNet train, DanceTrack train; evaluated on the splits below. - -| Dataset | HOTA | IDF1 | MOTA | -| :-------: | :------: | :------: | :------: | -| MOT17 | 63.0 | 79.1 | 77.2 | -| SportsMOT | 72.5 | 72.2 | 96.9 | -| SoccerNet | 85.5 | 79.6 | 99.3 | -| DanceTrack | 53.3 | 54.4 | 89.2 | +For comparisons with other trackers, plus default and tuned parameters, see the [tracker comparison](comparison.md) page. -C-BIoU is aimed at sports and dance scenes with irregular motion and similar-looking objects (SoccerNet, DanceTrack, SportsMOT), where the paper reports strong gains over SORT-style baselines. +| Dataset | HOTA | IDF1 | MOTA | +| :-------: | :--: | :--: | :--: | +| MOT17 | 63.0 | 79.1 | 77.4 | +| SportsMOT | 73.1 | 72.6 | 96.7 | +| SoccerNet | 82.6 | 76.6 | 97.0 | +| DanceTrack | 53.8 | 53.8 | 90.1 | -## Algorithm +## How does C-BIoU work? C-BIoU keeps the [ByteTrack](bytetrack.md)-style association pipeline used in [BoT-SORT](botsort.md) but replaces plain IoU with **cascaded Buffered IoU** at each association step. @@ -92,10 +68,7 @@ These examples use `opencv-python` for decoding and display. Replace ` Date: Tue, 19 May 2026 13:29:20 +0000 Subject: [PATCH 10/23] =?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/trackers/cbiou.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/trackers/cbiou.md b/docs/trackers/cbiou.md index eb117499..372c7513 100644 --- a/docs/trackers/cbiou.md +++ b/docs/trackers/cbiou.md @@ -1,7 +1,7 @@ --- title: C-BIoU — Cascaded-Buffered IoU Tracker | Trackers comments: true -description: "C-BIoU improves association under fast motion and similar appearances by matching with Buffered IoU instead of plain IoU, using a BoT-SORT-style pipeline without camera motion compensation." +description: C-BIoU improves association under fast motion and similar appearances by matching with Buffered IoU instead of plain IoU, using a BoT-SORT-style pipeline without camera motion compensation. --- # C-BIoU (Cascaded-Buffered IoU) @@ -14,11 +14,11 @@ C-BIoU builds on the same tracking pipeline as [ByteTrack](bytetrack.md) and [Bo For comparisons with other trackers, plus default and tuned parameters, see the [tracker comparison](comparison.md) page. -| Dataset | HOTA | IDF1 | MOTA | -| :-------: | :--: | :--: | :--: | -| MOT17 | 63.0 | 79.1 | 77.4 | -| SportsMOT | 73.1 | 72.6 | 96.7 | -| SoccerNet | 82.6 | 76.6 | 97.0 | +| Dataset | HOTA | IDF1 | MOTA | +| :--------: | :--: | :--: | :--: | +| MOT17 | 63.0 | 79.1 | 77.4 | +| SportsMOT | 73.1 | 72.6 | 96.7 | +| SoccerNet | 82.6 | 76.6 | 97.0 | | DanceTrack | 53.8 | 53.8 | 90.1 | ## How does C-BIoU work? From 9094128658a5a86feff0008ad39fbe4b4302db4e Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 19 May 2026 14:32:37 -0300 Subject: [PATCH 11/23] added expected cbiou results --- tests/data/tracker_expected_dancetrack.json | 6 ++++++ tests/data/tracker_expected_sportsmot.json | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/tests/data/tracker_expected_dancetrack.json b/tests/data/tracker_expected_dancetrack.json index 6b9f8cdf..1b527958 100644 --- a/tests/data/tracker_expected_dancetrack.json +++ b/tests/data/tracker_expected_dancetrack.json @@ -22,5 +22,11 @@ "MOTA": 99.605, "IDF1": 76.841, "IDSW": 608 + }, + "cbiou": { + "HOTA": 80.156, + "MOTA": 99.623, + "IDF1": 77.198, + "IDSW": 614 } } diff --git a/tests/data/tracker_expected_sportsmot.json b/tests/data/tracker_expected_sportsmot.json index f2cbda21..42afbf2a 100644 --- a/tests/data/tracker_expected_sportsmot.json +++ b/tests/data/tracker_expected_sportsmot.json @@ -22,5 +22,11 @@ "MOTA": 98.884, "IDF1": 80.626, "IDSW": 983 + }, + "cbiou": { + "HOTA": 87.386, + "MOTA": 99.547, + "IDF1": 82.762, + "IDSW": 605 } } From baec2304d5e1afee2f6d7de453aec5313b61e669 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 19 May 2026 15:20:03 -0300 Subject: [PATCH 12/23] small adjust in tests --- tests/core/shared_ids.py | 4 +- tests/core/test_cbiou_tracker.py | 61 +++++++++++++++- tests/core/test_trackers.py | 120 +------------------------------ 3 files changed, 62 insertions(+), 123 deletions(-) diff --git a/tests/core/shared_ids.py b/tests/core/shared_ids.py index 45eba79f..911e2784 100644 --- a/tests/core/shared_ids.py +++ b/tests/core/shared_ids.py @@ -9,5 +9,5 @@ ALL_TRACKER_IDS = ["sort", "bytetrack", "ocsort", "botsort", "cbiou"] # Trackers that accept a user-supplied ``iou=`` constructor argument. -# CBIoU is intentionally excluded: it is opinionated and always uses BIoU. -IOC_TRACKER_IDS = [tid for tid in ALL_TRACKER_IDS if tid != "cbiou"] +# C-BIoU is intentionally excluded: it is opinionated and always uses BIoU. +IOU_TRACKER_IDS = [tid for tid in ALL_TRACKER_IDS if tid != "cbiou"] diff --git a/tests/core/test_cbiou_tracker.py b/tests/core/test_cbiou_tracker.py index 368642c5..6ce6d4fd 100644 --- a/tests/core/test_cbiou_tracker.py +++ b/tests/core/test_cbiou_tracker.py @@ -22,6 +22,7 @@ import supervision as sv from trackers.core.botsort.tracker import BoTSORTTracker +from trackers.core.botsort.tracklet import BoTSORTTracklet from trackers.core.cbiou.tracker import CBIoUTracker from trackers.utils.iou import BIoU @@ -98,7 +99,7 @@ def test_near_miss_associated_with_buffer(self) -> None: Box A: [0, 0, 100, 100] (100x100) Box B: [110, 0, 210, 100] (gap of 10px = 10% of width) With buffer_ratio=0.15 each side expands by 15px, so A becomes - [-15, -15, 115, 115] and B becomes [93.5, -15, 226.5, 115] — + [-15, -15, 115, 115] and B becomes [95, -15, 225, 115] — they now overlap. """ # Frame 1: spawn a track at box A with high confidence @@ -120,6 +121,7 @@ def test_near_miss_associated_with_buffer(self) -> None: cbiou.update(_detection(box_a)) botsort.update(_detection(box_a)) + botsort_frame1_track_id = next((t.tracker_id for t in botsort.tracks), None) # Frame 2: detection slightly outside A — CBIoU buffer closes the gap cbiou_result = cbiou.update(_detection(box_b)) @@ -131,8 +133,61 @@ def test_near_miss_associated_with_buffer(self) -> None: assert cbiou_result.tracker_id[0] == cbiou_frame1_id botsort_ids = botsort_result.tracker_id - if botsort_ids is not None and len(botsort_ids) > 0: - assert botsort_ids[0] != cbiou_frame1_id or botsort_ids[0] == -1 + if botsort_ids is not None and len(botsort_ids) > 0 and botsort_frame1_track_id is not None: + assert botsort_ids[0] != botsort_frame1_track_id + + +class TestCBIoUZeroBufferEquivalence: + """With buffer_ratio=0, BIoU recovers IoU; C-BIoU should match BoT-SORT (no CMC).""" + + def test_zero_buffer_matches_botsort_without_cmc(self) -> None: + shared_kwargs = { + "minimum_consecutive_frames": 1, + "track_activation_threshold": 0.5, + "minimum_iou_threshold_first_assoc": 0.3, + "minimum_iou_threshold_second_assoc": 0.3, + "minimum_iou_threshold_unconfirmed_assoc": 0.3, + "high_conf_det_threshold": 0.6, + } + detections = [ + _detection((0.0, 0.0, 50.0, 50.0)), + _detection((5.0, 5.0, 55.0, 55.0)), + _detection((100.0, 100.0, 150.0, 150.0)), + _detection((105.0, 105.0, 155.0, 155.0)), + _detection((8.0, 8.0, 58.0, 58.0)), + ] + + def run_tracker(tracker: CBIoUTracker | BoTSORTTracker) -> list[sv.Detections]: + BoTSORTTracklet.count_id = 0 + tracker.reset() + return [tracker.update(det) for det in detections] + + cbiou = CBIoUTracker( + buffer_ratio_first=0.0, + buffer_ratio_second=0.0, + **shared_kwargs, + ) + botsort = BoTSORTTracker(enable_cmc=False, **shared_kwargs) + + cbiou_results = run_tracker(cbiou) + botsort_results = run_tracker(botsort) + + for frame_idx, (r_cbiou, r_botsort) in enumerate(zip(cbiou_results, botsort_results)): + assert len(r_cbiou) == len(r_botsort), ( + f"frame {frame_idx}: CBIoU(buffer=0) and BoTSORT(no CMC) returned different " + f"detection counts ({len(r_cbiou)} vs {len(r_botsort)})" + ) + np.testing.assert_array_equal( + r_cbiou.tracker_id, + r_botsort.tracker_id, + err_msg=f"frame {frame_idx}: different tracker IDs", + ) + if len(r_cbiou) > 0: + np.testing.assert_allclose( + r_cbiou.xyxy, + r_botsort.xyxy, + err_msg=f"frame {frame_idx}: different boxes", + ) class TestCBIoUSearchSpace: diff --git a/tests/core/test_trackers.py b/tests/core/test_trackers.py index 28f573d7..0d55ac47 100644 --- a/tests/core/test_trackers.py +++ b/tests/core/test_trackers.py @@ -30,7 +30,7 @@ from trackers.core.sort.tracker import SORTTracker from trackers.utils.iou import BaseIoU -from .shared_ids import ALL_TRACKER_IDS, IOC_TRACKER_IDS +from .shared_ids import ALL_TRACKER_IDS, IOU_TRACKER_IDS # --------------------------------------------------------------------------- # Helpers @@ -119,7 +119,7 @@ def test_tracker_update_empty_does_not_mutate_input(tracker_id: str) -> None: assert result is not dets, "update() must return a new sv.Detections instance" -@pytest.mark.parametrize("tracker_id", IOC_TRACKER_IDS) +@pytest.mark.parametrize("tracker_id", IOU_TRACKER_IDS) def test_tracker_uses_configured_iou_variant(tracker_id: str) -> None: """Trackers should use the configured IoU implementation for matching.""" tracking_iou = _TrackingIoU() @@ -129,122 +129,6 @@ def test_tracker_uses_configured_iou_variant(tracker_id: str) -> None: assert tracking_iou.compute_calls > 0 -@pytest.mark.parametrize("tracker_id", ALL_TRACKER_IDS) -def test_no_confidence_detections_can_spawn_confirmed_tracks(tracker_id: str) -> None: - """Missing confidence should behave like usable detections, not suppress tracking.""" - tracker = _instantiate(tracker_id, minimum_consecutive_frames=1) - detection = _no_confidence_detection((100.0, 100.0, 200.0, 200.0)) - - for _ in range(4): - result = tracker.update(detection) - if result.tracker_id is not None and np.any(result.tracker_id >= 0): - return - - raise AssertionError(f"{tracker_id} did not confirm any track for confidence=None detections") - - -@pytest.mark.parametrize( - "xyxy_boxes", - [ - np.array([[100.0, 100.0, 200.0, 200.0]], dtype=np.float32), - np.array( - [ - [100.0, 100.0, 200.0, 200.0], - [400.0, 400.0, 500.0, 500.0], - ], - dtype=np.float32, - ), - np.array( - [ - [10.0, 10.0, 60.0, 60.0], - [200.0, 200.0, 260.0, 260.0], - [500.0, 500.0, 560.0, 560.0], - ], - dtype=np.float32, - ), - ], - ids=["single_box", "two_boxes", "three_boxes_non_overlapping"], -) -def test_bytetrack_no_confidence_matches_explicit_ones_confidence(xyxy_boxes: np.ndarray) -> None: - """ByteTrack treats confidence=None the same as all-ones across multi-box batches. - - The batched scenarios exercise the high/low split machinery in - `ByteTrackTracker.update()` that single-box equivalence cannot trigger; a - regression that mis-buckets `confidence=None` in a multi-detection batch - would still pass single-box equality but would diverge here. - """ - no_confidence_tracker = ByteTrackTracker(minimum_consecutive_frames=1) - explicit_confidence_tracker = ByteTrackTracker(minimum_consecutive_frames=1) - class_ids = np.zeros(len(xyxy_boxes), dtype=int) - detection_without_confidence = sv.Detections(xyxy=xyxy_boxes.copy(), class_id=class_ids.copy()) - detection_with_ones_confidence = sv.Detections( - xyxy=xyxy_boxes.copy(), - confidence=np.ones(len(xyxy_boxes), dtype=np.float32), - class_id=class_ids.copy(), - ) - - no_confidence_tracker.reset() - no_confidence_results = [no_confidence_tracker.update(detection_without_confidence) for _ in range(4)] - explicit_confidence_tracker.reset() - explicit_confidence_results = [explicit_confidence_tracker.update(detection_with_ones_confidence) for _ in range(4)] - - for no_confidence_result, explicit_confidence_result in zip(no_confidence_results, explicit_confidence_results): - assert len(no_confidence_result) == len(explicit_confidence_result) - assert no_confidence_result.tracker_id is not None - assert explicit_confidence_result.tracker_id is not None - np.testing.assert_array_equal(no_confidence_result.tracker_id, explicit_confidence_result.tracker_id) - np.testing.assert_array_equal(no_confidence_result.xyxy, explicit_confidence_result.xyxy) - - -def test_bytetrack_no_confidence_spawns_tracks_below_activation_threshold() -> None: - """confidence=None must route every detection to Stage 1 even when explicit-low-conf would not spawn. - - Asserts the actual semantic of treating `None` as 1.0: explicit - confidences that fall under `track_activation_threshold` get suppressed - in the low-confidence branch, but the same boxes with `confidence=None` - still produce confirmed tracker IDs. - """ - activation_threshold = 0.6 - xyxy_boxes = np.array( - [ - [100.0, 100.0, 200.0, 200.0], - [400.0, 400.0, 500.0, 500.0], - ], - dtype=np.float32, - ) - class_ids = np.zeros(len(xyxy_boxes), dtype=int) - detection_without_confidence = sv.Detections(xyxy=xyxy_boxes.copy(), class_id=class_ids.copy()) - detection_with_low_confidence = sv.Detections( - xyxy=xyxy_boxes.copy(), - confidence=np.array([0.2, 0.3], dtype=np.float32), - class_id=class_ids.copy(), - ) - - no_confidence_tracker = ByteTrackTracker( - minimum_consecutive_frames=1, - track_activation_threshold=activation_threshold, - high_conf_det_threshold=activation_threshold, - ) - low_confidence_tracker = ByteTrackTracker( - minimum_consecutive_frames=1, - track_activation_threshold=activation_threshold, - high_conf_det_threshold=activation_threshold, - ) - - no_confidence_tracker.reset() - low_confidence_tracker.reset() - for _ in range(4): - no_confidence_result = no_confidence_tracker.update(detection_without_confidence) - low_confidence_result = low_confidence_tracker.update(detection_with_low_confidence) - - assert no_confidence_result.tracker_id is not None - assert low_confidence_result.tracker_id is not None - assert np.all(no_confidence_result.tracker_id >= 0), "confidence=None should spawn confirmed tracks for every box" - assert np.all(low_confidence_result.tracker_id < 0), ( - "explicit low confidence below activation threshold should NOT spawn tracks" - ) - - def test_bytetrack_calls_iou_in_low_confidence_branch() -> None: """ByteTrack must call the configured IoU in its low-confidence association branch.""" from trackers import ByteTrackTracker From 9bc87a66fd75393f72af9d29372e7efdd917ada5 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 19 May 2026 15:37:19 -0300 Subject: [PATCH 13/23] mypy fix --- tests/core/test_cbiou_tracker.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/core/test_cbiou_tracker.py b/tests/core/test_cbiou_tracker.py index 6ce6d4fd..f8a3f6b1 100644 --- a/tests/core/test_cbiou_tracker.py +++ b/tests/core/test_cbiou_tracker.py @@ -141,14 +141,6 @@ class TestCBIoUZeroBufferEquivalence: """With buffer_ratio=0, BIoU recovers IoU; C-BIoU should match BoT-SORT (no CMC).""" def test_zero_buffer_matches_botsort_without_cmc(self) -> None: - shared_kwargs = { - "minimum_consecutive_frames": 1, - "track_activation_threshold": 0.5, - "minimum_iou_threshold_first_assoc": 0.3, - "minimum_iou_threshold_second_assoc": 0.3, - "minimum_iou_threshold_unconfirmed_assoc": 0.3, - "high_conf_det_threshold": 0.6, - } detections = [ _detection((0.0, 0.0, 50.0, 50.0)), _detection((5.0, 5.0, 55.0, 55.0)), @@ -158,16 +150,28 @@ def test_zero_buffer_matches_botsort_without_cmc(self) -> None: ] def run_tracker(tracker: CBIoUTracker | BoTSORTTracker) -> list[sv.Detections]: - BoTSORTTracklet.count_id = 0 tracker.reset() return [tracker.update(det) for det in detections] cbiou = CBIoUTracker( buffer_ratio_first=0.0, buffer_ratio_second=0.0, - **shared_kwargs, + minimum_consecutive_frames=1, + track_activation_threshold=0.5, + minimum_iou_threshold_first_assoc=0.3, + minimum_iou_threshold_second_assoc=0.3, + minimum_iou_threshold_unconfirmed_assoc=0.3, + high_conf_det_threshold=0.6, + ) + botsort = BoTSORTTracker( + enable_cmc=False, + minimum_consecutive_frames=1, + track_activation_threshold=0.5, + minimum_iou_threshold_first_assoc=0.3, + minimum_iou_threshold_second_assoc=0.3, + minimum_iou_threshold_unconfirmed_assoc=0.3, + high_conf_det_threshold=0.6, ) - botsort = BoTSORTTracker(enable_cmc=False, **shared_kwargs) cbiou_results = run_tracker(cbiou) botsort_results = run_tracker(botsort) From f55b1c572eecf037670182db3c30450ae6a3a99f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 18:37:47 +0000 Subject: [PATCH 14/23] =?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 --- tests/core/test_cbiou_tracker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/core/test_cbiou_tracker.py b/tests/core/test_cbiou_tracker.py index f8a3f6b1..dd20b1ff 100644 --- a/tests/core/test_cbiou_tracker.py +++ b/tests/core/test_cbiou_tracker.py @@ -22,7 +22,6 @@ import supervision as sv from trackers.core.botsort.tracker import BoTSORTTracker -from trackers.core.botsort.tracklet import BoTSORTTracklet from trackers.core.cbiou.tracker import CBIoUTracker from trackers.utils.iou import BIoU From cd336177ec561658ac557f813930da9643ef1f4e Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 19 May 2026 15:41:26 -0300 Subject: [PATCH 15/23] docs fix --- docs/trackers/comparison.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/trackers/comparison.md b/docs/trackers/comparison.md index 87dcc0ae..53d0eea8 100644 --- a/docs/trackers/comparison.md +++ b/docs/trackers/comparison.md @@ -271,7 +271,6 @@ 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 - enable_cmc: false ``` ## [DanceTrack](https://arxiv.org/abs/2111.14690) @@ -364,7 +363,6 @@ Group dancing tracking with uniform appearance, diverse motions, and extreme art track_activation_threshold: 0.83 buffer_ratio_first: 0.23 buffer_ratio_second: 0.33 - enable_cmc: false ``` ## Methodology From ec23482a44f6d40ceb2b9f35c56ccf04d4c20877 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Tue, 19 May 2026 15:43:34 -0300 Subject: [PATCH 16/23] handling default confidence with utility --- src/trackers/core/cbiou/tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/trackers/core/cbiou/tracker.py b/src/trackers/core/cbiou/tracker.py index eef01e41..92df47b1 100644 --- a/src/trackers/core/cbiou/tracker.py +++ b/src/trackers/core/cbiou/tracker.py @@ -8,7 +8,7 @@ import numpy as np import supervision as sv - +from trackers.utils.detections import default_confidences from trackers.core.botsort.tracker import BoTSORTTracker from trackers.core.botsort.tracklet import BoTSORTTracklet from trackers.core.botsort.utils import _fuse_score, get_alive_tracklets @@ -161,7 +161,7 @@ def update( tracker.predict() detection_boxes = detections.xyxy - confidences = detections.confidence if detections.confidence is not None else np.ones(len(detections)) + confidences = default_confidences(detections) # Split detections into high / low / discarded by confidence high_mask = confidences >= self.high_conf_det_threshold From 36deef2ee5362a98d8abb404f8a0fbeda8841b2e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 18:44:24 +0000 Subject: [PATCH 17/23] =?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 --- src/trackers/core/cbiou/tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/trackers/core/cbiou/tracker.py b/src/trackers/core/cbiou/tracker.py index 92df47b1..79fde233 100644 --- a/src/trackers/core/cbiou/tracker.py +++ b/src/trackers/core/cbiou/tracker.py @@ -8,10 +8,11 @@ import numpy as np import supervision as sv -from trackers.utils.detections import default_confidences + from trackers.core.botsort.tracker import BoTSORTTracker from trackers.core.botsort.tracklet import BoTSORTTracklet from trackers.core.botsort.utils import _fuse_score, get_alive_tracklets +from trackers.utils.detections import default_confidences from trackers.utils.iou import BIoU from trackers.utils.state_representations import BaseStateEstimator, XCYCWHStateEstimator From aaa443262211c36031fb5f1b3b9773b9cdc050dd Mon Sep 17 00:00:00 2001 From: Alexander Bodner <61150961+AlexBodner@users.noreply.github.com> Date: Wed, 20 May 2026 16:37:00 -0300 Subject: [PATCH 18/23] Update docs/trackers/cbiou.md Co-authored-by: Jirka Borovec <6035284+Borda@users.noreply.github.com> --- docs/trackers/cbiou.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/trackers/cbiou.md b/docs/trackers/cbiou.md index 372c7513..0e1ebeb6 100644 --- a/docs/trackers/cbiou.md +++ b/docs/trackers/cbiou.md @@ -37,7 +37,7 @@ C-BIoU keeps the [ByteTrack](bytetrack.md)-style association pipeline used in [B | Parameter | Purpose | Tuning guidance | | ----------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `lost_track_buffer` | Frames to keep an unmatched track alive before deletion. | Higher tolerates longer occlusions but risks false re-association. 10-30 for most scenes; up to 60 for very long occlusions. | +| `lost_track_buffer` | Frames to keep an unmatched track alive before deletion. | Higher tolerates longer occlusions but risks false re-association. Use range (10, 30) for most scenes; up to 60 for very long occlusions. | | `track_activation_threshold` | Minimum detection confidence required to start a new track. | Higher reduces noisy track creation; lower retains harder objects. 0.5-0.9 typical depending on detector quality. This does not control low-confidence association, which still discards detections at a fixed `0.1` confidence floor. | | `minimum_consecutive_frames` | Consecutive matches required before confirming a new track. | 1 for immediate activation; 2-3 improves robustness against flicker and false positives. | | `minimum_iou_threshold_first_assoc` | Minimum fused BIoU similarity for the first association pass. | Paper uses very low values on some datasets (e.g. 0.01 on SoccerNet). Lower helps maintain matches under fast motion; higher is stricter. | From 91865b9050a91b92d83877fd4fd62d6cc59e6d43 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 20 May 2026 19:37:19 +0000 Subject: [PATCH 19/23] =?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/trackers/cbiou.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/trackers/cbiou.md b/docs/trackers/cbiou.md index 0e1ebeb6..8334b22b 100644 --- a/docs/trackers/cbiou.md +++ b/docs/trackers/cbiou.md @@ -37,7 +37,7 @@ C-BIoU keeps the [ByteTrack](bytetrack.md)-style association pipeline used in [B | Parameter | Purpose | Tuning guidance | | ----------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `lost_track_buffer` | Frames to keep an unmatched track alive before deletion. | Higher tolerates longer occlusions but risks false re-association. Use range (10, 30) for most scenes; up to 60 for very long occlusions. | +| `lost_track_buffer` | Frames to keep an unmatched track alive before deletion. | Higher tolerates longer occlusions but risks false re-association. Use range (10, 30) for most scenes; up to 60 for very long occlusions. | | `track_activation_threshold` | Minimum detection confidence required to start a new track. | Higher reduces noisy track creation; lower retains harder objects. 0.5-0.9 typical depending on detector quality. This does not control low-confidence association, which still discards detections at a fixed `0.1` confidence floor. | | `minimum_consecutive_frames` | Consecutive matches required before confirming a new track. | 1 for immediate activation; 2-3 improves robustness against flicker and false positives. | | `minimum_iou_threshold_first_assoc` | Minimum fused BIoU similarity for the first association pass. | Paper uses very low values on some datasets (e.g. 0.01 on SoccerNet). Lower helps maintain matches under fast motion; higher is stricter. | From fc66939015eab007e92269914357c8fa0e00f8a8 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Wed, 20 May 2026 17:21:00 -0300 Subject: [PATCH 20/23] fixed tests pointed out by Tomasz which were modified in PR #415 but not merged here --- tests/core/test_trackers.py | 116 ++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/tests/core/test_trackers.py b/tests/core/test_trackers.py index 0d55ac47..92654628 100644 --- a/tests/core/test_trackers.py +++ b/tests/core/test_trackers.py @@ -129,6 +129,122 @@ def test_tracker_uses_configured_iou_variant(tracker_id: str) -> None: assert tracking_iou.compute_calls > 0 +@pytest.mark.parametrize("tracker_id", ALL_TRACKER_IDS) +def test_no_confidence_detections_can_spawn_confirmed_tracks(tracker_id: str) -> None: + """Missing confidence should behave like usable detections, not suppress tracking.""" + tracker = _instantiate(tracker_id, minimum_consecutive_frames=1) + detection = _no_confidence_detection((100.0, 100.0, 200.0, 200.0)) + + for _ in range(4): + result = tracker.update(detection) + if result.tracker_id is not None and np.any(result.tracker_id >= 0): + return + + raise AssertionError(f"{tracker_id} did not confirm any track for confidence=None detections") + + +@pytest.mark.parametrize("tracker_id", ALL_TRACKER_IDS) +@pytest.mark.parametrize( + "xyxy_boxes", + [ + np.array([[100.0, 100.0, 200.0, 200.0]], dtype=np.float32), + np.array( + [ + [100.0, 100.0, 200.0, 200.0], + [400.0, 400.0, 500.0, 500.0], + ], + dtype=np.float32, + ), + np.array( + [ + [10.0, 10.0, 60.0, 60.0], + [200.0, 200.0, 260.0, 260.0], + [500.0, 500.0, 560.0, 560.0], + ], + dtype=np.float32, + ), + ], + ids=["single_box", "two_boxes", "three_boxes_non_overlapping"], +) +def test_no_confidence_matches_explicit_ones_confidence(tracker_id: str, xyxy_boxes: np.ndarray) -> None: + """Every tracker treats confidence=None the same as all-ones across multi-box batches. + + Multi-detection batches exercise confidence bucketing in trackers that split + high/low detections; a regression that mis-buckets ``confidence=None`` would + still pass single-box equality but diverge here. + """ + no_confidence_tracker = _instantiate(tracker_id, minimum_consecutive_frames=1) + explicit_confidence_tracker = _instantiate(tracker_id, minimum_consecutive_frames=1) + class_ids = np.zeros(len(xyxy_boxes), dtype=int) + detection_without_confidence = sv.Detections(xyxy=xyxy_boxes.copy(), class_id=class_ids.copy()) + detection_with_ones_confidence = sv.Detections( + xyxy=xyxy_boxes.copy(), + confidence=np.ones(len(xyxy_boxes), dtype=np.float32), + class_id=class_ids.copy(), + ) + + no_confidence_tracker.reset() + no_confidence_results = [no_confidence_tracker.update(detection_without_confidence) for _ in range(4)] + explicit_confidence_tracker.reset() + explicit_confidence_results = [explicit_confidence_tracker.update(detection_with_ones_confidence) for _ in range(4)] + + for no_confidence_result, explicit_confidence_result in zip(no_confidence_results, explicit_confidence_results): + assert len(no_confidence_result) == len(explicit_confidence_result) + assert no_confidence_result.tracker_id is not None + assert explicit_confidence_result.tracker_id is not None + np.testing.assert_array_equal(no_confidence_result.tracker_id, explicit_confidence_result.tracker_id) + np.testing.assert_array_equal(no_confidence_result.xyxy, explicit_confidence_result.xyxy) + + +def test_bytetrack_no_confidence_spawns_tracks_below_activation_threshold() -> None: + """confidence=None must route every detection to Stage 1 even when explicit-low-conf would not spawn. + + Asserts the actual semantic of treating `None` as 1.0: explicit + confidences that fall under `track_activation_threshold` get suppressed + in the low-confidence branch, but the same boxes with `confidence=None` + still produce confirmed tracker IDs. + """ + activation_threshold = 0.6 + xyxy_boxes = np.array( + [ + [100.0, 100.0, 200.0, 200.0], + [400.0, 400.0, 500.0, 500.0], + ], + dtype=np.float32, + ) + class_ids = np.zeros(len(xyxy_boxes), dtype=int) + detection_without_confidence = sv.Detections(xyxy=xyxy_boxes.copy(), class_id=class_ids.copy()) + detection_with_low_confidence = sv.Detections( + xyxy=xyxy_boxes.copy(), + confidence=np.array([0.2, 0.3], dtype=np.float32), + class_id=class_ids.copy(), + ) + + no_confidence_tracker = ByteTrackTracker( + minimum_consecutive_frames=1, + track_activation_threshold=activation_threshold, + high_conf_det_threshold=activation_threshold, + ) + low_confidence_tracker = ByteTrackTracker( + minimum_consecutive_frames=1, + track_activation_threshold=activation_threshold, + high_conf_det_threshold=activation_threshold, + ) + + no_confidence_tracker.reset() + low_confidence_tracker.reset() + for _ in range(4): + no_confidence_result = no_confidence_tracker.update(detection_without_confidence) + low_confidence_result = low_confidence_tracker.update(detection_with_low_confidence) + + assert no_confidence_result.tracker_id is not None + assert low_confidence_result.tracker_id is not None + assert np.all(no_confidence_result.tracker_id >= 0), "confidence=None should spawn confirmed tracks for every box" + assert np.all(low_confidence_result.tracker_id < 0), ( + "explicit low confidence below activation threshold should NOT spawn tracks" + ) + + def test_bytetrack_calls_iou_in_low_confidence_branch() -> None: """ByteTrack must call the configured IoU in its low-confidence association branch.""" from trackers import ByteTrackTracker From 31668b4d0a7201919fb949ca3003560d030877d2 Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Thu, 21 May 2026 11:59:28 -0300 Subject: [PATCH 21/23] applied some review comments to CBIoU md doc --- docs/trackers/cbiou.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/trackers/cbiou.md b/docs/trackers/cbiou.md index 8334b22b..11354bcf 100644 --- a/docs/trackers/cbiou.md +++ b/docs/trackers/cbiou.md @@ -1,14 +1,14 @@ --- title: C-BIoU — Cascaded-Buffered IoU Tracker | Trackers comments: true -description: C-BIoU improves association under fast motion and similar appearances by matching with Buffered IoU instead of plain IoU, using a BoT-SORT-style pipeline without camera motion compensation. +description: C-BIoU improves association under fast or irregualar motion by matching with Buffered IoU instead of plain IoU, using a ByteTrack-style pipeline. --- # C-BIoU (Cascaded-Buffered IoU) ## What is C-BIoU? -C-BIoU builds on the same tracking pipeline as [ByteTrack](bytetrack.md) and [BoT-SORT](botsort.md) but replaces plain IoU with **Buffered IoU (BIoU)**, expanding boxes before overlap is computed so tracks and detections can still match when motion is fast or boxes barely align. It runs two association passes with a small buffer first and a larger buffer second (`buffer_ratio_first` and `buffer_ratio_second`), and does not use camera motion compensation, so only bounding boxes are required. C-BIoU is a strong fit for sports and dance footage where objects move irregularly and look alike. +C-BIoU builds on the same tracking pipeline as [ByteTrack](bytetrack.md) but replaces plain IoU with **Buffered IoU (BIoU)**, expanding boxes before overlap is computed so tracks and detections can still match when motion of the object is fast or boxes barely align. It runs two association passes with a small buffer first and a larger buffer second (`buffer_ratio_first` and `buffer_ratio_second`), so only bounding boxes are required. C-BIoU is a strong fit for sports and dance footage where objects move fast and change direction. ## How does C-BIoU compare to other trackers? @@ -23,27 +23,27 @@ For comparisons with other trackers, plus default and tuned parameters, see the ## How does C-BIoU work? -C-BIoU keeps the [ByteTrack](bytetrack.md)-style association pipeline used in [BoT-SORT](botsort.md) but replaces plain IoU with **cascaded Buffered IoU** at each association step. +C-BIoU keeps the [ByteTrack](bytetrack.md)-style association pipeline used in [BoT-SORT](botsort.md) but replaces plain IoU with **Cascaded Buffered IoU** at each association step. **First association (b1).** High-confidence detections are matched to confirmed and lost tracks using BIoU with `buffer_ratio_first` (paper **b1**, small buffer). Costs are fused with detection confidence. -**Second association (b2).** Remaining *tracked* tracks (not lost) are matched to low-confidence detections using BIoU with `buffer_ratio_second` (paper **b2**, large buffer). Score fusion is not applied here. In the paper, the large buffer is applied to **remaining unmatched track/detection pairs** after the first cascade; here it is wired to ByteTrack's low-confidence recovery stage. +**Second association (b2).** Remaining *tracked* tracks (not lost) are matched to low-confidence detections using BIoU with `buffer_ratio_second` (paper **b2**, large buffer). In this implementation, this larger buffer corresponds to ByteTrack's recovery stage for unmatched tracks and low-confidence detections. -**Unconfirmed association (b1).** Leftover high-confidence detections are matched to unconfirmed tracks using the same buffer as pass 1. Unmatched unconfirmed tracks are removed. This step is from ByteTrack lifecycle logic, not the paper's two-buffer cascade. +**Unconfirmed association (b1).** Leftover high-confidence detections are matched to unconfirmed tracks using the same buffer as pass 1. Unmatched unconfirmed tracks are removed. This step is inherited from ByteTrack lifecycle logic, not the paper's two-buffer cascade. -**Track lifecycle.** New tracks are initiated and confirmed with a conservative policy (`minimum_consecutive_frames`) to reduce one-frame false positives. Tracks that remain unmatched longer than `lost_track_buffer` are removed. +**Track lifecycle.** New tracks are initiated and confirmed with a conservative policy (`minimum_consecutive_frames`) to reduce one-frame false positives. Existing tracks that remain unmatched longer than `lost_track_buffer` are removed. ## Key Parameters | Parameter | Purpose | Tuning guidance | | ----------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `lost_track_buffer` | Frames to keep an unmatched track alive before deletion. | Higher tolerates longer occlusions but risks false re-association. Use range (10, 30) for most scenes; up to 60 for very long occlusions. | -| `track_activation_threshold` | Minimum detection confidence required to start a new track. | Higher reduces noisy track creation; lower retains harder objects. 0.5-0.9 typical depending on detector quality. This does not control low-confidence association, which still discards detections at a fixed `0.1` confidence floor. | -| `minimum_consecutive_frames` | Consecutive matches required before confirming a new track. | 1 for immediate activation; 2-3 improves robustness against flicker and false positives. | -| `minimum_iou_threshold_first_assoc` | Minimum fused BIoU similarity for the first association pass. | Paper uses very low values on some datasets (e.g. 0.01 on SoccerNet). Lower helps maintain matches under fast motion; higher is stricter. | -| `minimum_iou_threshold_second_assoc` | Minimum BIoU similarity for the second association pass. | Usually set lower than the first-pass threshold to recover weak detections without over-matching. | -| `minimum_iou_threshold_unconfirmed_assoc` | Minimum fused BIoU similarity when associating unconfirmed tracks. | Higher values make tentative tracks harder to confirm spuriously; lower values help short-lived or noisy objects survive. | -| `high_conf_det_threshold` | Confidence split between stage-1 and stage-2 detections. | 0.5-0.7 common. Higher shifts more detections to recovery stage; lower gives stage-1 broader coverage. | +| `lost_track_buffer` | Number of frames to keep an unmatched track alive before deletion. | Higher value tolerates longer occlusions but risks false re-association. Use range (10, 30) for most scenes; up to 60 for very long occlusions. | +| `track_activation_threshold` | Minimum detection confidence required to start a new track. | Higher value reduces noisy track creation; lower value retains harder objects. 0.5-0.9 typical depending on detector quality. This does not control low-confidence association, which still discards detections at a fixed `0.1` confidence floor. | +| `minimum_consecutive_frames` | Number of consecutive matches required before confirming a new track. | 1 for immediate activation; 2-3 improves robustness against flicker and false positives. | +| `minimum_iou_threshold_first_assoc` | Minimum fused BIoU similarity for the first association pass. | Lower value helps maintain matches under fast motion; higher value is stricter. | +| `minimum_iou_threshold_second_assoc` | Minimum BIoU similarity for the second association pass. | Usually set to a lower value than the first-pass threshold to recover weak detections without over-matching. | +| `minimum_iou_threshold_unconfirmed_assoc` | Minimum fused BIoU similarity when associating unconfirmed tracks. | Higher value makes tentative tracks harder to confirm spuriously; lower value helps short-lived or noisy objects survive. | +| `high_conf_det_threshold` | Confidence split between stage-1 and stage-2 detections. | 0.5-0.7 common. Higher value shifts more detections to recovery stage; lower value gives stage-1 broader coverage. | | `buffer_ratio_first` | Paper **b1**, small BIoU buffer for the first association pass. | Typical range 0.1-0.7. Should be **less than** `buffer_ratio_second`. | | `buffer_ratio_second` | Paper **b2**, large BIoU buffer for the second association pass. | Typical range 0.2-1.0. Should be **greater than** `buffer_ratio_first`. | From a988af07b44d68e15f9e0d7cabf414852be94fe2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 14:59:51 +0000 Subject: [PATCH 22/23] =?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/trackers/cbiou.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/trackers/cbiou.md b/docs/trackers/cbiou.md index 11354bcf..b7591a18 100644 --- a/docs/trackers/cbiou.md +++ b/docs/trackers/cbiou.md @@ -35,17 +35,17 @@ C-BIoU keeps the [ByteTrack](bytetrack.md)-style association pipeline used in [B ## Key Parameters -| Parameter | Purpose | Tuning guidance | -| ----------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `lost_track_buffer` | Number of frames to keep an unmatched track alive before deletion. | Higher value tolerates longer occlusions but risks false re-association. Use range (10, 30) for most scenes; up to 60 for very long occlusions. | -| `track_activation_threshold` | Minimum detection confidence required to start a new track. | Higher value reduces noisy track creation; lower value retains harder objects. 0.5-0.9 typical depending on detector quality. This does not control low-confidence association, which still discards detections at a fixed `0.1` confidence floor. | -| `minimum_consecutive_frames` | Number of consecutive matches required before confirming a new track. | 1 for immediate activation; 2-3 improves robustness against flicker and false positives. | -| `minimum_iou_threshold_first_assoc` | Minimum fused BIoU similarity for the first association pass. | Lower value helps maintain matches under fast motion; higher value is stricter. | -| `minimum_iou_threshold_second_assoc` | Minimum BIoU similarity for the second association pass. | Usually set to a lower value than the first-pass threshold to recover weak detections without over-matching. | -| `minimum_iou_threshold_unconfirmed_assoc` | Minimum fused BIoU similarity when associating unconfirmed tracks. | Higher value makes tentative tracks harder to confirm spuriously; lower value helps short-lived or noisy objects survive. | -| `high_conf_det_threshold` | Confidence split between stage-1 and stage-2 detections. | 0.5-0.7 common. Higher value shifts more detections to recovery stage; lower value gives stage-1 broader coverage. | -| `buffer_ratio_first` | Paper **b1**, small BIoU buffer for the first association pass. | Typical range 0.1-0.7. Should be **less than** `buffer_ratio_second`. | -| `buffer_ratio_second` | Paper **b2**, large BIoU buffer for the second association pass. | Typical range 0.2-1.0. Should be **greater than** `buffer_ratio_first`. | +| Parameter | Purpose | Tuning guidance | +| ----------------------------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `lost_track_buffer` | Number of frames to keep an unmatched track alive before deletion. | Higher value tolerates longer occlusions but risks false re-association. Use range (10, 30) for most scenes; up to 60 for very long occlusions. | +| `track_activation_threshold` | Minimum detection confidence required to start a new track. | Higher value reduces noisy track creation; lower value retains harder objects. 0.5-0.9 typical depending on detector quality. This does not control low-confidence association, which still discards detections at a fixed `0.1` confidence floor. | +| `minimum_consecutive_frames` | Number of consecutive matches required before confirming a new track. | 1 for immediate activation; 2-3 improves robustness against flicker and false positives. | +| `minimum_iou_threshold_first_assoc` | Minimum fused BIoU similarity for the first association pass. | Lower value helps maintain matches under fast motion; higher value is stricter. | +| `minimum_iou_threshold_second_assoc` | Minimum BIoU similarity for the second association pass. | Usually set to a lower value than the first-pass threshold to recover weak detections without over-matching. | +| `minimum_iou_threshold_unconfirmed_assoc` | Minimum fused BIoU similarity when associating unconfirmed tracks. | Higher value makes tentative tracks harder to confirm spuriously; lower value helps short-lived or noisy objects survive. | +| `high_conf_det_threshold` | Confidence split between stage-1 and stage-2 detections. | 0.5-0.7 common. Higher value shifts more detections to recovery stage; lower value gives stage-1 broader coverage. | +| `buffer_ratio_first` | Paper **b1**, small BIoU buffer for the first association pass. | Typical range 0.1-0.7. Should be **less than** `buffer_ratio_second`. | +| `buffer_ratio_second` | Paper **b2**, large BIoU buffer for the second association pass. | Typical range 0.2-1.0. Should be **greater than** `buffer_ratio_first`. | !!! warning "Buffer ordering (b1 < b2)" From f1d9c6a1974f24499d60c34ceddcf4134d37494d Mon Sep 17 00:00:00 2001 From: Alex Bodner Date: Fri, 22 May 2026 11:47:53 -0300 Subject: [PATCH 23/23] adjusted some docs to be less strict about comparing to paper --- src/trackers/core/cbiou/tracker.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/trackers/core/cbiou/tracker.py b/src/trackers/core/cbiou/tracker.py index 79fde233..879bf80a 100644 --- a/src/trackers/core/cbiou/tracker.py +++ b/src/trackers/core/cbiou/tracker.py @@ -27,8 +27,7 @@ class CBIoUTracker(BoTSORTTracker): The paper proposes **Buffered IoU (BIoU)** — expanding boxes by a proportional margin before computing overlap — and **cascaded matching** with a small buffer - scale ``b1`` followed by a larger scale ``b2`` (typically ``b1 < b2``; e.g. - 0.7 and 1.0 on SoccerNet in the paper). + scale ``b1`` followed by a larger scale ``b2`` (typically ``b1 < b2``). Each association step uses its own ``buffer_ratio``: @@ -38,8 +37,7 @@ class CBIoUTracker(BoTSORTTracker): paper: large ``b2``). The ByteTrack-style unconfirmed-track step (leftover high-confidence - detections vs tentative tracks) reuses **b1** (``iou_first``); it is not a - separate paper hyperparameter. + detections vs tentative tracks) reuses **b1** (``iou_first``). Camera motion compensation is not used (detection-only / MOT-file workflows). @@ -62,9 +60,9 @@ class CBIoUTracker(BoTSORTTracker): instant_first_frame_activation: If ``True``, first-frame tracks receive a real ID immediately. state_estimator_class: Kalman state representation for tracklets. - buffer_ratio_first: Buffer scale ``b1`` for the first BIoU pass. Should + buffer_ratio_first: Buffer scale ``b1`` for the first BIoU pass. It is suggested to be **less than** ``buffer_ratio_second`` (``b1 < b2``) per the paper. - buffer_ratio_second: Buffer scale ``b2`` for the second BIoU pass. Should + buffer_ratio_second: Buffer scale ``b2`` for the second BIoU pass. It is suggested to be **greater than** ``buffer_ratio_first``. """ @@ -223,7 +221,7 @@ def update( out_tracker_ids.append(-1) # Step 3: match unconfirmed tracks with remaining unmatched high-confidence - # detections (ByteTrack lifecycle; reuses b1 / iou_first, not a paper parameter). + # detections (ByteTrack lifecycle; reuses b1 / iou_first). # Unmatched unconfirmed tracks are removed (not kept as lost). unmatched_high_list = sorted(unmatched_high) unmatched_uc_indices: list[int] = list(range(len(unconfirmed_tracks)))