1#[cfg(target_arch = "aarch64")]
19use crate::arg_max_i8;
20use crate::{
21 arg_max, float::jaccard, BBoxTypeTrait, BoundingBox, DecoderError, DecoderResult,
22 DetectBoxQuantized, Quantization,
23};
24use ndarray::{
25 parallel::prelude::{IntoParallelIterator, ParallelIterator as _},
26 Array1, ArrayView1, ArrayView2, Zip,
27};
28use num_traits::{AsPrimitive, PrimInt};
29use rayon::slice::ParallelSliceMut;
30
31#[cfg(target_arch = "aarch64")]
42unsafe fn column_max_update_neon(
43 col_ptr: *const u8,
44 max_ptr: *mut u8,
45 class_ptr: *mut u8,
46 n: usize,
47 class_idx: u8,
48 signed: bool,
49) {
50 unsafe {
51 use std::arch::aarch64::*; let class_vec = vdupq_n_u8(class_idx);
54 let chunks = n / 16;
55 let remainder = n % 16;
56
57 if signed {
58 for chunk in 0..chunks {
60 let offset = chunk * 16;
61 let col = vld1q_s8(col_ptr.add(offset) as *const i8);
62 let cur_max = vld1q_s8(max_ptr.add(offset) as *const i8);
63 let mask = vcgeq_s8(col, cur_max);
65 let new_max = vmaxq_s8(col, cur_max);
67 vst1q_s8(max_ptr.add(offset) as *mut i8, new_max);
68 let cur_class = vld1q_u8(class_ptr.add(offset));
70 let new_class = vbslq_u8(mask, class_vec, cur_class);
71 vst1q_u8(class_ptr.add(offset), new_class);
72 }
73 for i in (chunks * 16)..n {
75 let val = *(col_ptr.add(i) as *const i8);
76 let cur = *(max_ptr.add(i) as *const i8);
77 if val >= cur {
78 *(max_ptr.add(i) as *mut i8) = val;
79 *class_ptr.add(i) = class_idx;
80 }
81 }
82 } else {
83 for chunk in 0..chunks {
85 let offset = chunk * 16;
86 let col = vld1q_u8(col_ptr.add(offset));
87 let cur_max = vld1q_u8(max_ptr.add(offset));
88 let mask = vcgeq_u8(col, cur_max);
89 let new_max = vmaxq_u8(col, cur_max);
90 vst1q_u8(max_ptr.add(offset), new_max);
91 let cur_class = vld1q_u8(class_ptr.add(offset));
92 let new_class = vbslq_u8(mask, class_vec, cur_class);
93 vst1q_u8(class_ptr.add(offset), new_class);
94 }
95 for i in (chunks * 16)..n {
97 let val = *col_ptr.add(i);
98 let cur = *max_ptr.add(i);
99 if val >= cur {
100 *max_ptr.add(i) = val;
101 *class_ptr.add(i) = class_idx;
102 }
103 }
104 }
105 let _ = remainder; }
107}
108
109#[cfg(target_arch = "aarch64")]
122#[allow(dead_code)] unsafe fn column_max_update_neon_prefetch(
124 col_ptr: *const u8,
125 max_ptr: *mut u8,
126 class_ptr: *mut u8,
127 n: usize,
128 class_idx: u8,
129 signed: bool,
130) {
131 unsafe {
132 use std::arch::aarch64::*; const PREFETCH_AHEAD: usize = 128; let class_vec = vdupq_n_u8(class_idx);
137 let chunks = n / 16;
138
139 if signed {
140 for chunk in 0..chunks {
141 let offset = chunk * 16;
142 if offset + PREFETCH_AHEAD < n {
144 core::arch::asm!(
145 "prfm pldl1strm, [{ptr}]",
146 ptr = in(reg) col_ptr.add(offset + PREFETCH_AHEAD),
147 options(nostack, preserves_flags),
148 );
149 }
150 let col = vld1q_s8(col_ptr.add(offset) as *const i8);
151 let cur_max = vld1q_s8(max_ptr.add(offset) as *const i8);
152 let mask = vcgeq_s8(col, cur_max);
153 let new_max = vmaxq_s8(col, cur_max);
154 vst1q_s8(max_ptr.add(offset) as *mut i8, new_max);
155 let cur_class = vld1q_u8(class_ptr.add(offset));
156 let new_class = vbslq_u8(mask, class_vec, cur_class);
157 vst1q_u8(class_ptr.add(offset), new_class);
158 }
159 for i in (chunks * 16)..n {
160 let val = *(col_ptr.add(i) as *const i8);
161 let cur = *(max_ptr.add(i) as *const i8);
162 if val >= cur {
163 *(max_ptr.add(i) as *mut i8) = val;
164 *class_ptr.add(i) = class_idx;
165 }
166 }
167 } else {
168 for chunk in 0..chunks {
169 let offset = chunk * 16;
170 if offset + PREFETCH_AHEAD < n {
171 core::arch::asm!(
172 "prfm pldl1strm, [{ptr}]",
173 ptr = in(reg) col_ptr.add(offset + PREFETCH_AHEAD),
174 options(nostack, preserves_flags),
175 );
176 }
177 let col = vld1q_u8(col_ptr.add(offset));
178 let cur_max = vld1q_u8(max_ptr.add(offset));
179 let mask = vcgeq_u8(col, cur_max);
180 let new_max = vmaxq_u8(col, cur_max);
181 vst1q_u8(max_ptr.add(offset), new_max);
182 let cur_class = vld1q_u8(class_ptr.add(offset));
183 let new_class = vbslq_u8(mask, class_vec, cur_class);
184 vst1q_u8(class_ptr.add(offset), new_class);
185 }
186 for i in (chunks * 16)..n {
187 let val = *col_ptr.add(i);
188 let cur = *max_ptr.add(i);
189 if val >= cur {
190 *max_ptr.add(i) = val;
191 *class_ptr.add(i) = class_idx;
192 }
193 }
194 }
195 }
196}
197
198#[inline(always)]
200fn fast_arg_max<T: PrimInt + Copy>(score: ArrayView1<T>) -> (T, usize) {
201 #[cfg(target_arch = "aarch64")]
202 {
203 if std::mem::size_of::<T>() == 1 && score.as_slice().is_some() {
205 let slice = score.as_slice().unwrap();
206 let ptr = slice.as_ptr() as *const i8;
210 let i8_slice = unsafe { std::slice::from_raw_parts(ptr, slice.len()) };
211 if T::min_value() < T::zero() {
214 let (max_val, idx) = arg_max_i8(i8_slice);
215 let result: T = unsafe { std::mem::transmute_copy(&max_val) };
218 return (result, idx);
219 }
220 }
221 }
222 arg_max(score)
223}
224
225#[doc(hidden)]
230pub fn postprocess_boxes_quant<
231 B: BBoxTypeTrait,
232 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
233 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
234>(
235 threshold: Scores,
236 boxes: ArrayView2<Boxes>,
237 scores: ArrayView2<Scores>,
238 quant_boxes: Quantization,
239) -> Vec<DetectBoxQuantized<Scores>> {
240 assert_eq!(scores.dim().0, boxes.dim().0);
241 assert_eq!(boxes.dim().1, 4);
242
243 if scores.strides()[0] == 1 && scores.as_slice().is_none() {
245 return postprocess_boxes_quant_column_major::<B, _, _>(
246 threshold,
247 boxes,
248 scores,
249 quant_boxes,
250 );
251 }
252
253 Zip::from(scores.rows())
254 .and(boxes.rows())
255 .into_par_iter()
256 .filter_map(|(score, bbox)| {
257 let (score_, label) = fast_arg_max(score);
258 if score_ < threshold {
259 return None;
260 }
261
262 let bbox_quant = B::ndarray_to_xyxy_dequant(bbox.view(), quant_boxes);
263 Some(DetectBoxQuantized {
264 label,
265 score: score_,
266 bbox: BoundingBox::from(bbox_quant),
267 })
268 })
269 .collect()
270}
271
272fn postprocess_boxes_quant_column_major<
274 B: BBoxTypeTrait,
275 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
276 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
277>(
278 threshold: Scores,
279 boxes: ArrayView2<Boxes>,
280 scores: ArrayView2<Scores>,
281 quant_boxes: Quantization,
282) -> Vec<DetectBoxQuantized<Scores>> {
283 let (n_candidates, n_classes) = scores.dim();
284
285 if n_classes > 255 {
287 return Zip::from(scores.rows())
288 .and(boxes.rows())
289 .into_par_iter()
290 .filter_map(|(score, bbox)| {
291 let (score_, label) = fast_arg_max(score);
292 if score_ < threshold {
293 return None;
294 }
295 let bbox_quant = B::ndarray_to_xyxy_dequant(bbox.view(), quant_boxes);
296 Some(DetectBoxQuantized {
297 label,
298 score: score_,
299 bbox: BoundingBox::from(bbox_quant),
300 })
301 })
302 .collect();
303 }
304 let mut max_scores = vec![Scores::min_value(); n_candidates];
305 let mut max_classes = vec![0u8; n_candidates];
306
307 for class_idx in 0..n_classes {
308 let col = scores.column(class_idx);
309 if let Some(slice) = col.as_slice() {
310 #[cfg(target_arch = "aarch64")]
311 {
312 if std::mem::size_of::<Scores>() == 1 {
313 unsafe {
314 column_max_update_neon(
317 slice.as_ptr() as *const u8,
318 max_scores.as_mut_ptr() as *mut u8,
319 max_classes.as_mut_ptr(),
320 n_candidates,
321 class_idx as u8,
322 Scores::min_value() < Scores::zero(),
323 );
324 }
325 continue;
326 }
327 }
328 for (i, &val) in slice.iter().enumerate() {
329 if val >= max_scores[i] {
330 max_scores[i] = val;
331 max_classes[i] = class_idx as u8;
332 }
333 }
334 } else {
335 for (i, &val) in col.iter().enumerate() {
336 if val >= max_scores[i] {
337 max_scores[i] = val;
338 max_classes[i] = class_idx as u8;
339 }
340 }
341 }
342 }
343
344 let boxes_buf: [Vec<Boxes>; 4] = if boxes.strides()[0] == 1 && boxes.as_slice().is_none() {
346 let mut cols: [Vec<Boxes>; 4] = [
347 vec![Boxes::zero(); n_candidates],
348 vec![Boxes::zero(); n_candidates],
349 vec![Boxes::zero(); n_candidates],
350 vec![Boxes::zero(); n_candidates],
351 ];
352 for (dim, col_buf) in cols.iter_mut().enumerate() {
353 let col = boxes.column(dim);
354 if let Some(slice) = col.as_slice() {
355 col_buf.copy_from_slice(slice);
356 } else {
357 for (i, &val) in col.iter().enumerate() {
358 col_buf[i] = val;
359 }
360 }
361 }
362 cols
363 } else {
364 [vec![], vec![], vec![], vec![]]
365 };
366 let boxes_copied = !boxes_buf[0].is_empty();
367
368 let mut result = Vec::new();
369 for i in 0..n_candidates {
370 if max_scores[i] >= threshold {
371 let bbox_quant = if boxes_copied {
372 let raw = [
373 boxes_buf[0][i],
374 boxes_buf[1][i],
375 boxes_buf[2][i],
376 boxes_buf[3][i],
377 ];
378 B::to_xyxy_dequant(&raw, quant_boxes)
379 } else {
380 B::ndarray_to_xyxy_dequant(boxes.row(i), quant_boxes)
381 };
382 result.push(DetectBoxQuantized {
383 label: max_classes[i] as usize,
384 score: max_scores[i],
385 bbox: BoundingBox::from(bbox_quant),
386 });
387 }
388 }
389
390 result
391}
392
393#[doc(hidden)]
405pub fn postprocess_boxes_index_quant<
406 B: BBoxTypeTrait,
407 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
408 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
409>(
410 threshold: Scores,
411 boxes: ArrayView2<Boxes>,
412 scores: ArrayView2<Scores>,
413 quant_boxes: Quantization,
414) -> Vec<(DetectBoxQuantized<Scores>, usize)> {
415 assert_eq!(scores.dim().0, boxes.dim().0);
416 assert_eq!(boxes.dim().1, 4);
417
418 if scores.strides()[0] == 1 && scores.as_slice().is_none() {
422 return postprocess_boxes_index_quant_column_major::<B, _, _>(
423 threshold,
424 boxes,
425 scores,
426 quant_boxes,
427 );
428 }
429
430 let indices: Array1<usize> = (0..boxes.dim().0).collect();
431 Zip::from(scores.rows())
432 .and(boxes.rows())
433 .and(&indices)
434 .into_par_iter()
435 .filter_map(|(score, bbox, index)| {
436 let (score_, label) = fast_arg_max(score);
437 if score_ < threshold {
438 return None;
439 }
440
441 let bbox_quant = B::ndarray_to_xyxy_dequant(bbox.view(), quant_boxes);
442
443 Some((
444 DetectBoxQuantized {
445 label,
446 score: score_,
447 bbox: BoundingBox::from(bbox_quant),
448 },
449 *index,
450 ))
451 })
452 .collect()
453}
454
455pub fn postprocess_boxes_multilabel_index_quant<
463 B: BBoxTypeTrait,
464 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
465 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
466>(
467 threshold: Scores,
468 boxes: ArrayView2<Boxes>,
469 scores: ArrayView2<Scores>,
470 quant_boxes: Quantization,
471) -> DecoderResult<Vec<(DetectBoxQuantized<Scores>, usize)>> {
472 check_candidate_shapes(boxes.dim(), scores.dim())?;
473 if scores.strides()[0] == 1 && scores.as_slice().is_none() {
474 return Ok(postprocess_boxes_multilabel_index_quant_column_major::<
475 B,
476 _,
477 _,
478 >(threshold, boxes, scores, quant_boxes));
479 }
480 let indices: Array1<usize> = (0..boxes.dim().0).collect();
481 Ok(Zip::from(scores.rows())
482 .and(boxes.rows())
483 .and(&indices)
484 .into_par_iter()
485 .flat_map(|(score_row, bbox_row, &anchor_idx)| {
486 let bbox = BoundingBox::from(B::ndarray_to_xyxy_dequant(bbox_row.view(), quant_boxes));
487 score_row
488 .iter()
489 .enumerate()
490 .filter(|(_, &s)| s >= threshold)
491 .map(move |(label, &score)| (DetectBoxQuantized { label, score, bbox }, anchor_idx))
492 .collect::<Vec<_>>()
493 })
494 .collect())
495}
496
497pub fn postprocess_boxes_multilabel_quant<
502 B: BBoxTypeTrait,
503 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
504 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
505>(
506 threshold: Scores,
507 boxes: ArrayView2<Boxes>,
508 scores: ArrayView2<Scores>,
509 quant_boxes: Quantization,
510) -> DecoderResult<Vec<DetectBoxQuantized<Scores>>> {
511 Ok(
512 postprocess_boxes_multilabel_index_quant::<B, _, _>(threshold, boxes, scores, quant_boxes)?
513 .into_iter()
514 .map(|(b, _)| b)
515 .collect(),
516 )
517}
518
519fn postprocess_boxes_multilabel_index_quant_column_major<
525 B: BBoxTypeTrait,
526 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
527 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
528>(
529 threshold: Scores,
530 boxes: ArrayView2<Boxes>,
531 scores: ArrayView2<Scores>,
532 quant_boxes: Quantization,
533) -> Vec<(DetectBoxQuantized<Scores>, usize)> {
534 let mut hits: Vec<(usize, usize, Scores)> = (0..scores.dim().1)
535 .into_par_iter()
536 .flat_map_iter(|class_idx| {
537 let col = scores.column(class_idx);
538 let above = |(i, &s): (usize, &Scores)| (s >= threshold).then_some((i, class_idx, s));
539 match col.as_slice() {
540 Some(slice) => slice
541 .iter()
542 .enumerate()
543 .filter_map(above)
544 .collect::<Vec<_>>(),
545 None => col.iter().enumerate().filter_map(above).collect(),
546 }
547 })
548 .collect();
549 hits.par_sort_unstable_by_key(|&(anchor, class, _)| (anchor, class));
550
551 let mut out = Vec::with_capacity(hits.len());
552 let mut current: Option<(usize, BoundingBox)> = None;
553 for (anchor, label, score) in hits {
554 let bbox = match current {
555 Some((a, bbox)) if a == anchor => bbox,
556 _ => {
557 let bbox =
558 BoundingBox::from(B::ndarray_to_xyxy_dequant(boxes.row(anchor), quant_boxes));
559 current = Some((anchor, bbox));
560 bbox
561 }
562 };
563 out.push((DetectBoxQuantized { label, score, bbox }, anchor));
564 }
565 out
566}
567
568pub(crate) fn check_candidate_shapes(
570 boxes: (usize, usize),
571 scores: (usize, usize),
572) -> DecoderResult<()> {
573 if boxes.1 != 4 || boxes.0 != scores.0 {
574 return Err(DecoderError::InvalidShape(format!(
575 "candidate selection needs boxes [N, 4] and scores [N, C]; got boxes {boxes:?}, scores {scores:?}"
576 )));
577 }
578 Ok(())
579}
580
581fn postprocess_boxes_index_quant_column_major<
589 B: BBoxTypeTrait,
590 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
591 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
592>(
593 threshold: Scores,
594 boxes: ArrayView2<Boxes>,
595 scores: ArrayView2<Scores>,
596 quant_boxes: Quantization,
597) -> Vec<(DetectBoxQuantized<Scores>, usize)> {
598 let (n_candidates, n_classes) = scores.dim();
599
600 if n_classes > 255 {
604 let indices: Array1<usize> = (0..n_candidates).collect();
605 return Zip::from(scores.rows())
606 .and(boxes.rows())
607 .and(&indices)
608 .into_par_iter()
609 .filter_map(|(score, bbox, index)| {
610 let (score_, label) = fast_arg_max(score);
611 if score_ < threshold {
612 return None;
613 }
614 let bbox_quant = B::ndarray_to_xyxy_dequant(bbox.view(), quant_boxes);
615 Some((
616 DetectBoxQuantized {
617 label,
618 score: score_,
619 bbox: BoundingBox::from(bbox_quant),
620 },
621 *index,
622 ))
623 })
624 .collect();
625 }
626 let mut max_scores = vec![Scores::min_value(); n_candidates];
627 let mut max_classes = vec![0u8; n_candidates];
628 for class_idx in 0..n_classes {
629 let col = scores.column(class_idx);
630 if let Some(slice) = col.as_slice() {
631 #[cfg(target_arch = "aarch64")]
633 {
634 if std::mem::size_of::<Scores>() == 1 {
635 unsafe {
638 column_max_update_neon(
639 slice.as_ptr() as *const u8,
640 max_scores.as_mut_ptr() as *mut u8,
641 max_classes.as_mut_ptr(),
642 n_candidates,
643 class_idx as u8,
644 Scores::min_value() < Scores::zero(), );
646 }
647 continue;
648 }
649 }
650 for (i, &val) in slice.iter().enumerate() {
651 if val >= max_scores[i] {
652 max_scores[i] = val;
653 max_classes[i] = class_idx as u8;
654 }
655 }
656 } else {
657 for (i, &val) in col.iter().enumerate() {
658 if val >= max_scores[i] {
659 max_scores[i] = val;
660 max_classes[i] = class_idx as u8;
661 }
662 }
663 }
664 }
665
666 let boxes_buf: [Vec<Boxes>; 4] = if boxes.strides()[0] == 1 && boxes.as_slice().is_none() {
670 let mut cols: [Vec<Boxes>; 4] = [
671 vec![Boxes::zero(); n_candidates],
672 vec![Boxes::zero(); n_candidates],
673 vec![Boxes::zero(); n_candidates],
674 vec![Boxes::zero(); n_candidates],
675 ];
676 for (dim, col_buf) in cols.iter_mut().enumerate() {
677 let col = boxes.column(dim);
678 if let Some(slice) = col.as_slice() {
679 col_buf.copy_from_slice(slice);
680 } else {
681 for (i, &val) in col.iter().enumerate() {
682 col_buf[i] = val;
683 }
684 }
685 }
686 cols
687 } else {
688 [vec![], vec![], vec![], vec![]]
690 };
691 let boxes_copied = !boxes_buf[0].is_empty();
692
693 let mut result = Vec::new();
695 for i in 0..n_candidates {
696 if max_scores[i] >= threshold {
697 let bbox_quant = if boxes_copied {
698 let raw = [
699 boxes_buf[0][i],
700 boxes_buf[1][i],
701 boxes_buf[2][i],
702 boxes_buf[3][i],
703 ];
704 B::to_xyxy_dequant(&raw, quant_boxes)
705 } else {
706 B::ndarray_to_xyxy_dequant(boxes.row(i), quant_boxes)
707 };
708 result.push((
709 DetectBoxQuantized {
710 label: max_classes[i] as usize,
711 score: max_scores[i],
712 bbox: BoundingBox::from(bbox_quant),
713 },
714 i,
715 ));
716 }
717 }
718
719 result
720}
721
722#[doc(hidden)]
725#[must_use]
726pub fn nms_int<SCORE: PrimInt + AsPrimitive<f32> + Send + Sync>(
727 iou: f32,
728 max_det: Option<usize>,
729 mut boxes: Vec<DetectBoxQuantized<SCORE>>,
730) -> Vec<DetectBoxQuantized<SCORE>> {
731 boxes.par_sort_by(|a, b| b.score.cmp(&a.score));
735
736 if iou >= 1.0 {
739 return match max_det {
740 Some(n) => {
741 boxes.truncate(n);
742 boxes
743 }
744 None => boxes,
745 };
746 }
747
748 let min_val = SCORE::min_value();
749 let cap = max_det.unwrap_or(usize::MAX);
750 let mut survivors: usize = 0;
751 for i in 0..boxes.len() {
753 if boxes[i].score <= min_val {
754 continue;
756 }
757 for j in (i + 1)..boxes.len() {
758 if boxes[j].score <= min_val {
761 continue;
763 }
764
765 if jaccard(&boxes[j].bbox, &boxes[i].bbox, iou) {
766 boxes[j].score = min_val;
768 }
769 }
770 survivors += 1;
771 if survivors >= cap {
772 break;
773 }
774 }
775 boxes
778 .into_iter()
779 .filter(|b| b.score > min_val)
780 .take(cap)
781 .collect()
782}
783
784#[doc(hidden)]
790#[must_use]
791pub fn nms_extra_int<SCORE: PrimInt + AsPrimitive<f32> + Send + Sync, E: Send + Sync>(
792 iou: f32,
793 max_det: Option<usize>,
794 mut boxes: Vec<(DetectBoxQuantized<SCORE>, E)>,
795) -> Vec<(DetectBoxQuantized<SCORE>, E)> {
796 boxes.par_sort_by(|a, b| b.0.score.cmp(&a.0.score));
799
800 if iou >= 1.0 {
803 return match max_det {
804 Some(n) => {
805 boxes.truncate(n);
806 boxes
807 }
808 None => boxes,
809 };
810 }
811
812 let min_val = SCORE::min_value();
813 let cap = max_det.unwrap_or(usize::MAX);
814 let mut survivors: usize = 0;
815 for i in 0..boxes.len() {
817 if boxes[i].0.score <= min_val {
818 continue;
820 }
821 for j in (i + 1)..boxes.len() {
822 if boxes[j].0.score <= min_val {
825 continue;
827 }
828 if jaccard(&boxes[j].0.bbox, &boxes[i].0.bbox, iou) {
829 boxes[j].0.score = min_val;
831 }
832 }
833 survivors += 1;
834 if survivors >= cap {
835 break;
836 }
837 }
838
839 boxes
841 .into_iter()
842 .filter(|b| b.0.score > min_val)
843 .take(cap)
844 .collect()
845}
846
847#[doc(hidden)]
854#[must_use]
855pub fn nms_class_aware_int<SCORE: PrimInt + AsPrimitive<f32> + Send + Sync>(
856 iou: f32,
857 max_det: Option<usize>,
858 mut boxes: Vec<DetectBoxQuantized<SCORE>>,
859) -> Vec<DetectBoxQuantized<SCORE>> {
860 boxes.par_sort_by(|a, b| b.score.cmp(&a.score));
861
862 if iou >= 1.0 {
865 return match max_det {
866 Some(n) => {
867 boxes.truncate(n);
868 boxes
869 }
870 None => boxes,
871 };
872 }
873
874 let min_val = SCORE::min_value();
875 let cap = max_det.unwrap_or(usize::MAX);
876 let mut survivors: usize = 0;
877 for i in 0..boxes.len() {
878 if boxes[i].score <= min_val {
879 continue;
880 }
881 for j in (i + 1)..boxes.len() {
882 if boxes[j].score <= min_val {
883 continue;
884 }
885 if boxes[j].label == boxes[i].label && jaccard(&boxes[j].bbox, &boxes[i].bbox, iou) {
887 boxes[j].score = min_val;
888 }
889 }
890 survivors += 1;
891 if survivors >= cap {
892 break;
893 }
894 }
895 boxes
896 .into_iter()
897 .filter(|b| b.score > min_val)
898 .take(cap)
899 .collect()
900}
901
902#[doc(hidden)]
908#[must_use]
909pub fn nms_extra_class_aware_int<
910 SCORE: PrimInt + AsPrimitive<f32> + Send + Sync,
911 E: Send + Sync,
912>(
913 iou: f32,
914 max_det: Option<usize>,
915 mut boxes: Vec<(DetectBoxQuantized<SCORE>, E)>,
916) -> Vec<(DetectBoxQuantized<SCORE>, E)> {
917 boxes.par_sort_by(|a, b| b.0.score.cmp(&a.0.score));
918
919 if iou >= 1.0 {
922 return match max_det {
923 Some(n) => {
924 boxes.truncate(n);
925 boxes
926 }
927 None => boxes,
928 };
929 }
930
931 let min_val = SCORE::min_value();
932 let cap = max_det.unwrap_or(usize::MAX);
933 let mut survivors: usize = 0;
934 for i in 0..boxes.len() {
935 if boxes[i].0.score <= min_val {
936 continue;
937 }
938 for j in (i + 1)..boxes.len() {
939 if boxes[j].0.score <= min_val {
940 continue;
941 }
942 if boxes[j].0.label == boxes[i].0.label
944 && jaccard(&boxes[j].0.bbox, &boxes[i].0.bbox, iou)
945 {
946 boxes[j].0.score = min_val;
947 }
948 }
949 survivors += 1;
950 if survivors >= cap {
951 break;
952 }
953 }
954 boxes
955 .into_iter()
956 .filter(|b| b.0.score > min_val)
957 .take(cap)
958 .collect()
959}
960
961#[doc(hidden)]
976pub fn quantize_score_threshold<T: PrimInt + AsPrimitive<f32>>(score: f32, quant: Quantization) -> T
977where
978 f32: AsPrimitive<T>,
979{
980 if quant.scale == 0.0 {
981 return T::max_value();
982 }
983 let v = (score / quant.scale + quant.zero_point as f32).ceil();
984 let v = v.clamp(T::min_value().as_(), T::max_value().as_());
985 v.as_()
986}
987
988#[cfg(test)]
989mod tests {
990 use super::*;
991 use crate::{XYWH, XYXY};
992 use ndarray::Array2;
993
994 fn unit_quant() -> Quantization {
995 Quantization {
996 scale: 0.1,
997 zero_point: 0,
998 }
999 }
1000
1001 fn sorted_by_anchor_label(
1002 mut v: Vec<(DetectBoxQuantized<i8>, usize)>,
1003 ) -> Vec<(DetectBoxQuantized<i8>, usize)> {
1004 v.sort_by_key(|(b, i)| (*i, b.label));
1005 v
1006 }
1007
1008 #[test]
1009 fn multilabel_index_quant_emits_every_class_above_threshold() {
1010 let boxes = ndarray::arr2(&[[10i8, 10, 20, 20], [30, 30, 40, 40]]);
1011 let scores = ndarray::arr2(&[[60i8, 10, 70], [5, 90, 5]]);
1012 let got = sorted_by_anchor_label(
1013 postprocess_boxes_multilabel_index_quant::<XYXY, _, _>(
1014 50,
1015 boxes.view(),
1016 scores.view(),
1017 unit_quant(),
1018 )
1019 .unwrap(),
1020 );
1021 let labels: Vec<(usize, usize)> = got.iter().map(|(b, i)| (*i, b.label)).collect();
1022 assert_eq!(labels, vec![(0, 0), (0, 2), (1, 1)]);
1023 assert_eq!(got[0].0.bbox, got[1].0.bbox);
1024 assert_eq!(got[1].0.score, 70);
1025 }
1026
1027 #[test]
1028 fn multilabel_index_quant_column_major_matches_row_major() {
1029 let boxes = ndarray::arr2(&[[10i8, 10, 20, 20], [30, 30, 40, 40]]);
1030 let scores_t = ndarray::arr2(&[[60i8, 5], [10, 90], [70, 5]]);
1031 let scores_cm = scores_t.view().reversed_axes();
1032 let scores_rm = scores_cm.to_owned();
1033 let a = postprocess_boxes_multilabel_index_quant::<XYXY, _, _>(
1034 50,
1035 boxes.view(),
1036 scores_cm,
1037 unit_quant(),
1038 )
1039 .unwrap();
1040 let b = postprocess_boxes_multilabel_index_quant::<XYXY, _, _>(
1041 50,
1042 boxes.view(),
1043 scores_rm.view(),
1044 unit_quant(),
1045 )
1046 .unwrap();
1047 assert_eq!(sorted_by_anchor_label(a), sorted_by_anchor_label(b));
1048 }
1049
1050 #[test]
1051 fn multilabel_index_quant_rejects_mismatched_shapes() {
1052 let boxes = ndarray::arr2(&[[10i8, 10, 20, 20]]);
1053 let scores = ndarray::arr2(&[[60i8, 10], [5, 90]]);
1054 let r = postprocess_boxes_multilabel_index_quant::<XYXY, _, _>(
1055 50,
1056 boxes.view(),
1057 scores.view(),
1058 unit_quant(),
1059 );
1060 assert!(matches!(r, Err(crate::DecoderError::InvalidShape(_))));
1061 }
1062
1063 #[test]
1064 fn multilabel_quant_rejects_non_four_column_boxes() {
1065 let boxes = ndarray::arr2(&[[10i8, 10, 20, 20, 0]]);
1066 let scores = ndarray::arr2(&[[60i8]]);
1067 let r = postprocess_boxes_multilabel_quant::<XYXY, _, _>(
1068 50,
1069 boxes.view(),
1070 scores.view(),
1071 unit_quant(),
1072 );
1073 assert!(matches!(r, Err(crate::DecoderError::InvalidShape(_))));
1074 }
1075
1076 #[test]
1077 fn multilabel_quant_drops_indices_only() {
1078 let boxes = ndarray::arr2(&[[10i8, 10, 20, 20], [30, 30, 40, 40]]);
1079 let scores = ndarray::arr2(&[[60i8, 10, 70], [5, 90, 5]]);
1080 let with_idx = postprocess_boxes_multilabel_index_quant::<XYXY, _, _>(
1081 50,
1082 boxes.view(),
1083 scores.view(),
1084 unit_quant(),
1085 )
1086 .unwrap();
1087 let without = postprocess_boxes_multilabel_quant::<XYXY, _, _>(
1088 50,
1089 boxes.view(),
1090 scores.view(),
1091 unit_quant(),
1092 )
1093 .unwrap();
1094 let stripped: Vec<_> = with_idx.into_iter().map(|(b, _)| b).collect();
1095 assert_eq!(stripped, without);
1096 }
1097
1098 fn multilabel_layouts_agree<S>(n_classes: usize, n_candidates: usize, threshold: S)
1103 where
1104 S: PrimInt + AsPrimitive<f32> + Send + Sync + std::fmt::Debug,
1105 usize: AsPrimitive<S>,
1106 {
1107 let mut scores_physical = Array2::<S>::zeros((n_classes, n_candidates));
1108 for c in 0..n_classes {
1109 for i in 0..n_candidates {
1110 scores_physical[[c, i]] = ((c * 3 + i * 7) % 100).as_();
1111 }
1112 }
1113 let mut boxes_physical = Array2::<i16>::zeros((4, n_candidates));
1114 for i in 0..n_candidates {
1115 boxes_physical[[0, i]] = (i * 10) as i16;
1116 boxes_physical[[1, i]] = (i * 20) as i16;
1117 boxes_physical[[2, i]] = (i * 10 + 50) as i16;
1118 boxes_physical[[3, i]] = (i * 20 + 100) as i16;
1119 }
1120 let quant = Quantization {
1121 scale: 0.00390625,
1122 zero_point: 0,
1123 };
1124 let scores_rm = scores_physical.clone().reversed_axes().to_owned();
1125 let boxes_rm = boxes_physical.clone().reversed_axes().to_owned();
1126 let row = postprocess_boxes_multilabel_index_quant::<XYWH, _, _>(
1127 threshold,
1128 boxes_rm.view(),
1129 scores_rm.view(),
1130 quant,
1131 )
1132 .unwrap();
1133 let scores_cm = scores_physical.view().reversed_axes();
1134 assert!(scores_cm.as_slice().is_none() && scores_cm.strides()[0] == 1);
1135 let col = postprocess_boxes_multilabel_index_quant::<XYWH, _, _>(
1136 threshold,
1137 boxes_physical.view().reversed_axes(),
1138 scores_cm,
1139 quant,
1140 )
1141 .unwrap();
1142 assert!(!row.is_empty());
1143 assert_eq!(row, col);
1144 }
1145
1146 #[test]
1147 fn multilabel_column_major_matches_row_major_u8() {
1148 multilabel_layouts_agree::<u8>(80, 100, 60);
1149 }
1150
1151 #[test]
1152 fn multilabel_column_major_matches_row_major_i8_many_classes() {
1153 multilabel_layouts_agree::<i8>(300, 64, 90);
1154 }
1155
1156 #[test]
1157 fn candidate_shape_error_names_candidate_selection() {
1158 let err = check_candidate_shapes((3, 5), (3, 2)).unwrap_err();
1159 let msg = format!("{err}");
1160 assert!(msg.contains("candidate selection"), "{msg}");
1161 assert!(!msg.contains("multi-label"), "{msg}");
1162 }
1163
1164 #[test]
1165 fn column_major_matches_row_major() {
1166 let n_classes = 80usize;
1168 let n_candidates = 100usize;
1169 let mut scores_physical = Array2::<u8>::zeros((n_classes, n_candidates));
1170 for c in 0..n_classes {
1172 for i in 0..n_candidates {
1173 scores_physical[[c, i]] = ((c * 3 + i * 7) % 256) as u8;
1174 }
1175 }
1176
1177 let mut boxes_physical = Array2::<i16>::zeros((4, n_candidates));
1179 for i in 0..n_candidates {
1180 boxes_physical[[0, i]] = (i * 10) as i16; boxes_physical[[1, i]] = (i * 20) as i16; boxes_physical[[2, i]] = (i * 10 + 50) as i16; boxes_physical[[3, i]] = (i * 20 + 100) as i16; }
1185
1186 let quant = Quantization {
1187 scale: 0.00390625,
1188 zero_point: 0,
1189 };
1190
1191 let threshold: u8 = 10;
1192
1193 let scores_contiguous = scores_physical.clone().reversed_axes().to_owned();
1195 let boxes_contiguous = boxes_physical.clone().reversed_axes().to_owned();
1196 let row_result = postprocess_boxes_index_quant::<XYWH, _, _>(
1197 threshold,
1198 boxes_contiguous.view(),
1199 scores_contiguous.view(),
1200 quant,
1201 );
1202
1203 let scores_view = scores_physical.view().reversed_axes();
1205 let boxes_view = boxes_physical.view().reversed_axes();
1206 assert!(scores_view.as_slice().is_none(), "should be non-contiguous");
1207 assert_eq!(scores_view.strides()[0], 1);
1208 let col_result =
1209 postprocess_boxes_index_quant::<XYWH, _, _>(threshold, boxes_view, scores_view, quant);
1210
1211 assert_eq!(
1213 row_result.len(),
1214 col_result.len(),
1215 "different number of results: row={}, col={}",
1216 row_result.len(),
1217 col_result.len()
1218 );
1219 for (i, (row, col)) in row_result.iter().zip(col_result.iter()).enumerate() {
1220 assert_eq!(
1221 row.0.label, col.0.label,
1222 "candidate {i}: label mismatch row={} col={}",
1223 row.0.label, col.0.label
1224 );
1225 assert_eq!(row.0.score, col.0.score, "candidate {i}: score mismatch");
1226 assert_eq!(row.1, col.1, "candidate {i}: index mismatch");
1227 assert_eq!(row.0.bbox, col.0.bbox, "candidate {i}: bbox mismatch");
1228 }
1229 }
1230
1231 #[test]
1233 fn column_major_matches_row_major_i8() {
1234 let n_classes = 80usize;
1235 let n_candidates = 50usize;
1236 let mut scores_physical = Array2::<i8>::zeros((n_classes, n_candidates));
1237 for c in 0..n_classes {
1238 for i in 0..n_candidates {
1239 scores_physical[[c, i]] = ((c as i16 * 3 + i as i16 * 7) % 256 - 128) as i8;
1240 }
1241 }
1242
1243 let mut boxes_physical = Array2::<i16>::zeros((4, n_candidates));
1244 for i in 0..n_candidates {
1245 boxes_physical[[0, i]] = (i * 10) as i16;
1246 boxes_physical[[1, i]] = (i * 20) as i16;
1247 boxes_physical[[2, i]] = (i * 10 + 50) as i16;
1248 boxes_physical[[3, i]] = (i * 20 + 100) as i16;
1249 }
1250
1251 let quant = Quantization {
1252 scale: 0.0256,
1253 zero_point: -116,
1254 };
1255 let threshold: i8 = -100;
1256
1257 let scores_contiguous = scores_physical.clone().reversed_axes().to_owned();
1258 let boxes_contiguous = boxes_physical.clone().reversed_axes().to_owned();
1259 let row_result = postprocess_boxes_index_quant::<XYWH, _, _>(
1260 threshold,
1261 boxes_contiguous.view(),
1262 scores_contiguous.view(),
1263 quant,
1264 );
1265
1266 let scores_view = scores_physical.view().reversed_axes();
1267 let boxes_view = boxes_physical.view().reversed_axes();
1268 let col_result =
1269 postprocess_boxes_index_quant::<XYWH, _, _>(threshold, boxes_view, scores_view, quant);
1270
1271 assert_eq!(row_result.len(), col_result.len());
1272 for (i, (row, col)) in row_result.iter().zip(col_result.iter()).enumerate() {
1273 assert_eq!(row.0.label, col.0.label, "i8 candidate {i}: label mismatch");
1274 assert_eq!(row.0.score, col.0.score, "i8 candidate {i}: score mismatch");
1275 assert_eq!(row.1, col.1, "i8 candidate {i}: index mismatch");
1276 }
1277 }
1278
1279 fn make_nms_boxes_int(n: usize) -> Vec<DetectBoxQuantized<u8>> {
1281 (0..n)
1282 .map(|i| DetectBoxQuantized {
1283 bbox: BoundingBox {
1284 xmin: i as f32 * 100.0,
1285 ymin: 0.0,
1286 xmax: i as f32 * 100.0 + 10.0,
1287 ymax: 10.0,
1288 },
1289 label: 0,
1290 score: (200 - i as u32).min(255) as u8,
1291 })
1292 .collect()
1293 }
1294
1295 fn probe_boxes(n: usize) -> Array2<i16> {
1298 let mut boxes = Array2::<i16>::zeros((n, 4));
1299 for i in 0..n {
1300 boxes[[i, 0]] = i as i16 * 10;
1301 boxes[[i, 1]] = i as i16 * 10 + 1;
1302 boxes[[i, 2]] = i as i16 * 10 + 2;
1303 boxes[[i, 3]] = i as i16 * 10 + 3;
1304 }
1305 boxes
1306 }
1307
1308 fn probe_quant() -> Quantization {
1309 Quantization {
1310 scale: 1.0,
1311 zero_point: 0,
1312 }
1313 }
1314
1315 #[test]
1319 fn postprocess_boxes_quant_keeps_a_score_equal_to_the_threshold() {
1320 let boxes = probe_boxes(2);
1321 let scores = ndarray::array![[200_u8, 0], [199, 0]];
1322
1323 let kept = postprocess_boxes_quant::<XYXY, _, _>(
1324 200_u8,
1325 boxes.view(),
1326 scores.view(),
1327 probe_quant(),
1328 );
1329 assert_eq!(kept.len(), 1, "{kept:?}");
1330 assert_eq!(kept[0].score, 200);
1331
1332 let kept = postprocess_boxes_index_quant::<XYXY, _, _>(
1333 200_u8,
1334 boxes.view(),
1335 scores.view(),
1336 probe_quant(),
1337 );
1338 assert_eq!(kept.len(), 1, "{kept:?}");
1339 assert_eq!(kept[0].1, 0);
1340 }
1341
1342 #[test]
1346 fn column_major_falls_back_beyond_255_classes() {
1347 let n_classes = 300usize;
1348 let boxes = probe_boxes(2);
1349 let mut scores = Array2::<u8>::zeros((2, n_classes));
1350 scores[[0, 260]] = 200;
1351 scores[[1, 3]] = 10;
1352
1353 let kept = postprocess_boxes_quant_column_major::<XYXY, _, _>(
1354 200_u8,
1355 boxes.view(),
1356 scores.view(),
1357 probe_quant(),
1358 );
1359 assert_eq!(kept.len(), 1, "{kept:?}");
1360 assert_eq!(kept[0].label, 260);
1361 assert_eq!(kept[0].score, 200);
1362
1363 let kept = postprocess_boxes_index_quant_column_major::<XYXY, _, _>(
1364 200_u8,
1365 boxes.view(),
1366 scores.view(),
1367 probe_quant(),
1368 );
1369 assert_eq!(kept.len(), 1, "{kept:?}");
1370 assert_eq!(kept[0].0.label, 260);
1371 assert_eq!(kept[0].1, 0);
1372 }
1373
1374 #[test]
1379 fn column_major_scan_handles_row_major_input() {
1380 let boxes = probe_boxes(3);
1381 let scores = ndarray::array![[10_u8, 90, 30, 40], [5, 6, 7, 200], [1, 2, 3, 4]];
1384
1385 let kept = postprocess_boxes_quant_column_major::<XYXY, _, _>(
1386 50_u8,
1387 boxes.view(),
1388 scores.view(),
1389 probe_quant(),
1390 );
1391 assert_eq!(kept.len(), 2, "{kept:?}");
1392 assert_eq!((kept[0].label, kept[0].score), (1, 90));
1393 assert_eq!((kept[1].label, kept[1].score), (3, 200));
1394 assert_eq!(kept[1].bbox.xmin, 10.0);
1396
1397 let kept = postprocess_boxes_index_quant_column_major::<XYXY, _, _>(
1398 50_u8,
1399 boxes.view(),
1400 scores.view(),
1401 probe_quant(),
1402 );
1403 assert_eq!(kept.len(), 2, "{kept:?}");
1404 assert_eq!(kept[0].1, 0);
1405 assert_eq!(kept[1].1, 1);
1406 assert_eq!(kept[1].0.label, 3);
1407 }
1408
1409 fn overlapping_pair_int() -> Vec<DetectBoxQuantized<u8>> {
1413 vec![
1414 DetectBoxQuantized {
1415 bbox: BoundingBox {
1416 xmin: 0.0,
1417 ymin: 0.0,
1418 xmax: 10.0,
1419 ymax: 10.0,
1420 },
1421 label: 0,
1422 score: 200,
1423 },
1424 DetectBoxQuantized {
1425 bbox: BoundingBox {
1426 xmin: 1.0,
1427 ymin: 1.0,
1428 xmax: 11.0,
1429 ymax: 11.0,
1430 },
1431 label: 0,
1432 score: 180,
1433 },
1434 ]
1435 }
1436
1437 fn suppression_chain_int() -> Vec<DetectBoxQuantized<u8>> {
1439 let mk = |xmin: f32, xmax: f32, score: u8| DetectBoxQuantized {
1440 bbox: BoundingBox {
1441 xmin,
1442 ymin: 0.0,
1443 xmax,
1444 ymax: 10.0,
1445 },
1446 label: 0,
1447 score,
1448 };
1449 vec![mk(0.0, 10.0, 200), mk(2.0, 12.0, 180), mk(4.0, 14.0, 160)]
1450 }
1451
1452 #[test]
1453 fn nms_class_aware_int_suppresses_same_class_without_a_cap() {
1454 let result = nms_class_aware_int(0.5, None, overlapping_pair_int());
1455 assert_eq!(result.len(), 1, "{result:?}");
1456 assert_eq!(result[0].score, 200);
1457 }
1458
1459 #[test]
1460 fn nms_class_aware_int_keeps_overlapping_boxes_of_other_classes() {
1461 let mut boxes = overlapping_pair_int();
1462 boxes[1].label = 1;
1463 let result = nms_class_aware_int(0.5, None, boxes);
1464 assert_eq!(result.len(), 2, "{result:?}");
1465 }
1466
1467 #[test]
1468 fn nms_class_aware_int_does_not_suppress_on_behalf_of_a_suppressed_box() {
1469 let result = nms_class_aware_int(0.5, None, suppression_chain_int());
1470 assert_eq!(result.len(), 2, "{result:?}");
1471 assert_eq!(result[0].score, 200);
1472 assert_eq!(result[1].score, 160);
1473 }
1474
1475 #[test]
1479 fn nms_class_aware_int_drops_boxes_already_at_the_minimum_score() {
1480 let mut boxes = overlapping_pair_int();
1481 boxes.push(DetectBoxQuantized {
1482 bbox: BoundingBox {
1483 xmin: 50.0,
1484 ymin: 50.0,
1485 xmax: 60.0,
1486 ymax: 60.0,
1487 },
1488 label: 0,
1489 score: u8::MIN,
1490 });
1491 let result = nms_class_aware_int(0.5, None, boxes);
1492 assert_eq!(result.len(), 1, "{result:?}");
1493 assert_eq!(result[0].score, 200);
1494 }
1495
1496 fn isolated_then_overlapping_pair_int() -> Vec<DetectBoxQuantized<u8>> {
1502 let mk = |xmin: f32, xmax: f32, score: u8| DetectBoxQuantized {
1503 bbox: BoundingBox {
1504 xmin,
1505 ymin: 0.0,
1506 xmax,
1507 ymax: 10.0,
1508 },
1509 label: 0,
1510 score,
1511 };
1512 vec![
1513 mk(100.0, 110.0, 200),
1514 mk(0.0, 10.0, 180),
1515 mk(1.0, 11.0, 160),
1516 ]
1517 }
1518
1519 #[test]
1520 fn nms_class_aware_int_keeps_suppressing_after_the_first_survivor() {
1521 let result = nms_class_aware_int(0.5, None, isolated_then_overlapping_pair_int());
1522 assert_eq!(result.len(), 2, "{result:?}");
1523 assert_eq!(result[0].score, 200);
1524 assert_eq!(result[1].score, 180);
1525 }
1526
1527 fn with_index_int(boxes: Vec<DetectBoxQuantized<u8>>) -> Vec<(DetectBoxQuantized<u8>, usize)> {
1528 boxes.into_iter().enumerate().map(|(i, b)| (b, i)).collect()
1529 }
1530
1531 #[test]
1532 fn nms_extra_class_aware_int_keeps_suppressing_after_the_first_survivor() {
1533 let result = nms_extra_class_aware_int(
1534 0.5,
1535 None,
1536 with_index_int(isolated_then_overlapping_pair_int()),
1537 );
1538 assert_eq!(result.len(), 2, "{result:?}");
1539 assert_eq!(result[1].1, 1);
1540 }
1541
1542 #[test]
1543 fn nms_extra_class_aware_int_suppresses_same_class_without_a_cap() {
1544 let result = nms_extra_class_aware_int(0.5, None, with_index_int(overlapping_pair_int()));
1545 assert_eq!(result.len(), 1, "{result:?}");
1546 assert_eq!(result[0].0.score, 200);
1547 assert_eq!(result[0].1, 0, "the surviving box keeps its extra data");
1548 }
1549
1550 #[test]
1551 fn nms_extra_class_aware_int_keeps_overlapping_boxes_of_other_classes() {
1552 let mut boxes = overlapping_pair_int();
1553 boxes[1].label = 1;
1554 let result = nms_extra_class_aware_int(0.5, None, with_index_int(boxes));
1555 assert_eq!(result.len(), 2, "{result:?}");
1556 }
1557
1558 #[test]
1559 fn nms_extra_class_aware_int_does_not_suppress_on_behalf_of_a_suppressed_box() {
1560 let result = nms_extra_class_aware_int(0.5, None, with_index_int(suppression_chain_int()));
1561 assert_eq!(result.len(), 2, "{result:?}");
1562 assert_eq!(result[0].1, 0);
1563 assert_eq!(result[1].1, 2);
1564 }
1565
1566 #[test]
1567 fn nms_extra_class_aware_int_drops_boxes_already_at_the_minimum_score() {
1568 let mut boxes = overlapping_pair_int();
1569 boxes.push(DetectBoxQuantized {
1570 bbox: BoundingBox {
1571 xmin: 50.0,
1572 ymin: 50.0,
1573 xmax: 60.0,
1574 ymax: 60.0,
1575 },
1576 label: 0,
1577 score: u8::MIN,
1578 });
1579 let result = nms_extra_class_aware_int(0.5, None, with_index_int(boxes));
1580 assert_eq!(result.len(), 1, "{result:?}");
1581 assert_eq!(result[0].0.score, 200);
1582 }
1583
1584 #[test]
1588 fn nms_extra_class_aware_int_keeps_a_lone_box() {
1589 let boxes = with_index_int(vec![overlapping_pair_int().remove(0)]);
1590 let result = nms_extra_class_aware_int(0.5, None, boxes);
1591 assert_eq!(result.len(), 1, "{result:?}");
1592 assert_eq!(result[0].0.score, 200);
1593 }
1594
1595 #[test]
1596 fn nms_int_max_det_matches_full_truncated() {
1597 let boxes = make_nms_boxes_int(20);
1598 let n = 5;
1599 let full = nms_int(0.5, None, boxes.clone());
1600 let capped = nms_int(0.5, Some(n), boxes);
1601 assert_eq!(capped.len(), n);
1602 assert_eq!(&full[..n], &capped[..]);
1603 }
1604
1605 #[test]
1606 fn nms_int_max_det_zero_returns_empty() {
1607 let boxes = make_nms_boxes_int(10);
1608 let result = nms_int(0.5, Some(0), boxes);
1609 assert!(result.is_empty());
1610 }
1611
1612 #[test]
1613 fn nms_int_max_det_iou_ge_1_returns_sorted_truncated() {
1614 let boxes = make_nms_boxes_int(10);
1615 let result = nms_int(1.0, Some(3), boxes);
1616 assert_eq!(result.len(), 3);
1617 assert!(result[0].score >= result[1].score);
1619 assert!(result[1].score >= result[2].score);
1620 }
1621
1622 #[test]
1623 fn nms_int_max_det_larger_than_input() {
1624 let boxes = make_nms_boxes_int(5);
1625 let full = nms_int(0.5, None, boxes.clone());
1626 let capped = nms_int(0.5, Some(100), boxes);
1627 assert_eq!(full.len(), capped.len());
1628 }
1629}