1use crate::{
23 kalman::ConstantVelocityXYAHModel2, ActiveTrackInfo, DetectionBox, TrackInfo, Tracker,
24};
25use lapjv::{lapjv, Matrix};
26use log::trace;
27use nalgebra::{Dyn, OMatrix, U4};
28use uuid::Uuid;
29
30#[derive(Debug, Clone, Copy, PartialEq)]
48pub struct ByteTrackBuilder {
49 track_extra_lifespan: u64,
50 track_high_conf: f32,
51 track_iou: f32,
52 track_update: f32,
53}
54
55impl Default for ByteTrackBuilder {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl ByteTrackBuilder {
62 pub fn new() -> Self {
80 Self {
81 track_high_conf: 0.7,
82 track_iou: 0.25,
83 track_update: 0.25,
84 track_extra_lifespan: 500_000_000,
85 }
86 }
87
88 pub fn track_extra_lifespan(mut self, lifespan: u64) -> Self {
90 self.track_extra_lifespan = lifespan;
91 self
92 }
93
94 pub fn track_high_conf(mut self, conf: f32) -> Self {
96 self.track_high_conf = conf;
97 self
98 }
99
100 pub fn track_iou(mut self, iou: f32) -> Self {
102 self.track_iou = iou;
103 self
104 }
105
106 pub fn track_update(mut self, update: f32) -> Self {
108 self.track_update = update;
109 self
110 }
111
112 pub fn build<T: DetectionBox>(self) -> ByteTrack<T> {
130 ByteTrack {
131 track_extra_lifespan: self.track_extra_lifespan,
132 track_high_conf: self.track_high_conf,
133 track_iou: self.track_iou,
134 track_update: self.track_update,
135 tracklets: Vec::new(),
136 frame_count: 0,
137 }
138 }
139}
140
141#[allow(dead_code)]
169#[derive(Default, Debug, Clone)]
170pub struct ByteTrack<T: DetectionBox> {
171 pub track_extra_lifespan: u64,
174
175 pub track_high_conf: f32,
178
179 pub track_iou: f32,
183
184 pub track_update: f32,
188
189 pub tracklets: Vec<Tracklet<T>>,
191
192 pub frame_count: i32,
195}
196
197#[derive(Debug, Clone)]
204pub struct Tracklet<T: DetectionBox> {
205 pub id: Uuid,
208
209 pub filter: ConstantVelocityXYAHModel2<f32>,
212
213 pub count: i32,
216
217 pub created: u64,
220
221 pub last_updated: u64,
225
226 pub last_box: T,
229}
230
231impl<T: DetectionBox> Tracklet<T> {
232 fn update(&mut self, detect_box: &T, ts: u64) {
233 self.count += 1;
234 self.last_updated = ts;
235 self.filter.update(&xyxy_to_xyah(&detect_box.bbox()));
236 self.last_box = detect_box.clone();
237 }
238
239 pub fn get_predicted_location(&self) -> [f32; 4] {
245 let projected = self.filter.project().0;
246 let predicted_xyah = projected.as_slice();
247 xyah_to_xyxy(predicted_xyah)
248 }
249}
250
251fn xyxy_to_xyah(vaal_box: &[f32; 4]) -> [f32; 4] {
252 let x = (vaal_box[2] + vaal_box[0]) / 2.0;
253 let y = (vaal_box[3] + vaal_box[1]) / 2.0;
254 let w = (vaal_box[2] - vaal_box[0]).max(EPSILON);
255 let h = (vaal_box[3] - vaal_box[1]).max(EPSILON);
256 let a = w / h;
257
258 [x, y, a, h]
259}
260
261fn xyah_to_xyxy(xyah: &[f32]) -> [f32; 4] {
262 assert!(xyah.len() >= 4);
263 let [x, y, a, h] = xyah[0..4] else {
264 unreachable!()
265 };
266 let w = h * a;
267 [x - w / 2.0, y - h / 2.0, x + w / 2.0, y + h / 2.0]
268}
269
270const INVALID_MATCH: f32 = 1000000.0;
271const EPSILON: f32 = 0.00001;
272
273fn iou(box1: &[f32], box2: &[f32]) -> f32 {
274 let intersection = (box1[2].min(box2[2]) - box1[0].max(box2[0])).max(0.0)
275 * (box1[3].min(box2[3]) - box1[1].max(box2[1])).max(0.0);
276
277 let union = (box1[2] - box1[0]) * (box1[3] - box1[1])
278 + (box2[2] - box2[0]) * (box2[3] - box2[1])
279 - intersection;
280
281 if union <= EPSILON {
282 return 0.0;
283 }
284
285 intersection / union
286}
287
288fn box_cost<T: DetectionBox>(
289 track: &Tracklet<T>,
290 new_box: &T,
291 distance: f32,
292 score_threshold: f32,
293 iou_threshold: f32,
294) -> f32 {
295 let _ = distance;
296
297 if new_box.score() < score_threshold {
298 return INVALID_MATCH;
299 }
300
301 let predicted_xyah = track.filter.mean.as_slice();
303 let expected = xyah_to_xyxy(predicted_xyah);
304 let iou = iou(&expected, &new_box.bbox());
305 if iou < iou_threshold {
306 return INVALID_MATCH;
307 }
308 (1.5 - new_box.score()) + (1.5 - iou)
309}
310
311impl<T: DetectionBox> ByteTrack<T> {
312 fn compute_costs(
313 &mut self,
314 boxes: &[T],
315 score_threshold: f32,
316 iou_threshold: f32,
317 box_filter: &[bool],
318 track_filter: &[bool],
319 ) -> Matrix<f32> {
320 let dims = boxes.len().max(self.tracklets.len());
322 let mut measurements = OMatrix::<f32, Dyn, U4>::from_element(boxes.len(), 0.0);
323 for (i, mut row) in measurements.row_iter_mut().enumerate() {
324 row.copy_from_slice(&xyxy_to_xyah(&boxes[i].bbox()));
325 }
326
327 Matrix::from_shape_fn((dims, dims), |(x, y)| {
331 if x < boxes.len() && y < self.tracklets.len() {
332 if box_filter[x] || track_filter[y] {
333 INVALID_MATCH
334 } else {
335 box_cost(
336 &self.tracklets[y],
337 &boxes[x],
338 0.0,
340 score_threshold,
341 iou_threshold,
342 )
343 }
344 } else {
345 0.0
346 }
347 })
348 }
349
350 #[allow(clippy::too_many_arguments)]
353 fn process_assignments(
354 &mut self,
355 assignments: &[usize],
356 boxes: &[T],
357 costs: &Matrix<f32>,
358 matched: &mut [bool],
359 tracked: &mut [bool],
360 matched_info: &mut [Option<TrackInfo>],
361 timestamp: u64,
362 log_assignments: bool,
363 ) {
364 for (i, &x) in assignments.iter().enumerate() {
365 if i >= boxes.len() || x >= self.tracklets.len() {
366 continue;
367 }
368
369 if costs[(i, x)] >= INVALID_MATCH {
371 continue;
372 }
373
374 if matched[i] || tracked[x] {
376 continue;
377 }
378
379 if log_assignments {
380 trace!(
381 "Cost: {} Box: {:#?} UUID: {} Mean: {}",
382 costs[(i, x)],
383 boxes[i],
384 self.tracklets[x].id,
385 self.tracklets[x].filter.mean
386 );
387 }
388
389 matched[i] = true;
390 matched_info[i] = Some(TrackInfo {
391 uuid: self.tracklets[x].id,
392 count: self.tracklets[x].count,
393 created: self.tracklets[x].created,
394 tracked_location: self.tracklets[x].get_predicted_location(),
395 last_updated: timestamp,
396 });
397 tracked[x] = true;
398 self.tracklets[x].update(&boxes[i], timestamp);
399 }
400 }
401
402 fn remove_expired_tracklets(&mut self, timestamp: u64) {
404 for i in (0..self.tracklets.len()).rev() {
406 let expiry = self.tracklets[i].last_updated + self.track_extra_lifespan;
407 if expiry < timestamp {
408 trace!("Tracklet removed: {:?}", self.tracklets[i].id);
409 let _ = self.tracklets.swap_remove(i);
410 }
411 }
412 }
413
414 fn create_new_tracklets(
416 &mut self,
417 boxes: &[T],
418 high_conf_indices: &[usize],
419 matched: &[bool],
420 matched_info: &mut [Option<TrackInfo>],
421 timestamp: u64,
422 ) {
423 for &i in high_conf_indices {
424 if matched[i] {
425 continue;
426 }
427
428 let id = Uuid::new_v4();
429 let new_tracklet = Tracklet {
430 id,
431 filter: ConstantVelocityXYAHModel2::new(
432 &xyxy_to_xyah(&boxes[i].bbox()),
433 self.track_update,
434 ),
435 last_updated: timestamp,
436 count: 1,
437 created: timestamp,
438 last_box: boxes[i].clone(),
439 };
440 matched_info[i] = Some(TrackInfo {
441 uuid: new_tracklet.id,
442 count: new_tracklet.count,
443 created: new_tracklet.created,
444 tracked_location: new_tracklet.get_predicted_location(),
445 last_updated: timestamp,
446 });
447 self.tracklets.push(new_tracklet);
448 }
449 }
450}
451
452impl<T> Tracker<T> for ByteTrack<T>
453where
454 T: DetectionBox,
455{
456 fn update(&mut self, boxes: &[T], timestamp: u64) -> Vec<Option<TrackInfo>> {
457 let span = tracing::trace_span!(
458 "tracker.update",
459 n_detections = boxes.len(),
460 n_tracklets = self.tracklets.len(),
461 timestamp,
462 );
463 let _enter = span.enter();
464
465 self.frame_count += 1;
466
467 let high_conf_ind: Vec<usize> = boxes
469 .iter()
470 .enumerate()
471 .filter(|(_, b)| b.score() >= self.track_high_conf)
472 .map(|(x, _)| x)
473 .collect();
474
475 let mut matched = vec![false; boxes.len()];
476 let mut tracked = vec![false; self.tracklets.len()];
477 let mut matched_info = vec![None; boxes.len()];
478
479 if !self.tracklets.is_empty() {
481 let _s = tracing::trace_span!("tracker.update.predict").entered();
482 for track in &mut self.tracklets {
483 track.filter.predict();
484 }
485 }
486
487 if !self.tracklets.is_empty() {
488 let _s = tracing::trace_span!("tracker.update.match_high_conf").entered();
489 let costs = self.compute_costs(
490 boxes,
491 self.track_high_conf,
492 self.track_iou,
493 &matched,
494 &tracked,
495 );
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 false,
506 );
507 }
508 }
509
510 if !self.tracklets.is_empty() {
512 let _s = tracing::trace_span!("tracker.update.match_low_conf").entered();
513 let costs = self.compute_costs(boxes, 0.0, self.track_iou, &matched, &tracked);
514 if let Ok(ans) = lapjv(&costs) {
515 self.process_assignments(
516 &ans.0,
517 boxes,
518 &costs,
519 &mut matched,
520 &mut tracked,
521 &mut matched_info,
522 timestamp,
523 true,
524 );
525 }
526 }
527
528 self.remove_expired_tracklets(timestamp);
530
531 self.create_new_tracklets(
533 boxes,
534 &high_conf_ind,
535 &matched,
536 &mut matched_info,
537 timestamp,
538 );
539
540 matched_info
541 }
542
543 fn get_active_tracks(&self) -> Vec<ActiveTrackInfo<T>> {
544 self.tracklets
545 .iter()
546 .map(|t| ActiveTrackInfo {
547 info: TrackInfo {
548 uuid: t.id,
549 tracked_location: t.get_predicted_location(),
550 count: t.count,
551 created: t.created,
552 last_updated: t.last_updated,
553 },
554 last_box: t.last_box.clone(),
555 })
556 .collect()
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563 use crate::*;
564
565 #[test]
566 fn test_vaalbox_xyah_roundtrip() {
567 let box1 = [0.0134, 0.02135, 0.12438, 0.691];
568 let xyah = xyxy_to_xyah(&box1);
569 let box2 = xyah_to_xyxy(&xyah);
570
571 assert!((box1[0] - box2[0]).abs() < f32::EPSILON);
572 assert!((box1[1] - box2[1]).abs() < f32::EPSILON);
573 assert!((box1[2] - box2[2]).abs() < f32::EPSILON);
574 assert!((box1[3] - box2[3]).abs() < f32::EPSILON);
575 }
576
577 #[test]
578 fn test_iou_identical_boxes() {
579 let box1 = [0.1, 0.1, 0.5, 0.5];
580 let box2 = [0.1, 0.1, 0.5, 0.5];
581 let result = iou(&box1, &box2);
582 assert!(
583 (result - 1.0).abs() < 0.001,
584 "IOU of identical boxes should be 1.0"
585 );
586 }
587
588 #[test]
589 fn test_iou_no_overlap() {
590 let box1 = [0.0, 0.0, 0.2, 0.2];
591 let box2 = [0.5, 0.5, 0.7, 0.7];
592 let result = iou(&box1, &box2);
593 assert!(result < 0.001, "IOU of non-overlapping boxes should be ~0");
594 }
595
596 #[test]
597 fn test_iou_partial_overlap() {
598 let box1 = [0.0, 0.0, 0.5, 0.5];
599 let box2 = [0.25, 0.25, 0.75, 0.75];
600 let result = iou(&box1, &box2);
601 assert!(result > 0.1 && result < 0.2, "IOU should be ~0.14");
603 }
604
605 #[test]
606 fn test_bytetrack_new() {
607 let tracker: ByteTrack<MockDetection> = ByteTrackBuilder::new().build();
608 assert_eq!(tracker.frame_count, 0);
609 assert!(tracker.tracklets.is_empty());
610 assert_eq!(tracker.track_high_conf, 0.7);
611 assert_eq!(tracker.track_iou, 0.25);
612 }
613
614 #[test]
615 fn test_bytetrack_single_detection_creates_tracklet() {
616 let mut tracker = ByteTrackBuilder::new().build();
617 let detections = vec![MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.9, 0)];
618
619 let results = tracker.update(&detections, 1000);
620
621 assert_eq!(results.len(), 1);
622 assert!(
623 results[0].is_some(),
624 "High-confidence detection should create tracklet"
625 );
626 assert_eq!(tracker.tracklets.len(), 1);
627 assert_eq!(tracker.frame_count, 1);
628 }
629
630 #[test]
631 fn test_bytetrack_low_confidence_no_tracklet() {
632 let mut tracker = ByteTrackBuilder::new().build();
633 let detections = vec![MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.5, 0)];
635
636 let results = tracker.update(&detections, 1000);
637
638 assert_eq!(results.len(), 1);
639 assert!(
640 results[0].is_none(),
641 "Low-confidence detection should not create tracklet"
642 );
643 assert!(tracker.tracklets.is_empty());
644 }
645
646 #[test]
647 fn test_bytetrack_tracking_across_frames() {
648 let mut tracker = ByteTrackBuilder::new().build();
649
650 let det1 = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
652 let res1 = tracker.update(&det1, 1000);
653 assert!(res1[0].is_some());
654 let uuid1 = res1[0].unwrap().uuid;
655 assert_eq!(tracker.tracklets.len(), 1);
656 assert_eq!(tracker.tracklets[0].count, 1);
658
659 let det2 = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
661 let res2 = tracker.update(&det2, 2000);
662 assert!(res2[0].is_some());
663 let info2 = res2[0].unwrap();
664
665 assert_eq!(tracker.tracklets.len(), 1, "Should still have one tracklet");
667 assert_eq!(info2.uuid, uuid1, "Should match same tracklet");
668 assert_eq!(tracker.tracklets[0].count, 2, "Internal count should be 2");
670 }
671
672 #[test]
673 fn test_bytetrack_multiple_detections() {
674 let mut tracker = ByteTrackBuilder::new().build();
675
676 let detections = vec![
677 MockDetection::new([0.1, 0.1, 0.2, 0.2], 0.9, 0),
678 MockDetection::new([0.5, 0.5, 0.6, 0.6], 0.85, 0),
679 MockDetection::new([0.8, 0.8, 0.9, 0.9], 0.95, 0),
680 ];
681
682 let results = tracker.update(&detections, 1000);
683
684 assert_eq!(results.len(), 3);
685 assert!(results.iter().all(|r| r.is_some()));
686 assert_eq!(tracker.tracklets.len(), 3);
687 }
688
689 #[test]
690 fn test_bytetrack_tracklet_expiry() {
691 let mut tracker = ByteTrackBuilder::new().build();
692 tracker.track_extra_lifespan = 1000; let det1 = vec![MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.9, 0)];
696 tracker.update(&det1, 1000);
697 assert_eq!(tracker.tracklets.len(), 1);
698
699 let empty: Vec<MockDetection> = vec![];
701 tracker.update(&empty, 3000); assert!(tracker.tracklets.is_empty(), "Tracklet should have expired");
704 }
705
706 #[test]
707 fn test_bytetrack_get_active_tracks() {
708 let mut tracker = ByteTrackBuilder::new().build();
709
710 let detections = vec![
711 MockDetection::new([0.1, 0.1, 0.2, 0.2], 0.9, 0),
712 MockDetection::new([0.5, 0.5, 0.6, 0.6], 0.85, 0),
713 ];
714 tracker.update(&detections, 1000);
715
716 let active = tracker.get_active_tracks();
717 assert_eq!(active.len(), 2);
718 assert!(active.iter().all(|t| t.info.count == 1));
719 assert!(active.iter().all(|t| t.info.created == 1000));
720 }
721
722 #[test]
723 fn test_bytetrack_empty_detections() {
724 let mut tracker = ByteTrackBuilder::new().build();
725 let empty: Vec<MockDetection> = vec![];
726
727 let results = tracker.update(&empty, 1000);
728
729 assert!(results.is_empty());
730 assert!(tracker.tracklets.is_empty());
731 assert_eq!(tracker.frame_count, 1);
732 }
733
734 #[test]
735 fn test_two_stage_matching() {
736 let mut tracker = ByteTrackBuilder::new().build();
739
740 let det1 = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
742 let res1 = tracker.update(&det1, 1_000_000);
743 assert!(res1[0].is_some());
744 let uuid1 = res1[0].unwrap().uuid;
745 assert_eq!(tracker.tracklets.len(), 1);
746
747 let det2 = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.3, 0)];
750 let res2 = tracker.update(&det2, 2_000_000);
751 assert!(
752 res2[0].is_some(),
753 "Low-conf detection should match existing tracklet via second stage"
754 );
755 assert_eq!(
756 res2[0].unwrap().uuid,
757 uuid1,
758 "Should match the same tracklet"
759 );
760 assert_eq!(
761 tracker.tracklets.len(),
762 1,
763 "No new tracklet should be created"
764 );
765 assert_eq!(
766 tracker.tracklets[0].count, 2,
767 "Tracklet count should increment"
768 );
769 }
770
771 #[test]
772 fn test_builder_track_extra_lifespan() {
773 let lifespan_default = 500_000_000; let lifespan_extended = 2_000_000_000; let mut tracker_default: ByteTrack<MockDetection> = ByteTrackBuilder::new().build();
777 let mut tracker_extended: ByteTrack<MockDetection> = ByteTrackBuilder::new()
778 .track_extra_lifespan(lifespan_extended)
779 .build();
780
781 assert_eq!(tracker_default.track_extra_lifespan, lifespan_default);
782 assert_eq!(tracker_extended.track_extra_lifespan, lifespan_extended);
783
784 let ts_start = 1_000_000_000u64; let det = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
786
787 tracker_default.update(&det, ts_start);
788 tracker_extended.update(&det, ts_start);
789 assert_eq!(tracker_default.tracklets.len(), 1);
790 assert_eq!(tracker_extended.tracklets.len(), 1);
791
792 let ts_after = ts_start + 1_000_000_000;
795 let empty: Vec<MockDetection> = vec![];
796 tracker_default.update(&empty, ts_after);
797 tracker_extended.update(&empty, ts_after);
798
799 assert!(
800 tracker_default.tracklets.is_empty(),
801 "Default tracker should have expired the tracklet"
802 );
803 assert_eq!(
804 tracker_extended.tracklets.len(),
805 1,
806 "Extended tracker should still have the tracklet"
807 );
808 }
809
810 #[test]
811 fn test_builder_track_high_conf() {
812 let mut tracker: ByteTrack<MockDetection> =
813 ByteTrackBuilder::new().track_high_conf(0.9).build();
814 assert_eq!(tracker.track_high_conf, 0.9);
815
816 let det_low = vec![MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.8, 0)];
818 let res = tracker.update(&det_low, 1000);
819 assert!(
820 res[0].is_none(),
821 "Score 0.8 should not create a tracklet with threshold 0.9"
822 );
823 assert!(tracker.tracklets.is_empty());
824
825 let det_high = vec![MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.95, 0)];
827 let res = tracker.update(&det_high, 2000);
828 assert!(
829 res[0].is_some(),
830 "Score 0.95 should create a tracklet with threshold 0.9"
831 );
832 assert_eq!(tracker.tracklets.len(), 1);
833 }
834
835 #[test]
836 fn test_builder_track_iou() {
837 let mut tracker: ByteTrack<MockDetection> = ByteTrackBuilder::new().track_iou(0.8).build();
839
840 let det1 = vec![
842 MockDetection::new([0.1, 0.1, 0.3, 0.3], 0.9, 0),
843 MockDetection::new([0.5, 0.5, 0.7, 0.7], 0.9, 0),
844 ];
845 tracker.update(&det1, 1000);
846 assert_eq!(tracker.tracklets.len(), 2);
847
848 let det2 = vec![
851 MockDetection::new([0.15, 0.15, 0.35, 0.35], 0.9, 0),
852 MockDetection::new([0.5, 0.5, 0.7, 0.7], 0.9, 0),
853 ];
854 let res2 = tracker.update(&det2, 2000);
855 assert_eq!(res2.len(), 2);
856
857 assert!(
860 tracker.tracklets.len() >= 3,
861 "Shifted detection should create a new tracklet with tight IOU threshold, got {} tracklets",
862 tracker.tracklets.len()
863 );
864 }
865
866 #[test]
867 fn test_degenerate_zero_area_box() {
868 let mut tracker = ByteTrackBuilder::new().build();
870 let det = vec![
871 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), ];
874 let results = tracker.update(&det, 1000);
875 assert_eq!(results.len(), 2);
876
877 let zero_box = [0.5, 0.1, 0.5, 0.3];
879 let normal_box = [0.1, 0.1, 0.3, 0.3];
880 let iou_val = iou(&zero_box, &normal_box);
881 assert!(
882 iou_val < EPSILON,
883 "IOU with a zero-area box should be ~0, got {iou_val}"
884 );
885 }
886
887 #[test]
888 fn test_degenerate_high_velocity() {
889 let mut tracker = ByteTrackBuilder::new().build();
890
891 let det1 = vec![MockDetection::new([0.1, 0.1, 0.2, 0.2], 0.9, 0)];
893 let res1 = tracker.update(&det1, 1_000_000);
894 assert!(res1[0].is_some());
895 let uuid1 = res1[0].unwrap().uuid;
896 assert_eq!(tracker.tracklets.len(), 1);
897
898 let det2 = vec![MockDetection::new([0.8, 0.8, 0.9, 0.9], 0.9, 0)];
900 let res2 = tracker.update(&det2, 2_000_000);
901 assert!(res2[0].is_some());
902
903 assert_eq!(
906 tracker.tracklets.len(),
907 2,
908 "Far-displaced detection should create a new tracklet"
909 );
910 assert_ne!(
911 res2[0].unwrap().uuid,
912 uuid1,
913 "New detection should have a different UUID"
914 );
915 }
916
917 #[test]
918 fn test_many_detections_100() {
919 let mut tracker = ByteTrackBuilder::new().build();
920
921 let detections: Vec<MockDetection> = (0..100)
923 .map(|i| {
924 let x = (i % 10) as f32 * 0.1;
925 let y = (i / 10) as f32 * 0.1;
926 MockDetection::new([x, y, x + 0.05, y + 0.05], 0.9, 0)
927 })
928 .collect();
929
930 let results = tracker.update(&detections, 1000);
931 assert_eq!(results.len(), 100);
932 assert!(
933 results.iter().all(|r| r.is_some()),
934 "All 100 high-confidence detections should create tracklets"
935 );
936 assert_eq!(
937 tracker.tracklets.len(),
938 100,
939 "Should have 100 active tracklets"
940 );
941 }
942
943 #[test]
944 fn test_tracklet_count_increments_each_frame() {
945 let mut tracker = ByteTrackBuilder::new().build();
946 let det = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
947
948 for frame in 1..=5 {
949 tracker.update(&det, frame * 1000);
950 }
951
952 assert_eq!(tracker.tracklets.len(), 1);
953 assert_eq!(
954 tracker.tracklets[0].count, 5,
955 "Tracklet count should equal number of frames it was matched"
956 );
957 }
958
959 #[test]
960 fn test_tracklet_created_timestamp_preserved() {
961 let mut tracker = ByteTrackBuilder::new().build();
962 let det = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
963
964 tracker.update(&det, 1000);
965 tracker.update(&det, 2000);
966 tracker.update(&det, 3000);
967
968 let active = tracker.get_active_tracks();
969 assert_eq!(active.len(), 1);
970 assert_eq!(
971 active[0].info.created, 1000,
972 "Created timestamp should remain at the first frame"
973 );
974 assert_eq!(
975 active[0].info.last_updated, 3000,
976 "Last updated should be the most recent frame"
977 );
978 }
979
980 #[test]
981 fn test_mixed_confidence_detections() {
982 let mut tracker = ByteTrackBuilder::new().build();
984 let det = vec![
985 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), ];
990
991 let results = tracker.update(&det, 1000);
992 assert_eq!(results.len(), 4);
993
994 assert!(
996 results[0].is_some(),
997 "High-conf detection should create tracklet"
998 );
999 assert!(
1000 results[1].is_none(),
1001 "Low-conf detection should not create tracklet"
1002 );
1003 assert!(
1004 results[2].is_some(),
1005 "High-conf detection should create tracklet"
1006 );
1007 assert!(
1008 results[3].is_none(),
1009 "Low-conf detection should not create tracklet"
1010 );
1011 assert_eq!(tracker.tracklets.len(), 2);
1012 }
1013
1014 #[test]
1015 fn test_iou_contained_box() {
1016 let outer = [0.0, 0.0, 1.0, 1.0];
1018 let inner = [0.25, 0.25, 0.75, 0.75];
1019 let result = iou(&outer, &inner);
1020 assert!(
1022 (result - 0.25).abs() < 0.01,
1023 "IOU of contained box should be inner_area/outer_area = 0.25, got {result}"
1024 );
1025 }
1026
1027 #[test]
1028 fn test_xyxy_to_xyah_square_box() {
1029 let square = [0.1, 0.2, 0.3, 0.4];
1031 let xyah = xyxy_to_xyah(&square);
1032 assert!((xyah[0] - 0.2).abs() < 1e-5, "Center x should be 0.2");
1033 assert!((xyah[1] - 0.3).abs() < 1e-5, "Center y should be 0.3");
1034 assert!(
1035 (xyah[2] - 1.0).abs() < 1e-5,
1036 "Aspect ratio of square should be 1.0"
1037 );
1038 assert!((xyah[3] - 0.2).abs() < 1e-5, "Height should be 0.2");
1039 }
1040
1041 #[test]
1042 fn test_frame_count_increments() {
1043 let mut tracker = ByteTrackBuilder::new().build();
1044 let empty: Vec<MockDetection> = vec![];
1045
1046 for _ in 0..10 {
1047 tracker.update(&empty, 0);
1048 }
1049
1050 assert_eq!(
1051 tracker.frame_count, 10,
1052 "Frame count should increment each update"
1053 );
1054 }
1055
1056 #[test]
1057 fn test_tracklet_predicted_location_near_detection() {
1058 let mut tracker = ByteTrackBuilder::new().build();
1059 let det = vec![MockDetection::new([0.2, 0.2, 0.4, 0.4], 0.9, 0)];
1060 tracker.update(&det, 1000);
1061
1062 let pred = tracker.tracklets[0].get_predicted_location();
1063 assert!(
1065 (pred[0] - 0.2).abs() < 0.1,
1066 "Predicted xmin should be near 0.2, got {}",
1067 pred[0]
1068 );
1069 assert!(
1070 (pred[1] - 0.2).abs() < 0.1,
1071 "Predicted ymin should be near 0.2, got {}",
1072 pred[1]
1073 );
1074 assert!(
1075 (pred[2] - 0.4).abs() < 0.1,
1076 "Predicted xmax should be near 0.4, got {}",
1077 pred[2]
1078 );
1079 assert!(
1080 (pred[3] - 0.4).abs() < 0.1,
1081 "Predicted ymax should be near 0.4, got {}",
1082 pred[3]
1083 );
1084 }
1085}