1#[cfg(target_arch = "aarch64")]
19use crate::arg_max_i8;
20use crate::{
21 arg_max, float::jaccard, BBoxTypeTrait, BoundingBox, DetectBoxQuantized, Quantization,
22};
23use ndarray::{
24 parallel::prelude::{IntoParallelIterator, ParallelIterator as _},
25 Array1, ArrayView1, ArrayView2, Zip,
26};
27use num_traits::{AsPrimitive, PrimInt};
28use rayon::slice::ParallelSliceMut;
29
30#[cfg(target_arch = "aarch64")]
41unsafe fn column_max_update_neon(
42 col_ptr: *const u8,
43 max_ptr: *mut u8,
44 class_ptr: *mut u8,
45 n: usize,
46 class_idx: u8,
47 signed: bool,
48) {
49 use std::arch::aarch64::*;
50
51 let class_vec = vdupq_n_u8(class_idx);
52 let chunks = n / 16;
53 let remainder = n % 16;
54
55 if signed {
56 for chunk in 0..chunks {
58 let offset = chunk * 16;
59 let col = vld1q_s8(col_ptr.add(offset) as *const i8);
60 let cur_max = vld1q_s8(max_ptr.add(offset) as *const i8);
61 let mask = vcgeq_s8(col, cur_max);
63 let new_max = vmaxq_s8(col, cur_max);
65 vst1q_s8(max_ptr.add(offset) as *mut i8, new_max);
66 let cur_class = vld1q_u8(class_ptr.add(offset));
68 let new_class = vbslq_u8(mask, class_vec, cur_class);
69 vst1q_u8(class_ptr.add(offset), new_class);
70 }
71 for i in (chunks * 16)..n {
73 let val = *(col_ptr.add(i) as *const i8);
74 let cur = *(max_ptr.add(i) as *const i8);
75 if val >= cur {
76 *(max_ptr.add(i) as *mut i8) = val;
77 *class_ptr.add(i) = class_idx;
78 }
79 }
80 } else {
81 for chunk in 0..chunks {
83 let offset = chunk * 16;
84 let col = vld1q_u8(col_ptr.add(offset));
85 let cur_max = vld1q_u8(max_ptr.add(offset));
86 let mask = vcgeq_u8(col, cur_max);
87 let new_max = vmaxq_u8(col, cur_max);
88 vst1q_u8(max_ptr.add(offset), new_max);
89 let cur_class = vld1q_u8(class_ptr.add(offset));
90 let new_class = vbslq_u8(mask, class_vec, cur_class);
91 vst1q_u8(class_ptr.add(offset), new_class);
92 }
93 for i in (chunks * 16)..n {
95 let val = *col_ptr.add(i);
96 let cur = *max_ptr.add(i);
97 if val >= cur {
98 *max_ptr.add(i) = val;
99 *class_ptr.add(i) = class_idx;
100 }
101 }
102 }
103 let _ = remainder; }
105
106#[cfg(target_arch = "aarch64")]
119#[allow(dead_code)] unsafe fn column_max_update_neon_prefetch(
121 col_ptr: *const u8,
122 max_ptr: *mut u8,
123 class_ptr: *mut u8,
124 n: usize,
125 class_idx: u8,
126 signed: bool,
127) {
128 use std::arch::aarch64::*;
129
130 const PREFETCH_AHEAD: usize = 128; let class_vec = vdupq_n_u8(class_idx);
133 let chunks = n / 16;
134
135 if signed {
136 for chunk in 0..chunks {
137 let offset = chunk * 16;
138 if offset + PREFETCH_AHEAD < n {
140 core::arch::asm!(
141 "prfm pldl1strm, [{ptr}]",
142 ptr = in(reg) col_ptr.add(offset + PREFETCH_AHEAD),
143 options(nostack, preserves_flags),
144 );
145 }
146 let col = vld1q_s8(col_ptr.add(offset) as *const i8);
147 let cur_max = vld1q_s8(max_ptr.add(offset) as *const i8);
148 let mask = vcgeq_s8(col, cur_max);
149 let new_max = vmaxq_s8(col, cur_max);
150 vst1q_s8(max_ptr.add(offset) as *mut i8, new_max);
151 let cur_class = vld1q_u8(class_ptr.add(offset));
152 let new_class = vbslq_u8(mask, class_vec, cur_class);
153 vst1q_u8(class_ptr.add(offset), new_class);
154 }
155 for i in (chunks * 16)..n {
156 let val = *(col_ptr.add(i) as *const i8);
157 let cur = *(max_ptr.add(i) as *const i8);
158 if val >= cur {
159 *(max_ptr.add(i) as *mut i8) = val;
160 *class_ptr.add(i) = class_idx;
161 }
162 }
163 } else {
164 for chunk in 0..chunks {
165 let offset = chunk * 16;
166 if offset + PREFETCH_AHEAD < n {
167 core::arch::asm!(
168 "prfm pldl1strm, [{ptr}]",
169 ptr = in(reg) col_ptr.add(offset + PREFETCH_AHEAD),
170 options(nostack, preserves_flags),
171 );
172 }
173 let col = vld1q_u8(col_ptr.add(offset));
174 let cur_max = vld1q_u8(max_ptr.add(offset));
175 let mask = vcgeq_u8(col, cur_max);
176 let new_max = vmaxq_u8(col, cur_max);
177 vst1q_u8(max_ptr.add(offset), new_max);
178 let cur_class = vld1q_u8(class_ptr.add(offset));
179 let new_class = vbslq_u8(mask, class_vec, cur_class);
180 vst1q_u8(class_ptr.add(offset), new_class);
181 }
182 for i in (chunks * 16)..n {
183 let val = *col_ptr.add(i);
184 let cur = *max_ptr.add(i);
185 if val >= cur {
186 *max_ptr.add(i) = val;
187 *class_ptr.add(i) = class_idx;
188 }
189 }
190 }
191}
192
193#[inline(always)]
195fn fast_arg_max<T: PrimInt + Copy>(score: ArrayView1<T>) -> (T, usize) {
196 #[cfg(target_arch = "aarch64")]
197 {
198 if std::mem::size_of::<T>() == 1 && score.as_slice().is_some() {
200 let slice = score.as_slice().unwrap();
201 let ptr = slice.as_ptr() as *const i8;
205 let i8_slice = unsafe { std::slice::from_raw_parts(ptr, slice.len()) };
206 if T::min_value() < T::zero() {
209 let (max_val, idx) = arg_max_i8(i8_slice);
210 let result: T = unsafe { std::mem::transmute_copy(&max_val) };
213 return (result, idx);
214 }
215 }
216 }
217 arg_max(score)
218}
219
220#[doc(hidden)]
225pub fn postprocess_boxes_quant<
226 B: BBoxTypeTrait,
227 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
228 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
229>(
230 threshold: Scores,
231 boxes: ArrayView2<Boxes>,
232 scores: ArrayView2<Scores>,
233 quant_boxes: Quantization,
234) -> Vec<DetectBoxQuantized<Scores>> {
235 assert_eq!(scores.dim().0, boxes.dim().0);
236 assert_eq!(boxes.dim().1, 4);
237
238 if scores.strides()[0] == 1 && scores.as_slice().is_none() {
240 return postprocess_boxes_quant_column_major::<B, _, _>(
241 threshold,
242 boxes,
243 scores,
244 quant_boxes,
245 );
246 }
247
248 Zip::from(scores.rows())
249 .and(boxes.rows())
250 .into_par_iter()
251 .filter_map(|(score, bbox)| {
252 let (score_, label) = fast_arg_max(score);
253 if score_ < threshold {
254 return None;
255 }
256
257 let bbox_quant = B::ndarray_to_xyxy_dequant(bbox.view(), quant_boxes);
258 Some(DetectBoxQuantized {
259 label,
260 score: score_,
261 bbox: BoundingBox::from(bbox_quant),
262 })
263 })
264 .collect()
265}
266
267fn postprocess_boxes_quant_column_major<
269 B: BBoxTypeTrait,
270 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
271 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
272>(
273 threshold: Scores,
274 boxes: ArrayView2<Boxes>,
275 scores: ArrayView2<Scores>,
276 quant_boxes: Quantization,
277) -> Vec<DetectBoxQuantized<Scores>> {
278 let (n_candidates, n_classes) = scores.dim();
279
280 if n_classes > 255 {
282 return Zip::from(scores.rows())
283 .and(boxes.rows())
284 .into_par_iter()
285 .filter_map(|(score, bbox)| {
286 let (score_, label) = fast_arg_max(score);
287 if score_ < threshold {
288 return None;
289 }
290 let bbox_quant = B::ndarray_to_xyxy_dequant(bbox.view(), quant_boxes);
291 Some(DetectBoxQuantized {
292 label,
293 score: score_,
294 bbox: BoundingBox::from(bbox_quant),
295 })
296 })
297 .collect();
298 }
299 let mut max_scores = vec![Scores::min_value(); n_candidates];
300 let mut max_classes = vec![0u8; n_candidates];
301
302 for class_idx in 0..n_classes {
303 let col = scores.column(class_idx);
304 if let Some(slice) = col.as_slice() {
305 #[cfg(target_arch = "aarch64")]
306 {
307 if std::mem::size_of::<Scores>() == 1 {
308 unsafe {
309 column_max_update_neon(
312 slice.as_ptr() as *const u8,
313 max_scores.as_mut_ptr() as *mut u8,
314 max_classes.as_mut_ptr(),
315 n_candidates,
316 class_idx as u8,
317 Scores::min_value() < Scores::zero(),
318 );
319 }
320 continue;
321 }
322 }
323 for (i, &val) in slice.iter().enumerate() {
324 if val >= max_scores[i] {
325 max_scores[i] = val;
326 max_classes[i] = class_idx as u8;
327 }
328 }
329 } else {
330 for (i, &val) in col.iter().enumerate() {
331 if val >= max_scores[i] {
332 max_scores[i] = val;
333 max_classes[i] = class_idx as u8;
334 }
335 }
336 }
337 }
338
339 let boxes_buf: [Vec<Boxes>; 4] = if boxes.strides()[0] == 1 && boxes.as_slice().is_none() {
341 let mut cols: [Vec<Boxes>; 4] = [
342 vec![Boxes::zero(); n_candidates],
343 vec![Boxes::zero(); n_candidates],
344 vec![Boxes::zero(); n_candidates],
345 vec![Boxes::zero(); n_candidates],
346 ];
347 for (dim, col_buf) in cols.iter_mut().enumerate() {
348 let col = boxes.column(dim);
349 if let Some(slice) = col.as_slice() {
350 col_buf.copy_from_slice(slice);
351 } else {
352 for (i, &val) in col.iter().enumerate() {
353 col_buf[i] = val;
354 }
355 }
356 }
357 cols
358 } else {
359 [vec![], vec![], vec![], vec![]]
360 };
361 let boxes_copied = !boxes_buf[0].is_empty();
362
363 let mut result = Vec::new();
364 for i in 0..n_candidates {
365 if max_scores[i] >= threshold {
366 let bbox_quant = if boxes_copied {
367 let raw = [
368 boxes_buf[0][i],
369 boxes_buf[1][i],
370 boxes_buf[2][i],
371 boxes_buf[3][i],
372 ];
373 B::to_xyxy_dequant(&raw, quant_boxes)
374 } else {
375 B::ndarray_to_xyxy_dequant(boxes.row(i), quant_boxes)
376 };
377 result.push(DetectBoxQuantized {
378 label: max_classes[i] as usize,
379 score: max_scores[i],
380 bbox: BoundingBox::from(bbox_quant),
381 });
382 }
383 }
384
385 result
386}
387
388#[doc(hidden)]
400pub fn postprocess_boxes_index_quant<
401 B: BBoxTypeTrait,
402 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
403 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
404>(
405 threshold: Scores,
406 boxes: ArrayView2<Boxes>,
407 scores: ArrayView2<Scores>,
408 quant_boxes: Quantization,
409) -> Vec<(DetectBoxQuantized<Scores>, usize)> {
410 assert_eq!(scores.dim().0, boxes.dim().0);
411 assert_eq!(boxes.dim().1, 4);
412
413 if scores.strides()[0] == 1 && scores.as_slice().is_none() {
417 return postprocess_boxes_index_quant_column_major::<B, _, _>(
418 threshold,
419 boxes,
420 scores,
421 quant_boxes,
422 );
423 }
424
425 let indices: Array1<usize> = (0..boxes.dim().0).collect();
426 Zip::from(scores.rows())
427 .and(boxes.rows())
428 .and(&indices)
429 .into_par_iter()
430 .filter_map(|(score, bbox, index)| {
431 let (score_, label) = fast_arg_max(score);
432 if score_ < threshold {
433 return None;
434 }
435
436 let bbox_quant = B::ndarray_to_xyxy_dequant(bbox.view(), quant_boxes);
437
438 Some((
439 DetectBoxQuantized {
440 label,
441 score: score_,
442 bbox: BoundingBox::from(bbox_quant),
443 },
444 *index,
445 ))
446 })
447 .collect()
448}
449
450fn postprocess_boxes_index_quant_column_major<
458 B: BBoxTypeTrait,
459 Boxes: PrimInt + AsPrimitive<f32> + Send + Sync,
460 Scores: PrimInt + AsPrimitive<f32> + Send + Sync,
461>(
462 threshold: Scores,
463 boxes: ArrayView2<Boxes>,
464 scores: ArrayView2<Scores>,
465 quant_boxes: Quantization,
466) -> Vec<(DetectBoxQuantized<Scores>, usize)> {
467 let (n_candidates, n_classes) = scores.dim();
468
469 if n_classes > 255 {
473 let indices: Array1<usize> = (0..n_candidates).collect();
474 return Zip::from(scores.rows())
475 .and(boxes.rows())
476 .and(&indices)
477 .into_par_iter()
478 .filter_map(|(score, bbox, index)| {
479 let (score_, label) = fast_arg_max(score);
480 if score_ < threshold {
481 return None;
482 }
483 let bbox_quant = B::ndarray_to_xyxy_dequant(bbox.view(), quant_boxes);
484 Some((
485 DetectBoxQuantized {
486 label,
487 score: score_,
488 bbox: BoundingBox::from(bbox_quant),
489 },
490 *index,
491 ))
492 })
493 .collect();
494 }
495 let mut max_scores = vec![Scores::min_value(); n_candidates];
496 let mut max_classes = vec![0u8; n_candidates];
497 for class_idx in 0..n_classes {
498 let col = scores.column(class_idx);
499 if let Some(slice) = col.as_slice() {
500 #[cfg(target_arch = "aarch64")]
502 {
503 if std::mem::size_of::<Scores>() == 1 {
504 unsafe {
507 column_max_update_neon(
508 slice.as_ptr() as *const u8,
509 max_scores.as_mut_ptr() as *mut u8,
510 max_classes.as_mut_ptr(),
511 n_candidates,
512 class_idx as u8,
513 Scores::min_value() < Scores::zero(), );
515 }
516 continue;
517 }
518 }
519 for (i, &val) in slice.iter().enumerate() {
520 if val >= max_scores[i] {
521 max_scores[i] = val;
522 max_classes[i] = class_idx as u8;
523 }
524 }
525 } else {
526 for (i, &val) in col.iter().enumerate() {
527 if val >= max_scores[i] {
528 max_scores[i] = val;
529 max_classes[i] = class_idx as u8;
530 }
531 }
532 }
533 }
534
535 let boxes_buf: [Vec<Boxes>; 4] = if boxes.strides()[0] == 1 && boxes.as_slice().is_none() {
539 let mut cols: [Vec<Boxes>; 4] = [
540 vec![Boxes::zero(); n_candidates],
541 vec![Boxes::zero(); n_candidates],
542 vec![Boxes::zero(); n_candidates],
543 vec![Boxes::zero(); n_candidates],
544 ];
545 for (dim, col_buf) in cols.iter_mut().enumerate() {
546 let col = boxes.column(dim);
547 if let Some(slice) = col.as_slice() {
548 col_buf.copy_from_slice(slice);
549 } else {
550 for (i, &val) in col.iter().enumerate() {
551 col_buf[i] = val;
552 }
553 }
554 }
555 cols
556 } else {
557 [vec![], vec![], vec![], vec![]]
559 };
560 let boxes_copied = !boxes_buf[0].is_empty();
561
562 let mut result = Vec::new();
564 for i in 0..n_candidates {
565 if max_scores[i] >= threshold {
566 let bbox_quant = if boxes_copied {
567 let raw = [
568 boxes_buf[0][i],
569 boxes_buf[1][i],
570 boxes_buf[2][i],
571 boxes_buf[3][i],
572 ];
573 B::to_xyxy_dequant(&raw, quant_boxes)
574 } else {
575 B::ndarray_to_xyxy_dequant(boxes.row(i), quant_boxes)
576 };
577 result.push((
578 DetectBoxQuantized {
579 label: max_classes[i] as usize,
580 score: max_scores[i],
581 bbox: BoundingBox::from(bbox_quant),
582 },
583 i,
584 ));
585 }
586 }
587
588 result
589}
590
591#[doc(hidden)]
594#[must_use]
595pub fn nms_int<SCORE: PrimInt + AsPrimitive<f32> + Send + Sync>(
596 iou: f32,
597 max_det: Option<usize>,
598 mut boxes: Vec<DetectBoxQuantized<SCORE>>,
599) -> Vec<DetectBoxQuantized<SCORE>> {
600 boxes.par_sort_by(|a, b| b.score.cmp(&a.score));
604
605 if iou >= 1.0 {
608 return match max_det {
609 Some(n) => {
610 boxes.truncate(n);
611 boxes
612 }
613 None => boxes,
614 };
615 }
616
617 let min_val = SCORE::min_value();
618 let cap = max_det.unwrap_or(usize::MAX);
619 let mut survivors: usize = 0;
620 for i in 0..boxes.len() {
622 if boxes[i].score <= min_val {
623 continue;
625 }
626 for j in (i + 1)..boxes.len() {
627 if boxes[j].score <= min_val {
630 continue;
632 }
633
634 if jaccard(&boxes[j].bbox, &boxes[i].bbox, iou) {
635 boxes[j].score = min_val;
637 }
638 }
639 survivors += 1;
640 if survivors >= cap {
641 break;
642 }
643 }
644 boxes
647 .into_iter()
648 .filter(|b| b.score > min_val)
649 .take(cap)
650 .collect()
651}
652
653#[doc(hidden)]
659#[must_use]
660pub fn nms_extra_int<SCORE: PrimInt + AsPrimitive<f32> + Send + Sync, E: Send + Sync>(
661 iou: f32,
662 max_det: Option<usize>,
663 mut boxes: Vec<(DetectBoxQuantized<SCORE>, E)>,
664) -> Vec<(DetectBoxQuantized<SCORE>, E)> {
665 boxes.par_sort_by(|a, b| b.0.score.cmp(&a.0.score));
668
669 if iou >= 1.0 {
672 return match max_det {
673 Some(n) => {
674 boxes.truncate(n);
675 boxes
676 }
677 None => boxes,
678 };
679 }
680
681 let min_val = SCORE::min_value();
682 let cap = max_det.unwrap_or(usize::MAX);
683 let mut survivors: usize = 0;
684 for i in 0..boxes.len() {
686 if boxes[i].0.score <= min_val {
687 continue;
689 }
690 for j in (i + 1)..boxes.len() {
691 if boxes[j].0.score <= min_val {
694 continue;
696 }
697 if jaccard(&boxes[j].0.bbox, &boxes[i].0.bbox, iou) {
698 boxes[j].0.score = min_val;
700 }
701 }
702 survivors += 1;
703 if survivors >= cap {
704 break;
705 }
706 }
707
708 boxes
710 .into_iter()
711 .filter(|b| b.0.score > min_val)
712 .take(cap)
713 .collect()
714}
715
716#[doc(hidden)]
723#[must_use]
724pub fn nms_class_aware_int<SCORE: PrimInt + AsPrimitive<f32> + Send + Sync>(
725 iou: f32,
726 max_det: Option<usize>,
727 mut boxes: Vec<DetectBoxQuantized<SCORE>>,
728) -> Vec<DetectBoxQuantized<SCORE>> {
729 boxes.par_sort_by(|a, b| b.score.cmp(&a.score));
730
731 if iou >= 1.0 {
734 return match max_det {
735 Some(n) => {
736 boxes.truncate(n);
737 boxes
738 }
739 None => boxes,
740 };
741 }
742
743 let min_val = SCORE::min_value();
744 let cap = max_det.unwrap_or(usize::MAX);
745 let mut survivors: usize = 0;
746 for i in 0..boxes.len() {
747 if boxes[i].score <= min_val {
748 continue;
749 }
750 for j in (i + 1)..boxes.len() {
751 if boxes[j].score <= min_val {
752 continue;
753 }
754 if boxes[j].label == boxes[i].label && jaccard(&boxes[j].bbox, &boxes[i].bbox, iou) {
756 boxes[j].score = min_val;
757 }
758 }
759 survivors += 1;
760 if survivors >= cap {
761 break;
762 }
763 }
764 boxes
765 .into_iter()
766 .filter(|b| b.score > min_val)
767 .take(cap)
768 .collect()
769}
770
771#[doc(hidden)]
777#[must_use]
778pub fn nms_extra_class_aware_int<
779 SCORE: PrimInt + AsPrimitive<f32> + Send + Sync,
780 E: Send + Sync,
781>(
782 iou: f32,
783 max_det: Option<usize>,
784 mut boxes: Vec<(DetectBoxQuantized<SCORE>, E)>,
785) -> Vec<(DetectBoxQuantized<SCORE>, E)> {
786 boxes.par_sort_by(|a, b| b.0.score.cmp(&a.0.score));
787
788 if iou >= 1.0 {
791 return match max_det {
792 Some(n) => {
793 boxes.truncate(n);
794 boxes
795 }
796 None => boxes,
797 };
798 }
799
800 let min_val = SCORE::min_value();
801 let cap = max_det.unwrap_or(usize::MAX);
802 let mut survivors: usize = 0;
803 for i in 0..boxes.len() {
804 if boxes[i].0.score <= min_val {
805 continue;
806 }
807 for j in (i + 1)..boxes.len() {
808 if boxes[j].0.score <= min_val {
809 continue;
810 }
811 if boxes[j].0.label == boxes[i].0.label
813 && jaccard(&boxes[j].0.bbox, &boxes[i].0.bbox, iou)
814 {
815 boxes[j].0.score = min_val;
816 }
817 }
818 survivors += 1;
819 if survivors >= cap {
820 break;
821 }
822 }
823 boxes
824 .into_iter()
825 .filter(|b| b.0.score > min_val)
826 .take(cap)
827 .collect()
828}
829
830#[doc(hidden)]
845pub fn quantize_score_threshold<T: PrimInt + AsPrimitive<f32>>(score: f32, quant: Quantization) -> T
846where
847 f32: AsPrimitive<T>,
848{
849 if quant.scale == 0.0 {
850 return T::max_value();
851 }
852 let v = (score / quant.scale + quant.zero_point as f32).ceil();
853 let v = v.clamp(T::min_value().as_(), T::max_value().as_());
854 v.as_()
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use crate::XYWH;
861 use ndarray::Array2;
862
863 #[test]
866 fn column_major_matches_row_major() {
867 let n_classes = 80usize;
869 let n_candidates = 100usize;
870 let mut scores_physical = Array2::<u8>::zeros((n_classes, n_candidates));
871 for c in 0..n_classes {
873 for i in 0..n_candidates {
874 scores_physical[[c, i]] = ((c * 3 + i * 7) % 256) as u8;
875 }
876 }
877
878 let mut boxes_physical = Array2::<i16>::zeros((4, n_candidates));
880 for i in 0..n_candidates {
881 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; }
886
887 let quant = Quantization {
888 scale: 0.00390625,
889 zero_point: 0,
890 };
891
892 let threshold: u8 = 10;
893
894 let scores_contiguous = scores_physical.clone().reversed_axes().to_owned();
896 let boxes_contiguous = boxes_physical.clone().reversed_axes().to_owned();
897 let row_result = postprocess_boxes_index_quant::<XYWH, _, _>(
898 threshold,
899 boxes_contiguous.view(),
900 scores_contiguous.view(),
901 quant,
902 );
903
904 let scores_view = scores_physical.view().reversed_axes();
906 let boxes_view = boxes_physical.view().reversed_axes();
907 assert!(scores_view.as_slice().is_none(), "should be non-contiguous");
908 assert_eq!(scores_view.strides()[0], 1);
909 let col_result =
910 postprocess_boxes_index_quant::<XYWH, _, _>(threshold, boxes_view, scores_view, quant);
911
912 assert_eq!(
914 row_result.len(),
915 col_result.len(),
916 "different number of results: row={}, col={}",
917 row_result.len(),
918 col_result.len()
919 );
920 for (i, (row, col)) in row_result.iter().zip(col_result.iter()).enumerate() {
921 assert_eq!(
922 row.0.label, col.0.label,
923 "candidate {i}: label mismatch row={} col={}",
924 row.0.label, col.0.label
925 );
926 assert_eq!(row.0.score, col.0.score, "candidate {i}: score mismatch");
927 assert_eq!(row.1, col.1, "candidate {i}: index mismatch");
928 assert_eq!(row.0.bbox, col.0.bbox, "candidate {i}: bbox mismatch");
929 }
930 }
931
932 #[test]
934 fn column_major_matches_row_major_i8() {
935 let n_classes = 80usize;
936 let n_candidates = 50usize;
937 let mut scores_physical = Array2::<i8>::zeros((n_classes, n_candidates));
938 for c in 0..n_classes {
939 for i in 0..n_candidates {
940 scores_physical[[c, i]] = ((c as i16 * 3 + i as i16 * 7) % 256 - 128) as i8;
941 }
942 }
943
944 let mut boxes_physical = Array2::<i16>::zeros((4, n_candidates));
945 for i in 0..n_candidates {
946 boxes_physical[[0, i]] = (i * 10) as i16;
947 boxes_physical[[1, i]] = (i * 20) as i16;
948 boxes_physical[[2, i]] = (i * 10 + 50) as i16;
949 boxes_physical[[3, i]] = (i * 20 + 100) as i16;
950 }
951
952 let quant = Quantization {
953 scale: 0.0256,
954 zero_point: -116,
955 };
956 let threshold: i8 = -100;
957
958 let scores_contiguous = scores_physical.clone().reversed_axes().to_owned();
959 let boxes_contiguous = boxes_physical.clone().reversed_axes().to_owned();
960 let row_result = postprocess_boxes_index_quant::<XYWH, _, _>(
961 threshold,
962 boxes_contiguous.view(),
963 scores_contiguous.view(),
964 quant,
965 );
966
967 let scores_view = scores_physical.view().reversed_axes();
968 let boxes_view = boxes_physical.view().reversed_axes();
969 let col_result =
970 postprocess_boxes_index_quant::<XYWH, _, _>(threshold, boxes_view, scores_view, quant);
971
972 assert_eq!(row_result.len(), col_result.len());
973 for (i, (row, col)) in row_result.iter().zip(col_result.iter()).enumerate() {
974 assert_eq!(row.0.label, col.0.label, "i8 candidate {i}: label mismatch");
975 assert_eq!(row.0.score, col.0.score, "i8 candidate {i}: score mismatch");
976 assert_eq!(row.1, col.1, "i8 candidate {i}: index mismatch");
977 }
978 }
979
980 fn make_nms_boxes_int(n: usize) -> Vec<DetectBoxQuantized<u8>> {
982 (0..n)
983 .map(|i| DetectBoxQuantized {
984 bbox: BoundingBox {
985 xmin: i as f32 * 100.0,
986 ymin: 0.0,
987 xmax: i as f32 * 100.0 + 10.0,
988 ymax: 10.0,
989 },
990 label: 0,
991 score: (200 - i as u32).min(255) as u8,
992 })
993 .collect()
994 }
995
996 #[test]
997 fn nms_int_max_det_matches_full_truncated() {
998 let boxes = make_nms_boxes_int(20);
999 let n = 5;
1000 let full = nms_int(0.5, None, boxes.clone());
1001 let capped = nms_int(0.5, Some(n), boxes);
1002 assert_eq!(capped.len(), n);
1003 assert_eq!(&full[..n], &capped[..]);
1004 }
1005
1006 #[test]
1007 fn nms_int_max_det_zero_returns_empty() {
1008 let boxes = make_nms_boxes_int(10);
1009 let result = nms_int(0.5, Some(0), boxes);
1010 assert!(result.is_empty());
1011 }
1012
1013 #[test]
1014 fn nms_int_max_det_iou_ge_1_returns_sorted_truncated() {
1015 let boxes = make_nms_boxes_int(10);
1016 let result = nms_int(1.0, Some(3), boxes);
1017 assert_eq!(result.len(), 3);
1018 assert!(result[0].score >= result[1].score);
1020 assert!(result[1].score >= result[2].score);
1021 }
1022
1023 #[test]
1024 fn nms_int_max_det_larger_than_input() {
1025 let boxes = make_nms_boxes_int(5);
1026 let full = nms_int(0.5, None, boxes.clone());
1027 let capped = nms_int(0.5, Some(100), boxes);
1028 assert_eq!(full.len(), capped.len());
1029 }
1030}