1use nalgebra::{DMatrix, DVector};
4use std::collections::VecDeque;
5
6use crate::camera_motion::CoordinateTransformation;
7use crate::distances::{distance_function_by_name, DistanceFunction};
8use crate::filter::FilterFactoryEnum;
9use crate::internal::numpy::to_row_major_vec;
10use crate::matching::{get_unmatched, match_detections_and_objects};
11use crate::tracked_object::get_next_global_id;
12use crate::{Detection, Error, Result, TrackedObject};
13
14#[derive(Clone)]
16pub struct TrackerConfig {
17 pub distance_function: DistanceFunction,
19
20 pub distance_threshold: f64,
22
23 pub hit_counter_max: i32,
25
26 pub initialization_delay: i32,
28
29 pub pointwise_hit_counter_max: i32,
31
32 pub detection_threshold: f64,
34
35 pub filter_factory: FilterFactoryEnum,
37
38 pub past_detections_length: usize,
40
41 pub reid_distance_function: Option<DistanceFunction>,
43
44 pub reid_distance_threshold: f64,
46
47 pub reid_hit_counter_max: Option<i32>,
49}
50
51impl TrackerConfig {
52 pub fn new(distance_function: DistanceFunction, distance_threshold: f64) -> Self {
58 Self {
59 distance_function,
60 distance_threshold,
61 hit_counter_max: 15,
62 initialization_delay: -1, pointwise_hit_counter_max: 4,
64 detection_threshold: 0.0,
65 filter_factory: FilterFactoryEnum::default(),
66 past_detections_length: 4,
67 reid_distance_function: None,
68 reid_distance_threshold: 1.0,
69 reid_hit_counter_max: None,
70 }
71 }
72
73 pub fn from_distance_name(name: &str, distance_threshold: f64) -> Self {
75 Self::new(distance_function_by_name(name), distance_threshold)
76 }
77}
78
79pub struct Tracker {
84 pub config: TrackerConfig,
86
87 pub tracked_objects: Vec<TrackedObject>,
89
90 instance_id_counter: i32,
92
93 initializing_id_counter: i32,
95}
96
97impl Tracker {
98 pub fn new(mut config: TrackerConfig) -> Result<Self> {
100 if config.initialization_delay == -1 {
102 config.initialization_delay = config.hit_counter_max / 2;
103 }
104
105 if config.initialization_delay < 0 {
106 return Err(Error::InvalidConfig(
107 "initialization_delay must be non-negative".to_string(),
108 ));
109 }
110
111 if config.initialization_delay >= config.hit_counter_max {
112 return Err(Error::InvalidConfig(
113 "initialization_delay must be less than hit_counter_max".to_string(),
114 ));
115 }
116
117 Ok(Self {
118 config,
119 tracked_objects: Vec::new(),
120 instance_id_counter: 1,
122 initializing_id_counter: 1,
123 })
124 }
125
126 pub fn update(
136 &mut self,
137 mut detections: Vec<Detection>,
138 period: i32,
139 coord_transform: Option<&dyn CoordinateTransformation>,
140 ) -> Vec<&TrackedObject> {
141 if let Some(transform) = coord_transform {
143 for det in &mut detections {
144 let abs_points = transform.rel_to_abs(&det.points);
145 det.set_absolute_points(abs_points);
146 }
147 }
148
149 let dead_indices: Vec<usize> = if self.config.reid_hit_counter_max.is_none() {
154 self.tracked_objects
156 .retain(|obj| obj.hit_counter_is_positive());
157 vec![] } else {
159 self.tracked_objects
161 .retain(|obj| obj.reid_hit_counter_is_positive());
162 self.tracked_objects
164 .iter()
165 .enumerate()
166 .filter(|(_, obj)| !obj.hit_counter_is_positive())
167 .map(|(i, _)| i)
168 .collect()
169 };
170
171 let alive_initialized_indices: Vec<usize> = self
177 .tracked_objects
178 .iter()
179 .enumerate()
180 .filter(|(_, obj)| !obj.is_initializing && obj.hit_counter_is_positive())
181 .map(|(i, _)| i)
182 .collect();
183
184 let initializing_indices: Vec<usize> = self
185 .tracked_objects
186 .iter()
187 .enumerate()
188 .filter(|(_, obj)| obj.is_initializing)
189 .map(|(i, _)| i)
190 .collect();
191
192 for obj in &mut self.tracked_objects {
194 if obj.reid_hit_counter.is_none() {
196 if obj.hit_counter <= 0 {
197 obj.reid_hit_counter = self.config.reid_hit_counter_max;
199 }
200 } else {
201 obj.reid_hit_counter = obj.reid_hit_counter.map(|c| c - 1);
203 }
204
205 obj.age += 1;
206 obj.hit_counter -= 1;
209
210 for counter in &mut obj.point_hit_counter {
212 *counter = (*counter - 1).max(0);
213 }
214
215 obj.filter.predict();
217
218 obj.estimate = obj.filter.get_state();
220
221 let state = obj.filter.get_state_vector();
223 let dim_z = obj.filter.dim_z();
224 if state.len() >= dim_z * 2 {
225 let velocity_flat: Vec<f64> =
226 state.iter().skip(dim_z).take(dim_z).cloned().collect();
227 obj.estimate_velocity =
228 DMatrix::from_vec(obj.num_points, obj.dim_points, velocity_flat);
229 }
230
231 if let Some(transform) = coord_transform {
233 obj.last_coord_transform = Some(transform.clone_box());
234 }
235 }
236
237 let det_refs: Vec<&Detection> = detections.iter().collect();
239 let alive_init_obj_refs: Vec<&TrackedObject> = alive_initialized_indices
240 .iter()
241 .map(|&i| &self.tracked_objects[i])
242 .collect();
243
244 let distance_matrix = if !alive_init_obj_refs.is_empty() && !det_refs.is_empty() {
245 self.config
246 .distance_function
247 .get_distances(&alive_init_obj_refs, &det_refs)
248 } else {
249 DMatrix::zeros(det_refs.len(), alive_init_obj_refs.len())
250 };
251
252 let (matched_dets, matched_objs) =
253 match_detections_and_objects(&distance_matrix, self.config.distance_threshold);
254
255 for (&det_idx, &obj_local_idx) in matched_dets.iter().zip(matched_objs.iter()) {
257 let obj_idx = alive_initialized_indices[obj_local_idx];
258 self.hit_object(
259 obj_idx,
260 &detections[det_idx],
261 period,
262 distance_matrix[(det_idx, obj_local_idx)],
263 );
264 }
265
266 let unmatched_alive_init_indices: Vec<usize> =
268 get_unmatched(alive_initialized_indices.len(), &matched_objs)
269 .into_iter()
270 .map(|i| alive_initialized_indices[i])
271 .collect();
272
273 let unmatched_det_indices = get_unmatched(detections.len(), &matched_dets);
275
276 let unmatched_det_refs: Vec<&Detection> = unmatched_det_indices
278 .iter()
279 .map(|&i| &detections[i])
280 .collect();
281 let init_obj_refs: Vec<&TrackedObject> = initializing_indices
282 .iter()
283 .map(|&i| &self.tracked_objects[i])
284 .collect();
285
286 let init_distance_matrix = if !init_obj_refs.is_empty() && !unmatched_det_refs.is_empty() {
287 self.config
288 .distance_function
289 .get_distances(&init_obj_refs, &unmatched_det_refs)
290 } else {
291 DMatrix::zeros(unmatched_det_refs.len(), init_obj_refs.len())
292 };
293
294 let (init_matched_dets, init_matched_objs) =
295 match_detections_and_objects(&init_distance_matrix, self.config.distance_threshold);
296
297 let matched_init_obj_indices: Vec<usize> = init_matched_objs
299 .iter()
300 .map(|&i| initializing_indices[i])
301 .collect();
302
303 for (&local_det_idx, &obj_local_idx) in
305 init_matched_dets.iter().zip(init_matched_objs.iter())
306 {
307 let det_idx = unmatched_det_indices[local_det_idx];
308 let obj_idx = initializing_indices[obj_local_idx];
309 self.hit_object(
310 obj_idx,
311 &detections[det_idx],
312 period,
313 init_distance_matrix[(local_det_idx, obj_local_idx)],
314 );
315 }
316
317 if let Some(ref reid_distance) = self.config.reid_distance_function {
320 let reid_object_indices: Vec<usize> = unmatched_alive_init_indices
322 .iter()
323 .chain(dead_indices.iter())
324 .cloned()
325 .collect();
326
327 if !reid_object_indices.is_empty() && !matched_init_obj_indices.is_empty() {
329 let reid_obj_refs: Vec<&TrackedObject> = reid_object_indices
331 .iter()
332 .map(|&i| &self.tracked_objects[i])
333 .collect();
334 let candidate_refs: Vec<&TrackedObject> = matched_init_obj_indices
335 .iter()
336 .map(|&i| &self.tracked_objects[i])
337 .collect();
338
339 let reid_distance_matrix =
342 reid_distance.get_distances_objects(&reid_obj_refs, &candidate_refs);
343
344 let (reid_matched_cands, reid_matched_objs) = match_detections_and_objects(
346 &reid_distance_matrix,
347 self.config.reid_distance_threshold,
348 );
349
350 let mut to_remove: Vec<usize> = vec![];
352 for (&cand_local, &obj_local) in
353 reid_matched_cands.iter().zip(reid_matched_objs.iter())
354 {
355 let old_obj_idx = reid_object_indices[obj_local];
356 let new_obj_idx = matched_init_obj_indices[cand_local];
357
358 let new_obj_data = self.tracked_objects[new_obj_idx].clone();
360
361 self.tracked_objects[old_obj_idx]
363 .merge(&new_obj_data, self.config.past_detections_length);
364
365 to_remove.push(new_obj_idx);
366 }
367
368 to_remove.sort_unstable();
370 for idx in to_remove.into_iter().rev() {
371 self.tracked_objects.remove(idx);
372 }
373 }
374 }
375
376 let still_unmatched: Vec<_> =
378 get_unmatched(unmatched_det_indices.len(), &init_matched_dets)
379 .into_iter()
380 .map(|i| unmatched_det_indices[i])
381 .collect();
382
383 for det_idx in still_unmatched {
384 self.create_object(&detections[det_idx], period, coord_transform);
385 }
386
387 self.tracked_objects
390 .iter()
391 .filter(|obj| !obj.is_initializing && obj.hit_counter >= 0)
392 .collect()
393 }
394
395 pub fn total_object_count(&self) -> i32 {
398 self.instance_id_counter - 1
399 }
400
401 pub fn current_object_count(&self) -> usize {
403 self.tracked_objects
404 .iter()
405 .filter(|obj| !obj.is_initializing && obj.hit_counter >= 0)
406 .count()
407 }
408
409 fn hit_object(&mut self, obj_idx: usize, detection: &Detection, period: i32, distance: f64) {
411 let h = {
413 let obj = &self.tracked_objects[obj_idx];
414 self.build_observation_matrix_impl(obj, detection)
415 };
416
417 let obj = &mut self.tracked_objects[obj_idx];
419
420 obj.hit_counter = (obj.hit_counter + 2 * period).min(self.config.hit_counter_max);
423
424 if obj.is_initializing && obj.hit_counter > self.config.initialization_delay {
427 obj.is_initializing = false;
428 obj.id = Some(self.instance_id_counter);
429 self.instance_id_counter += 1;
430 if self.config.reid_hit_counter_max.is_some() {
434 obj.reid_hit_counter = None;
435 }
436 }
437
438 for (i, counter) in obj.point_hit_counter.iter_mut().enumerate() {
440 let score = detection.scores.as_ref().map(|s| s[i]).unwrap_or(1.0);
441 if score > self.config.detection_threshold {
442 *counter = (*counter + period).min(self.config.pointwise_hit_counter_max);
443 if i < obj.detected_at_least_once_points.len() {
445 obj.detected_at_least_once_points[i] = true;
446 }
447 }
448 }
449
450 let measurement = DVector::from_vec(to_row_major_vec(detection.get_absolute_points()));
453 obj.filter.update(&measurement, None, h.as_ref());
454
455 obj.estimate = obj.filter.get_state();
457
458 obj.last_detection = Some(detection.clone());
460 obj.last_distance = Some(distance);
461
462 if self.config.past_detections_length > 0 {
464 obj.past_detections.push_back(detection.clone());
465 while obj.past_detections.len() > self.config.past_detections_length {
466 obj.past_detections.pop_front();
467 }
468 }
469 }
470
471 fn create_object(
473 &mut self,
474 detection: &Detection,
475 period: i32,
476 coord_transform: Option<&dyn CoordinateTransformation>,
477 ) {
478 let global_id = get_next_global_id();
479 let initializing_id = self.initializing_id_counter;
480 self.initializing_id_counter += 1;
481
482 let num_points = detection.num_points();
483 let dim_points = detection.num_dims();
484
485 let filter = self
487 .config
488 .filter_factory
489 .create(detection.get_absolute_points());
490
491 let point_hit_counter = vec![period.min(self.config.pointwise_hit_counter_max); num_points];
493
494 let detected_at_least_once_points = if let Some(ref scores) = detection.scores {
496 scores
497 .iter()
498 .map(|&s| s > self.config.detection_threshold)
499 .collect()
500 } else {
501 vec![true; num_points]
502 };
503
504 let mut obj = TrackedObject {
505 id: None,
506 global_id,
507 initializing_id: Some(initializing_id),
508 age: 0,
509 hit_counter: period,
510 point_hit_counter,
511 last_detection: Some(detection.clone()),
512 last_distance: None,
513 current_min_distance: None,
514 past_detections: VecDeque::new(),
515 label: detection.label.clone(),
516 reid_hit_counter: None,
519 estimate: filter.get_state(),
520 estimate_velocity: DMatrix::zeros(num_points, dim_points),
521 is_initializing: true,
522 detected_at_least_once_points,
523 filter,
524 initial_period: period,
525 num_points,
526 dim_points,
527 last_coord_transform: coord_transform.map(|t| t.clone_box()),
528 };
529
530 if self.config.initialization_delay == 0 {
532 obj.is_initializing = false;
533 obj.id = Some(self.instance_id_counter);
534 self.instance_id_counter += 1;
535 }
537
538 self.tracked_objects.push(obj);
539 }
540
541 fn build_observation_matrix_impl(
543 &self,
544 obj: &TrackedObject,
545 detection: &Detection,
546 ) -> Option<DMatrix<f64>> {
547 let dim_z = obj.filter.dim_z();
548 let dim_x = obj.filter.dim_x();
549
550 let scores = detection.scores.as_ref();
552 let needs_mask = scores
553 .map(|s| {
554 s.iter()
555 .any(|&score| score <= self.config.detection_threshold)
556 })
557 .unwrap_or(false);
558
559 if !needs_mask {
560 return None;
561 }
562
563 let mut h = DMatrix::zeros(dim_z, dim_x);
565 for i in 0..dim_z {
566 let point_idx = i / obj.dim_points;
567 let score = scores.map(|s| s[point_idx]).unwrap_or(1.0);
568 if score > self.config.detection_threshold {
569 h[(i, i)] = 1.0;
570 }
571 }
572
573 Some(h)
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use crate::camera_motion::TranslationTransformation;
581
582 #[test]
586 fn test_tracker_new() {
587 let config = TrackerConfig::from_distance_name("euclidean", 100.0);
588 let tracker = Tracker::new(config).unwrap();
589
590 assert_eq!(tracker.tracked_objects.len(), 0);
591 assert_eq!(tracker.total_object_count(), 0);
592 assert_eq!(tracker.current_object_count(), 0);
593 }
594
595 #[test]
597 fn test_tracker_new_with_defaults() {
598 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
600 config.hit_counter_max = 15;
601 config.initialization_delay = -1; config.pointwise_hit_counter_max = 4;
603 config.detection_threshold = 0.0;
604 config.past_detections_length = 4;
605
606 let tracker = Tracker::new(config).unwrap();
607
608 assert_eq!(tracker.config.distance_threshold, 100.0);
610 assert_eq!(tracker.config.hit_counter_max, 15);
611 assert_eq!(tracker.config.initialization_delay, 7); assert_eq!(tracker.tracked_objects.len(), 0);
615 assert_eq!(tracker.current_object_count(), 0);
616 assert_eq!(tracker.total_object_count(), 0);
617 }
618
619 #[test]
621 fn test_tracker_invalid_config() {
622 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
624 config.hit_counter_max = 15;
625 config.initialization_delay = -2; assert!(
628 Tracker::new(config).is_err(),
629 "Expected error for negative initialization_delay"
630 );
631 }
632
633 #[test]
635 fn test_tracker_invalid_config_delay_too_high() {
636 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
638 config.hit_counter_max = 15;
639 config.initialization_delay = 15; assert!(
642 Tracker::new(config).is_err(),
643 "Expected error for initialization_delay >= hit_counter_max"
644 );
645 }
646
647 #[test]
649 fn test_tracker_simple_update() {
650 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
652 config.hit_counter_max = 5;
653 config.initialization_delay = -1; let mut tracker = Tracker::new(config).unwrap();
656
657 let det = Detection::from_slice(&[10.0, 20.0], 1, 2).unwrap();
659
660 let active = tracker.update(vec![det], 1, None);
662
663 assert_eq!(active.len(), 0, "Expected 0 active objects (initializing)");
665
666 assert_eq!(
668 tracker.tracked_objects.len(),
669 1,
670 "Expected 1 tracked object"
671 );
672
673 assert_eq!(
675 tracker.total_object_count(),
676 0,
677 "Expected total count 0 (still initializing)"
678 );
679
680 assert!(
682 tracker.tracked_objects[0].is_initializing,
683 "Expected object to be initializing"
684 );
685
686 assert!(
688 tracker.tracked_objects[0].initializing_id.is_some(),
689 "Expected initializing ID to be set"
690 );
691 assert!(
692 tracker.tracked_objects[0].id.is_none(),
693 "Expected permanent ID to be nil (still initializing)"
694 );
695 }
696
697 #[test]
699 fn test_tracker_update_empty_detections() {
700 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
702 config.hit_counter_max = 5;
703 config.initialization_delay = -1; let mut tracker = Tracker::new(config).unwrap();
706
707 let active = tracker.update(vec![], 1, None);
709
710 assert_eq!(active.len(), 0, "Expected 0 active objects");
711
712 let active = tracker.update(Vec::new(), 1, None);
714
715 assert_eq!(active.len(), 0, "Expected 0 active objects");
716 }
717
718 #[test]
719 fn test_tracker_initialization() {
720 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
721 config.hit_counter_max = 5;
722 config.initialization_delay = 2;
723
724 let mut tracker = Tracker::new(config).unwrap();
725
726 let det = Detection::from_slice(&[10.0, 20.0], 1, 2).unwrap();
728 let active = tracker.update(vec![det.clone()], 1, None);
729 assert_eq!(active.len(), 0);
730
731 let active = tracker.update(vec![det.clone()], 1, None);
734 assert_eq!(active.len(), 0);
735
736 let active = tracker.update(vec![det], 1, None);
739 assert_eq!(active.len(), 1);
740 assert!(active[0].id.is_some());
741 }
742
743 #[test]
747 fn test_detection_creation_2d() {
748 let det = Detection::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 3, 2).unwrap();
750
751 assert_eq!(det.points.nrows(), 3, "Expected 3 rows");
753 assert_eq!(det.points.ncols(), 2, "Expected 2 cols");
754 }
755
756 #[test]
758 fn test_detection_creation_3d() {
759 let det = Detection::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 2, 3).unwrap();
761
762 assert_eq!(det.points.nrows(), 2, "Expected 2 rows");
764 assert_eq!(det.points.ncols(), 3, "Expected 3 cols");
765 }
766
767 #[test]
771 fn test_tracked_object_creation_via_tracker() {
772 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
774 config.hit_counter_max = 15;
775 config.initialization_delay = 7;
776
777 let mut tracker = Tracker::new(config).unwrap();
778
779 let det = Detection::from_slice(&[10.0, 20.0, 30.0, 40.0], 2, 2).unwrap();
781
782 tracker.update(vec![det], 1, None);
784
785 assert_eq!(tracker.tracked_objects.len(), 1);
787 let obj = &tracker.tracked_objects[0];
788
789 assert_eq!(obj.num_points, 2, "Expected 2 points");
791 assert_eq!(obj.dim_points, 2, "Expected 2D points");
792 assert_eq!(obj.hit_counter, 1, "Expected hit counter 1");
793 assert!(obj.is_initializing, "Expected object to be initializing");
794 assert!(
795 obj.initializing_id.is_some(),
796 "Expected initializing ID to be set"
797 );
798 assert!(
799 obj.id.is_none(),
800 "Expected permanent ID to be nil (still initializing)"
801 );
802 }
803
804 #[test]
808 fn test_tracker_camera_motion() {
809 let mut config = TrackerConfig::from_distance_name("euclidean", 1.0);
811 config.hit_counter_max = 1;
812 config.initialization_delay = 0; let mut tracker = Tracker::new(config).unwrap();
815
816 let coord_transform = TranslationTransformation::new([1.0, 1.0]);
820
821 let det = Detection::from_slice(&[2.0, 2.0], 1, 2).unwrap();
823
824 let active = tracker.update(vec![det], 1, Some(&coord_transform));
826
827 assert_eq!(active.len(), 1, "Expected 1 active object");
829
830 let obj = active[0];
831
832 assert_eq!(obj.num_points, 1);
843 assert_eq!(obj.dim_points, 2);
844 }
845
846 #[test]
848 fn test_tracker_immediate_initialization() {
849 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
850 config.hit_counter_max = 5;
851 config.initialization_delay = 0;
852
853 let mut tracker = Tracker::new(config).unwrap();
854
855 let det = Detection::from_slice(&[10.0, 20.0], 1, 2).unwrap();
857 let active = tracker.update(vec![det], 1, None);
858
859 assert_eq!(active.len(), 1, "Expected 1 active object with delay=0");
861 assert!(active[0].id.is_some(), "Expected permanent ID with delay=0");
862 assert!(
863 !active[0].is_initializing,
864 "Should not be initializing with delay=0"
865 );
866
867 assert_eq!(tracker.total_object_count(), 1);
869 }
870
871 #[test]
873 fn test_tracker_object_counts() {
874 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
875 config.hit_counter_max = 5;
876 config.initialization_delay = 0; let mut tracker = Tracker::new(config).unwrap();
879
880 assert_eq!(tracker.total_object_count(), 0);
882 assert_eq!(tracker.current_object_count(), 0);
883
884 let det1 = Detection::from_slice(&[10.0, 20.0], 1, 2).unwrap();
886 tracker.update(vec![det1], 1, None);
887
888 assert_eq!(tracker.total_object_count(), 1);
889 assert_eq!(tracker.current_object_count(), 1);
890
891 let det2 = Detection::from_slice(&[1000.0, 2000.0], 1, 2).unwrap();
893 tracker.update(vec![det2], 1, None);
894
895 assert_eq!(tracker.total_object_count(), 2);
896 }
899
900 #[test]
904 #[should_panic(expected = "Unknown distance function")]
905 fn test_tracker_params_bad_distance() {
906 let config = TrackerConfig::from_distance_name("_bad_distance", 10.0);
907 Tracker::new(config).unwrap();
909 }
910
911 #[test]
914 fn test_tracker_simple_hit_counter_dynamics() {
915 let delay = 1;
916 let counter_max = delay + 2; let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
919 config.hit_counter_max = counter_max;
920 config.initialization_delay = delay;
921
922 let mut tracker = Tracker::new(config).unwrap();
923
924 let det = Detection::from_slice(&[1.0, 1.0], 1, 2).unwrap();
925
926 for _age in 0..delay {
928 let active = tracker.update(vec![det.clone()], 1, None);
929 assert_eq!(active.len(), 0, "Expected 0 active objects during delay");
930 }
931
932 let active = tracker.update(vec![det.clone()], 1, None);
934 assert_eq!(active.len(), 1, "Expected 1 active object after delay");
935
936 for _ in 0..5 {
938 let active = tracker.update(vec![det.clone()], 1, None);
939 assert_eq!(active.len(), 1);
940 assert!(
941 active[0].hit_counter <= counter_max,
942 "Hit counter should be capped at {}, got {}",
943 counter_max,
944 active[0].hit_counter
945 );
946 }
947
948 let mut prev_counter = counter_max;
950 for _ in 0..counter_max {
951 let active = tracker.update(vec![], 1, None);
952 if active.len() == 1 {
953 assert!(
954 active[0].hit_counter < prev_counter,
955 "Hit counter should decrease without detections"
956 );
957 prev_counter = active[0].hit_counter;
958 }
959 }
960
961 let active = tracker.update(vec![], 1, None);
963 assert_eq!(
964 active.len(),
965 0,
966 "Object should disappear when hit_counter reaches 0"
967 );
968 }
969
970 #[test]
973 fn test_tracker_moving_object() {
974 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
975 config.hit_counter_max = 5;
976 config.initialization_delay = 0; let mut tracker = Tracker::new(config).unwrap();
979
980 let active = tracker.update(
983 vec![Detection::from_slice(&[1.0, 1.0], 1, 2).unwrap()],
984 1,
985 None,
986 );
987 assert_eq!(
988 active.len(),
989 1,
990 "First detection should create active object"
991 );
992
993 tracker.update(
994 vec![Detection::from_slice(&[1.0, 2.0], 1, 2).unwrap()],
995 1,
996 None,
997 );
998 tracker.update(
999 vec![Detection::from_slice(&[1.0, 3.0], 1, 2).unwrap()],
1000 1,
1001 None,
1002 );
1003 let active = tracker.update(
1004 vec![Detection::from_slice(&[1.0, 4.0], 1, 2).unwrap()],
1005 1,
1006 None,
1007 );
1008
1009 assert_eq!(active.len(), 1, "Expected 1 active object");
1010
1011 let estimate = &active[0].estimate;
1014 assert!(
1015 (estimate[(0, 0)] - 1.0).abs() < 0.5,
1016 "X should be close to 1.0, got {}",
1017 estimate[(0, 0)]
1018 );
1019 assert!(
1020 estimate[(0, 1)] > 3.0 && estimate[(0, 1)] <= 4.5,
1021 "Y should be between 3 and 4.5, got {}",
1022 estimate[(0, 1)]
1023 );
1024 }
1025
1026 #[test]
1029 fn test_tracker_distance_threshold() {
1030 let mut config = TrackerConfig::from_distance_name("euclidean", 0.5); config.hit_counter_max = 5;
1032 config.initialization_delay = 0; let mut tracker = Tracker::new(config).unwrap();
1035
1036 let active = tracker.update(
1038 vec![Detection::from_slice(&[1.0, 1.0], 1, 2).unwrap()],
1039 1,
1040 None,
1041 );
1042 assert_eq!(active.len(), 1, "First detection should create object");
1043
1044 let active = tracker.update(
1048 vec![Detection::from_slice(&[1.0, 2.0], 1, 2).unwrap()],
1049 1,
1050 None,
1051 );
1052 assert!(active.len() >= 1, "Should have at least 1 object");
1054
1055 let active = tracker.update(
1057 vec![Detection::from_slice(&[1.0, 2.3], 1, 2).unwrap()],
1058 1,
1059 None,
1060 );
1061 assert!(
1062 active.len() >= 1,
1063 "Expected match when distance < threshold"
1064 );
1065 }
1066
1067 #[test]
1070 fn test_tracker_1d_points() {
1071 let mut config = TrackerConfig::from_distance_name("euclidean", 100.0);
1072 config.hit_counter_max = 5;
1073 config.initialization_delay = 0;
1074
1075 let mut tracker = Tracker::new(config).unwrap();
1076
1077 let det = Detection::from_slice(&[1.0, 1.0], 1, 2).unwrap();
1079
1080 assert_eq!(det.points.nrows(), 1);
1082 assert_eq!(det.points.ncols(), 2);
1083
1084 let active = tracker.update(vec![det], 1, None);
1085 assert_eq!(active.len(), 1, "Expected 1 active object");
1086
1087 assert_eq!(active[0].estimate.nrows(), 1);
1089 assert_eq!(active[0].estimate.ncols(), 2);
1090 }
1091
1092 #[test]
1095 fn test_tracker_count_comprehensive() {
1096 let delay = 1;
1097 let counter_max = delay + 2; let mut config = TrackerConfig::from_distance_name("euclidean", 1.0);
1100 config.hit_counter_max = counter_max;
1101 config.initialization_delay = delay;
1102
1103 let mut tracker = Tracker::new(config).unwrap();
1104
1105 let det1 = Detection::from_slice(&[1.0, 1.0], 1, 2).unwrap();
1106
1107 for _ in 0..delay {
1109 let active = tracker.update(vec![det1.clone()], 1, None);
1110 assert_eq!(active.len(), 0);
1111 assert_eq!(
1112 tracker.total_object_count(),
1113 0,
1114 "Total count should be 0 during init"
1115 );
1116 assert_eq!(
1117 tracker.current_object_count(),
1118 0,
1119 "Current count should be 0 during init"
1120 );
1121 }
1122
1123 let active = tracker.update(vec![det1.clone()], 1, None);
1125 assert_eq!(active.len(), 1);
1126 assert_eq!(tracker.total_object_count(), 1);
1127 assert_eq!(tracker.current_object_count(), 1);
1128
1129 for _ in 0..counter_max - 1 {
1131 let active = tracker.update(vec![], 1, None);
1132 if !active.is_empty() {
1133 assert_eq!(tracker.total_object_count(), 1);
1134 assert_eq!(tracker.current_object_count(), 1);
1135 }
1136 }
1137
1138 let active = tracker.update(vec![], 1, None);
1140 assert_eq!(active.len(), 0);
1141 assert_eq!(
1142 tracker.total_object_count(),
1143 1,
1144 "Total should stay 1 after object dies"
1145 );
1146 assert_eq!(
1147 tracker.current_object_count(),
1148 0,
1149 "Current should be 0 after object dies"
1150 );
1151
1152 let det2 = Detection::from_slice(&[100.0, 100.0], 1, 2).unwrap();
1154 let det3 = Detection::from_slice(&[200.0, 200.0], 1, 2).unwrap();
1155
1156 for _ in 0..delay {
1158 let active = tracker.update(vec![det2.clone(), det3.clone()], 1, None);
1159 assert_eq!(active.len(), 0);
1160 assert_eq!(
1161 tracker.total_object_count(),
1162 1,
1163 "Total should still be 1 during init"
1164 );
1165 assert_eq!(tracker.current_object_count(), 0);
1166 }
1167
1168 let active = tracker.update(vec![det2, det3], 1, None);
1170 assert_eq!(active.len(), 2);
1171 assert_eq!(
1172 tracker.total_object_count(),
1173 3,
1174 "Total should be 3 (1 dead + 2 new)"
1175 );
1176 assert_eq!(tracker.current_object_count(), 2);
1177 }
1178
1179 #[test]
1182 fn test_multiple_trackers_independent() {
1183 let mut config1 = TrackerConfig::from_distance_name("euclidean", 1.0);
1184 config1.hit_counter_max = 2;
1185 config1.initialization_delay = 0;
1186
1187 let mut config2 = TrackerConfig::from_distance_name("euclidean", 1.0);
1188 config2.hit_counter_max = 2;
1189 config2.initialization_delay = 0;
1190
1191 let mut tracker1 = Tracker::new(config1).unwrap();
1192 let mut tracker2 = Tracker::new(config2).unwrap();
1193
1194 let det1 = Detection::from_slice(&[1.0, 1.0], 1, 2).unwrap();
1195 let det2 = Detection::from_slice(&[2.0, 2.0], 1, 2).unwrap();
1196
1197 let active1 = tracker1.update(vec![det1], 1, None);
1198 assert_eq!(active1.len(), 1);
1199
1200 let active2 = tracker2.update(vec![det2], 1, None);
1201 assert_eq!(active2.len(), 1);
1202
1203 assert_eq!(tracker1.total_object_count(), 1);
1205 assert_eq!(tracker2.total_object_count(), 1);
1206
1207 }
1210}