@@ -55,13 +55,17 @@ class ByteTrackTracker(BaseTracker):
5555 low-confidence association. Lower values are more permissive when
5656 reviving tracks from low-confidence detections.
5757 iou_age_weight: `float` specifying how much to discount IoU
58- similarity for lost tracks in stage-1 association. Each track's
59- IoU row is scaled by ``1 / (1 + iou_age_weight * lost_frames)``
60- where ``lost_frames = max(0, time_since_update - 1)``. This
61- makes the assignment prefer active tracks over stale predictions,
62- reducing identity switches. ``0`` disables the discount.
63- Only applied in stage 1; stage 2 is unaffected so that lost
64- tracks can still recover via low-confidence detections.
58+ similarity for lost tracks in stage-1 assignment ranking.
59+ Each lost track's IoU row is scaled by
60+ ``1 / (1 + iou_age_weight * lost_frames)`` where
61+ ``lost_frames = max(0, time_since_update - 1)``. The discount
62+ biases the solver to prefer active tracks over stale
63+ predictions (reducing identity switches) but only affects
64+ ranking — the minimum-IoU threshold is checked against the
65+ *raw* IoU so that valid matches are never rejected by the
66+ discount alone. ``0`` disables the discount. Only applied in
67+ stage 1; stage 2 is unaffected so that lost tracks can still
68+ recover via low-confidence detections.
6569 high_conf_det_threshold: `float` specifying threshold for separating
6670 high and low confidence detections in the two-stage association.
6771 """
@@ -141,16 +145,25 @@ def update(
141145 # algorithm prefers active tracks over stale predictions. This
142146 # reduces identity switches from a drifted prediction "stealing"
143147 # a detection that should go to the correct active track.
148+ #
149+ # The discount is applied only to the cost matrix used by the
150+ # solver for ranking; the threshold check uses the *raw* IoU so
151+ # that valid matches are never rejected by the discount alone.
152+ # Only tracks that have been lost for at least 1 frame are
153+ # discounted — freshly-seen tracks (time_since_update == 1 after
154+ # predict, i.e. lost_frames == 0) are never penalised.
144155 if self .iou_age_weight > 0 and iou_matrix .size > 0 :
145156 lost_frames = np .array (
146157 [max (0 , t .time_since_update - 1 ) for t in self .tracks ],
147158 dtype = np .float32 ,
148159 )
149160 discount = 1.0 / (1.0 + self .iou_age_weight * lost_frames )
150- iou_matrix = iou_matrix * discount [:, np .newaxis ]
161+ solver_iou = iou_matrix * discount [:, np .newaxis ]
162+ else :
163+ solver_iou = iou_matrix
151164
152165 matched , unmatched_tracks , unmatched_high = self ._get_associated_indices (
153- iou_matrix , self .minimum_iou_threshold
166+ solver_iou , self .minimum_iou_threshold , raw_similarity = iou_matrix
154167 )
155168
156169 for row , col in matched :
@@ -219,32 +232,41 @@ def _get_associated_indices(
219232 self ,
220233 similarity_matrix : np .ndarray ,
221234 min_similarity_thresh : float ,
235+ raw_similarity : np .ndarray | None = None ,
222236 ) -> tuple [list [tuple [int , int ]], set [int ], set [int ]]:
223- """
224- Associate detections to tracks based on Similarity (IoU) using the
225- Jonker-Volgenant algorithm approach with no initialization instead of the
226- Hungarian algorithm as mentioned in the SORT paper, but it solves the
227- assignment problem in an optimal way.
237+ """Associate detections to tracks based on similarity (IoU).
238+
239+ Uses the Jonker-Volgenant algorithm (via ``linear_sum_assignment``)
240+ to solve the assignment optimally.
228241
229242 Args:
230- similarity_matrix: Similarity matrix between tracks (rows) and detections (columns).
231- min_similarity_thresh: Minimum similarity threshold for a valid match.
243+ similarity_matrix: Similarity matrix between tracks (rows) and
244+ detections (columns). Used by the solver for ranking.
245+ min_similarity_thresh: Minimum similarity threshold for a valid
246+ match.
247+ raw_similarity: Optional unmodified similarity matrix. When
248+ provided, the threshold check uses this matrix instead of
249+ ``similarity_matrix`` so that solver-side discounts (e.g.
250+ the age discount) cannot reject otherwise valid matches.
232251
233252 Returns:
234- Matched indices (list of (tracker_idx, detection_idx)), indices of
235- unmatched tracks, indices of unmatched detections.
236- """ # noqa: E501
253+ Matched indices (list of (tracker_idx, detection_idx)), indices
254+ of unmatched tracks, indices of unmatched detections.
255+ """
237256 matched_indices = []
238257 n_tracks , n_detections = similarity_matrix .shape
239258 unmatched_tracks = set (range (n_tracks ))
240259 unmatched_detections = set (range (n_detections ))
241260
261+ # Use raw similarity for threshold gating when available
262+ thresh_matrix = raw_similarity if raw_similarity is not None else similarity_matrix
263+
242264 if n_tracks > 0 and n_detections > 0 :
243265 row_indices , col_indices = linear_sum_assignment (
244266 similarity_matrix , maximize = True
245267 )
246268 for row , col in zip (row_indices , col_indices ):
247- if similarity_matrix [row , col ] >= min_similarity_thresh :
269+ if thresh_matrix [row , col ] >= min_similarity_thresh :
248270 matched_indices .append ((row , col ))
249271 unmatched_tracks .remove (row )
250272 unmatched_detections .remove (col )
0 commit comments