Skip to content

Commit 199b8eb

Browse files
AlexBodnerclaude
andcommitted
feat(reid): add Deep OC-SORT adaptive appearance fusion
BoT-SORT fuses appearance by taking the minimum of the geometry and appearance costs, gated by two fixed thresholds. Deep OC-SORT instead adds a weighted appearance term whose weight grows when the best match stands clear of the runner-up, which helps most in crowded scenes where a fixed weight is either too timid or too aggressive. Adds fuse_adaptive_reid_association alongside the existing fusion and a reid_fusion selector on BoTSORTTracker, with reid_appearance_weight and reid_adaptive_weight_cap for its two knobs. Default stays "botsort", so behaviour is unchanged unless opted into. Implements equations (4)-(6) of the paper, matching compute_aw_new_metric in the authors' integrated_ocsort_embedding tracker (the variant behind their published results) rather than the ratio-based variant that other ports use. Named for what it does rather than where it came from: only the adaptive weighting is ported, onto a BoT-SORT base, without Dynamic Appearance, the velocity term or OCR/OOS. Two documented departures from the reference: negative cosine similarities are clamped to zero, and appearance is gated by reid_proximity_threshold. Deep OC-SORT drops implausible pairs after matching, but this integration thresholds the fused similarity directly, so the gate has to happen during fusion. The discriminativeness gap is measured across all candidates before that gate is applied, so a candidate excluded on geometry cannot read as similarity 0 and earn the bonus for being spatially alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ab852da commit 199b8eb

3 files changed

Lines changed: 376 additions & 6 deletions

File tree

src/trackers/core/botsort/tracker.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
55
# ------------------------------------------------------------------------
66

7-
from typing import ClassVar, cast
7+
from typing import ClassVar, cast, get_args
88

99
import numpy as np
1010
import supervision as sv
@@ -17,7 +17,11 @@
1717
from trackers.core.reid.appearance import appearance_similarity, extract_detection_embeddings
1818
from trackers.core.reid.encoder import ReIDEncoder
1919
from trackers.core.reid.feature_bank import FeatureBank
20-
from trackers.core.reid.fusion import fuse_botsort_reid_association
20+
from trackers.core.reid.fusion import (
21+
ReidFusionMethod,
22+
fuse_adaptive_reid_association,
23+
fuse_botsort_reid_association,
24+
)
2125
from trackers.utils.cmc import CMC, CMCConfig, CMCMethod
2226
from trackers.utils.detections import default_confidences
2327
from trackers.utils.iou import BaseIoU, IoU
@@ -95,11 +99,25 @@ class BoTSORTTracker(BaseTracker):
9599
reid_appearance_threshold: Appearance distance gate. Drops the appearance term
96100
when the halved cosine distance ``0.5 * (1 - cos_sim)`` exceeds this
97101
value, leaving the pair scored on geometry alone. Default ``0.25``
98-
(BoT-SORT ``appearance_thresh``).
102+
(BoT-SORT ``appearance_thresh``). Used by ``reid_fusion="botsort"``
103+
only.
99104
reid_proximity_threshold: Standard-IoU distance gate applied before appearance
100105
is used. Computed from true IoU even when ``iou`` is GIoU/DIoU/CIoU.
101106
Default ``0.5`` (BoT-SORT ``proximity_thresh``; requires
102107
``IoU >= 1 - reid_proximity_threshold``).
108+
reid_fusion: How appearance is combined with geometry. ``"botsort"``
109+
(default) takes the minimum of the geometry and appearance costs.
110+
``"adaptive"`` adds an adaptively weighted appearance term, which
111+
weighs appearance more heavily when the best match stands clear of
112+
the runner-up. The two produce different similarity ranges, so
113+
``minimum_iou_threshold_first_assoc`` needs retuning when switching
114+
(see :func:`~trackers.core.reid.fusion.fuse_adaptive_reid_association`).
115+
reid_appearance_weight: Base appearance weight for ``reid_fusion="adaptive"``.
116+
Ignored otherwise. Default ``0.75``, the value Deep OC-SORT reports
117+
for MOT17 and MOT20; they use ``1.25`` for DanceTrack.
118+
reid_adaptive_weight_cap: Upper bound on the adaptive appearance bonus for
119+
``reid_fusion="adaptive"``. Ignored otherwise. Default ``0.5``, the value
120+
Deep OC-SORT reports for MOT17 and MOT20; they use ``1.0`` for DanceTrack.
103121
104122
Notes:
105123
- Positive `maximum_frames_without_update` values are scaled by
@@ -144,6 +162,9 @@ def __init__(
144162
reid_ema_alpha: float = 0.9,
145163
reid_appearance_threshold: float = 0.25,
146164
reid_proximity_threshold: float = 0.5,
165+
reid_fusion: ReidFusionMethod = "botsort",
166+
reid_appearance_weight: float = 0.75,
167+
reid_adaptive_weight_cap: float = 0.5,
147168
) -> None:
148169
self.maximum_frames_without_update = self._compute_maximum_frames_without_update(
149170
lost_track_buffer=lost_track_buffer,
@@ -176,6 +197,15 @@ def __init__(
176197
raise ValueError(f"reid_proximity_threshold must be in [0, 1], got {reid_proximity_threshold}")
177198
self.reid_appearance_threshold = reid_appearance_threshold
178199
self.reid_proximity_threshold = reid_proximity_threshold
200+
if reid_fusion not in get_args(ReidFusionMethod):
201+
raise ValueError(f"Unknown reid_fusion {reid_fusion!r}. Valid options are: {get_args(ReidFusionMethod)}.")
202+
if reid_appearance_weight < 0.0:
203+
raise ValueError(f"reid_appearance_weight must be non-negative, got {reid_appearance_weight}")
204+
if reid_adaptive_weight_cap < 0.0:
205+
raise ValueError(f"reid_adaptive_weight_cap must be non-negative, got {reid_adaptive_weight_cap}")
206+
self.reid_fusion = reid_fusion
207+
self.reid_appearance_weight = reid_appearance_weight
208+
self.reid_adaptive_weight_cap = reid_adaptive_weight_cap
179209

180210
self._init_timestamp_state(frame_rate)
181211

@@ -432,7 +462,7 @@ def _association_similarity(
432462
scores: np.ndarray,
433463
embeddings: np.ndarray | None,
434464
) -> np.ndarray:
435-
"""Score-fused association similarity, with optional BoT-SORT ReID fusion."""
465+
"""Score-fused association similarity, with optional ReID fusion."""
436466
iou_sim_raw = self.iou.normalize_for_fusion(self._get_iou_matrix(tracklets, boxes, tracklet_boxes_by_id))
437467
iou_sim_fused = _fuse_score(iou_sim_raw, scores)
438468
if embeddings is None or len(tracklets) == 0:
@@ -444,9 +474,19 @@ def _association_similarity(
444474
if isinstance(self.iou, IoU)
445475
else self._get_iou_matrix(tracklets, boxes, tracklet_boxes_by_id, metric=IoU())
446476
)
477+
appearance = appearance_similarity(track_feats, embeddings, det_embeddings_normalized=True)
478+
if self.reid_fusion == "adaptive":
479+
return fuse_adaptive_reid_association(
480+
iou_sim_fused,
481+
appearance,
482+
proximity_iou_similarity=proximity_iou,
483+
reid_proximity_threshold=self.reid_proximity_threshold,
484+
reid_appearance_weight=self.reid_appearance_weight,
485+
reid_adaptive_weight_cap=self.reid_adaptive_weight_cap,
486+
)
447487
return fuse_botsort_reid_association(
448488
iou_sim_fused,
449-
appearance_similarity(track_feats, embeddings, det_embeddings_normalized=True),
489+
appearance,
450490
proximity_iou_similarity=proximity_iou,
451491
reid_proximity_threshold=self.reid_proximity_threshold,
452492
reid_appearance_threshold=self.reid_appearance_threshold,

src/trackers/core/reid/fusion.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
# Copyright (c) 2022 Nir Aharon
99
# Source: https://github.com/NirAharon/BoT-SORT
1010
# Reference: tracker/bot_sort.py (ReID appearance-IoU cost fusion)
11+
#
12+
# Adapted from GerardMaggiolino/Deep-OC-SORT (MIT)
13+
# Copyright (c) 2023 Gerard Maggiolino
14+
# Source: https://github.com/GerardMaggiolino/Deep-OC-SORT
15+
# Reference: trackers/integrated_ocsort_embedding/association.py
16+
# (compute_aw_new_metric, adaptive appearance weighting)
1117
# ------------------------------------------------------------------------
1218

1319
"""Appearance-IoU fusion methods for ReID association.
@@ -18,8 +24,12 @@
1824

1925
from __future__ import annotations
2026

27+
from typing import Literal
28+
2129
import numpy as np
2230

31+
ReidFusionMethod = Literal["botsort", "adaptive"]
32+
2333

2434
def fuse_botsort_reid_association(
2535
association_similarity: np.ndarray,
@@ -65,3 +75,91 @@ def fuse_botsort_reid_association(
6575
d_app = np.where(d_iou_proximity > reid_proximity_threshold, 1.0, d_app)
6676
fused_cost = np.minimum(d_iou, d_app)
6777
return 1.0 - fused_cost
78+
79+
80+
def _top_two_margin(similarity: np.ndarray, axis: int, cap: float) -> np.ndarray:
81+
"""Margin between the best and second-best similarity along ``axis``, capped.
82+
83+
A large margin means the best candidate stands clear of the rest, so appearance is discriminative for that row or
84+
column. Zero when there are fewer than two candidates to compare.
85+
"""
86+
if similarity.shape[axis] < 2:
87+
return np.zeros(similarity.shape[1 - axis], dtype=float)
88+
partitioned = np.partition(similarity, -2, axis=axis)
89+
best = np.take(partitioned, -1, axis=axis)
90+
runner_up = np.take(partitioned, -2, axis=axis)
91+
return np.minimum(best - runner_up, cap)
92+
93+
94+
def fuse_adaptive_reid_association(
95+
association_similarity: np.ndarray,
96+
appearance_similarity: np.ndarray,
97+
*,
98+
reid_appearance_weight: float,
99+
reid_adaptive_weight_cap: float,
100+
reid_proximity_threshold: float,
101+
proximity_iou_similarity: np.ndarray | None = None,
102+
) -> np.ndarray:
103+
"""Fuse IoU and appearance with Deep OC-SORT's adaptive weighting.
104+
105+
Adds a weighted appearance term to the geometric similarity instead of
106+
taking a minimum. The weight grows when the best appearance match stands
107+
clear of the runner-up and falls back to ``reid_appearance_weight`` when
108+
the top candidates are hard to tell apart.
109+
110+
Implements equations (4)-(6) of the Deep OC-SORT paper, matching
111+
``compute_aw_new_metric`` in the authors' ``integrated_ocsort_embedding``
112+
tracker, the variant behind their published results.
113+
114+
``proximity_iou_similarity`` is the standard-IoU gate (defaults to
115+
``association_similarity``). Pass it separately when association uses
116+
GIoU/DIoU/CIoU so proximity still uses plain IoU.
117+
118+
Args:
119+
association_similarity: Geometry-based track-detection similarities with
120+
shape ``(T, N)``.
121+
appearance_similarity: Cosine similarities for the same pairs with shape
122+
``(T, N)``.
123+
reid_appearance_weight: Base appearance weight, ``a_w`` in the paper.
124+
The authors report ``0.75`` for MOT17 and MOT20 and ``1.25`` for
125+
DanceTrack.
126+
reid_adaptive_weight_cap: Upper bound on the adaptive bonus, ``epsilon``
127+
in the paper. The authors report ``0.5`` for MOT17 and MOT20 and
128+
``1.0`` for DanceTrack.
129+
reid_proximity_threshold: Maximum standard-IoU distance at which
130+
appearance may contribute.
131+
proximity_iou_similarity: Standard-IoU similarities with shape ``(T, N)``.
132+
Defaults to ``association_similarity``.
133+
134+
Returns:
135+
Fused track-detection similarities with shape ``(T, N)``, spanning
136+
``[0, 1 + reid_appearance_weight + reid_adaptive_weight_cap]``.
137+
Association thresholds tuned against the BoT-SORT fusion do not carry
138+
over and need retuning.
139+
140+
Notes:
141+
Negative cosine similarities are clamped to zero, so appearance can
142+
only ever raise a similarity. Appearance is gated by
143+
``reid_proximity_threshold``; Deep OC-SORT instead drops implausible
144+
pairs after matching, but this integration thresholds the fused
145+
similarity directly, so the gate has to apply here. The gate applies
146+
only to the appearance term: the margin is measured over all
147+
candidates, as in the reference, so a candidate excluded on geometry
148+
cannot read as similarity ``0`` and widen the margin.
149+
150+
The velocity-direction term of the full Deep OC-SORT cost is not
151+
included; it belongs to OC-SORT's motion model.
152+
"""
153+
if proximity_iou_similarity is None:
154+
proximity_iou_similarity = association_similarity
155+
156+
appearance = np.clip(appearance_similarity, 0.0, None)
157+
158+
track_margin = _top_two_margin(appearance, axis=1, cap=reid_adaptive_weight_cap)
159+
detection_margin = _top_two_margin(appearance, axis=0, cap=reid_adaptive_weight_cap)
160+
adaptive_bonus = (track_margin[:, np.newaxis] + detection_margin[np.newaxis, :]) / 2.0
161+
162+
out_of_range = (1.0 - proximity_iou_similarity) > reid_proximity_threshold
163+
gated_appearance = np.where(out_of_range, 0.0, appearance)
164+
165+
return association_similarity + (reid_appearance_weight + adaptive_bonus) * gated_appearance

0 commit comments

Comments
 (0)