1use crate::{
5 kalman::ConstantVelocityXYAHModel2, ActiveTrackInfo, DetectionBox, TrackInfo, Tracker,
6};
7use lapjv::{lapjv, Matrix};
8use log::trace;
9use nalgebra::{Dyn, OMatrix, U4};
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
30pub struct ByteTrackBuilder {
31 track_extra_lifespan: u64,
32 track_high_conf: f32,
33 track_iou: f32,
34 track_update: f32,
35}
36
37impl Default for ByteTrackBuilder {
38 fn default() -> Self {
39 Self::new()
40 }
41}
42
43impl ByteTrackBuilder {
44 pub fn new() -> Self {
62 Self {
63 track_high_conf: 0.7,
64 track_iou: 0.25,
65 track_update: 0.25,
66 track_extra_lifespan: 500_000_000,
67 }
68 }
69
70 pub fn track_extra_lifespan(mut self, lifespan: u64) -> Self {
72 self.track_extra_lifespan = lifespan;
73 self
74 }
75
76 pub fn track_high_conf(mut self, conf: f32) -> Self {
78 self.track_high_conf = conf;
79 self
80 }
81
82 pub fn track_iou(mut self, iou: f32) -> Self {
84 self.track_iou = iou;
85 self
86 }
87
88 pub fn track_update(mut self, update: f32) -> Self {
90 self.track_update = update;
91 self
92 }
93
94 pub fn build<T: DetectionBox>(self) -> ByteTrack<T> {
112 ByteTrack {
113 track_extra_lifespan: self.track_extra_lifespan,
114 track_high_conf: self.track_high_conf,
115 track_iou: self.track_iou,
116 track_update: self.track_update,
117 tracklets: Vec::new(),
118 frame_count: 0,
119 }
120 }
121}
122
123#[allow(dead_code)]
151#[derive(Default, Debug, Clone)]
152pub struct ByteTrack<T: DetectionBox> {
153 pub track_extra_lifespan: u64,
156
157 pub track_high_conf: f32,
160
161 pub track_iou: f32,
165
166 pub track_update: f32,
170
171 pub tracklets: Vec<Tracklet<T>>,
173
174 pub frame_count: i32,
177}
178
179#[derive(Debug, Clone)]
186pub struct Tracklet<T: DetectionBox> {
187 pub id: Uuid,
190
191 pub filter: ConstantVelocityXYAHModel2<f32>,
194
195 pub count: i32,
198
199 pub created: u64,
202
203 pub last_updated: u64,
207
208 pub last_box: T,
211}
212
213impl<T: DetectionBox> Tracklet<T> {
214 fn update(&mut self, detect_box: &T, ts: u64) {
215 self.count += 1;
216 self.last_updated = ts;
217 self.filter.update(&xyxy_to_xyah(&detect_box.bbox()));
218 self.last_box = detect_box.clone();
219 }
220
221 pub fn get_predicted_location(&self) -> [f32; 4] {
227 let projected = self.filter.project().0;
228 let predicted_xyah = projected.as_slice();
229 xyah_to_xyxy(predicted_xyah)
230 }
231}
232
233fn xyxy_to_xyah(vaal_box: &[f32; 4]) -> [f32; 4] {
234 let x = (vaal_box[2] + vaal_box[0]) / 2.0;
235 let y = (vaal_box[3] + vaal_box[1]) / 2.0;
236 let w = (vaal_box[2] - vaal_box[0]).max(EPSILON);
237 let h = (vaal_box[3] - vaal_box[1]).max(EPSILON);
238 let a = w / h;
239
240 [x, y, a, h]
241}
242
243fn xyah_to_xyxy(xyah: &[f32]) -> [f32; 4] {
244 assert!(xyah.len() >= 4);
245 let [x, y, a, h] = xyah[0..4] else {
246 unreachable!()
247 };
248 let w = h * a;
249 [x - w / 2.0, y - h / 2.0, x + w / 2.0, y + h / 2.0]
250}
251
252const INVALID_MATCH: f32 = 1000000.0;
253const EPSILON: f32 = 0.00001;
254
255fn iou(box1: &[f32], box2: &[f32]) -> f32 {
256 let intersection = (box1[2].min(box2[2]) - box1[0].max(box2[0])).max(0.0)
257 * (box1[3].min(box2[3]) - box1[1].max(box2[1])).max(0.0);
258
259 let union = (box1[2] - box1[0]) * (box1[3] - box1[1])
260 + (box2[2] - box2[0]) * (box2[3] - box2[1])
261 - intersection;
262
263 if union <= EPSILON {
264 return 0.0;
265 }
266
267 intersection / union
268}
269
270fn box_cost<T: DetectionBox>(
271 track: &Tracklet<T>,
272 new_box: &T,
273 distance: f32,
274 score_threshold: f32,
275 iou_threshold: f32,
276) -> f32 {
277 let _ = distance;
278
279 if new_box.score() < score_threshold {
280 return INVALID_MATCH;
281 }
282
283 let predicted_xyah = track.filter.mean.as_slice();
285 let expected = xyah_to_xyxy(predicted_xyah);
286 let iou = iou(&expected, &new_box.bbox());
287 if iou < iou_threshold {
288 return INVALID_MATCH;
289 }
290 (1.5 - new_box.score()) + (1.5 - iou)
291}
292
293impl<T: DetectionBox> ByteTrack<T> {
294 fn compute_costs(
295 &mut self,
296 boxes: &[T],
297 score_threshold: f32,
298 iou_threshold: f32,
299 box_filter: &[bool],
300 track_filter: &[bool],
301 ) -> Matrix<f32> {
302 let dims = boxes.len().max(self.tracklets.len());
304 let mut measurements = OMatrix::<f32, Dyn, U4>::from_element(boxes.len(), 0.0);
305 for (i, mut row) in measurements.row_iter_mut().enumerate() {
306 row.copy_from_slice(&xyxy_to_xyah(&boxes[i].bbox()));
307 }
308
309 Matrix::from_shape_fn((dims, dims), |(x, y)| {
313 if x < boxes.len() && y < self.tracklets.len() {
314 if box_filter[x] || track_filter[y] {
315 INVALID_MATCH
316 } else {
317 box_cost(
318 &self.tracklets[y],
319 &boxes[x],
320 0.0,
322 score_threshold,
323 iou_threshold,
324 )
325 }
326 } else {
327 0.0
328 }
329 })
330 }
331
332 #[allow(clippy::too_many_arguments)]
335 fn process_assignments(
336 &mut self,
337 assignments: &[usize],
338 boxes: &[T],
339 costs: &Matrix<f32>,
340 matched: &mut [bool],
341 tracked: &mut [bool],
342 matched_info: &mut [Option<TrackInfo>],
343 timestamp: u64,
344 log_assignments: bool,
345 ) {
346 for (i, &x) in assignments.iter().enumerate() {
347 if i >= boxes.len() || x >= self.tracklets.len() {
348 continue;
349 }
350
351 if costs[(i, x)] >= INVALID_MATCH {
353 continue;
354 }
355
356 if matched[i] || tracked[x] {
358 continue;
359 }
360
361 if log_assignments {
362 trace!(
363 "Cost: {} Box: {:#?} UUID: {} Mean: {}",
364 costs[(i, x)],
365 boxes[i],
366 self.tracklets[x].id,
367 self.tracklets[x].filter.mean
368 );
369 }
370
371 matched[i] = true;
372 matched_info[i] = Some(TrackInfo {
373 uuid: self.tracklets[x].id,
374 count: self.tracklets[x].count,
375 created: self.tracklets[x].created,
376 tracked_location: self.tracklets[x].get_predicted_location(),
377 last_updated: timestamp,
378 });
379 tracked[x] = true;
380 self.tracklets[x].update(&boxes[i], timestamp);
381 }
382 }
383
384 fn remove_expired_tracklets(&mut self, timestamp: u64) {
386 for i in (0..self.tracklets.len()).rev() {
388 let expiry = self.tracklets[i].last_updated + self.track_extra_lifespan;
389 if expiry < timestamp {
390 trace!("Tracklet removed: {:?}", self.tracklets[i].id);
391 let _ = self.tracklets.swap_remove(i);
392 }
393 }
394 }
395
396 fn create_new_tracklets(
398 &mut self,
399 boxes: &[T],
400 high_conf_indices: &[usize],
401 matched: &[bool],
402 matched_info: &mut [Option<TrackInfo>],
403 timestamp: u64,
404 ) {
405 for &i in high_conf_indices {
406 if matched[i] {
407 continue;
408 }
409
410 let id = Uuid::new_v4();
411 let new_tracklet = Tracklet {
412 id,
413 filter: ConstantVelocityXYAHModel2::new(
414 &xyxy_to_xyah(&boxes[i].bbox()),
415 self.track_update,
416 ),
417 last_updated: timestamp,
418 count: 1,
419 created: timestamp,
420 last_box: boxes[i].clone(),
421 };
422 matched_info[i] = Some(TrackInfo {
423 uuid: new_tracklet.id,
424 count: new_tracklet.count,
425 created: new_tracklet.created,
426 tracked_location: new_tracklet.get_predicted_location(),
427 last_updated: timestamp,
428 });
429 self.tracklets.push(new_tracklet);
430 }
431 }
432}
433
434impl<T> Tracker<T> for ByteTrack<T>
435where
436 T: DetectionBox,
437{
438 fn update(&mut self, boxes: &[T], timestamp: u64) -> Vec<Option<TrackInfo>> {
439 let span = tracing::trace_span!(
440 "tracker.update",
441 n_detections = boxes.len(),
442 n_tracklets = self.tracklets.len(),
443 timestamp,
444 );
445 let _enter = span.enter();
446
447 self.frame_count += 1;
448
449 let high_conf_ind: Vec<usize> = boxes
451 .iter()
452 .enumerate()
453 .filter(|(_, b)| b.score() >= self.track_high_conf)
454 .map(|(x, _)| x)
455 .collect();
456
457 let mut matched = vec![false; boxes.len()];
458 let mut tracked = vec![false; self.tracklets.len()];
459 let mut matched_info = vec![None; boxes.len()];
460
461 if !self.tracklets.is_empty() {
463 let _s = tracing::trace_span!("tracker.update.predict").entered();
464 for track in &mut self.tracklets {
465 track.filter.predict();
466 }
467 }
468
469 if !self.tracklets.is_empty() {
470 let _s = tracing::trace_span!("tracker.update.match_high_conf").entered();
471 let costs = self.compute_costs(
472 boxes,
473 self.track_high_conf,
474 self.track_iou,
475 &matched,
476 &tracked,
477 );
478 if let Ok(ans) = lapjv(&costs) {
479 self.process_assignments(
480 &ans.0,
481 boxes,
482 &costs,
483 &mut matched,
484 &mut tracked,
485 &mut matched_info,
486 timestamp,
487 false,
488 );
489 }
490 }
491
492 if !self.tracklets.is_empty() {
494 let _s = tracing::trace_span!("tracker.update.match_low_conf").entered();
495 let costs = self.compute_costs(boxes, 0.0, self.track_iou, &matched, &tracked);
496 if let Ok(ans) = lapjv(&costs) {
497 self.process_assignments(
498 &ans.0,
499 boxes,
500 &costs,
501 &mut matched,
502 &mut tracked,
503 &mut matched_info,
504 timestamp,
505 true,
506 );
507 }
508 }
509
510 self.remove_expired_tracklets(timestamp);
512
513 self.create_new_tracklets(
515 boxes,
516 &high_conf_ind,
517 &matched,
518 &mut matched_info,
519 timestamp,
520 );
521
522 matched_info
523 }
524
525 fn get_active_tracks(&self) -> Vec<ActiveTrackInfo<T>> {
526 self.tracklets
527 .iter()
528 .map(|t| ActiveTrackInfo {
529 info: TrackInfo {
530 uuid: t.id,
531 tracked_location: t.get_predicted_location(),
532 count: t.count,
533 created: t.created,
534 last_updated: t.last_updated,
535 },
536 last_box: t.last_box.clone(),
537 })
538 .collect()
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545 use crate::*;
546
547 #[test]
548 fn test_vaalbox_xyah_roundtrip() {
549 let box1 = [0.0134, 0.02135, 0.12438, 0.691];
550 let xyah = xyxy_to_xyah(&box1);
551 let box2 = xyah_to_xyxy(&xyah);
552
553 assert!((box1[0] - box2[0]).abs() < f32::EPSILON);
554 assert!((box1[1] - box2[1]).abs() < f32::EPSILON);
555 assert!((box1[2] - box2[2]).abs() < f32::EPSILON);
556 assert!((box1[3] - box2[3]).abs() < f32::EPSILON);
557 }
558
559 #[test]
560 fn test_iou_identical_boxes() {
561 let box1 = [0.1, 0.1, 0.5, 0.5];
562 let box2 = [0.1, 0.1, 0.5, 0.5];
563 let result = iou(&box1, &box2);
564 assert!(
565 (result - 1.0).abs() < 0.001,
566 "IOU of identical boxes should be 1.0"
567 );
568 }
569
570 #[test]
571 fn test_iou_no_overlap() {
572 let box1 = [0.0, 0.0, 0.2, 0.2];
573 let box2 = [0.5, 0.5, 0.7, 0.7];
574 let result = iou(&box1, &box2);
575 assert!(result < 0.001, "IOU of non-overlapping boxes should be ~0");
576 }
577
578 #[test]
579 fn test_iou_partial_overlap() {
580 let box1 = [0.0, 0.0, 0.5, 0.5];
581 let box2 = [0.25, 0.25, 0.75, 0.75];
582 let result = iou(&box1, &box2);
583 assert!(result > 0.1 && result < 0.2, "IOU should be ~0.14");
585 }
586
587 #[test]
588 fn test_bytetrack_new() {
589 let tracker: ByteTrack<MockDetection> = ByteTrackBuilder::new().build();
590 assert_eq!(tracker.frame_count, 0);
591 assert!(tracker.tracklets.is_empty());
592 assert_eq!(tracker.track_high_conf, 0.7);
593 assert_eq!(tracker.track_iou, 0.25);
594 }
595
596 #[test]
597 fn test_bytetrack_single_detection_creates_tracklet() {
598 let mut tracker = ByteTrackBuilder::new().build();
599 let detections = vec![MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.9, 0)];
600
601 let results = tracker.update(&detections, 1000);
602
603 assert_eq!(results.len(), 1);
604 assert!(
605 results[0].is_some(),
606 "High-confidence detection should create tracklet"
607 );
608 assert_eq!(tracker.tracklets.len(), 1);
609 assert_eq!(tracker.frame_count, 1);
610 }
611
612 #[test]
613 fn test_bytetrack_low_confidence_no_tracklet() {
614 let mut tracker = ByteTrackBuilder::new().build();
615 let detections = vec![MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.5, 0)];
617
618 let results = tracker.update(&detections, 1000);
619
620 assert_eq!(results.len(), 1);
621 assert!(
622 results[0].is_none(),
623 "Low-confidence detection should not create tracklet"
624 );
625 assert!(tracker.tracklets.is_empty());
626 }
627
628 #[test]
629 fn test_bytetrack_tracking_across_frames() {
630 let mut tracker = ByteTrackBuilder::new().build();
631
632 let det1 = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
634 let res1 = tracker.update(&det1, 1000);
635 assert!(res1[0].is_some());
636 let uuid1 = res1[0].unwrap().uuid;
637 assert_eq!(tracker.tracklets.len(), 1);
638 assert_eq!(tracker.tracklets[0].count, 1);
640
641 let det2 = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
643 let res2 = tracker.update(&det2, 2000);
644 assert!(res2[0].is_some());
645 let info2 = res2[0].unwrap();
646
647 assert_eq!(tracker.tracklets.len(), 1, "Should still have one tracklet");
649 assert_eq!(info2.uuid, uuid1, "Should match same tracklet");
650 assert_eq!(tracker.tracklets[0].count, 2, "Internal count should be 2");
652 }
653
654 #[test]
655 fn test_bytetrack_multiple_detections() {
656 let mut tracker = ByteTrackBuilder::new().build();
657
658 let detections = vec![
659 MockDetection::new([0.1, 0.1, 0.2, 0.2], 0.9, 0),
660 MockDetection::new([0.5, 0.5, 0.6, 0.6], 0.85, 0),
661 MockDetection::new([0.8, 0.8, 0.9, 0.9], 0.95, 0),
662 ];
663
664 let results = tracker.update(&detections, 1000);
665
666 assert_eq!(results.len(), 3);
667 assert!(results.iter().all(|r| r.is_some()));
668 assert_eq!(tracker.tracklets.len(), 3);
669 }
670
671 #[test]
672 fn test_bytetrack_tracklet_expiry() {
673 let mut tracker = ByteTrackBuilder::new().build();
674 tracker.track_extra_lifespan = 1000; let det1 = vec![MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.9, 0)];
678 tracker.update(&det1, 1000);
679 assert_eq!(tracker.tracklets.len(), 1);
680
681 let empty: Vec<MockDetection> = vec![];
683 tracker.update(&empty, 3000); assert!(tracker.tracklets.is_empty(), "Tracklet should have expired");
686 }
687
688 #[test]
689 fn test_bytetrack_get_active_tracks() {
690 let mut tracker = ByteTrackBuilder::new().build();
691
692 let detections = vec![
693 MockDetection::new([0.1, 0.1, 0.2, 0.2], 0.9, 0),
694 MockDetection::new([0.5, 0.5, 0.6, 0.6], 0.85, 0),
695 ];
696 tracker.update(&detections, 1000);
697
698 let active = tracker.get_active_tracks();
699 assert_eq!(active.len(), 2);
700 assert!(active.iter().all(|t| t.info.count == 1));
701 assert!(active.iter().all(|t| t.info.created == 1000));
702 }
703
704 #[test]
705 fn test_bytetrack_empty_detections() {
706 let mut tracker = ByteTrackBuilder::new().build();
707 let empty: Vec<MockDetection> = vec![];
708
709 let results = tracker.update(&empty, 1000);
710
711 assert!(results.is_empty());
712 assert!(tracker.tracklets.is_empty());
713 assert_eq!(tracker.frame_count, 1);
714 }
715
716 #[test]
717 fn test_two_stage_matching() {
718 let mut tracker = ByteTrackBuilder::new().build();
721
722 let det1 = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
724 let res1 = tracker.update(&det1, 1_000_000);
725 assert!(res1[0].is_some());
726 let uuid1 = res1[0].unwrap().uuid;
727 assert_eq!(tracker.tracklets.len(), 1);
728
729 let det2 = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.3, 0)];
732 let res2 = tracker.update(&det2, 2_000_000);
733 assert!(
734 res2[0].is_some(),
735 "Low-conf detection should match existing tracklet via second stage"
736 );
737 assert_eq!(
738 res2[0].unwrap().uuid,
739 uuid1,
740 "Should match the same tracklet"
741 );
742 assert_eq!(
743 tracker.tracklets.len(),
744 1,
745 "No new tracklet should be created"
746 );
747 assert_eq!(
748 tracker.tracklets[0].count, 2,
749 "Tracklet count should increment"
750 );
751 }
752
753 #[test]
754 fn test_builder_track_extra_lifespan() {
755 let lifespan_default = 500_000_000; let lifespan_extended = 2_000_000_000; let mut tracker_default: ByteTrack<MockDetection> = ByteTrackBuilder::new().build();
759 let mut tracker_extended: ByteTrack<MockDetection> = ByteTrackBuilder::new()
760 .track_extra_lifespan(lifespan_extended)
761 .build();
762
763 assert_eq!(tracker_default.track_extra_lifespan, lifespan_default);
764 assert_eq!(tracker_extended.track_extra_lifespan, lifespan_extended);
765
766 let ts_start = 1_000_000_000u64; let det = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
768
769 tracker_default.update(&det, ts_start);
770 tracker_extended.update(&det, ts_start);
771 assert_eq!(tracker_default.tracklets.len(), 1);
772 assert_eq!(tracker_extended.tracklets.len(), 1);
773
774 let ts_after = ts_start + 1_000_000_000;
777 let empty: Vec<MockDetection> = vec![];
778 tracker_default.update(&empty, ts_after);
779 tracker_extended.update(&empty, ts_after);
780
781 assert!(
782 tracker_default.tracklets.is_empty(),
783 "Default tracker should have expired the tracklet"
784 );
785 assert_eq!(
786 tracker_extended.tracklets.len(),
787 1,
788 "Extended tracker should still have the tracklet"
789 );
790 }
791
792 #[test]
793 fn test_builder_track_high_conf() {
794 let mut tracker: ByteTrack<MockDetection> =
795 ByteTrackBuilder::new().track_high_conf(0.9).build();
796 assert_eq!(tracker.track_high_conf, 0.9);
797
798 let det_low = vec![MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.8, 0)];
800 let res = tracker.update(&det_low, 1000);
801 assert!(
802 res[0].is_none(),
803 "Score 0.8 should not create a tracklet with threshold 0.9"
804 );
805 assert!(tracker.tracklets.is_empty());
806
807 let det_high = vec![MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.95, 0)];
809 let res = tracker.update(&det_high, 2000);
810 assert!(
811 res[0].is_some(),
812 "Score 0.95 should create a tracklet with threshold 0.9"
813 );
814 assert_eq!(tracker.tracklets.len(), 1);
815 }
816
817 #[test]
818 fn test_builder_track_iou() {
819 let mut tracker: ByteTrack<MockDetection> = ByteTrackBuilder::new().track_iou(0.8).build();
821
822 let det1 = vec![
824 MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.9, 0),
825 MockDetection::new([0.5, 0.5, 0.7, 0.7], 0.9, 0),
826 ];
827 tracker.update(&det1, 1000);
828 assert_eq!(tracker.tracklets.len(), 2);
829
830 let det2 = vec![
833 MockDetection::new([0.15, 0.15, 0.35, 0.35], 0.9, 0),
834 MockDetection::new([0.5, 0.5, 0.7, 0.7], 0.9, 0),
835 ];
836 let res2 = tracker.update(&det2, 2000);
837 assert_eq!(res2.len(), 2);
838
839 assert!(
842 tracker.tracklets.len() >= 3,
843 "Shifted detection should create a new tracklet with tight IOU threshold, got {} tracklets",
844 tracker.tracklets.len()
845 );
846 }
847
848 #[test]
849 fn test_degenerate_zero_area_box() {
850 let mut tracker = ByteTrackBuilder::new().build();
852 let det = vec![
853 MockDetection::new([0.5, 0.1, 0.5, 0.3], 0.9, 0), MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.9, 0), ];
856 let results = tracker.update(&det, 1000);
857 assert_eq!(results.len(), 2);
858
859 let zero_box = [0.5, 0.1, 0.5, 0.3];
861 let normal_box = [0.1, 0.1, 0.3, 0.3];
862 let iou_val = iou(&zero_box, &normal_box);
863 assert!(
864 iou_val < EPSILON,
865 "IOU with a zero-area box should be ~0, got {iou_val}"
866 );
867 }
868
869 #[test]
870 fn test_degenerate_high_velocity() {
871 let mut tracker = ByteTrackBuilder::new().build();
872
873 let det1 = vec![MockDetection::new([0.1, 0.1, 0.2, 0.2], 0.9, 0)];
875 let res1 = tracker.update(&det1, 1_000_000);
876 assert!(res1[0].is_some());
877 let uuid1 = res1[0].unwrap().uuid;
878 assert_eq!(tracker.tracklets.len(), 1);
879
880 let det2 = vec![MockDetection::new([0.8, 0.8, 0.9, 0.9], 0.9, 0)];
882 let res2 = tracker.update(&det2, 2_000_000);
883 assert!(res2[0].is_some());
884
885 assert_eq!(
888 tracker.tracklets.len(),
889 2,
890 "Far-displaced detection should create a new tracklet"
891 );
892 assert_ne!(
893 res2[0].unwrap().uuid,
894 uuid1,
895 "New detection should have a different UUID"
896 );
897 }
898
899 #[test]
900 fn test_many_detections_100() {
901 let mut tracker = ByteTrackBuilder::new().build();
902
903 let detections: Vec<MockDetection> = (0..100)
905 .map(|i| {
906 let x = (i % 10) as f32 * 0.1;
907 let y = (i / 10) as f32 * 0.1;
908 MockDetection::new([x, y, x + 0.05, y + 0.05], 0.9, 0)
909 })
910 .collect();
911
912 let results = tracker.update(&detections, 1000);
913 assert_eq!(results.len(), 100);
914 assert!(
915 results.iter().all(|r| r.is_some()),
916 "All 100 high-confidence detections should create tracklets"
917 );
918 assert_eq!(
919 tracker.tracklets.len(),
920 100,
921 "Should have 100 active tracklets"
922 );
923 }
924
925 #[test]
926 fn test_tracklet_count_increments_each_frame() {
927 let mut tracker = ByteTrackBuilder::new().build();
928 let det = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
929
930 for frame in 1..=5 {
931 tracker.update(&det, frame * 1000);
932 }
933
934 assert_eq!(tracker.tracklets.len(), 1);
935 assert_eq!(
936 tracker.tracklets[0].count, 5,
937 "Tracklet count should equal number of frames it was matched"
938 );
939 }
940
941 #[test]
942 fn test_tracklet_created_timestamp_preserved() {
943 let mut tracker = ByteTrackBuilder::new().build();
944 let det = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
945
946 tracker.update(&det, 1000);
947 tracker.update(&det, 2000);
948 tracker.update(&det, 3000);
949
950 let active = tracker.get_active_tracks();
951 assert_eq!(active.len(), 1);
952 assert_eq!(
953 active[0].info.created, 1000,
954 "Created timestamp should remain at the first frame"
955 );
956 assert_eq!(
957 active[0].info.last_updated, 3000,
958 "Last updated should be the most recent frame"
959 );
960 }
961
962 #[test]
963 fn test_mixed_confidence_detections() {
964 let mut tracker = ByteTrackBuilder::new().build();
966 let det = vec![
967 MockDetection::new([0.1, 0.1, 0.2, 0.2], 0.9, 0), MockDetection::new([0.3, 0.3, 0.4, 0.4], 0.3, 0), MockDetection::new([0.5, 0.5, 0.6, 0.6], 0.85, 0), MockDetection::new([0.7, 0.7, 0.8, 0.8], 0.1, 0), ];
972
973 let results = tracker.update(&det, 1000);
974 assert_eq!(results.len(), 4);
975
976 assert!(
978 results[0].is_some(),
979 "High-conf detection should create tracklet"
980 );
981 assert!(
982 results[1].is_none(),
983 "Low-conf detection should not create tracklet"
984 );
985 assert!(
986 results[2].is_some(),
987 "High-conf detection should create tracklet"
988 );
989 assert!(
990 results[3].is_none(),
991 "Low-conf detection should not create tracklet"
992 );
993 assert_eq!(tracker.tracklets.len(), 2);
994 }
995
996 #[test]
997 fn test_iou_contained_box() {
998 let outer = [0.0, 0.0, 1.0, 1.0];
1000 let inner = [0.25, 0.25, 0.75, 0.75];
1001 let result = iou(&outer, &inner);
1002 assert!(
1004 (result - 0.25).abs() < 0.01,
1005 "IOU of contained box should be inner_area/outer_area = 0.25, got {result}"
1006 );
1007 }
1008
1009 #[test]
1010 fn test_xyxy_to_xyah_square_box() {
1011 let square = [0.1, 0.2, 0.3, 0.4];
1013 let xyah = xyxy_to_xyah(&square);
1014 assert!((xyah[0] - 0.2).abs() < 1e-5, "Center x should be 0.2");
1015 assert!((xyah[1] - 0.3).abs() < 1e-5, "Center y should be 0.3");
1016 assert!(
1017 (xyah[2] - 1.0).abs() < 1e-5,
1018 "Aspect ratio of square should be 1.0"
1019 );
1020 assert!((xyah[3] - 0.2).abs() < 1e-5, "Height should be 0.2");
1021 }
1022
1023 #[test]
1024 fn test_frame_count_increments() {
1025 let mut tracker = ByteTrackBuilder::new().build();
1026 let empty: Vec<MockDetection> = vec![];
1027
1028 for _ in 0..10 {
1029 tracker.update(&empty, 0);
1030 }
1031
1032 assert_eq!(
1033 tracker.frame_count, 10,
1034 "Frame count should increment each update"
1035 );
1036 }
1037
1038 #[test]
1039 fn test_tracklet_predicted_location_near_detection() {
1040 let mut tracker = ByteTrackBuilder::new().build();
1041 let det = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
1042 tracker.update(&det, 1000);
1043
1044 let pred = tracker.tracklets[0].get_predicted_location();
1045 assert!(
1047 (pred[0] - 0.2).abs() < 0.1,
1048 "Predicted xmin should be near 0.2, got {}",
1049 pred[0]
1050 );
1051 assert!(
1052 (pred[1] - 0.2).abs() < 0.1,
1053 "Predicted ymin should be near 0.2, got {}",
1054 pred[1]
1055 );
1056 assert!(
1057 (pred[2] - 0.4).abs() < 0.1,
1058 "Predicted xmax should be near 0.4, got {}",
1059 pred[2]
1060 );
1061 assert!(
1062 (pred[3] - 0.4).abs() < 0.1,
1063 "Predicted ymax should be near 0.4, got {}",
1064 pred[3]
1065 );
1066 }
1067}