1use crate::{arg_max, BBoxTypeTrait, BoundingBox, DetectBox};
21use ndarray::{
22 parallel::prelude::{IntoParallelIterator, ParallelIterator as _},
23 Array1, ArrayView2, Zip,
24};
25use num_traits::{AsPrimitive, Float};
26use rayon::slice::ParallelSliceMut;
27
28pub fn postprocess_boxes_float<
32 B: BBoxTypeTrait,
33 BOX: Float + AsPrimitive<f32> + Send + Sync,
34 SCORE: Float + AsPrimitive<f32> + Send + Sync,
35>(
36 threshold: SCORE,
37 boxes: ArrayView2<BOX>,
38 scores: ArrayView2<SCORE>,
39) -> Vec<DetectBox> {
40 assert_eq!(scores.dim().0, boxes.dim().0);
41 assert_eq!(boxes.dim().1, 4);
42 Zip::from(scores.rows())
43 .and(boxes.rows())
44 .into_par_iter()
45 .filter_map(|(score, bbox)| {
46 let (score_, label) = arg_max(score);
47 if score_ < threshold {
48 return None;
49 }
50
51 let bbox = B::ndarray_to_xyxy_float(bbox);
52 Some(DetectBox {
53 label,
54 score: score_.as_(),
55 bbox: bbox.into(),
56 })
57 })
58 .collect()
59}
60
61pub fn postprocess_boxes_index_float<
68 B: BBoxTypeTrait,
69 BOX: Float + AsPrimitive<f32> + Send + Sync,
70 SCORE: Float + AsPrimitive<f32> + Send + Sync,
71>(
72 threshold: SCORE,
73 boxes: ArrayView2<BOX>,
74 scores: ArrayView2<SCORE>,
75) -> Vec<(DetectBox, usize)> {
76 assert_eq!(scores.dim().0, boxes.dim().0);
77 assert_eq!(boxes.dim().1, 4);
78 let indices: Array1<usize> = (0..boxes.dim().0).collect();
79 Zip::from(scores.rows())
80 .and(boxes.rows())
81 .and(&indices)
82 .into_par_iter()
83 .filter_map(|(score, bbox, i)| {
84 let (score_, label) = arg_max(score);
85 if score_ < threshold {
86 return None;
87 }
88
89 let bbox = B::ndarray_to_xyxy_float(bbox);
90 Some((
91 DetectBox {
92 label,
93 score: score_.as_(),
94 bbox: bbox.into(),
95 },
96 *i,
97 ))
98 })
99 .collect()
100}
101
102pub fn postprocess_boxes_multilabel_index_float<
118 B: BBoxTypeTrait,
119 BOX: Float + AsPrimitive<f32> + Send + Sync,
120 SCORE: Float + AsPrimitive<f32> + Send + Sync,
121>(
122 threshold: SCORE,
123 boxes: ArrayView2<BOX>,
124 scores: ArrayView2<SCORE>,
125) -> Vec<(DetectBox, usize)> {
126 assert_eq!(scores.dim().0, boxes.dim().0);
127 assert_eq!(boxes.dim().1, 4);
128 let n = boxes.dim().0;
129 let indices: Array1<usize> = (0..n).collect();
130 Zip::from(scores.rows())
131 .and(boxes.rows())
132 .and(&indices)
133 .into_par_iter()
134 .flat_map(|(score_row, bbox_row, &anchor_idx)| {
135 let bbox = B::ndarray_to_xyxy_float(bbox_row);
137 let bbox: crate::BoundingBox = bbox.into();
138 score_row
139 .iter()
140 .enumerate()
141 .filter(|(_, &s)| s >= threshold)
142 .map(move |(c, &s)| {
143 (
144 DetectBox {
145 label: c,
146 score: s.as_(),
147 bbox,
148 },
149 anchor_idx,
150 )
151 })
152 .collect::<Vec<_>>()
153 })
154 .collect()
155}
156
157pub fn postprocess_boxes_multilabel_float<
163 B: BBoxTypeTrait,
164 BOX: Float + AsPrimitive<f32> + Send + Sync,
165 SCORE: Float + AsPrimitive<f32> + Send + Sync,
166>(
167 threshold: SCORE,
168 boxes: ArrayView2<BOX>,
169 scores: ArrayView2<SCORE>,
170) -> crate::DecoderResult<Vec<DetectBox>> {
171 crate::byte::check_candidate_shapes(boxes.dim(), scores.dim())?;
172 Ok(
173 postprocess_boxes_multilabel_index_float::<B, _, _>(threshold, boxes, scores)
174 .into_iter()
175 .map(|(b, _)| b)
176 .collect(),
177 )
178}
179
180#[must_use]
188pub fn nms_float(iou: f32, max_det: Option<usize>, mut boxes: Vec<DetectBox>) -> Vec<DetectBox> {
189 boxes.par_sort_by(|a, b| b.score.total_cmp(&a.score));
192
193 if iou >= 1.0 {
196 return match max_det {
197 Some(n) => {
198 boxes.truncate(n);
199 boxes
200 }
201 None => boxes,
202 };
203 }
204
205 let cap = max_det.unwrap_or(usize::MAX);
206 let mut survivors: usize = 0;
207
208 for i in 0..boxes.len() {
210 if boxes[i].score < 0.0 {
211 continue;
213 }
214 for j in (i + 1)..boxes.len() {
215 if boxes[j].score < 0.0 {
218 continue;
220 }
221 if jaccard(&boxes[j].bbox, &boxes[i].bbox, iou) {
222 boxes[j].score = -1.0;
224 }
225 }
226
227 survivors += 1;
232 if survivors >= cap {
233 break;
234 }
235 }
236 boxes
239 .into_iter()
240 .filter(|b| b.score >= 0.0)
241 .take(cap)
242 .collect()
243}
244
245#[must_use]
251pub fn nms_extra_float<E: Send + Sync>(
252 iou: f32,
253 max_det: Option<usize>,
254 mut boxes: Vec<(DetectBox, E)>,
255) -> Vec<(DetectBox, E)> {
256 boxes.par_sort_by(|a, b| b.0.score.total_cmp(&a.0.score));
259
260 if iou >= 1.0 {
263 return match max_det {
264 Some(n) => {
265 boxes.truncate(n);
266 boxes
267 }
268 None => boxes,
269 };
270 }
271
272 let cap = max_det.unwrap_or(usize::MAX);
273 let mut survivors: usize = 0;
274
275 for i in 0..boxes.len() {
277 if boxes[i].0.score < 0.0 {
278 continue;
280 }
281 for j in (i + 1)..boxes.len() {
282 if boxes[j].0.score < 0.0 {
285 continue;
287 }
288 if jaccard(&boxes[j].0.bbox, &boxes[i].0.bbox, iou) {
289 boxes[j].0.score = -1.0;
291 }
292 }
293 survivors += 1;
294 if survivors >= cap {
295 break;
296 }
297 }
298
299 boxes
302 .into_iter()
303 .filter(|b| b.0.score >= 0.0)
304 .take(cap)
305 .collect()
306}
307
308#[must_use]
334pub fn nms_class_aware_float(
335 iou: f32,
336 max_det: Option<usize>,
337 mut boxes: Vec<DetectBox>,
338) -> Vec<DetectBox> {
339 boxes.par_sort_by(|a, b| b.score.total_cmp(&a.score));
340
341 if iou >= 1.0 {
342 return match max_det {
343 Some(n) => {
344 boxes.truncate(n);
345 boxes
346 }
347 None => boxes,
348 };
349 }
350
351 let cap = max_det.unwrap_or(usize::MAX);
352 let mut survivors: usize = 0;
353
354 for i in 0..boxes.len() {
355 if boxes[i].score < 0.0 {
356 continue;
357 }
358 for j in (i + 1)..boxes.len() {
359 if boxes[j].score < 0.0 {
360 continue;
361 }
362 if boxes[j].label == boxes[i].label && jaccard(&boxes[j].bbox, &boxes[i].bbox, iou) {
364 boxes[j].score = -1.0;
365 }
366 }
367 survivors += 1;
368 if survivors >= cap {
369 break;
370 }
371 }
372 boxes
373 .into_iter()
374 .filter(|b| b.score >= 0.0)
375 .take(cap)
376 .collect()
377}
378
379#[must_use]
384pub fn nms_extra_class_aware_float<E: Send + Sync>(
385 iou: f32,
386 max_det: Option<usize>,
387 mut boxes: Vec<(DetectBox, E)>,
388) -> Vec<(DetectBox, E)> {
389 boxes.par_sort_by(|a, b| b.0.score.total_cmp(&a.0.score));
390
391 if iou >= 1.0 {
394 return match max_det {
395 Some(n) => {
396 boxes.truncate(n);
397 boxes
398 }
399 None => boxes,
400 };
401 }
402
403 let cap = max_det.unwrap_or(usize::MAX);
404 let mut survivors: usize = 0;
405
406 for i in 0..boxes.len() {
407 if boxes[i].0.score < 0.0 {
408 continue;
409 }
410 for j in (i + 1)..boxes.len() {
411 if boxes[j].0.score < 0.0 {
412 continue;
413 }
414 if boxes[j].0.label == boxes[i].0.label
416 && jaccard(&boxes[j].0.bbox, &boxes[i].0.bbox, iou)
417 {
418 boxes[j].0.score = -1.0;
419 }
420 }
421 survivors += 1;
422 if survivors >= cap {
423 break;
424 }
425 }
426 boxes
427 .into_iter()
428 .filter(|b| b.0.score >= 0.0)
429 .take(cap)
430 .collect()
431}
432
433#[must_use]
436#[inline]
437pub fn intersection_area(a: &BoundingBox, b: &BoundingBox) -> f32 {
438 let left = a.xmin.max(b.xmin);
439 let top = a.ymin.max(b.ymin);
440 let right = a.xmax.min(b.xmax);
441 let bottom = a.ymax.min(b.ymax);
442 (right - left).max(0.0) * (bottom - top).max(0.0)
443}
444
445#[must_use]
447#[inline]
448pub fn box_area(b: &BoundingBox) -> f32 {
449 (b.xmax - b.xmin).max(0.0) * (b.ymax - b.ymin).max(0.0)
450}
451
452#[must_use]
455#[inline]
456pub fn iou_value(a: &BoundingBox, b: &BoundingBox) -> f32 {
457 let inter = intersection_area(a, b);
458 let union = (box_area(a) + box_area(b) - inter).max(1e-9);
459 inter / union
460}
461
462#[must_use]
466#[inline]
467pub fn ios_value(a: &BoundingBox, b: &BoundingBox) -> f32 {
468 let inter = intersection_area(a, b);
469 let denom = box_area(a).min(box_area(b)).max(1e-9);
470 inter / denom
471}
472
473pub fn jaccard(a: &BoundingBox, b: &BoundingBox, iou: f32) -> bool {
491 let left = a.xmin.max(b.xmin);
492 let top = a.ymin.max(b.ymin);
493 let right = a.xmax.min(b.xmax);
494 let bottom = a.ymax.min(b.ymax);
495
496 let intersection = (right - left).max(0.0) * (bottom - top).max(0.0);
497 let area_a = (a.xmax - a.xmin) * (a.ymax - a.ymin);
498 let area_b = (b.xmax - b.xmin) * (b.ymax - b.ymin);
499
500 let union = area_a + area_b - intersection;
502
503 intersection > iou * union
504}
505
506#[inline]
515pub fn jaccard_batch4(a: &BoundingBox, boxes: &[BoundingBox; 4], iou: f32) -> [bool; 4] {
516 #[cfg(target_arch = "aarch64")]
517 {
518 unsafe { jaccard_batch4_neon(a, boxes, iou) }
520 }
521 #[cfg(not(target_arch = "aarch64"))]
522 {
523 [
524 jaccard(a, &boxes[0], iou),
525 jaccard(a, &boxes[1], iou),
526 jaccard(a, &boxes[2], iou),
527 jaccard(a, &boxes[3], iou),
528 ]
529 }
530}
531
532#[cfg(target_arch = "aarch64")]
538#[target_feature(enable = "neon")]
539unsafe fn jaccard_batch4_neon(a: &BoundingBox, boxes: &[BoundingBox; 4], iou: f32) -> [bool; 4] {
540 unsafe {
541 use std::arch::aarch64::*; let zero = vdupq_n_f32(0.0);
544 let iou_v = vdupq_n_f32(iou);
545
546 let a_xmin = vdupq_n_f32(a.xmin);
548 let a_ymin = vdupq_n_f32(a.ymin);
549 let a_xmax = vdupq_n_f32(a.xmax);
550 let a_ymax = vdupq_n_f32(a.ymax);
551 let area_a = vmulq_f32(vsubq_f32(a_xmax, a_xmin), vsubq_f32(a_ymax, a_ymin));
552
553 let b0 = vld1q_f32(&boxes[0].xmin as *const f32);
555 let b1 = vld1q_f32(&boxes[1].xmin as *const f32);
556 let b2 = vld1q_f32(&boxes[2].xmin as *const f32);
557 let b3 = vld1q_f32(&boxes[3].xmin as *const f32);
558
559 let t01_lo = vtrn1q_f32(b0, b1); let t01_hi = vtrn2q_f32(b0, b1); let t23_lo = vtrn1q_f32(b2, b3);
563 let t23_hi = vtrn2q_f32(b2, b3);
564
565 let b_xmin = vreinterpretq_f32_f64(vtrn1q_f64(
566 vreinterpretq_f64_f32(t01_lo),
567 vreinterpretq_f64_f32(t23_lo),
568 ));
569 let b_ymin = vreinterpretq_f32_f64(vtrn1q_f64(
570 vreinterpretq_f64_f32(t01_hi),
571 vreinterpretq_f64_f32(t23_hi),
572 ));
573 let b_xmax = vreinterpretq_f32_f64(vtrn2q_f64(
574 vreinterpretq_f64_f32(t01_lo),
575 vreinterpretq_f64_f32(t23_lo),
576 ));
577 let b_ymax = vreinterpretq_f32_f64(vtrn2q_f64(
578 vreinterpretq_f64_f32(t01_hi),
579 vreinterpretq_f64_f32(t23_hi),
580 ));
581
582 let left = vmaxq_f32(a_xmin, b_xmin);
584 let top = vmaxq_f32(a_ymin, b_ymin);
585 let right = vminq_f32(a_xmax, b_xmax);
586 let bottom = vminq_f32(a_ymax, b_ymax);
587 let w = vmaxq_f32(vsubq_f32(right, left), zero);
588 let h = vmaxq_f32(vsubq_f32(bottom, top), zero);
589 let intersection = vmulq_f32(w, h);
590
591 let area_b = vmulq_f32(vsubq_f32(b_xmax, b_xmin), vsubq_f32(b_ymax, b_ymin));
593
594 let union = vsubq_f32(vaddq_f32(area_a, area_b), intersection);
596
597 let iou_union = vmulq_f32(iou_v, union);
599 let mask = vcgtq_f32(intersection, iou_union);
600
601 [
603 vgetq_lane_u32(mask, 0) != 0,
604 vgetq_lane_u32(mask, 1) != 0,
605 vgetq_lane_u32(mask, 2) != 0,
606 vgetq_lane_u32(mask, 3) != 0,
607 ]
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use crate::BoundingBox;
615
616 #[test]
617 fn multilabel_float_drops_indices_only() {
618 let boxes = ndarray::arr2(&[[0.1f32, 0.1, 0.2, 0.2], [0.3, 0.3, 0.4, 0.4]]);
619 let scores = ndarray::arr2(&[[0.6f32, 0.1, 0.7], [0.05, 0.9, 0.05]]);
620 let with_idx = postprocess_boxes_multilabel_index_float::<crate::XYXY, _, _>(
621 0.5,
622 boxes.view(),
623 scores.view(),
624 );
625 let without = postprocess_boxes_multilabel_float::<crate::XYXY, _, _>(
626 0.5,
627 boxes.view(),
628 scores.view(),
629 )
630 .unwrap();
631 let stripped: Vec<_> = with_idx.into_iter().map(|(b, _)| b).collect();
632 assert_eq!(stripped, without);
633 assert_eq!(without.len(), 3);
634 }
635
636 #[test]
637 fn multilabel_float_rejects_non_four_column_boxes() {
638 let boxes = ndarray::arr2(&[[0.1f32, 0.1, 0.2, 0.2, 0.0]]);
639 let scores = ndarray::arr2(&[[0.6f32]]);
640 let r = postprocess_boxes_multilabel_float::<crate::XYXY, _, _>(
641 0.5,
642 boxes.view(),
643 scores.view(),
644 );
645 assert!(matches!(r, Err(crate::DecoderError::InvalidShape(_))));
646 }
647
648 fn make_nms_boxes_float(n: usize) -> Vec<DetectBox> {
650 (0..n)
651 .map(|i| DetectBox {
652 bbox: BoundingBox {
653 xmin: i as f32 * 100.0,
654 ymin: 0.0,
655 xmax: i as f32 * 100.0 + 10.0,
656 ymax: 10.0,
657 },
658 label: 0,
659 score: 1.0 - i as f32 * 0.01,
660 })
661 .collect()
662 }
663
664 fn overlapping_pair(score: f32) -> Vec<DetectBox> {
669 vec![
670 DetectBox {
671 bbox: BoundingBox {
672 xmin: 0.0,
673 ymin: 0.0,
674 xmax: 10.0,
675 ymax: 10.0,
676 },
677 label: 0,
678 score,
679 },
680 DetectBox {
681 bbox: BoundingBox {
682 xmin: 1.0,
683 ymin: 1.0,
684 xmax: 11.0,
685 ymax: 11.0,
686 },
687 label: 0,
688 score,
689 },
690 ]
691 }
692
693 #[test]
697 fn nms_float_suppresses_overlapping_zero_score_boxes() {
698 let result = nms_float(0.5, None, overlapping_pair(0.0));
699 assert_eq!(result.len(), 1, "{result:?}");
700 assert_eq!(result[0].score, 0.0);
701 }
702
703 fn suppression_chain() -> Vec<DetectBox> {
714 let mk = |xmin: f32, xmax: f32, score: f32| DetectBox {
715 bbox: BoundingBox {
716 xmin,
717 ymin: 0.0,
718 xmax,
719 ymax: 10.0,
720 },
721 label: 0,
722 score,
723 };
724 vec![mk(0.0, 10.0, 0.9), mk(2.0, 12.0, 0.8), mk(4.0, 14.0, 0.7)]
725 }
726
727 #[test]
728 fn nms_float_does_not_suppress_on_behalf_of_a_suppressed_box() {
729 let result = nms_float(0.5, None, suppression_chain());
730 assert_eq!(result.len(), 2, "{result:?}");
731 assert_eq!(result[0].score, 0.9);
732 assert_eq!(result[1].score, 0.7);
733 }
734
735 #[test]
736 fn nms_extra_float_suppresses_overlapping_zero_score_boxes() {
737 let boxes = overlapping_pair(0.0)
738 .into_iter()
739 .enumerate()
740 .map(|(i, b)| (b, i))
741 .collect();
742 let result = nms_extra_float(0.5, None, boxes);
743 assert_eq!(result.len(), 1, "{result:?}");
744 assert!(result[0].1 == 0 || result[0].1 == 1, "{result:?}");
748 }
749
750 #[test]
751 fn nms_extra_float_does_not_suppress_on_behalf_of_a_suppressed_box() {
752 let boxes = suppression_chain()
753 .into_iter()
754 .enumerate()
755 .map(|(i, b)| (b, i))
756 .collect();
757 let result = nms_extra_float(0.5, None, boxes);
758 assert_eq!(result.len(), 2, "{result:?}");
759 assert_eq!(result[0].1, 0);
760 assert_eq!(result[1].1, 2);
761 }
762
763 #[test]
767 fn nms_class_aware_float_suppresses_same_class_without_a_cap() {
768 let mut boxes = overlapping_pair(0.9);
769 boxes[1].score = 0.8;
770 let result = nms_class_aware_float(0.5, None, boxes);
771 assert_eq!(result.len(), 1, "{result:?}");
772 assert_eq!(result[0].score, 0.9);
773 }
774
775 #[test]
778 fn nms_class_aware_float_keeps_overlapping_boxes_of_other_classes() {
779 let mut boxes = overlapping_pair(0.9);
780 boxes[1].score = 0.8;
781 boxes[1].label = 1;
782 let result = nms_class_aware_float(0.5, None, boxes);
783 assert_eq!(result.len(), 2, "{result:?}");
784 }
785
786 #[test]
787 fn nms_class_aware_float_suppresses_overlapping_zero_score_boxes() {
788 let result = nms_class_aware_float(0.5, None, overlapping_pair(0.0));
789 assert_eq!(result.len(), 1, "{result:?}");
790 }
791
792 #[test]
793 fn nms_class_aware_float_does_not_suppress_on_behalf_of_a_suppressed_box() {
794 let result = nms_class_aware_float(0.5, None, suppression_chain());
795 assert_eq!(result.len(), 2, "{result:?}");
796 assert_eq!(result[1].score, 0.7);
797 }
798
799 fn isolated_then_overlapping_pair() -> Vec<DetectBox> {
805 let mk = |xmin: f32, xmax: f32, score: f32| DetectBox {
806 bbox: BoundingBox {
807 xmin,
808 ymin: 0.0,
809 xmax,
810 ymax: 10.0,
811 },
812 label: 0,
813 score,
814 };
815 vec![
816 mk(100.0, 110.0, 0.9),
817 mk(0.0, 10.0, 0.8),
818 mk(1.0, 11.0, 0.7),
819 ]
820 }
821
822 #[test]
823 fn nms_class_aware_float_keeps_suppressing_after_the_first_survivor() {
824 let result = nms_class_aware_float(0.5, None, isolated_then_overlapping_pair());
825 assert_eq!(result.len(), 2, "{result:?}");
826 assert_eq!(result[0].score, 0.9);
827 assert_eq!(result[1].score, 0.8);
828 }
829
830 fn with_index(boxes: Vec<DetectBox>) -> Vec<(DetectBox, usize)> {
831 boxes.into_iter().enumerate().map(|(i, b)| (b, i)).collect()
832 }
833
834 #[test]
835 fn nms_extra_class_aware_float_keeps_suppressing_after_the_first_survivor() {
836 let result =
837 nms_extra_class_aware_float(0.5, None, with_index(isolated_then_overlapping_pair()));
838 assert_eq!(result.len(), 2, "{result:?}");
839 assert_eq!(result[1].1, 1);
840 }
841
842 #[test]
843 fn nms_extra_class_aware_float_suppresses_same_class_without_a_cap() {
844 let mut boxes = overlapping_pair(0.9);
845 boxes[1].score = 0.8;
846 let result = nms_extra_class_aware_float(0.5, None, with_index(boxes));
847 assert_eq!(result.len(), 1, "{result:?}");
848 assert_eq!(result[0].0.score, 0.9);
849 assert_eq!(result[0].1, 0);
850 }
851
852 #[test]
853 fn nms_extra_class_aware_float_keeps_overlapping_boxes_of_other_classes() {
854 let mut boxes = overlapping_pair(0.9);
855 boxes[1].score = 0.8;
856 boxes[1].label = 1;
857 let result = nms_extra_class_aware_float(0.5, None, with_index(boxes));
858 assert_eq!(result.len(), 2, "{result:?}");
859 }
860
861 #[test]
862 fn nms_extra_class_aware_float_suppresses_overlapping_zero_score_boxes() {
863 let result = nms_extra_class_aware_float(0.5, None, with_index(overlapping_pair(0.0)));
864 assert_eq!(result.len(), 1, "{result:?}");
865 }
866
867 #[test]
868 fn nms_extra_class_aware_float_does_not_suppress_on_behalf_of_a_suppressed_box() {
869 let result = nms_extra_class_aware_float(0.5, None, with_index(suppression_chain()));
870 assert_eq!(result.len(), 2, "{result:?}");
871 assert_eq!(result[1].1, 2);
872 }
873
874 #[test]
876 fn postprocess_boxes_float_keeps_a_score_equal_to_the_threshold() {
877 let boxes = ndarray::array![[0.0_f32, 0.0, 10.0, 10.0], [0.0, 0.0, 10.0, 10.0]];
878 let scores = ndarray::array![[0.5_f32], [0.25]];
879
880 let kept = postprocess_boxes_float::<crate::XYXY, _, _>(0.5, boxes.view(), scores.view());
881 assert_eq!(kept.len(), 1, "{kept:?}");
882 assert_eq!(kept[0].score, 0.5);
883
884 let kept =
885 postprocess_boxes_index_float::<crate::XYXY, _, _>(0.5, boxes.view(), scores.view());
886 assert_eq!(kept.len(), 1, "{kept:?}");
887 assert_eq!(kept[0].1, 0);
888 }
889
890 #[test]
891 fn nms_float_max_det_matches_full_truncated() {
892 let boxes = make_nms_boxes_float(20);
893 let n = 5;
894 let full = nms_float(0.5, None, boxes.clone());
895 let capped = nms_float(0.5, Some(n), boxes);
896 assert_eq!(capped.len(), n);
897 for (f, c) in full[..n].iter().zip(capped.iter()) {
898 assert_eq!(f.bbox, c.bbox);
899 assert_eq!(f.score, c.score);
900 }
901 }
902
903 #[test]
904 fn nms_float_max_det_zero_returns_empty() {
905 let boxes = make_nms_boxes_float(10);
906 let result = nms_float(0.5, Some(0), boxes);
907 assert!(result.is_empty());
908 }
909
910 #[test]
911 fn nms_float_max_det_iou_ge_1_returns_sorted_truncated() {
912 let boxes = make_nms_boxes_float(10);
913 let result = nms_float(1.0, Some(3), boxes);
914 assert_eq!(result.len(), 3);
915 assert!(result[0].score >= result[1].score);
916 assert!(result[1].score >= result[2].score);
917 }
918
919 #[test]
920 fn nms_float_max_det_larger_than_input() {
921 let boxes = make_nms_boxes_float(5);
922 let full = nms_float(0.5, None, boxes.clone());
923 let capped = nms_float(0.5, Some(100), boxes);
924 assert_eq!(full.len(), capped.len());
925 }
926
927 #[test]
931 fn metric_values_match_modelpack_match() {
932 let a = BoundingBox::new(100.0, 100.0, 400.0, 300.0);
933 let b = BoundingBox::new(350.0, 100.0, 400.0, 300.0);
934 assert!((intersection_area(&a, &b) - 10000.0).abs() < 1e-3);
935 assert!((box_area(&a) - 60000.0).abs() < 1e-3);
936 assert!((box_area(&b) - 10000.0).abs() < 1e-3);
937 assert!((ios_value(&a, &b) - 1.0).abs() < 1e-6);
938 assert!((iou_value(&a, &b) - (1.0 / 6.0)).abs() < 1e-6);
939 }
940
941 #[test]
942 fn metric_values_disjoint_are_zero() {
943 let a = BoundingBox::new(0.0, 0.0, 10.0, 10.0);
944 let b = BoundingBox::new(20.0, 20.0, 30.0, 30.0);
945 assert_eq!(intersection_area(&a, &b), 0.0);
946 assert_eq!(ios_value(&a, &b), 0.0);
947 assert_eq!(iou_value(&a, &b), 0.0);
948 }
949
950 #[test]
951 fn metric_values_zero_area_box_no_panic() {
952 let degenerate = BoundingBox::new(10.0, 10.0, 10.0, 20.0);
954 let other = BoundingBox::new(0.0, 0.0, 30.0, 30.0);
955 assert_eq!(box_area(°enerate), 0.0);
956 assert!(ios_value(°enerate, &other).is_finite());
957 assert!(iou_value(°enerate, &other).is_finite());
958 }
959
960 #[test]
961 fn ios_high_where_iou_low_for_contained_box() {
962 let big = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
964 let small = BoundingBox::new(10.0, 10.0, 20.0, 20.0);
965 assert!((ios_value(&big, &small) - 1.0).abs() < 1e-6); assert!(iou_value(&big, &small) < 0.05); }
968
969 #[test]
970 fn jaccard_batch4_matches_scalar() {
971 let a = BoundingBox::new(0.0, 0.0, 10.0, 10.0);
972 let boxes = [
973 BoundingBox::new(5.0, 5.0, 15.0, 15.0), BoundingBox::new(20.0, 20.0, 30.0, 30.0), BoundingBox::new(0.0, 0.0, 10.0, 10.0), BoundingBox::new(8.0, 8.0, 18.0, 18.0), ];
978 let iou_threshold = 0.1;
979 let batch = jaccard_batch4(&a, &boxes, iou_threshold);
980 for (i, b) in boxes.iter().enumerate() {
981 let scalar = jaccard(&a, b, iou_threshold);
982 assert_eq!(
983 batch[i], scalar,
984 "batch4 mismatch at {i}: batch={} scalar={}",
985 batch[i], scalar
986 );
987 }
988 }
989}