1use crate::{arg_max, BBoxTypeTrait, BoundingBox, DetectBox};
21use ndarray::{ArrayView1, ArrayView2};
22use num_traits::{AsPrimitive, Float};
23use rayon::iter::{IntoParallelIterator, ParallelIterator};
24use rayon::slice::ParallelSliceMut;
25
26const PAR_MIN_SCORES: usize = 1 << 16;
30
31fn filter_rows<BOX, SCORE, T, F>(boxes: ArrayView2<BOX>, scores: ArrayView2<SCORE>, f: F) -> Vec<T>
34where
35 BOX: Sync,
36 SCORE: Sync,
37 T: Send,
38 F: Fn(usize, ArrayView1<SCORE>, ArrayView1<BOX>) -> Option<T> + Sync + Send,
39{
40 let (n, nc) = scores.dim();
41 let row = |i: usize| f(i, scores.row(i), boxes.row(i));
42 if n * nc < PAR_MIN_SCORES {
43 (0..n).filter_map(row).collect()
44 } else {
45 (0..n).into_par_iter().filter_map(row).collect()
46 }
47}
48
49pub fn postprocess_boxes_float<
53 B: BBoxTypeTrait,
54 BOX: Float + AsPrimitive<f32> + Send + Sync,
55 SCORE: Float + AsPrimitive<f32> + Send + Sync,
56>(
57 threshold: SCORE,
58 boxes: ArrayView2<BOX>,
59 scores: ArrayView2<SCORE>,
60) -> Vec<DetectBox> {
61 assert_eq!(scores.dim().0, boxes.dim().0);
62 assert_eq!(boxes.dim().1, 4);
63 filter_rows(boxes, scores, |_, score, bbox| {
64 let (score_, label) = arg_max(score);
65 if score_ < threshold {
66 return None;
67 }
68
69 let bbox = B::ndarray_to_xyxy_float(bbox);
70 Some(DetectBox {
71 label,
72 score: score_.as_(),
73 bbox: bbox.into(),
74 })
75 })
76}
77
78pub fn postprocess_boxes_index_float<
85 B: BBoxTypeTrait,
86 BOX: Float + AsPrimitive<f32> + Send + Sync,
87 SCORE: Float + AsPrimitive<f32> + Send + Sync,
88>(
89 threshold: SCORE,
90 boxes: ArrayView2<BOX>,
91 scores: ArrayView2<SCORE>,
92) -> Vec<(DetectBox, usize)> {
93 assert_eq!(scores.dim().0, boxes.dim().0);
94 assert_eq!(boxes.dim().1, 4);
95 filter_rows(boxes, scores, |i, score, bbox| {
96 let (score_, label) = arg_max(score);
97 if score_ < threshold {
98 return None;
99 }
100
101 let bbox = B::ndarray_to_xyxy_float(bbox);
102 Some((
103 DetectBox {
104 label,
105 score: score_.as_(),
106 bbox: bbox.into(),
107 },
108 i,
109 ))
110 })
111}
112
113pub fn postprocess_boxes_multilabel_index_float<
129 B: BBoxTypeTrait,
130 BOX: Float + AsPrimitive<f32> + Send + Sync,
131 SCORE: Float + AsPrimitive<f32> + Send + Sync,
132>(
133 threshold: SCORE,
134 boxes: ArrayView2<BOX>,
135 scores: ArrayView2<SCORE>,
136) -> Vec<(DetectBox, usize)> {
137 assert_eq!(scores.dim().0, boxes.dim().0);
138 assert_eq!(boxes.dim().1, 4);
139 filter_rows(boxes, scores, |anchor_idx, score_row, bbox_row| {
140 if !score_row.iter().any(|&s| s >= threshold) {
141 return None;
142 }
143 let bbox = B::ndarray_to_xyxy_float(bbox_row);
145 let bbox: crate::BoundingBox = bbox.into();
146 Some(
147 score_row
148 .iter()
149 .enumerate()
150 .filter(|(_, &s)| s >= threshold)
151 .map(|(c, &s)| {
152 (
153 DetectBox {
154 label: c,
155 score: s.as_(),
156 bbox,
157 },
158 anchor_idx,
159 )
160 })
161 .collect::<Vec<_>>(),
162 )
163 })
164 .into_iter()
165 .flatten()
166 .collect()
167}
168
169pub fn postprocess_boxes_multilabel_float<
175 B: BBoxTypeTrait,
176 BOX: Float + AsPrimitive<f32> + Send + Sync,
177 SCORE: Float + AsPrimitive<f32> + Send + Sync,
178>(
179 threshold: SCORE,
180 boxes: ArrayView2<BOX>,
181 scores: ArrayView2<SCORE>,
182) -> crate::DecoderResult<Vec<DetectBox>> {
183 crate::byte::check_candidate_shapes(boxes.dim(), scores.dim())?;
184 Ok(
185 postprocess_boxes_multilabel_index_float::<B, _, _>(threshold, boxes, scores)
186 .into_iter()
187 .map(|(b, _)| b)
188 .collect(),
189 )
190}
191
192#[must_use]
200pub fn nms_float(iou: f32, max_det: Option<usize>, mut boxes: Vec<DetectBox>) -> Vec<DetectBox> {
201 boxes.par_sort_by(|a, b| b.score.total_cmp(&a.score));
204
205 if iou >= 1.0 {
208 return match max_det {
209 Some(n) => {
210 boxes.truncate(n);
211 boxes
212 }
213 None => boxes,
214 };
215 }
216
217 let cap = max_det.unwrap_or(usize::MAX);
218 let mut survivors: usize = 0;
219
220 for i in 0..boxes.len() {
222 if boxes[i].score < 0.0 {
223 continue;
225 }
226 for j in (i + 1)..boxes.len() {
227 if boxes[j].score < 0.0 {
230 continue;
232 }
233 if jaccard(&boxes[j].bbox, &boxes[i].bbox, iou) {
234 boxes[j].score = -1.0;
236 }
237 }
238
239 survivors += 1;
244 if survivors >= cap {
245 break;
246 }
247 }
248 boxes
251 .into_iter()
252 .filter(|b| b.score >= 0.0)
253 .take(cap)
254 .collect()
255}
256
257#[must_use]
263pub fn nms_extra_float<E: Send + Sync>(
264 iou: f32,
265 max_det: Option<usize>,
266 mut boxes: Vec<(DetectBox, E)>,
267) -> Vec<(DetectBox, E)> {
268 boxes.par_sort_by(|a, b| b.0.score.total_cmp(&a.0.score));
271
272 if iou >= 1.0 {
275 return match max_det {
276 Some(n) => {
277 boxes.truncate(n);
278 boxes
279 }
280 None => boxes,
281 };
282 }
283
284 let cap = max_det.unwrap_or(usize::MAX);
285 let mut survivors: usize = 0;
286
287 for i in 0..boxes.len() {
289 if boxes[i].0.score < 0.0 {
290 continue;
292 }
293 for j in (i + 1)..boxes.len() {
294 if boxes[j].0.score < 0.0 {
297 continue;
299 }
300 if jaccard(&boxes[j].0.bbox, &boxes[i].0.bbox, iou) {
301 boxes[j].0.score = -1.0;
303 }
304 }
305 survivors += 1;
306 if survivors >= cap {
307 break;
308 }
309 }
310
311 boxes
314 .into_iter()
315 .filter(|b| b.0.score >= 0.0)
316 .take(cap)
317 .collect()
318}
319
320#[must_use]
346pub fn nms_class_aware_float(
347 iou: f32,
348 max_det: Option<usize>,
349 mut boxes: Vec<DetectBox>,
350) -> Vec<DetectBox> {
351 boxes.par_sort_by(|a, b| b.score.total_cmp(&a.score));
352
353 if iou >= 1.0 {
354 return match max_det {
355 Some(n) => {
356 boxes.truncate(n);
357 boxes
358 }
359 None => boxes,
360 };
361 }
362
363 let cap = max_det.unwrap_or(usize::MAX);
364 let mut survivors: usize = 0;
365
366 for i in 0..boxes.len() {
367 if boxes[i].score < 0.0 {
368 continue;
369 }
370 for j in (i + 1)..boxes.len() {
371 if boxes[j].score < 0.0 {
372 continue;
373 }
374 if boxes[j].label == boxes[i].label && jaccard(&boxes[j].bbox, &boxes[i].bbox, iou) {
376 boxes[j].score = -1.0;
377 }
378 }
379 survivors += 1;
380 if survivors >= cap {
381 break;
382 }
383 }
384 boxes
385 .into_iter()
386 .filter(|b| b.score >= 0.0)
387 .take(cap)
388 .collect()
389}
390
391#[must_use]
396pub fn nms_extra_class_aware_float<E: Send + Sync>(
397 iou: f32,
398 max_det: Option<usize>,
399 mut boxes: Vec<(DetectBox, E)>,
400) -> Vec<(DetectBox, E)> {
401 boxes.par_sort_by(|a, b| b.0.score.total_cmp(&a.0.score));
402
403 if iou >= 1.0 {
406 return match max_det {
407 Some(n) => {
408 boxes.truncate(n);
409 boxes
410 }
411 None => boxes,
412 };
413 }
414
415 let cap = max_det.unwrap_or(usize::MAX);
416 let mut survivors: usize = 0;
417
418 for i in 0..boxes.len() {
419 if boxes[i].0.score < 0.0 {
420 continue;
421 }
422 for j in (i + 1)..boxes.len() {
423 if boxes[j].0.score < 0.0 {
424 continue;
425 }
426 if boxes[j].0.label == boxes[i].0.label
428 && jaccard(&boxes[j].0.bbox, &boxes[i].0.bbox, iou)
429 {
430 boxes[j].0.score = -1.0;
431 }
432 }
433 survivors += 1;
434 if survivors >= cap {
435 break;
436 }
437 }
438 boxes
439 .into_iter()
440 .filter(|b| b.0.score >= 0.0)
441 .take(cap)
442 .collect()
443}
444
445#[must_use]
448#[inline]
449pub fn intersection_area(a: &BoundingBox, b: &BoundingBox) -> f32 {
450 let left = a.xmin.max(b.xmin);
451 let top = a.ymin.max(b.ymin);
452 let right = a.xmax.min(b.xmax);
453 let bottom = a.ymax.min(b.ymax);
454 (right - left).max(0.0) * (bottom - top).max(0.0)
455}
456
457#[must_use]
459#[inline]
460pub fn box_area(b: &BoundingBox) -> f32 {
461 (b.xmax - b.xmin).max(0.0) * (b.ymax - b.ymin).max(0.0)
462}
463
464#[must_use]
467#[inline]
468pub fn iou_value(a: &BoundingBox, b: &BoundingBox) -> f32 {
469 let inter = intersection_area(a, b);
470 let union = (box_area(a) + box_area(b) - inter).max(1e-9);
471 inter / union
472}
473
474#[must_use]
478#[inline]
479pub fn ios_value(a: &BoundingBox, b: &BoundingBox) -> f32 {
480 let inter = intersection_area(a, b);
481 let denom = box_area(a).min(box_area(b)).max(1e-9);
482 inter / denom
483}
484
485pub fn jaccard(a: &BoundingBox, b: &BoundingBox, iou: f32) -> bool {
503 let left = a.xmin.max(b.xmin);
504 let top = a.ymin.max(b.ymin);
505 let right = a.xmax.min(b.xmax);
506 let bottom = a.ymax.min(b.ymax);
507
508 let intersection = (right - left).max(0.0) * (bottom - top).max(0.0);
509 let area_a = (a.xmax - a.xmin) * (a.ymax - a.ymin);
510 let area_b = (b.xmax - b.xmin) * (b.ymax - b.ymin);
511
512 let union = area_a + area_b - intersection;
514
515 intersection > iou * union
516}
517
518#[inline]
527pub fn jaccard_batch4(a: &BoundingBox, boxes: &[BoundingBox; 4], iou: f32) -> [bool; 4] {
528 #[cfg(target_arch = "aarch64")]
529 {
530 unsafe { jaccard_batch4_neon(a, boxes, iou) }
532 }
533 #[cfg(not(target_arch = "aarch64"))]
534 {
535 [
536 jaccard(a, &boxes[0], iou),
537 jaccard(a, &boxes[1], iou),
538 jaccard(a, &boxes[2], iou),
539 jaccard(a, &boxes[3], iou),
540 ]
541 }
542}
543
544#[cfg(target_arch = "aarch64")]
550#[target_feature(enable = "neon")]
551unsafe fn jaccard_batch4_neon(a: &BoundingBox, boxes: &[BoundingBox; 4], iou: f32) -> [bool; 4] {
552 unsafe {
553 use std::arch::aarch64::*; let zero = vdupq_n_f32(0.0);
556 let iou_v = vdupq_n_f32(iou);
557
558 let a_xmin = vdupq_n_f32(a.xmin);
560 let a_ymin = vdupq_n_f32(a.ymin);
561 let a_xmax = vdupq_n_f32(a.xmax);
562 let a_ymax = vdupq_n_f32(a.ymax);
563 let area_a = vmulq_f32(vsubq_f32(a_xmax, a_xmin), vsubq_f32(a_ymax, a_ymin));
564
565 let b0 = vld1q_f32(&boxes[0].xmin as *const f32);
567 let b1 = vld1q_f32(&boxes[1].xmin as *const f32);
568 let b2 = vld1q_f32(&boxes[2].xmin as *const f32);
569 let b3 = vld1q_f32(&boxes[3].xmin as *const f32);
570
571 let t01_lo = vtrn1q_f32(b0, b1); let t01_hi = vtrn2q_f32(b0, b1); let t23_lo = vtrn1q_f32(b2, b3);
575 let t23_hi = vtrn2q_f32(b2, b3);
576
577 let b_xmin = vreinterpretq_f32_f64(vtrn1q_f64(
578 vreinterpretq_f64_f32(t01_lo),
579 vreinterpretq_f64_f32(t23_lo),
580 ));
581 let b_ymin = vreinterpretq_f32_f64(vtrn1q_f64(
582 vreinterpretq_f64_f32(t01_hi),
583 vreinterpretq_f64_f32(t23_hi),
584 ));
585 let b_xmax = vreinterpretq_f32_f64(vtrn2q_f64(
586 vreinterpretq_f64_f32(t01_lo),
587 vreinterpretq_f64_f32(t23_lo),
588 ));
589 let b_ymax = vreinterpretq_f32_f64(vtrn2q_f64(
590 vreinterpretq_f64_f32(t01_hi),
591 vreinterpretq_f64_f32(t23_hi),
592 ));
593
594 let left = vmaxq_f32(a_xmin, b_xmin);
596 let top = vmaxq_f32(a_ymin, b_ymin);
597 let right = vminq_f32(a_xmax, b_xmax);
598 let bottom = vminq_f32(a_ymax, b_ymax);
599 let w = vmaxq_f32(vsubq_f32(right, left), zero);
600 let h = vmaxq_f32(vsubq_f32(bottom, top), zero);
601 let intersection = vmulq_f32(w, h);
602
603 let area_b = vmulq_f32(vsubq_f32(b_xmax, b_xmin), vsubq_f32(b_ymax, b_ymin));
605
606 let union = vsubq_f32(vaddq_f32(area_a, area_b), intersection);
608
609 let iou_union = vmulq_f32(iou_v, union);
611 let mask = vcgtq_f32(intersection, iou_union);
612
613 [
615 vgetq_lane_u32(mask, 0) != 0,
616 vgetq_lane_u32(mask, 1) != 0,
617 vgetq_lane_u32(mask, 2) != 0,
618 vgetq_lane_u32(mask, 3) != 0,
619 ]
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use crate::BoundingBox;
627
628 #[test]
629 fn multilabel_float_drops_indices_only() {
630 let boxes = ndarray::arr2(&[[0.1f32, 0.1, 0.2, 0.2], [0.3, 0.3, 0.4, 0.4]]);
631 let scores = ndarray::arr2(&[[0.6f32, 0.1, 0.7], [0.05, 0.9, 0.05]]);
632 let with_idx = postprocess_boxes_multilabel_index_float::<crate::XYXY, _, _>(
633 0.5,
634 boxes.view(),
635 scores.view(),
636 );
637 let without = postprocess_boxes_multilabel_float::<crate::XYXY, _, _>(
638 0.5,
639 boxes.view(),
640 scores.view(),
641 )
642 .unwrap();
643 let stripped: Vec<_> = with_idx.into_iter().map(|(b, _)| b).collect();
644 assert_eq!(stripped, without);
645 assert_eq!(without.len(), 3);
646 }
647
648 #[test]
649 fn multilabel_float_rejects_non_four_column_boxes() {
650 let boxes = ndarray::arr2(&[[0.1f32, 0.1, 0.2, 0.2, 0.0]]);
651 let scores = ndarray::arr2(&[[0.6f32]]);
652 let r = postprocess_boxes_multilabel_float::<crate::XYXY, _, _>(
653 0.5,
654 boxes.view(),
655 scores.view(),
656 );
657 assert!(matches!(r, Err(crate::DecoderError::InvalidShape(_))));
658 }
659
660 fn make_nms_boxes_float(n: usize) -> Vec<DetectBox> {
662 (0..n)
663 .map(|i| DetectBox {
664 bbox: BoundingBox {
665 xmin: i as f32 * 100.0,
666 ymin: 0.0,
667 xmax: i as f32 * 100.0 + 10.0,
668 ymax: 10.0,
669 },
670 label: 0,
671 score: 1.0 - i as f32 * 0.01,
672 })
673 .collect()
674 }
675
676 fn overlapping_pair(score: f32) -> Vec<DetectBox> {
681 vec![
682 DetectBox {
683 bbox: BoundingBox {
684 xmin: 0.0,
685 ymin: 0.0,
686 xmax: 10.0,
687 ymax: 10.0,
688 },
689 label: 0,
690 score,
691 },
692 DetectBox {
693 bbox: BoundingBox {
694 xmin: 1.0,
695 ymin: 1.0,
696 xmax: 11.0,
697 ymax: 11.0,
698 },
699 label: 0,
700 score,
701 },
702 ]
703 }
704
705 #[test]
709 fn nms_float_suppresses_overlapping_zero_score_boxes() {
710 let result = nms_float(0.5, None, overlapping_pair(0.0));
711 assert_eq!(result.len(), 1, "{result:?}");
712 assert_eq!(result[0].score, 0.0);
713 }
714
715 fn suppression_chain() -> Vec<DetectBox> {
726 let mk = |xmin: f32, xmax: f32, score: f32| DetectBox {
727 bbox: BoundingBox {
728 xmin,
729 ymin: 0.0,
730 xmax,
731 ymax: 10.0,
732 },
733 label: 0,
734 score,
735 };
736 vec![mk(0.0, 10.0, 0.9), mk(2.0, 12.0, 0.8), mk(4.0, 14.0, 0.7)]
737 }
738
739 #[test]
740 fn nms_float_does_not_suppress_on_behalf_of_a_suppressed_box() {
741 let result = nms_float(0.5, None, suppression_chain());
742 assert_eq!(result.len(), 2, "{result:?}");
743 assert_eq!(result[0].score, 0.9);
744 assert_eq!(result[1].score, 0.7);
745 }
746
747 #[test]
748 fn nms_extra_float_suppresses_overlapping_zero_score_boxes() {
749 let boxes = overlapping_pair(0.0)
750 .into_iter()
751 .enumerate()
752 .map(|(i, b)| (b, i))
753 .collect();
754 let result = nms_extra_float(0.5, None, boxes);
755 assert_eq!(result.len(), 1, "{result:?}");
756 assert!(result[0].1 == 0 || result[0].1 == 1, "{result:?}");
760 }
761
762 #[test]
763 fn nms_extra_float_does_not_suppress_on_behalf_of_a_suppressed_box() {
764 let boxes = suppression_chain()
765 .into_iter()
766 .enumerate()
767 .map(|(i, b)| (b, i))
768 .collect();
769 let result = nms_extra_float(0.5, None, boxes);
770 assert_eq!(result.len(), 2, "{result:?}");
771 assert_eq!(result[0].1, 0);
772 assert_eq!(result[1].1, 2);
773 }
774
775 #[test]
779 fn nms_class_aware_float_suppresses_same_class_without_a_cap() {
780 let mut boxes = overlapping_pair(0.9);
781 boxes[1].score = 0.8;
782 let result = nms_class_aware_float(0.5, None, boxes);
783 assert_eq!(result.len(), 1, "{result:?}");
784 assert_eq!(result[0].score, 0.9);
785 }
786
787 #[test]
790 fn nms_class_aware_float_keeps_overlapping_boxes_of_other_classes() {
791 let mut boxes = overlapping_pair(0.9);
792 boxes[1].score = 0.8;
793 boxes[1].label = 1;
794 let result = nms_class_aware_float(0.5, None, boxes);
795 assert_eq!(result.len(), 2, "{result:?}");
796 }
797
798 #[test]
799 fn nms_class_aware_float_suppresses_overlapping_zero_score_boxes() {
800 let result = nms_class_aware_float(0.5, None, overlapping_pair(0.0));
801 assert_eq!(result.len(), 1, "{result:?}");
802 }
803
804 #[test]
805 fn nms_class_aware_float_does_not_suppress_on_behalf_of_a_suppressed_box() {
806 let result = nms_class_aware_float(0.5, None, suppression_chain());
807 assert_eq!(result.len(), 2, "{result:?}");
808 assert_eq!(result[1].score, 0.7);
809 }
810
811 fn isolated_then_overlapping_pair() -> Vec<DetectBox> {
817 let mk = |xmin: f32, xmax: f32, score: f32| DetectBox {
818 bbox: BoundingBox {
819 xmin,
820 ymin: 0.0,
821 xmax,
822 ymax: 10.0,
823 },
824 label: 0,
825 score,
826 };
827 vec![
828 mk(100.0, 110.0, 0.9),
829 mk(0.0, 10.0, 0.8),
830 mk(1.0, 11.0, 0.7),
831 ]
832 }
833
834 #[test]
835 fn nms_class_aware_float_keeps_suppressing_after_the_first_survivor() {
836 let result = nms_class_aware_float(0.5, None, isolated_then_overlapping_pair());
837 assert_eq!(result.len(), 2, "{result:?}");
838 assert_eq!(result[0].score, 0.9);
839 assert_eq!(result[1].score, 0.8);
840 }
841
842 fn with_index(boxes: Vec<DetectBox>) -> Vec<(DetectBox, usize)> {
843 boxes.into_iter().enumerate().map(|(i, b)| (b, i)).collect()
844 }
845
846 #[test]
847 fn nms_extra_class_aware_float_keeps_suppressing_after_the_first_survivor() {
848 let result =
849 nms_extra_class_aware_float(0.5, None, with_index(isolated_then_overlapping_pair()));
850 assert_eq!(result.len(), 2, "{result:?}");
851 assert_eq!(result[1].1, 1);
852 }
853
854 #[test]
855 fn nms_extra_class_aware_float_suppresses_same_class_without_a_cap() {
856 let mut boxes = overlapping_pair(0.9);
857 boxes[1].score = 0.8;
858 let result = nms_extra_class_aware_float(0.5, None, with_index(boxes));
859 assert_eq!(result.len(), 1, "{result:?}");
860 assert_eq!(result[0].0.score, 0.9);
861 assert_eq!(result[0].1, 0);
862 }
863
864 #[test]
865 fn nms_extra_class_aware_float_keeps_overlapping_boxes_of_other_classes() {
866 let mut boxes = overlapping_pair(0.9);
867 boxes[1].score = 0.8;
868 boxes[1].label = 1;
869 let result = nms_extra_class_aware_float(0.5, None, with_index(boxes));
870 assert_eq!(result.len(), 2, "{result:?}");
871 }
872
873 #[test]
874 fn nms_extra_class_aware_float_suppresses_overlapping_zero_score_boxes() {
875 let result = nms_extra_class_aware_float(0.5, None, with_index(overlapping_pair(0.0)));
876 assert_eq!(result.len(), 1, "{result:?}");
877 }
878
879 #[test]
880 fn nms_extra_class_aware_float_does_not_suppress_on_behalf_of_a_suppressed_box() {
881 let result = nms_extra_class_aware_float(0.5, None, with_index(suppression_chain()));
882 assert_eq!(result.len(), 2, "{result:?}");
883 assert_eq!(result[1].1, 2);
884 }
885
886 #[test]
888 fn postprocess_boxes_float_keeps_a_score_equal_to_the_threshold() {
889 let boxes = ndarray::array![[0.0_f32, 0.0, 10.0, 10.0], [0.0, 0.0, 10.0, 10.0]];
890 let scores = ndarray::array![[0.5_f32], [0.25]];
891
892 let kept = postprocess_boxes_float::<crate::XYXY, _, _>(0.5, boxes.view(), scores.view());
893 assert_eq!(kept.len(), 1, "{kept:?}");
894 assert_eq!(kept[0].score, 0.5);
895
896 let kept =
897 postprocess_boxes_index_float::<crate::XYXY, _, _>(0.5, boxes.view(), scores.view());
898 assert_eq!(kept.len(), 1, "{kept:?}");
899 assert_eq!(kept[0].1, 0);
900 }
901
902 #[test]
903 fn nms_float_max_det_matches_full_truncated() {
904 let boxes = make_nms_boxes_float(20);
905 let n = 5;
906 let full = nms_float(0.5, None, boxes.clone());
907 let capped = nms_float(0.5, Some(n), boxes);
908 assert_eq!(capped.len(), n);
909 for (f, c) in full[..n].iter().zip(capped.iter()) {
910 assert_eq!(f.bbox, c.bbox);
911 assert_eq!(f.score, c.score);
912 }
913 }
914
915 #[test]
916 fn nms_float_max_det_zero_returns_empty() {
917 let boxes = make_nms_boxes_float(10);
918 let result = nms_float(0.5, Some(0), boxes);
919 assert!(result.is_empty());
920 }
921
922 #[test]
923 fn nms_float_max_det_iou_ge_1_returns_sorted_truncated() {
924 let boxes = make_nms_boxes_float(10);
925 let result = nms_float(1.0, Some(3), boxes);
926 assert_eq!(result.len(), 3);
927 assert!(result[0].score >= result[1].score);
928 assert!(result[1].score >= result[2].score);
929 }
930
931 #[test]
932 fn nms_float_max_det_larger_than_input() {
933 let boxes = make_nms_boxes_float(5);
934 let full = nms_float(0.5, None, boxes.clone());
935 let capped = nms_float(0.5, Some(100), boxes);
936 assert_eq!(full.len(), capped.len());
937 }
938
939 #[test]
943 fn metric_values_match_modelpack_match() {
944 let a = BoundingBox::new(100.0, 100.0, 400.0, 300.0);
945 let b = BoundingBox::new(350.0, 100.0, 400.0, 300.0);
946 assert!((intersection_area(&a, &b) - 10000.0).abs() < 1e-3);
947 assert!((box_area(&a) - 60000.0).abs() < 1e-3);
948 assert!((box_area(&b) - 10000.0).abs() < 1e-3);
949 assert!((ios_value(&a, &b) - 1.0).abs() < 1e-6);
950 assert!((iou_value(&a, &b) - (1.0 / 6.0)).abs() < 1e-6);
951 }
952
953 #[test]
954 fn metric_values_disjoint_are_zero() {
955 let a = BoundingBox::new(0.0, 0.0, 10.0, 10.0);
956 let b = BoundingBox::new(20.0, 20.0, 30.0, 30.0);
957 assert_eq!(intersection_area(&a, &b), 0.0);
958 assert_eq!(ios_value(&a, &b), 0.0);
959 assert_eq!(iou_value(&a, &b), 0.0);
960 }
961
962 #[test]
963 fn metric_values_zero_area_box_no_panic() {
964 let degenerate = BoundingBox::new(10.0, 10.0, 10.0, 20.0);
966 let other = BoundingBox::new(0.0, 0.0, 30.0, 30.0);
967 assert_eq!(box_area(°enerate), 0.0);
968 assert!(ios_value(°enerate, &other).is_finite());
969 assert!(iou_value(°enerate, &other).is_finite());
970 }
971
972 #[test]
973 fn ios_high_where_iou_low_for_contained_box() {
974 let big = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
976 let small = BoundingBox::new(10.0, 10.0, 20.0, 20.0);
977 assert!((ios_value(&big, &small) - 1.0).abs() < 1e-6); assert!(iou_value(&big, &small) < 0.05); }
980
981 #[test]
982 fn jaccard_batch4_matches_scalar() {
983 let a = BoundingBox::new(0.0, 0.0, 10.0, 10.0);
984 let boxes = [
985 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), ];
990 let iou_threshold = 0.1;
991 let batch = jaccard_batch4(&a, &boxes, iou_threshold);
992 for (i, b) in boxes.iter().enumerate() {
993 let scalar = jaccard(&a, b, iou_threshold);
994 assert_eq!(
995 batch[i], scalar,
996 "batch4 mismatch at {i}: batch={} scalar={}",
997 batch[i], scalar
998 );
999 }
1000 }
1001}