1use ndarray::{Array2, ArrayView2, ArrayView3};
12use num_traits::{AsPrimitive, Float, PrimInt};
13
14use crate::{
15 byte::{postprocess_boxes_multilabel_quant, postprocess_boxes_quant, quantize_score_threshold},
16 configs::{Detection, Nms},
17 dequant_detect_box,
18 float::{postprocess_boxes_float, postprocess_boxes_multilabel_float},
19 yolo::nms_and_cap,
20 BBoxTypeTrait, DecoderError, DecoderResult, DetectBox, Quantization, XYWH, XYXY,
21};
22
23#[derive(Debug, Clone, PartialEq)]
26pub(crate) struct ModelPackDetectionConfig {
27 pub(crate) anchors: Vec<[f32; 2]>,
28 pub(crate) quantization: Option<Quantization>,
29}
30
31impl TryFrom<&Detection> for ModelPackDetectionConfig {
32 type Error = DecoderError;
33
34 fn try_from(value: &Detection) -> Result<Self, DecoderError> {
35 Ok(Self {
36 anchors: value.anchors.clone().ok_or_else(|| {
37 DecoderError::InvalidConfig("ModelPack Split Detection missing anchors".to_string())
38 })?,
39 quantization: value.quantization.map(Quantization::from),
40 })
41 }
42}
43
44#[allow(clippy::too_many_arguments)]
59pub(crate) fn decode_modelpack_det<
60 BOX: PrimInt + AsPrimitive<f32> + Send + Sync,
61 SCORE: PrimInt + AsPrimitive<f32> + Send + Sync,
62>(
63 boxes_tensor: (ArrayView2<BOX>, Quantization),
64 scores_tensor: (ArrayView2<SCORE>, Quantization),
65 score_threshold: f32,
66 iou_threshold: f32,
67 nms: Option<Nms>,
68 pre_nms_top_k: usize,
69 max_det: usize,
70 multi_label: bool,
71 output_boxes: &mut Vec<DetectBox>,
72) -> DecoderResult<()>
73where
74 f32: AsPrimitive<SCORE>,
75{
76 impl_modelpack_quant::<XYXY, _, _>(
77 boxes_tensor,
78 scores_tensor,
79 score_threshold,
80 iou_threshold,
81 nms,
82 pre_nms_top_k,
83 max_det,
84 multi_label,
85 output_boxes,
86 )
87}
88
89#[allow(clippy::too_many_arguments)]
103pub(crate) fn decode_modelpack_float<
104 BOX: Float + AsPrimitive<f32> + Send + Sync,
105 SCORE: Float + AsPrimitive<f32> + Send + Sync,
106>(
107 boxes_tensor: ArrayView2<BOX>,
108 scores_tensor: ArrayView2<SCORE>,
109 score_threshold: f32,
110 iou_threshold: f32,
111 nms: Option<Nms>,
112 pre_nms_top_k: usize,
113 max_det: usize,
114 multi_label: bool,
115 output_boxes: &mut Vec<DetectBox>,
116) -> DecoderResult<()>
117where
118 f32: AsPrimitive<SCORE>,
119{
120 impl_modelpack_float::<XYXY, _, _>(
121 boxes_tensor,
122 scores_tensor,
123 score_threshold,
124 iou_threshold,
125 nms,
126 pre_nms_top_k,
127 max_det,
128 multi_label,
129 output_boxes,
130 )
131}
132
133#[cfg(test)]
148#[allow(clippy::too_many_arguments)]
149pub(crate) fn decode_modelpack_split_quant<D: AsPrimitive<f32>>(
150 outputs: &[ArrayView3<D>],
151 configs: &[ModelPackDetectionConfig],
152 score_threshold: f32,
153 iou_threshold: f32,
154 nms: Option<Nms>,
155 pre_nms_top_k: usize,
156 max_det: usize,
157 multi_label: bool,
158 output_boxes: &mut Vec<DetectBox>,
159) -> DecoderResult<()> {
160 impl_modelpack_split_quant::<XYWH, D>(
161 outputs,
162 configs,
163 score_threshold,
164 iou_threshold,
165 nms,
166 pre_nms_top_k,
167 max_det,
168 multi_label,
169 output_boxes,
170 )
171}
172
173#[allow(clippy::too_many_arguments)]
188pub(crate) fn decode_modelpack_split_float<D: AsPrimitive<f32>>(
189 outputs: &[ArrayView3<D>],
190 configs: &[ModelPackDetectionConfig],
191 score_threshold: f32,
192 iou_threshold: f32,
193 nms: Option<Nms>,
194 pre_nms_top_k: usize,
195 max_det: usize,
196 multi_label: bool,
197 output_boxes: &mut Vec<DetectBox>,
198) -> DecoderResult<()> {
199 impl_modelpack_split_float::<XYWH, D>(
200 outputs,
201 configs,
202 score_threshold,
203 iou_threshold,
204 nms,
205 pre_nms_top_k,
206 max_det,
207 multi_label,
208 output_boxes,
209 )
210}
211#[allow(clippy::too_many_arguments)]
224pub(crate) fn impl_modelpack_quant<
225 B: BBoxTypeTrait,
226 BOX: PrimInt + AsPrimitive<f32> + Send + Sync,
227 SCORE: PrimInt + AsPrimitive<f32> + Send + Sync,
228>(
229 boxes: (ArrayView2<BOX>, Quantization),
230 scores: (ArrayView2<SCORE>, Quantization),
231 score_threshold: f32,
232 iou_threshold: f32,
233 nms: Option<Nms>,
234 pre_nms_top_k: usize,
235 max_det: usize,
236 multi_label: bool,
237 output_boxes: &mut Vec<DetectBox>,
238) -> DecoderResult<()>
239where
240 f32: AsPrimitive<SCORE>,
241{
242 let (boxes_tensor, quant_boxes) = boxes;
243 let (scores_tensor, quant_scores) = scores;
244 let boxes = {
245 let score_threshold = quantize_score_threshold(score_threshold, quant_scores);
246 if multi_label {
247 postprocess_boxes_multilabel_quant::<B, _, _>(
248 score_threshold,
249 boxes_tensor,
250 scores_tensor,
251 quant_boxes,
252 )?
253 } else {
254 postprocess_boxes_quant::<B, _, _>(
255 score_threshold,
256 boxes_tensor,
257 scores_tensor,
258 quant_boxes,
259 )
260 }
261 };
262 let boxes = nms_and_cap(boxes, nms, iou_threshold, pre_nms_top_k, max_det);
263 output_boxes.clear();
264 output_boxes.extend(boxes.iter().map(|b| dequant_detect_box(b, quant_scores)));
265 Ok(())
266}
267
268#[allow(clippy::too_many_arguments)]
281pub(crate) fn impl_modelpack_float<
282 B: BBoxTypeTrait,
283 BOX: Float + AsPrimitive<f32> + Send + Sync,
284 SCORE: Float + AsPrimitive<f32> + Send + Sync,
285>(
286 boxes_tensor: ArrayView2<BOX>,
287 scores_tensor: ArrayView2<SCORE>,
288 score_threshold: f32,
289 iou_threshold: f32,
290 nms: Option<Nms>,
291 pre_nms_top_k: usize,
292 max_det: usize,
293 multi_label: bool,
294 output_boxes: &mut Vec<DetectBox>,
295) -> DecoderResult<()>
296where
297 f32: AsPrimitive<SCORE>,
298{
299 let boxes = if multi_label {
300 postprocess_boxes_multilabel_float::<B, _, _>(
301 score_threshold.as_(),
302 boxes_tensor,
303 scores_tensor,
304 )?
305 } else {
306 postprocess_boxes_float::<B, _, _>(score_threshold.as_(), boxes_tensor, scores_tensor)
307 };
308 output_boxes.clear();
309 output_boxes.extend(nms_and_cap(
310 boxes,
311 nms,
312 iou_threshold,
313 pre_nms_top_k,
314 max_det,
315 ));
316 Ok(())
317}
318
319#[cfg(test)]
332#[allow(clippy::too_many_arguments)]
333pub(crate) fn impl_modelpack_split_quant<B: BBoxTypeTrait, D: AsPrimitive<f32>>(
334 outputs: &[ArrayView3<D>],
335 configs: &[ModelPackDetectionConfig],
336 score_threshold: f32,
337 iou_threshold: f32,
338 nms: Option<Nms>,
339 pre_nms_top_k: usize,
340 max_det: usize,
341 multi_label: bool,
342 output_boxes: &mut Vec<DetectBox>,
343) -> DecoderResult<()> {
344 let (boxes_tensor, scores_tensor) = postprocess_modelpack_split_quant(outputs, configs);
345 let boxes = if multi_label {
346 postprocess_boxes_multilabel_float::<B, _, _>(
347 score_threshold,
348 boxes_tensor.view(),
349 scores_tensor.view(),
350 )?
351 } else {
352 postprocess_boxes_float::<B, _, _>(
353 score_threshold,
354 boxes_tensor.view(),
355 scores_tensor.view(),
356 )
357 };
358 output_boxes.clear();
359 output_boxes.extend(nms_and_cap(
360 boxes,
361 nms,
362 iou_threshold,
363 pre_nms_top_k,
364 max_det,
365 ));
366 Ok(())
367}
368
369#[allow(clippy::too_many_arguments)]
383pub(crate) fn impl_modelpack_split_float<B: BBoxTypeTrait, D: AsPrimitive<f32>>(
384 outputs: &[ArrayView3<D>],
385 configs: &[ModelPackDetectionConfig],
386 score_threshold: f32,
387 iou_threshold: f32,
388 nms: Option<Nms>,
389 pre_nms_top_k: usize,
390 max_det: usize,
391 multi_label: bool,
392 output_boxes: &mut Vec<DetectBox>,
393) -> DecoderResult<()> {
394 let (boxes_tensor, scores_tensor) = postprocess_modelpack_split_float(outputs, configs);
395 let boxes = if multi_label {
396 postprocess_boxes_multilabel_float::<B, _, _>(
397 score_threshold,
398 boxes_tensor.view(),
399 scores_tensor.view(),
400 )?
401 } else {
402 postprocess_boxes_float::<B, _, _>(
403 score_threshold,
404 boxes_tensor.view(),
405 scores_tensor.view(),
406 )
407 };
408 output_boxes.clear();
409 output_boxes.extend(nms_and_cap(
410 boxes,
411 nms,
412 iou_threshold,
413 pre_nms_top_k,
414 max_det,
415 ));
416 Ok(())
417}
418
419#[cfg(test)]
423pub(crate) fn postprocess_modelpack_split_quant<T: AsPrimitive<f32>>(
424 outputs: &[ArrayView3<T>],
425 config: &[ModelPackDetectionConfig],
426) -> (Array2<f32>, Array2<f32>) {
427 let mut total_capacity = 0;
428 let mut nc = 0;
429 for (p, detail) in outputs.iter().zip(config) {
430 let shape = p.shape();
431 let na = detail.anchors.len();
432 nc = *shape
433 .last()
434 .expect("Shape must have at least one dimension")
435 / na
436 - 5;
437 total_capacity += shape[0] * shape[1] * na;
438 }
439 let mut bboxes = Vec::with_capacity(total_capacity * 4);
440 let mut bscores = Vec::with_capacity(total_capacity * nc);
441
442 for (p, detail) in outputs.iter().zip(config) {
443 let anchors = &detail.anchors;
444 let na = detail.anchors.len();
445 let shape = p.shape();
446 assert_eq!(
447 shape.iter().product::<usize>(),
448 p.len(),
449 "Shape product doesn't match tensor length"
450 );
451 let p_sigmoid = if let Some(quant) = &detail.quantization {
452 let scaled_zero = -quant.zero_point as f32 * quant.scale;
453 p.mapv(|x| fast_sigmoid_impl(x.as_() * quant.scale + scaled_zero))
454 } else {
455 p.mapv(|x| fast_sigmoid_impl(x.as_()))
456 };
457 let p_sigmoid = p_sigmoid.as_standard_layout();
458
459 let p = p_sigmoid
461 .as_slice()
462 .expect("Sigmoids are not in standard layout");
463 let height = shape[0];
464 let width = shape[1];
465
466 let div_width = 1.0 / width as f32;
467 let div_height = 1.0 / height as f32;
468
469 let mut grid = Vec::with_capacity(height * width * na * 2);
470 for y in 0..height {
471 for x in 0..width {
472 for _ in 0..na {
473 grid.push(x as f32 - 0.5);
474 grid.push(y as f32 - 0.5);
475 }
476 }
477 }
478 for ((p, g), anchor) in p
479 .chunks_exact(nc + 5)
480 .zip(grid.as_chunks::<2>().0)
481 .zip(anchors.iter().cycle())
482 {
483 let (x, y) = (p[0], p[1]);
484 let x = (x * 2.0 + g[0]) * div_width;
485 let y = (y * 2.0 + g[1]) * div_height;
486 let (w, h) = (p[2], p[3]);
487 let w = w * w * 4.0 * anchor[0];
488 let h = h * h * 4.0 * anchor[1];
489
490 bboxes.push(x);
491 bboxes.push(y);
492 bboxes.push(w);
493 bboxes.push(h);
494
495 if nc == 1 {
496 bscores.push(p[4]);
497 } else {
498 let obj = p[4];
499 let probs = p[5..].iter().map(|x| *x * obj);
500 bscores.extend(probs);
501 }
502 }
503 }
504 debug_assert_eq!(bboxes.len() % 4, 0);
507 debug_assert_eq!(bscores.len() % nc, 0);
508
509 let bboxes = Array2::from_shape_vec((bboxes.len() / 4, 4), bboxes)
510 .expect("Failed to create bboxes array");
511 let bscores = Array2::from_shape_vec((bscores.len() / nc, nc), bscores)
512 .expect("Failed to create bscores array");
513 (bboxes, bscores)
514}
515
516pub(crate) fn postprocess_modelpack_split_float<T: AsPrimitive<f32>>(
520 outputs: &[ArrayView3<T>],
521 config: &[ModelPackDetectionConfig],
522) -> (Array2<f32>, Array2<f32>) {
523 let mut total_capacity = 0;
524 let mut nc = 0;
525 for (p, detail) in outputs.iter().zip(config) {
526 let shape = p.shape();
527 let na = detail.anchors.len();
528 nc = *shape
529 .last()
530 .expect("Shape must have at least one dimension")
531 / na
532 - 5;
533 total_capacity += shape[0] * shape[1] * na;
534 }
535 let mut bboxes = Vec::with_capacity(total_capacity * 4);
536 let mut bscores = Vec::with_capacity(total_capacity * nc);
537
538 for (p, detail) in outputs.iter().zip(config) {
539 let anchors = &detail.anchors;
540 let na = detail.anchors.len();
541 let shape = p.shape();
542 assert_eq!(
543 shape.iter().product::<usize>(),
544 p.len(),
545 "Shape product doesn't match tensor length"
546 );
547 let p_sigmoid = p.mapv(|x| fast_sigmoid_impl(x.as_()));
548 let p_sigmoid = p_sigmoid.as_standard_layout();
549
550 let p = p_sigmoid
552 .as_slice()
553 .expect("Sigmoids are not in standard layout");
554 let height = shape[0];
555 let width = shape[1];
556
557 let div_width = 1.0 / width as f32;
558 let div_height = 1.0 / height as f32;
559
560 let mut grid = Vec::with_capacity(height * width * na * 2);
561 for y in 0..height {
562 for x in 0..width {
563 for _ in 0..na {
564 grid.push(x as f32 - 0.5);
565 grid.push(y as f32 - 0.5);
566 }
567 }
568 }
569 for ((p, g), anchor) in p
570 .chunks_exact(nc + 5)
571 .zip(grid.as_chunks::<2>().0)
572 .zip(anchors.iter().cycle())
573 {
574 let (x, y) = (p[0], p[1]);
575 let x = (x * 2.0 + g[0]) * div_width;
576 let y = (y * 2.0 + g[1]) * div_height;
577 let (w, h) = (p[2], p[3]);
578 let w = w * w * 4.0 * anchor[0];
579 let h = h * h * 4.0 * anchor[1];
580
581 bboxes.push(x);
582 bboxes.push(y);
583 bboxes.push(w);
584 bboxes.push(h);
585
586 if nc == 1 {
587 bscores.push(p[4]);
588 } else {
589 let obj = p[4];
590 let probs = p[5..].iter().map(|x| *x * obj);
591 bscores.extend(probs);
592 }
593 }
594 }
595 debug_assert_eq!(bboxes.len() % 4, 0);
598 debug_assert_eq!(bscores.len() % nc, 0);
599
600 let bboxes = Array2::from_shape_vec((bboxes.len() / 4, 4), bboxes)
601 .expect("Failed to create bboxes array");
602 let bscores = Array2::from_shape_vec((bscores.len() / nc, nc), bscores)
603 .expect("Failed to create bscores array");
604 (bboxes, bscores)
605}
606
607#[inline(always)]
614#[allow(clippy::excessive_precision)]
617fn exp_raw(x: f32) -> f32 {
618 const A: f32 = (1u32 << 23) as f32; const MASK: i32 = 0xff800000u32 as i32;
620 const EXP2_23: f32 = 1.1920929e-7;
621 const C0: f32 = 0.3371894346 * EXP2_23 * EXP2_23;
622 const C1: f32 = 0.657636276 * EXP2_23;
623 const C2: f32 = 1.00172476;
624
625 let a = A * core::f32::consts::LOG2_E;
626 let mul = (a * x) as i32;
627 let floor = mul & MASK;
628 let frac = (mul - floor) as f32;
629 let approx = (C0 * frac + C1) * frac + C2;
630 f32::from_bits(approx.to_bits().wrapping_add(floor as u32))
631}
632
633#[inline(always)]
634fn fast_sigmoid_impl(f: f32) -> f32 {
635 if f.abs() > 80.0 {
636 f.signum() * 0.5 + 0.5
637 } else {
638 1.0 / (1.0 + exp_raw(-f))
640 }
641}
642
643pub(crate) fn modelpack_segmentation_to_mask(segmentation: ArrayView3<u8>) -> Array2<u8> {
652 use argminmax::ArgMinMax;
653 assert!(
654 segmentation.shape()[2] > 1,
655 "Model Instance Segmentation should have shape (H, W, x) where x > 1"
656 );
657 let height = segmentation.shape()[0];
658 let width = segmentation.shape()[1];
659 let channels = segmentation.shape()[2];
660 let segmentation = segmentation.as_standard_layout();
661 let seg = segmentation
663 .as_slice()
664 .expect("Segmentation is not in standard layout");
665 let argmax = seg
666 .chunks_exact(channels)
667 .map(|x| x.argmax() as u8)
668 .collect::<Vec<_>>();
669
670 Array2::from_shape_vec((height, width), argmax).expect("Failed to create mask array")
671}
672
673#[cfg(test)]
674#[cfg_attr(coverage_nightly, coverage(off))]
675mod modelpack_tests {
676 #![allow(clippy::excessive_precision)]
677 use ndarray::Array3;
678
679 use crate::configs::{DecoderType, DimName};
680
681 use super::*;
682 #[test]
683 fn test_detection_config() {
684 let det = Detection {
685 anchors: Some(vec![[0.1, 0.13], [0.16, 0.30], [0.33, 0.23]]),
686 quantization: Some((0.1, 128).into()),
687 decoder: DecoderType::ModelPack,
688 shape: vec![1, 9, 17, 18],
689 dshape: vec![
690 (DimName::Batch, 1),
691 (DimName::Height, 9),
692 (DimName::Width, 17),
693 (DimName::NumAnchorsXFeatures, 18),
694 ],
695 normalized: Some(true),
696 };
697 let config = ModelPackDetectionConfig::try_from(&det).unwrap();
698 assert_eq!(
699 config,
700 ModelPackDetectionConfig {
701 anchors: vec![[0.1, 0.13], [0.16, 0.30], [0.33, 0.23]],
702 quantization: Some(Quantization::new(0.1, 128)),
703 }
704 );
705
706 let det = Detection {
707 anchors: None,
708 quantization: Some((0.1, 128).into()),
709 decoder: DecoderType::ModelPack,
710 shape: vec![1, 9, 17, 18],
711 dshape: vec![
712 (DimName::Batch, 1),
713 (DimName::Height, 9),
714 (DimName::Width, 17),
715 (DimName::NumAnchorsXFeatures, 18),
716 ],
717 normalized: Some(true),
718 };
719 let result = ModelPackDetectionConfig::try_from(&det);
720 assert!(
721 matches!(result, Err(DecoderError::InvalidConfig(s)) if s == "ModelPack Split Detection missing anchors")
722 );
723 }
724
725 #[test]
726 fn test_fast_sigmoid() {
727 fn full_sigmoid(x: f32) -> f32 {
728 1.0 / (1.0 + (-x).exp())
729 }
730 for i in -2550..=2550 {
731 let x = i as f32 * 0.1;
732 let fast = fast_sigmoid_impl(x);
733 let full = full_sigmoid(x);
734 let diff = (fast - full).abs();
735 assert!(
736 diff < 0.0005,
737 "Fast sigmoid differs from full sigmoid by {} at input {}",
738 diff,
739 x
740 );
741 }
742 }
743
744 #[test]
748 fn fast_sigmoid_saturates_beyond_the_cutoff_not_at_it() {
749 assert!(
750 fast_sigmoid_impl(-80.0) > 0.0,
751 "-80.0 is inside the smooth range, got {}",
752 fast_sigmoid_impl(-80.0)
753 );
754 assert_eq!(fast_sigmoid_impl(-80.5), 0.0);
755 assert_eq!(fast_sigmoid_impl(80.5), 1.0);
756 }
757
758 #[test]
762 fn modelpack_split_float_decodes_boxes_and_objectness_weighted_scores() {
763 fn full_sigmoid(x: f32) -> f32 {
764 1.0 / (1.0 + (-x).exp())
765 }
766
767 let raw = Array3::from_shape_vec((1, 1, 7), vec![0.0, 0.0, 0.0, 0.0, 2.0, 1.0, -1.0])
769 .expect("shape matches the data");
770 let config = [ModelPackDetectionConfig {
771 anchors: vec![[2.0, 3.0]],
772 quantization: None,
773 }];
774
775 let (boxes, scores) = postprocess_modelpack_split_float(&[raw.view()], &config);
776
777 assert_eq!(boxes.shape(), &[1, 4]);
778 assert_eq!(scores.shape(), &[1, 2]);
779
780 let obj = full_sigmoid(2.0);
783 let expected_box = [
784 0.5 * 2.0 - 0.5,
785 0.5 * 2.0 - 0.5,
786 0.5 * 0.5 * 4.0 * 2.0,
787 0.5 * 0.5 * 4.0 * 3.0,
788 ];
789 let expected_scores = [full_sigmoid(1.0) * obj, full_sigmoid(-1.0) * obj];
790
791 for (i, want) in expected_box.iter().enumerate() {
795 assert!(
796 (boxes[[0, i]] - want).abs() < 1e-2,
797 "box[{i}] = {} want {want}",
798 boxes[[0, i]]
799 );
800 }
801 for (i, want) in expected_scores.iter().enumerate() {
802 assert!(
803 (scores[[0, i]] - want).abs() < 2e-3,
804 "score[{i}] = {} want {want}",
805 scores[[0, i]]
806 );
807 }
808 }
809
810 #[test]
813 fn modelpack_split_float_single_class_uses_objectness_as_the_score() {
814 let raw = Array3::from_shape_vec((1, 1, 6), vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
815 .expect("shape matches the data");
816 let config = [ModelPackDetectionConfig {
817 anchors: vec![[1.0, 1.0]],
818 quantization: None,
819 }];
820
821 let (boxes, scores) = postprocess_modelpack_split_float(&[raw.view()], &config);
822
823 assert_eq!(boxes.shape(), &[1, 4]);
824 assert_eq!(scores.shape(), &[1, 1]);
825 assert!((scores[[0, 0]] - 0.5).abs() < 2e-3, "{}", scores[[0, 0]]);
826 }
827
828 #[test]
829 fn test_modelpack_segmentation_to_mask() {
830 let seg = Array3::from_shape_vec(
831 (2, 2, 3),
832 vec![
833 0u8, 10, 5, 20, 15, 25, 30, 5, 10, 0, 0, 0, ],
838 )
839 .unwrap();
840 let mask = modelpack_segmentation_to_mask(seg.view());
841 let expected_mask = Array2::from_shape_vec((2, 2), vec![1u8, 2, 0, 0]).unwrap();
842 assert_eq!(mask, expected_mask);
843 }
844
845 #[test]
846 #[should_panic(
847 expected = "Model Instance Segmentation should have shape (H, W, x) where x > 1"
848 )]
849 fn test_modelpack_segmentation_to_mask_invalid() {
850 let seg = Array3::from_shape_vec((2, 2, 1), vec![0u8, 10, 20, 30]).unwrap();
851 let _ = modelpack_segmentation_to_mask(seg.view());
852 }
853
854 #[test]
855 fn modelpack_split_float_multi_label_emits_every_class() {
856 let p = ndarray::Array3::from_shape_vec(
858 (1, 1, 7),
859 vec![0.0f32, 0.0, 0.0, 0.0, 10.0, 10.0, 10.0],
860 )
861 .unwrap();
862 let cfg = ModelPackDetectionConfig {
863 anchors: vec![[0.5, 0.5]],
864 quantization: None,
865 };
866 let run = |nms: Nms, multi_label: bool| {
867 let mut out = Vec::with_capacity(8);
868 decode_modelpack_split_float(
869 &[p.view()],
870 std::slice::from_ref(&cfg),
871 0.5,
872 0.5,
873 Some(nms),
874 0,
875 8,
876 multi_label,
877 &mut out,
878 )
879 .unwrap();
880 let mut labels: Vec<usize> = out.iter().map(|b| b.label).collect();
881 labels.sort_unstable();
882 labels
883 };
884 assert_eq!(run(Nms::ClassAware, false).len(), 1);
885 assert_eq!(run(Nms::ClassAware, true), vec![0, 1]);
886 assert_eq!(run(Nms::ClassAgnostic, true).len(), 1);
889 }
890}