zye0616 Claude Opus 4.6 (1M context) commited on
Commit
fc033d4
·
1 Parent(s): 082b546

refactor(tracker): clean up ByteTracker implementation

Browse files

- Extract _xyah_from_xyxy to module-level function (was duplicated in KalmanFilter and STrack)
- Remove unused _sync_data method
- Simplify linear_assignment unmatched index computation
- Use sets for duplicate track removal
- Remove dead code and stale comments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. utils/tracker.py +69 -134
utils/tracker.py CHANGED
@@ -3,6 +3,23 @@ import numpy as np
3
  from scipy.optimize import linear_sum_assignment
4
  import scipy.linalg
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  class KalmanFilter:
7
  """
8
  A simple Kalman Filter for tracking bounding boxes in image space.
@@ -27,19 +44,19 @@ class KalmanFilter:
27
 
28
  def initiate(self, measurement):
29
  """Create track from unassociated measurement.
30
-
31
  Parameters
32
  ----------
33
- measurement : dbo
34
  Bounding box coordinates (x1, y1, x2, y2) with confidence score.
35
-
36
  Returns
37
  -------
38
  (mean, covariance)
39
  Returns the mean vector (8 dimensional) and covariance matrix (8x8)
40
  of the new track.
41
  """
42
- mean_pos = self._xyah_from_xyxy(measurement)
43
  mean = np.r_[mean_pos, np.zeros_like(mean_pos)]
44
 
45
  std = [
@@ -79,14 +96,14 @@ class KalmanFilter:
79
  1e-2,
80
  self._std_weight_position * mean[3],
81
  ]
82
-
83
  std_vel = [
84
  self._std_weight_velocity * mean[3],
85
  self._std_weight_velocity * mean[3],
86
  1e-5,
87
  self._std_weight_velocity * mean[3],
88
  ]
89
-
90
  motion_cov = np.diag(np.square(np.r_[std_pos, std_vel]))
91
  mean = np.dot(self._motion_mat, mean)
92
  covariance = (
@@ -117,7 +134,7 @@ class KalmanFilter:
117
  1e-1,
118
  self._std_weight_position * mean[3],
119
  ]
120
-
121
  innovation_cov = np.diag(np.square(std))
122
  mean = np.dot(self._update_mat, mean)
123
  covariance = np.linalg.multi_dot((self._update_mat, covariance, self._update_mat.T))
@@ -177,34 +194,13 @@ class KalmanFilter:
177
  else:
178
  raise ValueError("invalid distance metric")
179
 
180
- def _xyah_from_xyxy(self, xyxy):
181
- """Convert bounding box to format `(center x, center y, aspect ratio,
182
- height)`, where the aspect ratio is `width / height`.
183
- """
184
- bbox = np.asarray(xyxy).copy()
185
- cx = (bbox[0] + bbox[2]) / 2.0
186
- cy = (bbox[1] + bbox[3]) / 2.0
187
- w = bbox[2] - bbox[0]
188
- h = bbox[3] - bbox[1]
189
-
190
- ret = np.zeros(4, dtype=bbox.dtype)
191
- ret[0] = cx
192
- ret[1] = cy
193
- ret[2] = w / h
194
- ret[3] = h
195
- return ret
196
-
197
 
198
  class STrack:
199
  """
200
  Single object track. Wrapper around KalmanFilter state.
201
  """
202
-
203
- def __init__(self, tlwh, score, label):
204
- # wait, input is xyxy usually in our pipeline
205
- # ByteTrack usually uses tlwh internally.
206
- # Let's standardize to input xyxy.
207
 
 
208
  self._tlwh = np.asarray(self._tlwh_from_xyxy(tlwh), dtype=np.float32)
209
  self.is_activated = False
210
  self.track_id = 0
@@ -216,22 +212,22 @@ class STrack:
216
  self.frame_id = 0
217
  self.time_since_update = 0
218
  self.hits = 0
219
-
220
  # Multi-frame history
221
  self.history = []
222
-
223
  # Kalman Filter
224
  self.kalman_filter = None
225
  self.mean = None
226
  self.covariance = None
227
-
228
 
229
  def _tlwh_from_xyxy(self, xyxy):
230
  """Convert xyxy to tlwh."""
231
  w = xyxy[2] - xyxy[0]
232
  h = xyxy[3] - xyxy[1]
233
  return [xyxy[0], xyxy[1], w, h]
234
-
235
  def _xyxy_from_tlwh(self, tlwh):
236
  """Convert tlwh to xyxy."""
237
  x1 = tlwh[0]
@@ -239,7 +235,7 @@ class STrack:
239
  x2 = x1 + tlwh[2]
240
  y2 = y1 + tlwh[3]
241
  return [x1, y1, x2, y2]
242
-
243
  @property
244
  def tlwh(self):
245
  """Get current position in bounding box format `(top left x, top left y,
@@ -265,7 +261,7 @@ class STrack:
265
  """Start a new track (tentative until min_hits reached)."""
266
  self.kalman_filter = kalman_filter
267
  self.track_id = self.next_id()
268
- self.mean, self.covariance = self.kalman_filter.initiate(self.tlbr) # Initiate needs xyxy
269
 
270
  self.state = 2 # Tracked
271
  self.frame_id = frame_id
@@ -276,13 +272,13 @@ class STrack:
276
  def re_activate(self, new_track, frame_id, new_id=False):
277
  """Reactivate a lost track with a new detection."""
278
  self.mean, self.covariance = self.kalman_filter.update(
279
- self.mean, self.covariance, self._xyah_from_xyxy(new_track.tlbr)
280
  )
281
  self.time_since_update = 0
282
  self.state = 2 # Tracked
283
  self.frame_id = frame_id
284
  self.score = new_track.score
285
-
286
  if new_id:
287
  self.track_id = self.next_id()
288
 
@@ -294,35 +290,17 @@ class STrack:
294
  self.hits += 1
295
 
296
  self.mean, self.covariance = self.kalman_filter.update(
297
- self.mean, self.covariance, self._xyah_from_xyxy(new_track.tlbr)
298
  )
299
  self.state = 2 # Tracked
300
  if self.hits >= min_hits:
301
  self.is_activated = True # Confirmed after N consecutive hits
302
-
303
  def predict(self):
304
  """Propagate tracking state distribution one time step forward."""
305
  if self.mean is None: return
306
- if self.state != 2: # Only predict if tracked? ByteTrack predicts always?
307
- # Standard implementation predicts for all active/lost tracks
308
- pass
309
  self.mean, self.covariance = self.kalman_filter.predict(self.mean, self.covariance)
310
 
311
- def _xyah_from_xyxy(self, xyxy):
312
- """Internal helper for measurement conversion."""
313
- bbox = np.asarray(xyxy).copy()
314
- cx = (bbox[0] + bbox[2]) / 2.0
315
- cy = (bbox[1] + bbox[3]) / 2.0
316
- w = bbox[2] - bbox[0]
317
- h = bbox[3] - bbox[1]
318
-
319
- ret = np.zeros(4, dtype=bbox.dtype)
320
- ret[0] = cx
321
- ret[1] = cy
322
- ret[2] = w / h
323
- ret[3] = h
324
- return ret
325
-
326
  @staticmethod
327
  def next_id():
328
  # Global counter
@@ -359,39 +337,32 @@ class ByteTracker:
359
  def update(self, detections_list):
360
  """
361
  Update the tracker with a list of detections.
362
-
363
  Args:
364
  detections_list: List of dicts, each having:
365
  - bbox: [x1, y1, x2, y2]
366
  - score: float
367
  - label: str
368
  - (optional) other keys preserved
369
-
370
  Returns:
371
  List of dicts with 'track_id' added/updated.
372
  """
373
  self.frame_id += 1
374
-
375
- # 0. STrack Conversion using generic interface
376
  activated_stracks = []
377
  refind_stracks = []
378
  lost_stracks = []
379
  removed_stracks = []
380
 
381
- scores = [d['score'] for d in detections_list]
382
- bboxes = [d['bbox'] for d in detections_list]
383
-
384
  # Split into high and low confidence
385
  detections = []
386
  detections_second = []
387
-
388
- # Need to keep mapping to original dict to populate results later
389
- # We wrap original dict in STrack
390
-
391
  for d in detections_list:
392
  score = d['score']
393
  if score < self.track_low_thresh:
394
- continue # Background noise discard entirely
395
 
396
  t = STrack(d['bbox'], score, d['label'])
397
  t.original_data = d # Link back
@@ -416,7 +387,7 @@ class ByteTracker:
416
 
417
  # 2. First association (High score)
418
  dists = iou_distance(strack_pool, detections)
419
- dists = fuse_score(dists, detections) # Optional? ByteTrack uses it
420
  matches, u_track, u_detection = linear_assignment(dists, thresh=self.match_thresh)
421
 
422
  for itracked, idet in matches:
@@ -429,9 +400,6 @@ class ByteTracker:
429
  track.re_activate(det, self.frame_id, new_id=False)
430
  refind_stracks.append(track)
431
 
432
- # Persist data
433
- self._sync_data(track, det)
434
-
435
  # 3. Second association (Low score)
436
  # Match unmatched tracks to low score detections
437
  r_tracked_stracks = [strack_pool[i] for i in u_track if strack_pool[i].state == 2]
@@ -448,8 +416,6 @@ class ByteTracker:
448
  track.re_activate(det, self.frame_id, new_id=False)
449
  refind_stracks.append(track)
450
 
451
- self._sync_data(track, det)
452
-
453
  for it in u_track:
454
  track = r_tracked_stracks[it]
455
  if not track.state == 3: # If not already lost
@@ -467,20 +433,19 @@ class ByteTracker:
467
  det = remaining_dets[idet]
468
  track.update(det, self.frame_id, min_hits=self.min_hits)
469
  activated_stracks.append(track)
470
- self._sync_data(track, det)
471
 
472
  # Update u_detection to only contain indices not matched to unconfirmed
473
  matched_det_indices = set(u_detection[idet] for _, idet in matches_unc) if len(matches_unc) > 0 else set()
474
  u_detection = [i for i in u_detection if i not in matched_det_indices]
475
 
476
- # Unconfirmed tracks that didn't match remove (too noisy to keep)
477
  for it in u_unconfirmed:
478
  track = unconfirmed[it]
479
  track.state = 4 # Removed
480
  removed_stracks.append(track)
481
 
482
  elif unconfirmed:
483
- # No detections left to match remove all unconfirmed
484
  for track in unconfirmed:
485
  track.state = 4 # Removed
486
  removed_stracks.append(track)
@@ -496,7 +461,6 @@ class ByteTracker:
496
 
497
  track.activate(self.kalman_filter, self.frame_id)
498
  activated_stracks.append(track)
499
- self._sync_data(track, track) # Sync self
500
 
501
  if rejected_by_thresh > 0 and self.frame_id <= 5:
502
  logging.warning(
@@ -521,19 +485,9 @@ class ByteTracker:
521
  self.removed_stracks.append(track)
522
  self.lost_stracks = [t for t in self.lost_stracks if self.frame_id - t.frame_id <= self.track_buffer]
523
 
524
- # 7. Final Output Construction
525
- # We need to update the original dictionaries in detections_list IN PLACE,
526
- # or return a new list. The logic in inference.py expects us to modify detections dicts
527
- # or we might want to return the tracked ones.
528
- # But wait, we iterate `detections_list` at start.
529
- # We want to return ONLY the currently tracked/active objects?
530
- # Usually inference pipeline draws ALL detections, but standard tracking ONLY output active tracks.
531
- # If we only output active tracks, we might suppress valid high-confidence detections that just started?
532
- # No, activated_stracks includes new ones.
533
-
534
- # Let's collect all active tracks
535
  output_stracks = [t for t in self.tracked_stracks if t.is_activated]
536
-
537
  results = []
538
  for track in output_stracks:
539
  d_out = track.original_data.copy() if hasattr(track, 'original_data') else {}
@@ -563,10 +517,6 @@ class ByteTracker:
563
 
564
  return results
565
 
566
- def _sync_data(self, track, det_source):
567
- """Sync label from detection source to track."""
568
- pass
569
-
570
 
571
  # --- Helper Functions ---
572
 
@@ -574,48 +524,35 @@ def linear_assignment(cost_matrix, thresh):
574
  """Linear assignment with threshold using scipy."""
575
  if cost_matrix.size == 0:
576
  return np.empty((0, 2), dtype=int), tuple(range(cost_matrix.shape[0])), tuple(range(cost_matrix.shape[1]))
577
-
578
- matches, unmatched_a, unmatched_b = [], [], []
579
-
580
- # Scipy linear_sum_assignment finds min cost
581
  row_ind, col_ind = linear_sum_assignment(cost_matrix)
582
-
 
 
 
 
 
 
 
583
  for r, c in zip(row_ind, col_ind):
584
  if cost_matrix[r, c] <= thresh:
585
  matches.append((r, c))
586
  else:
587
  unmatched_a.append(r)
588
  unmatched_b.append(c)
589
-
590
- # Add accumulation of indices that weren't selected
591
- # (scipy returns perfect matching for square, but partial for rectangular)
592
- # Actually scipy matches rows to cols. Any row not in row_ind is unmatched?
593
- # No, row_ind covers all rows if N < M.
594
-
595
- if cost_matrix.shape[0] > cost_matrix.shape[1]: # More rows than cols
596
- unmatched_a += list(set(range(cost_matrix.shape[0])) - set(row_ind))
597
- elif cost_matrix.shape[0] < cost_matrix.shape[1]: # More cols than rows
598
- unmatched_b += list(set(range(cost_matrix.shape[1])) - set(col_ind))
599
-
600
- # Also filter out threshold failures
601
- for r, c in zip(row_ind, col_ind):
602
- if cost_matrix[r, c] > thresh:
603
- if r not in unmatched_a: unmatched_a.append(r)
604
- if c not in unmatched_b: unmatched_b.append(c)
605
 
606
- # Clean up
607
  matches = np.array(matches) if len(matches) > 0 else np.empty((0, 2), dtype=int)
608
  return matches, unmatched_a, unmatched_b
609
 
610
 
611
  def iou_distance(atracks, btracks):
612
  """Compute IOU cost matrix between tracks and detections."""
613
- if (len(atracks) == 0 and len(btracks) == 0) or len(atracks) == 0 or len(btracks) == 0:
614
  return np.zeros((len(atracks), len(btracks)), dtype=float)
615
-
616
  atlbrs = [track.tlbr for track in atracks]
617
  btlbrs = [track.tlbr for track in btracks]
618
-
619
  _ious = bbox_ious(np.array(atlbrs), np.array(btlbrs))
620
  cost_matrix = 1 - _ious
621
  return cost_matrix
@@ -629,12 +566,12 @@ def bbox_ious(boxes1, boxes2):
629
  inter_rect_y1 = np.maximum(b1_y1[:, None], b2_y1)
630
  inter_rect_x2 = np.minimum(b1_x2[:, None], b2_x2)
631
  inter_rect_y2 = np.minimum(b1_y2[:, None], b2_y2)
632
-
633
  inter_area = np.maximum(inter_rect_x2 - inter_rect_x1, 0) * np.maximum(inter_rect_y2 - inter_rect_y1, 0)
634
-
635
  b1_area = (b1_x2 - b1_x1) * (b1_y2 - b1_y1)
636
  b2_area = (b2_x2 - b2_x1) * (b2_y2 - b2_y1)
637
-
638
  iou = inter_area / (b1_area[:, None] + b2_area - inter_area + 1e-6)
639
  return iou
640
 
@@ -678,26 +615,24 @@ def sub_stracks(tlist_a, tlist_b):
678
  def remove_duplicate_stracks(stracksa, stracksb):
679
  pdist = iou_distance(stracksa, stracksb)
680
  pairs = np.where(pdist < 0.15)
681
- dupa, dupb = list(pairs[0]), list(pairs[1])
682
- for a, b in zip(dupa, dupb):
683
  time_a = stracksa[a].frame_id - stracksa[a].start_frame
684
  time_b = stracksb[b].frame_id - stracksb[b].start_frame
685
  if time_a > time_b:
686
- dupb.append(b) # Bug in orig ByteTrack? It assumes removing from list.
687
- # We mark for removal.
688
  else:
689
- dupa.append(a)
690
-
691
- res_a = [t for i, t in enumerate(stracksa) if not i in dupa]
692
- res_b = [t for i, t in enumerate(stracksb) if not i in dupb]
693
  return res_a, res_b
694
 
695
 
696
- # Monkey patch for multi_predict since STrack is not in a module
697
- def multi_predict(stracks, kalman_filter):
698
  for t in stracks:
699
  if t.state != 2:
700
  t.mean[7] = 0 # reset velocity h if lost
701
  t.mean, t.covariance = kalman_filter.predict(t.mean, t.covariance)
702
 
703
- STrack.multi_predict = static_method_multi_predict = multi_predict
 
3
  from scipy.optimize import linear_sum_assignment
4
  import scipy.linalg
5
 
6
+
7
+ def _xyah_from_xyxy(xyxy):
8
+ """Convert bounding box (x1, y1, x2, y2) to (center_x, center_y, aspect_ratio, height)."""
9
+ bbox = np.asarray(xyxy).copy()
10
+ cx = (bbox[0] + bbox[2]) / 2.0
11
+ cy = (bbox[1] + bbox[3]) / 2.0
12
+ w = bbox[2] - bbox[0]
13
+ h = bbox[3] - bbox[1]
14
+
15
+ ret = np.zeros(4, dtype=bbox.dtype)
16
+ ret[0] = cx
17
+ ret[1] = cy
18
+ ret[2] = w / h
19
+ ret[3] = h
20
+ return ret
21
+
22
+
23
  class KalmanFilter:
24
  """
25
  A simple Kalman Filter for tracking bounding boxes in image space.
 
44
 
45
  def initiate(self, measurement):
46
  """Create track from unassociated measurement.
47
+
48
  Parameters
49
  ----------
50
+ measurement : ndarray
51
  Bounding box coordinates (x1, y1, x2, y2) with confidence score.
52
+
53
  Returns
54
  -------
55
  (mean, covariance)
56
  Returns the mean vector (8 dimensional) and covariance matrix (8x8)
57
  of the new track.
58
  """
59
+ mean_pos = _xyah_from_xyxy(measurement)
60
  mean = np.r_[mean_pos, np.zeros_like(mean_pos)]
61
 
62
  std = [
 
96
  1e-2,
97
  self._std_weight_position * mean[3],
98
  ]
99
+
100
  std_vel = [
101
  self._std_weight_velocity * mean[3],
102
  self._std_weight_velocity * mean[3],
103
  1e-5,
104
  self._std_weight_velocity * mean[3],
105
  ]
106
+
107
  motion_cov = np.diag(np.square(np.r_[std_pos, std_vel]))
108
  mean = np.dot(self._motion_mat, mean)
109
  covariance = (
 
134
  1e-1,
135
  self._std_weight_position * mean[3],
136
  ]
137
+
138
  innovation_cov = np.diag(np.square(std))
139
  mean = np.dot(self._update_mat, mean)
140
  covariance = np.linalg.multi_dot((self._update_mat, covariance, self._update_mat.T))
 
194
  else:
195
  raise ValueError("invalid distance metric")
196
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
  class STrack:
199
  """
200
  Single object track. Wrapper around KalmanFilter state.
201
  """
 
 
 
 
 
202
 
203
+ def __init__(self, tlwh, score, label):
204
  self._tlwh = np.asarray(self._tlwh_from_xyxy(tlwh), dtype=np.float32)
205
  self.is_activated = False
206
  self.track_id = 0
 
212
  self.frame_id = 0
213
  self.time_since_update = 0
214
  self.hits = 0
215
+
216
  # Multi-frame history
217
  self.history = []
218
+
219
  # Kalman Filter
220
  self.kalman_filter = None
221
  self.mean = None
222
  self.covariance = None
223
+
224
 
225
  def _tlwh_from_xyxy(self, xyxy):
226
  """Convert xyxy to tlwh."""
227
  w = xyxy[2] - xyxy[0]
228
  h = xyxy[3] - xyxy[1]
229
  return [xyxy[0], xyxy[1], w, h]
230
+
231
  def _xyxy_from_tlwh(self, tlwh):
232
  """Convert tlwh to xyxy."""
233
  x1 = tlwh[0]
 
235
  x2 = x1 + tlwh[2]
236
  y2 = y1 + tlwh[3]
237
  return [x1, y1, x2, y2]
238
+
239
  @property
240
  def tlwh(self):
241
  """Get current position in bounding box format `(top left x, top left y,
 
261
  """Start a new track (tentative until min_hits reached)."""
262
  self.kalman_filter = kalman_filter
263
  self.track_id = self.next_id()
264
+ self.mean, self.covariance = self.kalman_filter.initiate(self.tlbr)
265
 
266
  self.state = 2 # Tracked
267
  self.frame_id = frame_id
 
272
  def re_activate(self, new_track, frame_id, new_id=False):
273
  """Reactivate a lost track with a new detection."""
274
  self.mean, self.covariance = self.kalman_filter.update(
275
+ self.mean, self.covariance, _xyah_from_xyxy(new_track.tlbr)
276
  )
277
  self.time_since_update = 0
278
  self.state = 2 # Tracked
279
  self.frame_id = frame_id
280
  self.score = new_track.score
281
+
282
  if new_id:
283
  self.track_id = self.next_id()
284
 
 
290
  self.hits += 1
291
 
292
  self.mean, self.covariance = self.kalman_filter.update(
293
+ self.mean, self.covariance, _xyah_from_xyxy(new_track.tlbr)
294
  )
295
  self.state = 2 # Tracked
296
  if self.hits >= min_hits:
297
  self.is_activated = True # Confirmed after N consecutive hits
298
+
299
  def predict(self):
300
  """Propagate tracking state distribution one time step forward."""
301
  if self.mean is None: return
 
 
 
302
  self.mean, self.covariance = self.kalman_filter.predict(self.mean, self.covariance)
303
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  @staticmethod
305
  def next_id():
306
  # Global counter
 
337
  def update(self, detections_list):
338
  """
339
  Update the tracker with a list of detections.
340
+
341
  Args:
342
  detections_list: List of dicts, each having:
343
  - bbox: [x1, y1, x2, y2]
344
  - score: float
345
  - label: str
346
  - (optional) other keys preserved
347
+
348
  Returns:
349
  List of dicts with 'track_id' added/updated.
350
  """
351
  self.frame_id += 1
352
+
 
353
  activated_stracks = []
354
  refind_stracks = []
355
  lost_stracks = []
356
  removed_stracks = []
357
 
 
 
 
358
  # Split into high and low confidence
359
  detections = []
360
  detections_second = []
361
+
 
 
 
362
  for d in detections_list:
363
  score = d['score']
364
  if score < self.track_low_thresh:
365
+ continue # Background noise -- discard entirely
366
 
367
  t = STrack(d['bbox'], score, d['label'])
368
  t.original_data = d # Link back
 
387
 
388
  # 2. First association (High score)
389
  dists = iou_distance(strack_pool, detections)
390
+ dists = fuse_score(dists, detections)
391
  matches, u_track, u_detection = linear_assignment(dists, thresh=self.match_thresh)
392
 
393
  for itracked, idet in matches:
 
400
  track.re_activate(det, self.frame_id, new_id=False)
401
  refind_stracks.append(track)
402
 
 
 
 
403
  # 3. Second association (Low score)
404
  # Match unmatched tracks to low score detections
405
  r_tracked_stracks = [strack_pool[i] for i in u_track if strack_pool[i].state == 2]
 
416
  track.re_activate(det, self.frame_id, new_id=False)
417
  refind_stracks.append(track)
418
 
 
 
419
  for it in u_track:
420
  track = r_tracked_stracks[it]
421
  if not track.state == 3: # If not already lost
 
433
  det = remaining_dets[idet]
434
  track.update(det, self.frame_id, min_hits=self.min_hits)
435
  activated_stracks.append(track)
 
436
 
437
  # Update u_detection to only contain indices not matched to unconfirmed
438
  matched_det_indices = set(u_detection[idet] for _, idet in matches_unc) if len(matches_unc) > 0 else set()
439
  u_detection = [i for i in u_detection if i not in matched_det_indices]
440
 
441
+ # Unconfirmed tracks that didn't match -> remove (too noisy to keep)
442
  for it in u_unconfirmed:
443
  track = unconfirmed[it]
444
  track.state = 4 # Removed
445
  removed_stracks.append(track)
446
 
447
  elif unconfirmed:
448
+ # No detections left to match -- remove all unconfirmed
449
  for track in unconfirmed:
450
  track.state = 4 # Removed
451
  removed_stracks.append(track)
 
461
 
462
  track.activate(self.kalman_filter, self.frame_id)
463
  activated_stracks.append(track)
 
464
 
465
  if rejected_by_thresh > 0 and self.frame_id <= 5:
466
  logging.warning(
 
485
  self.removed_stracks.append(track)
486
  self.lost_stracks = [t for t in self.lost_stracks if self.frame_id - t.frame_id <= self.track_buffer]
487
 
488
+ # 7. Collect all active tracks for output
 
 
 
 
 
 
 
 
 
 
489
  output_stracks = [t for t in self.tracked_stracks if t.is_activated]
490
+
491
  results = []
492
  for track in output_stracks:
493
  d_out = track.original_data.copy() if hasattr(track, 'original_data') else {}
 
517
 
518
  return results
519
 
 
 
 
 
520
 
521
  # --- Helper Functions ---
522
 
 
524
  """Linear assignment with threshold using scipy."""
525
  if cost_matrix.size == 0:
526
  return np.empty((0, 2), dtype=int), tuple(range(cost_matrix.shape[0])), tuple(range(cost_matrix.shape[1]))
527
+
 
 
 
528
  row_ind, col_ind = linear_sum_assignment(cost_matrix)
529
+
530
+ matched_rows = set(row_ind)
531
+ matched_cols = set(col_ind)
532
+
533
+ matches = []
534
+ unmatched_a = list(set(range(cost_matrix.shape[0])) - matched_rows)
535
+ unmatched_b = list(set(range(cost_matrix.shape[1])) - matched_cols)
536
+
537
  for r, c in zip(row_ind, col_ind):
538
  if cost_matrix[r, c] <= thresh:
539
  matches.append((r, c))
540
  else:
541
  unmatched_a.append(r)
542
  unmatched_b.append(c)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
 
 
544
  matches = np.array(matches) if len(matches) > 0 else np.empty((0, 2), dtype=int)
545
  return matches, unmatched_a, unmatched_b
546
 
547
 
548
  def iou_distance(atracks, btracks):
549
  """Compute IOU cost matrix between tracks and detections."""
550
+ if len(atracks) == 0 or len(btracks) == 0:
551
  return np.zeros((len(atracks), len(btracks)), dtype=float)
552
+
553
  atlbrs = [track.tlbr for track in atracks]
554
  btlbrs = [track.tlbr for track in btracks]
555
+
556
  _ious = bbox_ious(np.array(atlbrs), np.array(btlbrs))
557
  cost_matrix = 1 - _ious
558
  return cost_matrix
 
566
  inter_rect_y1 = np.maximum(b1_y1[:, None], b2_y1)
567
  inter_rect_x2 = np.minimum(b1_x2[:, None], b2_x2)
568
  inter_rect_y2 = np.minimum(b1_y2[:, None], b2_y2)
569
+
570
  inter_area = np.maximum(inter_rect_x2 - inter_rect_x1, 0) * np.maximum(inter_rect_y2 - inter_rect_y1, 0)
571
+
572
  b1_area = (b1_x2 - b1_x1) * (b1_y2 - b1_y1)
573
  b2_area = (b2_x2 - b2_x1) * (b2_y2 - b2_y1)
574
+
575
  iou = inter_area / (b1_area[:, None] + b2_area - inter_area + 1e-6)
576
  return iou
577
 
 
615
  def remove_duplicate_stracks(stracksa, stracksb):
616
  pdist = iou_distance(stracksa, stracksb)
617
  pairs = np.where(pdist < 0.15)
618
+ dupa, dupb = set(), set()
619
+ for a, b in zip(pairs[0], pairs[1]):
620
  time_a = stracksa[a].frame_id - stracksa[a].start_frame
621
  time_b = stracksb[b].frame_id - stracksb[b].start_frame
622
  if time_a > time_b:
623
+ dupb.add(b)
 
624
  else:
625
+ dupa.add(a)
626
+
627
+ res_a = [t for i, t in enumerate(stracksa) if i not in dupa]
628
+ res_b = [t for i, t in enumerate(stracksb) if i not in dupb]
629
  return res_a, res_b
630
 
631
 
632
+ def _multi_predict(stracks, kalman_filter):
 
633
  for t in stracks:
634
  if t.state != 2:
635
  t.mean[7] = 0 # reset velocity h if lost
636
  t.mean, t.covariance = kalman_filter.predict(t.mean, t.covariance)
637
 
638
+ STrack.multi_predict = staticmethod(_multi_predict)