1use std::fmt::Debug;
13
14use ndarray::{
15 parallel::prelude::{IntoParallelIterator, ParallelIterator},
16 s, Array2, Array3, ArrayView1, ArrayView2, ArrayView3,
17};
18use num_traits::{AsPrimitive, Float, PrimInt, Signed};
19
20use crate::{
21 byte::{
22 nms_class_aware_int, nms_extra_class_aware_int, nms_extra_int, nms_int,
23 postprocess_boxes_index_quant, postprocess_boxes_quant, quantize_score_threshold,
24 },
25 configs::Nms,
26 dequant_detect_box,
27 float::{
28 nms_class_aware_float, nms_extra_class_aware_float, nms_extra_float, nms_float,
29 postprocess_boxes_float, postprocess_boxes_index_float,
30 postprocess_boxes_multilabel_index_float,
31 },
32 BBoxTypeTrait, BoundingBox, DetectBox, DetectBoxQuantized, ProtoData, ProtoLayout,
33 Quantization, Segmentation, XYWH, XYXY,
34};
35
36#[cfg(test)]
54pub(crate) const MAX_NMS_CANDIDATES: usize = 30_000;
55
56pub(crate) const DEFAULT_MAX_DETECTIONS: usize = 300;
63
64fn truncate_to_top_k_by_score<E: Send>(boxes: &mut Vec<(DetectBox, E)>, top_k: usize) {
69 if top_k > 0 && boxes.len() > top_k {
70 boxes.select_nth_unstable_by(top_k, |a, b| b.0.score.total_cmp(&a.0.score));
71 boxes.truncate(top_k);
72 }
73}
74
75fn truncate_to_top_k_by_score_quant<S: PrimInt + AsPrimitive<f32> + Send + Sync, E: Send>(
80 boxes: &mut Vec<(DetectBoxQuantized<S>, E)>,
81 top_k: usize,
82) {
83 if top_k > 0 && boxes.len() > top_k {
84 boxes.select_nth_unstable_by(top_k, |a, b| b.0.score.cmp(&a.0.score));
85 boxes.truncate(top_k);
86 }
87}
88
89fn dispatch_nms_float(
96 nms: Option<Nms>,
97 iou: f32,
98 max_det: Option<usize>,
99 boxes: Vec<DetectBox>,
100) -> Vec<DetectBox> {
101 match nms {
102 Some(Nms::ClassAgnostic | Nms::Auto) => nms_float(iou, max_det, boxes),
103 Some(Nms::ClassAware) => nms_class_aware_float(iou, max_det, boxes),
104 None => boxes, }
106}
107
108pub(super) fn dispatch_nms_extra_float<E: Send + Sync>(
111 nms: Option<Nms>,
112 iou: f32,
113 max_det: Option<usize>,
114 boxes: Vec<(DetectBox, E)>,
115) -> Vec<(DetectBox, E)> {
116 match nms {
117 Some(Nms::ClassAgnostic | Nms::Auto) => nms_extra_float(iou, max_det, boxes),
118 Some(Nms::ClassAware) => nms_extra_class_aware_float(iou, max_det, boxes),
119 None => boxes, }
121}
122
123fn dispatch_nms_int<SCORE: PrimInt + AsPrimitive<f32> + Send + Sync>(
126 nms: Option<Nms>,
127 iou: f32,
128 max_det: Option<usize>,
129 boxes: Vec<DetectBoxQuantized<SCORE>>,
130) -> Vec<DetectBoxQuantized<SCORE>> {
131 match nms {
132 Some(Nms::ClassAgnostic | Nms::Auto) => nms_int(iou, max_det, boxes),
133 Some(Nms::ClassAware) => nms_class_aware_int(iou, max_det, boxes),
134 None => boxes, }
136}
137
138fn dispatch_nms_extra_int<SCORE: PrimInt + AsPrimitive<f32> + Send + Sync, E: Send + Sync>(
141 nms: Option<Nms>,
142 iou: f32,
143 max_det: Option<usize>,
144 boxes: Vec<(DetectBoxQuantized<SCORE>, E)>,
145) -> Vec<(DetectBoxQuantized<SCORE>, E)> {
146 match nms {
147 Some(Nms::ClassAgnostic | Nms::Auto) => nms_extra_int(iou, max_det, boxes),
148 Some(Nms::ClassAware) => nms_extra_class_aware_int(iou, max_det, boxes),
149 None => boxes, }
151}
152
153#[inline]
160fn cap_or_default<T>(v: &Vec<T>) -> usize {
161 if v.capacity() > 0 {
162 v.capacity()
163 } else {
164 DEFAULT_MAX_DETECTIONS
165 }
166}
167
168pub(crate) fn decode_yolo_det<BOX: PrimInt + AsPrimitive<f32> + Send + Sync>(
202 output: (ArrayView2<BOX>, Quantization),
203 score_threshold: f32,
204 iou_threshold: f32,
205 nms: Option<Nms>,
206 output_boxes: &mut Vec<DetectBox>,
207) where
208 f32: AsPrimitive<BOX>,
209{
210 impl_yolo_quant::<XYWH, _>(output, score_threshold, iou_threshold, nms, output_boxes);
211}
212
213pub(crate) fn decode_yolo_det_float<T>(
220 output: ArrayView2<T>,
221 score_threshold: f32,
222 iou_threshold: f32,
223 nms: Option<Nms>,
224 output_boxes: &mut Vec<DetectBox>,
225) where
226 T: Float + AsPrimitive<f32> + Send + Sync + 'static,
227 f32: AsPrimitive<T>,
228{
229 impl_yolo_float::<XYWH, _>(output, score_threshold, iou_threshold, nms, output_boxes);
230}
231
232#[cfg(test)]
246pub(crate) fn decode_yolo_segdet_quant<
247 BOX: PrimInt + AsPrimitive<i64> + AsPrimitive<i128> + AsPrimitive<f32> + Send + Sync,
248 PROTO: PrimInt + AsPrimitive<i64> + AsPrimitive<i128> + AsPrimitive<f32> + Send + Sync,
249>(
250 boxes: (ArrayView2<BOX>, Quantization),
251 protos: (ArrayView3<PROTO>, Quantization),
252 score_threshold: f32,
253 iou_threshold: f32,
254 nms: Option<Nms>,
255 output_boxes: &mut Vec<DetectBox>,
256 output_masks: &mut Vec<Segmentation>,
257) -> Result<(), crate::DecoderError>
258where
259 f32: AsPrimitive<BOX>,
260{
261 let cap = cap_or_default(output_boxes);
266 impl_yolo_segdet_quant::<XYWH, _, _>(
267 boxes,
268 protos,
269 score_threshold,
270 iou_threshold,
271 nms,
272 MAX_NMS_CANDIDATES,
273 cap,
274 None,
275 None,
276 output_boxes,
277 output_masks,
278 )
279}
280
281#[cfg(test)]
283pub(crate) fn decode_yolo_segdet_float<T>(
284 boxes: ArrayView2<T>,
285 protos: ArrayView3<T>,
286 score_threshold: f32,
287 iou_threshold: f32,
288 nms: Option<Nms>,
289 output_boxes: &mut Vec<DetectBox>,
290 output_masks: &mut Vec<Segmentation>,
291) -> Result<(), crate::DecoderError>
292where
293 T: Float + AsPrimitive<f32> + Send + Sync + 'static,
294 f32: AsPrimitive<T>,
295{
296 let cap = cap_or_default(output_boxes);
299 impl_yolo_segdet_float::<XYWH, _, _>(
300 boxes,
301 protos,
302 score_threshold,
303 iou_threshold,
304 nms,
305 MAX_NMS_CANDIDATES,
306 cap,
307 None,
308 None,
309 false, output_boxes,
311 output_masks,
312 )
313}
314
315pub(crate) fn decode_yolo_split_det_quant<
327 BOX: PrimInt + AsPrimitive<i32> + AsPrimitive<f32> + Send + Sync,
328 SCORE: PrimInt + AsPrimitive<f32> + Send + Sync,
329>(
330 boxes: (ArrayView2<BOX>, Quantization),
331 scores: (ArrayView2<SCORE>, Quantization),
332 score_threshold: f32,
333 iou_threshold: f32,
334 nms: Option<Nms>,
335 output_boxes: &mut Vec<DetectBox>,
336) where
337 f32: AsPrimitive<SCORE>,
338{
339 impl_yolo_split_quant::<XYWH, _, _>(
340 boxes,
341 scores,
342 score_threshold,
343 iou_threshold,
344 nms,
345 output_boxes,
346 );
347}
348
349pub(crate) fn decode_yolo_split_det_float<T>(
361 boxes: ArrayView2<T>,
362 scores: ArrayView2<T>,
363 score_threshold: f32,
364 iou_threshold: f32,
365 nms: Option<Nms>,
366 output_boxes: &mut Vec<DetectBox>,
367) where
368 T: Float + AsPrimitive<f32> + Send + Sync + 'static,
369 f32: AsPrimitive<T>,
370{
371 impl_yolo_split_float::<XYWH, _, _>(
372 boxes,
373 scores,
374 score_threshold,
375 iou_threshold,
376 nms,
377 output_boxes,
378 );
379}
380
381pub(crate) fn decode_yolo_end_to_end_det_float<T>(
396 output: ArrayView2<T>,
397 score_threshold: f32,
398 output_boxes: &mut Vec<DetectBox>,
399) -> Result<(), crate::DecoderError>
400where
401 T: Float + AsPrimitive<f32> + Send + Sync + 'static,
402 f32: AsPrimitive<T>,
403{
404 if output.shape()[0] < 6 {
406 return Err(crate::DecoderError::InvalidShape(format!(
407 "End-to-end detection output requires at least 6 rows, got {}",
408 output.shape()[0]
409 )));
410 }
411
412 let boxes = output.slice(s![0..4, ..]).reversed_axes();
414 let scores = output.slice(s![4..5, ..]).reversed_axes();
415 let classes = output.slice(s![5, ..]);
416 let mut boxes =
417 postprocess_boxes_index_float::<XYXY, _, _>(score_threshold.as_(), boxes, scores);
418 boxes.truncate(cap_or_default(output_boxes));
419 output_boxes.clear();
420 for (mut b, i) in boxes.into_iter() {
421 b.label = classes[i].as_() as usize;
422 output_boxes.push(b);
423 }
424 Ok(())
426}
427
428pub(crate) fn decode_yolo_end_to_end_segdet_float<T>(
446 output: ArrayView2<T>,
447 protos: ArrayView3<T>,
448 score_threshold: f32,
449 output_boxes: &mut Vec<DetectBox>,
450 output_masks: &mut Vec<crate::Segmentation>,
451) -> Result<(), crate::DecoderError>
452where
453 T: Float + AsPrimitive<f32> + Send + Sync + 'static,
454 f32: AsPrimitive<T>,
455{
456 let (boxes, scores, classes, mask_coeff) =
457 postprocess_yolo_end_to_end_segdet(&output, protos.dim().2)?;
458 let cap = cap_or_default(output_boxes);
459 let boxes = impl_yolo_end_to_end_segdet_get_boxes::<XYXY, _, _, _>(
460 boxes,
461 scores,
462 classes,
463 score_threshold,
464 cap,
465 );
466
467 impl_yolo_split_segdet_process_masks(boxes, mask_coeff, protos, output_boxes, output_masks)
470}
471
472pub(crate) fn decode_yolo_split_end_to_end_det_float<T: Float + AsPrimitive<f32>>(
481 boxes: ArrayView2<T>,
482 scores: ArrayView2<T>,
483 classes: ArrayView2<T>,
484 score_threshold: f32,
485 output_boxes: &mut Vec<DetectBox>,
486) -> Result<(), crate::DecoderError> {
487 let n = boxes.shape()[1];
488
489 let cap = cap_or_default(output_boxes);
490 output_boxes.clear();
491
492 let (boxes, scores, classes) = postprocess_yolo_split_end_to_end_det(boxes, scores, &classes)?;
493
494 for i in 0..n {
495 let score: f32 = scores[[i, 0]].as_();
496 if score < score_threshold {
497 continue;
498 }
499 if output_boxes.len() >= cap {
500 break;
501 }
502 output_boxes.push(DetectBox {
503 bbox: BoundingBox {
504 xmin: boxes[[i, 0]].as_(),
505 ymin: boxes[[i, 1]].as_(),
506 xmax: boxes[[i, 2]].as_(),
507 ymax: boxes[[i, 3]].as_(),
508 },
509 score,
510 label: classes[i].as_() as usize,
511 });
512 }
513 Ok(())
514}
515
516#[allow(clippy::too_many_arguments)]
525pub(crate) fn decode_yolo_split_end_to_end_segdet_float<T>(
526 boxes: ArrayView2<T>,
527 scores: ArrayView2<T>,
528 classes: ArrayView2<T>,
529 mask_coeff: ArrayView2<T>,
530 protos: ArrayView3<T>,
531 score_threshold: f32,
532 output_boxes: &mut Vec<DetectBox>,
533 output_masks: &mut Vec<crate::Segmentation>,
534) -> Result<(), crate::DecoderError>
535where
536 T: Float + AsPrimitive<f32> + Send + Sync + 'static,
537 f32: AsPrimitive<T>,
538{
539 let (boxes, scores, classes, mask_coeff) =
540 postprocess_yolo_split_end_to_end_segdet(boxes, scores, &classes, mask_coeff)?;
541 let cap = cap_or_default(output_boxes);
542 let boxes = impl_yolo_end_to_end_segdet_get_boxes::<XYXY, _, _, _>(
543 boxes,
544 scores,
545 classes,
546 score_threshold,
547 cap,
548 );
549
550 impl_yolo_split_segdet_process_masks(boxes, mask_coeff, protos, output_boxes, output_masks)
551}
552
553#[allow(clippy::type_complexity)]
554pub(crate) fn postprocess_yolo_end_to_end_segdet<'a, T>(
555 output: &'a ArrayView2<'_, T>,
556 num_protos: usize,
557) -> Result<
558 (
559 ArrayView2<'a, T>,
560 ArrayView2<'a, T>,
561 ArrayView1<'a, T>,
562 ArrayView2<'a, T>,
563 ),
564 crate::DecoderError,
565> {
566 if output.shape()[0] < 7 {
568 return Err(crate::DecoderError::InvalidShape(format!(
569 "End-to-end segdet output requires at least 7 rows, got {}",
570 output.shape()[0]
571 )));
572 }
573
574 let num_mask_coeffs = output.shape()[0] - 6;
575 if num_mask_coeffs != num_protos {
576 return Err(crate::DecoderError::InvalidShape(format!(
577 "Mask coefficients count ({}) doesn't match protos count ({})",
578 num_mask_coeffs, num_protos
579 )));
580 }
581
582 let boxes = output.slice(s![0..4, ..]).reversed_axes();
584 let scores = output.slice(s![4..5, ..]).reversed_axes();
585 let classes = output.slice(s![5, ..]);
586 let mask_coeff = output.slice(s![6.., ..]).reversed_axes();
587 Ok((boxes, scores, classes, mask_coeff))
588}
589
590#[allow(clippy::type_complexity)]
597pub(crate) fn postprocess_yolo_split_end_to_end_det<'a, 'b, 'c, BOXES, SCORES, CLASS>(
598 boxes: ArrayView2<'a, BOXES>,
599 scores: ArrayView2<'b, SCORES>,
600 classes: &'c ArrayView2<CLASS>,
601) -> Result<
602 (
603 ArrayView2<'a, BOXES>,
604 ArrayView2<'b, SCORES>,
605 ArrayView1<'c, CLASS>,
606 ),
607 crate::DecoderError,
608> {
609 let num_boxes = boxes.shape()[1];
610 if boxes.shape()[0] != 4 {
611 return Err(crate::DecoderError::InvalidShape(format!(
612 "Split end-to-end box_coords must be 4, got {}",
613 boxes.shape()[0]
614 )));
615 }
616
617 if scores.shape()[0] != 1 {
618 return Err(crate::DecoderError::InvalidShape(format!(
619 "Split end-to-end scores num_classes must be 1, got {}",
620 scores.shape()[0]
621 )));
622 }
623
624 if classes.shape()[0] != 1 {
625 return Err(crate::DecoderError::InvalidShape(format!(
626 "Split end-to-end classes num_classes must be 1, got {}",
627 classes.shape()[0]
628 )));
629 }
630
631 if scores.shape()[1] != num_boxes {
632 return Err(crate::DecoderError::InvalidShape(format!(
633 "Split end-to-end scores must have same num_boxes as boxes ({}), got {}",
634 num_boxes,
635 scores.shape()[1]
636 )));
637 }
638
639 if classes.shape()[1] != num_boxes {
640 return Err(crate::DecoderError::InvalidShape(format!(
641 "Split end-to-end classes must have same num_boxes as boxes ({}), got {}",
642 num_boxes,
643 classes.shape()[1]
644 )));
645 }
646
647 let boxes = boxes.reversed_axes();
648 let scores = scores.reversed_axes();
649 let classes = classes.slice(s![0, ..]);
650 Ok((boxes, scores, classes))
651}
652
653#[allow(clippy::type_complexity)]
656pub(crate) fn postprocess_yolo_split_end_to_end_segdet<
657 'a,
658 'b,
659 'c,
660 'd,
661 BOXES,
662 SCORES,
663 CLASS,
664 MASK,
665>(
666 boxes: ArrayView2<'a, BOXES>,
667 scores: ArrayView2<'b, SCORES>,
668 classes: &'c ArrayView2<CLASS>,
669 mask_coeff: ArrayView2<'d, MASK>,
670) -> Result<
671 (
672 ArrayView2<'a, BOXES>,
673 ArrayView2<'b, SCORES>,
674 ArrayView1<'c, CLASS>,
675 ArrayView2<'d, MASK>,
676 ),
677 crate::DecoderError,
678> {
679 let num_boxes = boxes.shape()[1];
680 if boxes.shape()[0] != 4 {
681 return Err(crate::DecoderError::InvalidShape(format!(
682 "Split end-to-end box_coords must be 4, got {}",
683 boxes.shape()[0]
684 )));
685 }
686
687 if scores.shape()[0] != 1 {
688 return Err(crate::DecoderError::InvalidShape(format!(
689 "Split end-to-end scores num_classes must be 1, got {}",
690 scores.shape()[0]
691 )));
692 }
693
694 if classes.shape()[0] != 1 {
695 return Err(crate::DecoderError::InvalidShape(format!(
696 "Split end-to-end classes num_classes must be 1, got {}",
697 classes.shape()[0]
698 )));
699 }
700
701 if scores.shape()[1] != num_boxes {
702 return Err(crate::DecoderError::InvalidShape(format!(
703 "Split end-to-end scores must have same num_boxes as boxes ({}), got {}",
704 num_boxes,
705 scores.shape()[1]
706 )));
707 }
708
709 if classes.shape()[1] != num_boxes {
710 return Err(crate::DecoderError::InvalidShape(format!(
711 "Split end-to-end classes must have same num_boxes as boxes ({}), got {}",
712 num_boxes,
713 classes.shape()[1]
714 )));
715 }
716
717 if mask_coeff.shape()[1] != num_boxes {
718 return Err(crate::DecoderError::InvalidShape(format!(
719 "Split end-to-end mask_coeff must have same num_boxes as boxes ({}), got {}",
720 num_boxes,
721 mask_coeff.shape()[1]
722 )));
723 }
724
725 let boxes = boxes.reversed_axes();
726 let scores = scores.reversed_axes();
727 let classes = classes.slice(s![0, ..]);
728 let mask_coeff = mask_coeff.reversed_axes();
729 Ok((boxes, scores, classes, mask_coeff))
730}
731pub(crate) fn impl_yolo_quant<B: BBoxTypeTrait, T: PrimInt + AsPrimitive<f32> + Send + Sync>(
736 output: (ArrayView2<T>, Quantization),
737 score_threshold: f32,
738 iou_threshold: f32,
739 nms: Option<Nms>,
740 output_boxes: &mut Vec<DetectBox>,
741) where
742 f32: AsPrimitive<T>,
743{
744 let _span = tracing::trace_span!("decoder.decode.yolo_quant_flat").entered();
745 let (boxes, quant_boxes) = output;
746 let (boxes_tensor, scores_tensor) = postprocess_yolo(&boxes);
747
748 let boxes = {
749 let score_threshold = quantize_score_threshold(score_threshold, quant_boxes);
750 postprocess_boxes_quant::<B, _, _>(
751 score_threshold,
752 boxes_tensor,
753 scores_tensor,
754 quant_boxes,
755 )
756 };
757
758 let cap = cap_or_default(output_boxes);
759 let boxes = dispatch_nms_int(nms, iou_threshold, Some(cap), boxes);
760 let len = cap.min(boxes.len());
763 output_boxes.clear();
764 for b in boxes.iter().take(len) {
765 output_boxes.push(dequant_detect_box(b, quant_boxes));
766 }
767}
768
769pub(crate) fn impl_yolo_float<B: BBoxTypeTrait, T: Float + AsPrimitive<f32> + Send + Sync>(
774 output: ArrayView2<T>,
775 score_threshold: f32,
776 iou_threshold: f32,
777 nms: Option<Nms>,
778 output_boxes: &mut Vec<DetectBox>,
779) where
780 f32: AsPrimitive<T>,
781{
782 let _span = tracing::trace_span!("decoder.decode.yolo_float_flat").entered();
783 let (boxes_tensor, scores_tensor) = postprocess_yolo(&output);
784 let boxes =
785 postprocess_boxes_float::<B, _, _>(score_threshold.as_(), boxes_tensor, scores_tensor);
786 let cap = cap_or_default(output_boxes);
787 let boxes = dispatch_nms_float(nms, iou_threshold, Some(cap), boxes);
788 let len = cap.min(boxes.len());
791 output_boxes.clear();
792 for b in boxes.into_iter().take(len) {
793 output_boxes.push(b);
794 }
795}
796
797pub(crate) fn impl_yolo_split_quant<
807 B: BBoxTypeTrait,
808 BOX: PrimInt + AsPrimitive<f32> + Send + Sync,
809 SCORE: PrimInt + AsPrimitive<f32> + Send + Sync,
810>(
811 boxes: (ArrayView2<BOX>, Quantization),
812 scores: (ArrayView2<SCORE>, Quantization),
813 score_threshold: f32,
814 iou_threshold: f32,
815 nms: Option<Nms>,
816 output_boxes: &mut Vec<DetectBox>,
817) where
818 f32: AsPrimitive<SCORE>,
819{
820 let _span = tracing::trace_span!("decoder.decode.yolo_quant_split").entered();
821 let (boxes_tensor, quant_boxes) = boxes;
822 let (scores_tensor, quant_scores) = scores;
823
824 let boxes_tensor = boxes_tensor.reversed_axes();
825 let scores_tensor = scores_tensor.reversed_axes();
826
827 let boxes = {
828 let score_threshold = quantize_score_threshold(score_threshold, quant_scores);
829 postprocess_boxes_quant::<B, _, _>(
830 score_threshold,
831 boxes_tensor,
832 scores_tensor,
833 quant_boxes,
834 )
835 };
836
837 let cap = cap_or_default(output_boxes);
838 let boxes = dispatch_nms_int(nms, iou_threshold, Some(cap), boxes);
839 let len = cap.min(boxes.len());
842 output_boxes.clear();
843 for b in boxes.iter().take(len) {
844 output_boxes.push(dequant_detect_box(b, quant_scores));
845 }
846}
847
848pub(crate) fn impl_yolo_split_float<
857 B: BBoxTypeTrait,
858 BOX: Float + AsPrimitive<f32> + Send + Sync,
859 SCORE: Float + AsPrimitive<f32> + Send + Sync,
860>(
861 boxes_tensor: ArrayView2<BOX>,
862 scores_tensor: ArrayView2<SCORE>,
863 score_threshold: f32,
864 iou_threshold: f32,
865 nms: Option<Nms>,
866 output_boxes: &mut Vec<DetectBox>,
867) where
868 f32: AsPrimitive<SCORE>,
869{
870 let _span = tracing::trace_span!("decoder.decode.yolo_float_split").entered();
871 let boxes_tensor = boxes_tensor.reversed_axes();
872 let scores_tensor = scores_tensor.reversed_axes();
873 let boxes =
874 postprocess_boxes_float::<B, _, _>(score_threshold.as_(), boxes_tensor, scores_tensor);
875 let cap = cap_or_default(output_boxes);
876 let boxes = dispatch_nms_float(nms, iou_threshold, Some(cap), boxes);
877 let len = cap.min(boxes.len());
880 output_boxes.clear();
881 for b in boxes.into_iter().take(len) {
882 output_boxes.push(b);
883 }
884}
885
886#[inline]
894pub(crate) fn maybe_normalize_boxes_in_place(
895 boxes: &mut [(DetectBox, usize)],
896 normalized: Option<bool>,
897 input_dims: Option<(usize, usize)>,
898) {
899 if normalized != Some(false) {
900 return;
901 }
902 let Some((w, h)) = input_dims else {
903 return;
904 };
905 if w == 0 || h == 0 {
906 return;
907 }
908 let inv_w = 1.0 / w as f32;
909 let inv_h = 1.0 / h as f32;
910 for (b, _) in boxes.iter_mut() {
911 b.bbox.xmin *= inv_w;
912 b.bbox.ymin *= inv_h;
913 b.bbox.xmax *= inv_w;
914 b.bbox.ymax *= inv_h;
915 }
916}
917
918#[allow(clippy::too_many_arguments)]
928pub(crate) fn impl_yolo_segdet_quant<
929 B: BBoxTypeTrait,
930 BOX: PrimInt + AsPrimitive<i64> + AsPrimitive<i128> + AsPrimitive<f32> + Send + Sync,
931 PROTO: PrimInt + AsPrimitive<i64> + AsPrimitive<i128> + AsPrimitive<f32> + Send + Sync,
932>(
933 boxes: (ArrayView2<BOX>, Quantization),
934 protos: (ArrayView3<PROTO>, Quantization),
935 score_threshold: f32,
936 iou_threshold: f32,
937 nms: Option<Nms>,
938 pre_nms_top_k: usize,
939 max_det: usize,
940 normalized: Option<bool>,
941 input_dims: Option<(usize, usize)>,
942 output_boxes: &mut Vec<DetectBox>,
943 output_masks: &mut Vec<Segmentation>,
944) -> Result<(), crate::DecoderError>
945where
946 f32: AsPrimitive<BOX>,
947{
948 let (boxes, quant_boxes) = boxes;
949 let num_protos = protos.0.dim().2;
950
951 let (boxes_tensor, scores_tensor, mask_tensor) = postprocess_yolo_seg(&boxes, num_protos);
952 let mut boxes = impl_yolo_split_segdet_quant_get_boxes::<B, _, _>(
953 (boxes_tensor, quant_boxes),
954 (scores_tensor, quant_boxes),
955 score_threshold,
956 iou_threshold,
957 nms,
958 pre_nms_top_k,
959 max_det,
960 );
961 maybe_normalize_boxes_in_place(&mut boxes, normalized, input_dims);
962
963 impl_yolo_split_segdet_quant_process_masks::<_, _>(
964 boxes,
965 (mask_tensor, quant_boxes),
966 protos,
967 output_boxes,
968 output_masks,
969 )
970}
971
972#[allow(clippy::too_many_arguments)]
982pub(crate) fn impl_yolo_segdet_float<
983 B: BBoxTypeTrait,
984 BOX: Float + AsPrimitive<f32> + Send + Sync,
985 PROTO: Float + AsPrimitive<f32> + Send + Sync,
986>(
987 boxes: ArrayView2<BOX>,
988 protos: ArrayView3<PROTO>,
989 score_threshold: f32,
990 iou_threshold: f32,
991 nms: Option<Nms>,
992 pre_nms_top_k: usize,
993 max_det: usize,
994 normalized: Option<bool>,
995 input_dims: Option<(usize, usize)>,
996 multi_label: bool,
997 output_boxes: &mut Vec<DetectBox>,
998 output_masks: &mut Vec<Segmentation>,
999) -> Result<(), crate::DecoderError>
1000where
1001 f32: AsPrimitive<BOX>,
1002{
1003 let num_protos = protos.dim().2;
1004 let (boxes_tensor, scores_tensor, mask_tensor) = postprocess_yolo_seg(&boxes, num_protos);
1005 let mut boxes = impl_yolo_segdet_get_boxes::<B, _, _>(
1006 boxes_tensor,
1007 scores_tensor,
1008 score_threshold,
1009 iou_threshold,
1010 nms,
1011 pre_nms_top_k,
1012 max_det,
1013 multi_label,
1014 );
1015 maybe_normalize_boxes_in_place(&mut boxes, normalized, input_dims);
1016 impl_yolo_split_segdet_process_masks(boxes, mask_tensor, protos, output_boxes, output_masks)
1017}
1018
1019#[allow(clippy::too_many_arguments)]
1020pub(crate) fn impl_yolo_segdet_get_boxes<
1021 B: BBoxTypeTrait,
1022 BOX: Float + AsPrimitive<f32> + Send + Sync,
1023 SCORE: Float + AsPrimitive<f32> + Send + Sync,
1024>(
1025 boxes_tensor: ArrayView2<BOX>,
1026 scores_tensor: ArrayView2<SCORE>,
1027 score_threshold: f32,
1028 iou_threshold: f32,
1029 nms: Option<Nms>,
1030 pre_nms_top_k: usize,
1031 max_det: usize,
1032 multi_label: bool,
1033) -> Vec<(DetectBox, usize)>
1034where
1035 f32: AsPrimitive<SCORE>,
1036{
1037 let span = tracing::trace_span!(
1038 "decoder.nms_get_boxes",
1039 n_candidates = tracing::field::Empty,
1040 n_after_topk = tracing::field::Empty,
1041 n_after_nms = tracing::field::Empty,
1042 n_detections = tracing::field::Empty,
1043 );
1044 let _guard = span.enter();
1045
1046 let (mut boxes, effective_nms) = {
1051 let _s = tracing::trace_span!("decoder.nms_get_boxes.score_filter").entered();
1052 if multi_label {
1053 let candidates = postprocess_boxes_multilabel_index_float::<B, _, _>(
1054 score_threshold.as_(),
1055 boxes_tensor,
1056 scores_tensor,
1057 );
1058 let nms_override = Some(Nms::ClassAware);
1062 (candidates, nms_override)
1063 } else {
1064 let candidates = postprocess_boxes_index_float::<B, _, _>(
1065 score_threshold.as_(),
1066 boxes_tensor,
1067 scores_tensor,
1068 );
1069 (candidates, nms)
1070 }
1071 };
1072 span.record("n_candidates", boxes.len());
1073
1074 if effective_nms.is_some() {
1075 let _s = tracing::trace_span!("decoder.nms_get_boxes.top_k", k = pre_nms_top_k).entered();
1076 truncate_to_top_k_by_score(&mut boxes, pre_nms_top_k);
1077 }
1078 span.record("n_after_topk", boxes.len());
1079
1080 let mut boxes = {
1081 let _s = tracing::trace_span!("decoder.nms_get_boxes.suppress").entered();
1082 dispatch_nms_extra_float(effective_nms, iou_threshold, Some(max_det), boxes)
1083 };
1084 span.record("n_after_nms", boxes.len());
1085
1086 boxes.sort_unstable_by(|a, b| b.0.score.total_cmp(&a.0.score));
1089 boxes.truncate(max_det);
1090 span.record("n_detections", boxes.len());
1091
1092 boxes
1093}
1094
1095pub(crate) fn impl_yolo_end_to_end_segdet_get_boxes<
1096 B: BBoxTypeTrait,
1097 BOX: Float + AsPrimitive<f32> + Send + Sync,
1098 SCORE: Float + AsPrimitive<f32> + Send + Sync,
1099 CLASS: AsPrimitive<f32> + Send + Sync,
1100>(
1101 boxes: ArrayView2<BOX>,
1102 scores: ArrayView2<SCORE>,
1103 classes: ArrayView1<CLASS>,
1104 score_threshold: f32,
1105 max_boxes: usize,
1106) -> Vec<(DetectBox, usize)>
1107where
1108 f32: AsPrimitive<SCORE>,
1109{
1110 let mut boxes = postprocess_boxes_index_float::<B, _, _>(score_threshold.as_(), boxes, scores);
1111 boxes.truncate(max_boxes);
1112 for (b, ind) in &mut boxes {
1113 b.label = classes[*ind].as_().round() as usize;
1114 }
1115 boxes
1116}
1117
1118pub(crate) fn impl_yolo_split_segdet_process_masks<
1119 MASK: Float + AsPrimitive<f32> + Send + Sync,
1120 PROTO: Float + AsPrimitive<f32> + Send + Sync,
1121>(
1122 boxes: Vec<(DetectBox, usize)>,
1123 masks_tensor: ArrayView2<MASK>,
1124 protos_tensor: ArrayView3<PROTO>,
1125 output_boxes: &mut Vec<DetectBox>,
1126 output_masks: &mut Vec<Segmentation>,
1127) -> Result<(), crate::DecoderError> {
1128 let _span = tracing::trace_span!(
1129 "decoder.decode.process_masks",
1130 n = boxes.len(),
1131 mode = "float"
1132 )
1133 .entered();
1134 let boxes = decode_segdet_f32(boxes, masks_tensor, protos_tensor)?;
1138 output_boxes.clear();
1139 output_masks.clear();
1140 for (b, roi, m) in boxes.into_iter() {
1141 output_boxes.push(b);
1142 output_masks.push(Segmentation {
1143 xmin: roi.xmin,
1144 ymin: roi.ymin,
1145 xmax: roi.xmax,
1146 ymax: roi.ymax,
1147 segmentation: crate::mask_to_tensor(m.view())?,
1148 });
1149 }
1150 Ok(())
1151}
1152pub(crate) fn impl_yolo_split_segdet_quant_get_boxes<
1156 B: BBoxTypeTrait,
1157 BOX: PrimInt + AsPrimitive<f32> + Send + Sync,
1158 SCORE: PrimInt + AsPrimitive<f32> + Send + Sync,
1159>(
1160 boxes: (ArrayView2<BOX>, Quantization),
1161 scores: (ArrayView2<SCORE>, Quantization),
1162 score_threshold: f32,
1163 iou_threshold: f32,
1164 nms: Option<Nms>,
1165 pre_nms_top_k: usize,
1166 max_det: usize,
1167) -> Vec<(DetectBox, usize)>
1168where
1169 f32: AsPrimitive<SCORE>,
1170{
1171 let (boxes_tensor, quant_boxes) = boxes;
1172 let (scores_tensor, quant_scores) = scores;
1173
1174 let span = tracing::trace_span!(
1175 "decoder.nms_get_boxes",
1176 n_candidates = tracing::field::Empty,
1177 n_after_topk = tracing::field::Empty,
1178 n_after_nms = tracing::field::Empty,
1179 n_detections = tracing::field::Empty,
1180 );
1181 let _guard = span.enter();
1182
1183 let mut boxes = {
1184 let _s = tracing::trace_span!("decoder.nms_get_boxes.score_filter").entered();
1185 let score_threshold = quantize_score_threshold(score_threshold, quant_scores);
1186 postprocess_boxes_index_quant::<B, _, _>(
1187 score_threshold,
1188 boxes_tensor,
1189 scores_tensor,
1190 quant_boxes,
1191 )
1192 };
1193 span.record("n_candidates", boxes.len());
1194
1195 if nms.is_some() {
1196 let _s = tracing::trace_span!("decoder.nms_get_boxes.top_k", k = pre_nms_top_k).entered();
1197 truncate_to_top_k_by_score_quant(&mut boxes, pre_nms_top_k);
1198 }
1199 span.record("n_after_topk", boxes.len());
1200
1201 let mut boxes = {
1202 let _s = tracing::trace_span!("decoder.nms_get_boxes.suppress").entered();
1203 dispatch_nms_extra_int(nms, iou_threshold, Some(max_det), boxes)
1204 };
1205 span.record("n_after_nms", boxes.len());
1206
1207 boxes.sort_unstable_by_key(|b| std::cmp::Reverse(b.0.score));
1210 boxes.truncate(max_det);
1211 let result: Vec<_> = {
1212 let _s =
1213 tracing::trace_span!("decoder.nms_get_boxes.dequant_boxes", n = boxes.len()).entered();
1214 boxes
1215 .into_iter()
1216 .map(|(b, i)| (dequant_detect_box(&b, quant_scores), i))
1217 .collect()
1218 };
1219 span.record("n_detections", result.len());
1220
1221 result
1222}
1223
1224pub(crate) fn impl_yolo_split_segdet_quant_process_masks<
1225 MASK: PrimInt + AsPrimitive<i64> + AsPrimitive<i128> + AsPrimitive<f32> + Send + Sync,
1226 PROTO: PrimInt + AsPrimitive<i64> + AsPrimitive<i128> + AsPrimitive<f32> + Send + Sync,
1227>(
1228 boxes: Vec<(DetectBox, usize)>,
1229 mask_coeff: (ArrayView2<MASK>, Quantization),
1230 protos: (ArrayView3<PROTO>, Quantization),
1231 output_boxes: &mut Vec<DetectBox>,
1232 output_masks: &mut Vec<Segmentation>,
1233) -> Result<(), crate::DecoderError> {
1234 let _span = tracing::trace_span!(
1235 "decoder.decode.process_masks",
1236 n = boxes.len(),
1237 mode = "quant"
1238 )
1239 .entered();
1240 let (masks, quant_masks) = mask_coeff;
1241 let (protos, quant_protos) = protos;
1242
1243 let boxes = decode_segdet_quant(boxes, masks, protos, quant_masks, quant_protos)?;
1247 output_boxes.clear();
1248 output_masks.clear();
1249 for (b, roi, m) in boxes.into_iter() {
1250 output_boxes.push(b);
1251 output_masks.push(Segmentation {
1252 xmin: roi.xmin,
1253 ymin: roi.ymin,
1254 xmax: roi.xmax,
1255 ymax: roi.ymax,
1256 segmentation: crate::mask_to_tensor(m.view())?,
1257 });
1258 }
1259 Ok(())
1260}
1261
1262#[allow(clippy::too_many_arguments)]
1274pub(crate) fn impl_yolo_split_segdet_float<
1275 B: BBoxTypeTrait,
1276 BOX: Float + AsPrimitive<f32> + Send + Sync,
1277 SCORE: Float + AsPrimitive<f32> + Send + Sync,
1278 MASK: Float + AsPrimitive<f32> + Send + Sync,
1279 PROTO: Float + AsPrimitive<f32> + Send + Sync,
1280>(
1281 boxes_tensor: ArrayView2<BOX>,
1282 scores_tensor: ArrayView2<SCORE>,
1283 mask_tensor: ArrayView2<MASK>,
1284 protos: ArrayView3<PROTO>,
1285 score_threshold: f32,
1286 iou_threshold: f32,
1287 nms: Option<Nms>,
1288 pre_nms_top_k: usize,
1289 max_det: usize,
1290 normalized: Option<bool>,
1291 input_dims: Option<(usize, usize)>,
1292 output_boxes: &mut Vec<DetectBox>,
1293 output_masks: &mut Vec<Segmentation>,
1294) -> Result<(), crate::DecoderError>
1295where
1296 f32: AsPrimitive<SCORE>,
1297{
1298 let (boxes_tensor, scores_tensor, mask_tensor) =
1299 postprocess_yolo_split_segdet(boxes_tensor, scores_tensor, mask_tensor);
1300
1301 let mut boxes = impl_yolo_segdet_get_boxes::<B, _, _>(
1302 boxes_tensor,
1303 scores_tensor,
1304 score_threshold,
1305 iou_threshold,
1306 nms,
1307 pre_nms_top_k,
1308 max_det,
1309 false, );
1311 maybe_normalize_boxes_in_place(&mut boxes, normalized, input_dims);
1312 impl_yolo_split_segdet_process_masks(boxes, mask_tensor, protos, output_boxes, output_masks)
1313}
1314
1315#[allow(clippy::too_many_arguments)]
1322pub(crate) fn impl_yolo_segdet_quant_proto<
1323 B: BBoxTypeTrait,
1324 BOX: PrimInt
1325 + AsPrimitive<i64>
1326 + AsPrimitive<i128>
1327 + AsPrimitive<f32>
1328 + AsPrimitive<i8>
1329 + Send
1330 + Sync,
1331 PROTO: PrimInt
1332 + AsPrimitive<i64>
1333 + AsPrimitive<i128>
1334 + AsPrimitive<f32>
1335 + AsPrimitive<i8>
1336 + Send
1337 + Sync,
1338>(
1339 boxes: (ArrayView2<BOX>, Quantization),
1340 protos: (ArrayView3<PROTO>, Quantization),
1341 score_threshold: f32,
1342 iou_threshold: f32,
1343 nms: Option<Nms>,
1344 pre_nms_top_k: usize,
1345 max_det: usize,
1346 normalized: Option<bool>,
1347 input_dims: Option<(usize, usize)>,
1348 output_boxes: &mut Vec<DetectBox>,
1349) -> ProtoData
1350where
1351 f32: AsPrimitive<BOX>,
1352{
1353 let (boxes_arr, quant_boxes) = boxes;
1354 let (protos_arr, quant_protos) = protos;
1355 let num_protos = protos_arr.dim().2;
1356
1357 let (boxes_tensor, scores_tensor, mask_tensor) = postprocess_yolo_seg(&boxes_arr, num_protos);
1358
1359 let mut det_indices = impl_yolo_split_segdet_quant_get_boxes::<B, _, _>(
1360 (boxes_tensor, quant_boxes),
1361 (scores_tensor, quant_boxes),
1362 score_threshold,
1363 iou_threshold,
1364 nms,
1365 pre_nms_top_k,
1366 max_det,
1367 );
1368 maybe_normalize_boxes_in_place(&mut det_indices, normalized, input_dims);
1369
1370 extract_proto_data_quant(
1371 det_indices,
1372 mask_tensor,
1373 quant_boxes,
1374 protos_arr,
1375 quant_protos,
1376 output_boxes,
1377 )
1378}
1379
1380#[allow(clippy::too_many_arguments)]
1383pub(crate) fn impl_yolo_segdet_float_proto<
1384 B: BBoxTypeTrait,
1385 BOX: Float + AsPrimitive<f32> + Copy + Send + Sync + FloatProtoElem,
1386 PROTO: Float + AsPrimitive<f32> + Copy + Send + Sync + FloatProtoElem,
1387>(
1388 boxes: ArrayView2<BOX>,
1389 protos: ArrayView3<PROTO>,
1390 score_threshold: f32,
1391 iou_threshold: f32,
1392 nms: Option<Nms>,
1393 pre_nms_top_k: usize,
1394 max_det: usize,
1395 normalized: Option<bool>,
1396 input_dims: Option<(usize, usize)>,
1397 multi_label: bool,
1398 output_boxes: &mut Vec<DetectBox>,
1399) -> ProtoData
1400where
1401 f32: AsPrimitive<BOX>,
1402{
1403 let num_protos = protos.dim().2;
1404 let (boxes_tensor, scores_tensor, mask_tensor) = postprocess_yolo_seg(&boxes, num_protos);
1405
1406 let mut boxes = impl_yolo_segdet_get_boxes::<B, _, _>(
1407 boxes_tensor,
1408 scores_tensor,
1409 score_threshold,
1410 iou_threshold,
1411 nms,
1412 pre_nms_top_k,
1413 max_det,
1414 multi_label,
1415 );
1416 maybe_normalize_boxes_in_place(&mut boxes, normalized, input_dims);
1417
1418 extract_proto_data_float(boxes, mask_tensor, protos, output_boxes)
1419}
1420
1421#[allow(clippy::too_many_arguments)]
1424pub(crate) fn impl_yolo_split_segdet_float_proto<
1425 B: BBoxTypeTrait,
1426 BOX: Float + AsPrimitive<f32> + Send + Sync,
1427 SCORE: Float + AsPrimitive<f32> + Send + Sync,
1428 MASK: Float + AsPrimitive<f32> + Copy + Send + Sync + FloatProtoElem,
1429 PROTO: Float + AsPrimitive<f32> + Copy + Send + Sync + FloatProtoElem,
1430>(
1431 boxes_tensor: ArrayView2<BOX>,
1432 scores_tensor: ArrayView2<SCORE>,
1433 mask_tensor: ArrayView2<MASK>,
1434 protos: ArrayView3<PROTO>,
1435 score_threshold: f32,
1436 iou_threshold: f32,
1437 nms: Option<Nms>,
1438 pre_nms_top_k: usize,
1439 max_det: usize,
1440 normalized: Option<bool>,
1441 input_dims: Option<(usize, usize)>,
1442 output_boxes: &mut Vec<DetectBox>,
1443) -> ProtoData
1444where
1445 f32: AsPrimitive<SCORE>,
1446{
1447 let (boxes_tensor, scores_tensor, mask_tensor) =
1448 postprocess_yolo_split_segdet(boxes_tensor, scores_tensor, mask_tensor);
1449 let mut det_indices = impl_yolo_segdet_get_boxes::<B, _, _>(
1450 boxes_tensor,
1451 scores_tensor,
1452 score_threshold,
1453 iou_threshold,
1454 nms,
1455 pre_nms_top_k,
1456 max_det,
1457 false, );
1459 maybe_normalize_boxes_in_place(&mut det_indices, normalized, input_dims);
1460
1461 extract_proto_data_float(det_indices, mask_tensor, protos, output_boxes)
1462}
1463
1464pub(crate) fn decode_yolo_end_to_end_segdet_float_proto<T>(
1466 output: ArrayView2<T>,
1467 protos: ArrayView3<T>,
1468 score_threshold: f32,
1469 output_boxes: &mut Vec<DetectBox>,
1470) -> Result<ProtoData, crate::DecoderError>
1471where
1472 T: Float + AsPrimitive<f32> + Copy + Send + Sync + FloatProtoElem,
1473 f32: AsPrimitive<T>,
1474{
1475 let (boxes, scores, classes, mask_coeff) =
1476 postprocess_yolo_end_to_end_segdet(&output, protos.dim().2)?;
1477 let cap = cap_or_default(output_boxes);
1478 let boxes = impl_yolo_end_to_end_segdet_get_boxes::<XYXY, _, _, _>(
1479 boxes,
1480 scores,
1481 classes,
1482 score_threshold,
1483 cap,
1484 );
1485
1486 Ok(extract_proto_data_float(
1487 boxes,
1488 mask_coeff,
1489 protos,
1490 output_boxes,
1491 ))
1492}
1493
1494#[allow(clippy::too_many_arguments)]
1496pub(crate) fn decode_yolo_split_end_to_end_segdet_float_proto<T>(
1497 boxes: ArrayView2<T>,
1498 scores: ArrayView2<T>,
1499 classes: ArrayView2<T>,
1500 mask_coeff: ArrayView2<T>,
1501 protos: ArrayView3<T>,
1502 score_threshold: f32,
1503 output_boxes: &mut Vec<DetectBox>,
1504) -> Result<ProtoData, crate::DecoderError>
1505where
1506 T: Float + AsPrimitive<f32> + Copy + Send + Sync + FloatProtoElem,
1507 f32: AsPrimitive<T>,
1508{
1509 let (boxes, scores, classes, mask_coeff) =
1510 postprocess_yolo_split_end_to_end_segdet(boxes, scores, &classes, mask_coeff)?;
1511 let cap = cap_or_default(output_boxes);
1512 let boxes = impl_yolo_end_to_end_segdet_get_boxes::<XYXY, _, _, _>(
1513 boxes,
1514 scores,
1515 classes,
1516 score_threshold,
1517 cap,
1518 );
1519
1520 Ok(extract_proto_data_float(
1521 boxes,
1522 mask_coeff,
1523 protos,
1524 output_boxes,
1525 ))
1526}
1527
1528pub(super) fn extract_proto_data_float<
1535 MASK: Float + AsPrimitive<f32> + Copy + Send + Sync + FloatProtoElem,
1536 PROTO: Float + AsPrimitive<f32> + Copy + Send + Sync + FloatProtoElem,
1537>(
1538 det_indices: Vec<(DetectBox, usize)>,
1539 mask_tensor: ArrayView2<MASK>,
1540 protos: ArrayView3<PROTO>,
1541 output_boxes: &mut Vec<DetectBox>,
1542) -> ProtoData {
1543 let _span = tracing::trace_span!(
1544 "decoder.decode_proto.extract_proto_data",
1545 mode = "float",
1546 n = det_indices.len(),
1547 num_protos = mask_tensor.ncols(),
1548 layout = "nhwc",
1549 )
1550 .entered();
1551
1552 let num_protos = mask_tensor.ncols();
1553 let n = det_indices.len();
1554
1555 let mut coeff_rows: Vec<MASK> = Vec::with_capacity(n * num_protos);
1560 output_boxes.clear();
1561 for (det, idx) in det_indices {
1562 output_boxes.push(det);
1563 let row = mask_tensor.row(idx);
1564 coeff_rows.extend(row.iter().copied());
1565 }
1566
1567 let mask_coefficients = MASK::slice_into_tensor_dyn(&coeff_rows, &[n, num_protos])
1568 .expect("allocating mask_coefficients TensorDyn");
1569 let protos_tensor =
1570 PROTO::arrayview3_into_tensor_dyn(protos).expect("allocating protos TensorDyn");
1571
1572 ProtoData {
1573 mask_coefficients,
1574 protos: protos_tensor,
1575 layout: ProtoLayout::Nhwc,
1576 }
1577}
1578
1579pub(crate) fn extract_proto_data_quant<
1588 MASK: PrimInt + AsPrimitive<f32> + AsPrimitive<i8> + Send + Sync + 'static,
1589 PROTO: PrimInt + AsPrimitive<f32> + AsPrimitive<i8> + Send + Sync + 'static,
1590>(
1591 det_indices: Vec<(DetectBox, usize)>,
1592 mask_tensor: ArrayView2<MASK>,
1593 quant_masks: Quantization,
1594 protos: ArrayView3<PROTO>,
1595 quant_protos: Quantization,
1596 output_boxes: &mut Vec<DetectBox>,
1597) -> ProtoData {
1598 use edgefirst_tensor::{Tensor, TensorDyn, TensorMapTrait, TensorMemory, TensorTrait};
1599
1600 let span = tracing::trace_span!(
1601 "decoder.decode_proto.extract_proto_data",
1602 mode = "quant",
1603 n = det_indices.len(),
1604 num_protos = tracing::field::Empty,
1605 layout = tracing::field::Empty,
1606 );
1607 let _guard = span.enter();
1608
1609 let num_protos = mask_tensor.ncols();
1610 let n = det_indices.len();
1611 span.record("num_protos", num_protos);
1612
1613 if n == 0 {
1619 output_boxes.clear();
1620 let (h, w, k) = protos.dim();
1621
1622 let (proto_shape, proto_layout) = if std::any::TypeId::of::<PROTO>()
1624 == std::any::TypeId::of::<i8>()
1625 {
1626 if protos.is_standard_layout() {
1627 (&[h, w, k][..], ProtoLayout::Nhwc)
1628 } else if protos.ndim() == 3 && protos.strides() == [w as isize, 1, (h * w) as isize] {
1629 (&[k, h, w][..], ProtoLayout::Nchw)
1630 } else {
1631 (&[h, w, k][..], ProtoLayout::Nhwc)
1632 }
1633 } else {
1634 (&[h, w, k][..], ProtoLayout::Nhwc)
1635 };
1636
1637 let coeff_tensor = Tensor::<i8>::new(&[0, num_protos], Some(TensorMemory::Mem), None)
1638 .expect("allocating empty mask_coefficients tensor");
1639 let coeff_quant =
1640 edgefirst_tensor::Quantization::per_tensor(quant_masks.scale, quant_masks.zero_point);
1641 let coeff_tensor = coeff_tensor
1642 .with_quantization(coeff_quant)
1643 .expect("per-tensor quantization on mask coefficients");
1644 let protos_tensor = Tensor::<i8>::new(proto_shape, Some(TensorMemory::Mem), None)
1645 .expect("allocating protos tensor");
1646 let tensor_quant =
1647 edgefirst_tensor::Quantization::per_tensor(quant_protos.scale, quant_protos.zero_point);
1648 let protos_tensor = protos_tensor
1649 .with_quantization(tensor_quant)
1650 .expect("per-tensor quantization on protos tensor");
1651 return ProtoData {
1652 mask_coefficients: coeff_tensor.into(),
1653 protos: protos_tensor.into(),
1654 layout: proto_layout,
1655 };
1656 }
1657
1658 let mask_coefficients: TensorDyn = if std::any::TypeId::of::<MASK>()
1664 == std::any::TypeId::of::<i8>()
1665 {
1666 let mut coeff_i8 = Vec::<i8>::with_capacity(n * num_protos);
1667 output_boxes.clear();
1668 for (det, idx) in det_indices {
1669 output_boxes.push(det);
1670 let row = mask_tensor.row(idx);
1671 coeff_i8.extend(row.iter().map(|v| {
1672 let v_i8: i8 = v.as_();
1673 v_i8
1674 }));
1675 }
1676 let coeff_tensor = Tensor::<i8>::new(&[n, num_protos], Some(TensorMemory::Mem), None)
1677 .expect("allocating mask_coefficients tensor");
1678 if n > 0 {
1679 let mut m = coeff_tensor
1680 .map()
1681 .expect("mapping mask_coefficients tensor");
1682 m.as_mut_slice().copy_from_slice(&coeff_i8);
1683 }
1684 let coeff_quant =
1685 edgefirst_tensor::Quantization::per_tensor(quant_masks.scale, quant_masks.zero_point);
1686 let coeff_tensor = coeff_tensor
1687 .with_quantization(coeff_quant)
1688 .expect("per-tensor quantization on mask coefficients");
1689 coeff_tensor.into()
1690 } else if std::any::TypeId::of::<MASK>() == std::any::TypeId::of::<i16>() {
1691 let mut coeff_i16 = Vec::<i16>::with_capacity(n * num_protos);
1694 output_boxes.clear();
1695 for (det, idx) in det_indices {
1696 output_boxes.push(det);
1697 let row = mask_tensor.row(idx);
1698 coeff_i16.extend(row.iter().map(|v| {
1699 let v_f32: f32 = v.as_();
1700 v_f32 as i16
1701 }));
1702 }
1703 let coeff_tensor = Tensor::<i16>::new(&[n, num_protos], Some(TensorMemory::Mem), None)
1704 .expect("allocating mask_coefficients tensor");
1705 if n > 0 {
1706 let mut m = coeff_tensor
1707 .map()
1708 .expect("mapping mask_coefficients tensor");
1709 m.as_mut_slice().copy_from_slice(&coeff_i16);
1710 }
1711 let coeff_quant =
1712 edgefirst_tensor::Quantization::per_tensor(quant_masks.scale, quant_masks.zero_point);
1713 let coeff_tensor = coeff_tensor
1714 .with_quantization(coeff_quant)
1715 .expect("per-tensor quantization on mask coefficients");
1716 coeff_tensor.into()
1717 } else {
1718 let scale = quant_masks.scale;
1720 let zp = quant_masks.zero_point as f32;
1721 let mut coeff_f32 = Vec::<f32>::with_capacity(n * num_protos);
1722 output_boxes.clear();
1723 for (det, idx) in det_indices {
1724 output_boxes.push(det);
1725 let row = mask_tensor.row(idx);
1726 coeff_f32.extend(row.iter().map(|v| {
1727 let v_f32: f32 = v.as_();
1728 (v_f32 - zp) * scale
1729 }));
1730 }
1731 let coeff_tensor = Tensor::<f32>::new(&[n, num_protos], Some(TensorMemory::Mem), None)
1732 .expect("allocating mask_coefficients tensor");
1733 if n > 0 {
1734 let mut m = coeff_tensor
1735 .map()
1736 .expect("mapping mask_coefficients tensor");
1737 m.as_mut_slice().copy_from_slice(&coeff_f32);
1738 }
1739 coeff_tensor.into()
1740 };
1741
1742 let (h, w, k) = protos.dim();
1746
1747 let (proto_shape, proto_layout) =
1749 if std::any::TypeId::of::<PROTO>() == std::any::TypeId::of::<i8>() {
1750 if protos.is_standard_layout() {
1751 (&[h, w, k][..], ProtoLayout::Nhwc)
1753 } else if protos.ndim() == 3 && protos.strides() == [w as isize, 1, (h * w) as isize] {
1754 (&[k, h, w][..], ProtoLayout::Nchw)
1758 } else {
1759 (&[h, w, k][..], ProtoLayout::Nhwc)
1761 }
1762 } else {
1763 (&[h, w, k][..], ProtoLayout::Nhwc)
1764 };
1765
1766 let protos_tensor = Tensor::<i8>::new(proto_shape, Some(TensorMemory::Mem), None)
1767 .expect("allocating protos tensor");
1768 {
1769 let mut m = protos_tensor.map().expect("mapping protos tensor");
1770 let dst = m.as_mut_slice();
1771 if std::any::TypeId::of::<PROTO>() == std::any::TypeId::of::<i8>() {
1772 if protos.is_standard_layout() {
1775 let src: &[i8] = unsafe {
1776 std::slice::from_raw_parts(protos.as_ptr() as *const i8, protos.len())
1777 };
1778 dst.copy_from_slice(src);
1779 } else if protos.ndim() == 3 && protos.strides() == [w as isize, 1, (h * w) as isize] {
1780 let total = h * w * k;
1784 let src: &[i8] =
1787 unsafe { std::slice::from_raw_parts(protos.as_ptr() as *const i8, total) };
1788 dst.copy_from_slice(src);
1789 } else {
1790 for (d, s) in dst.iter_mut().zip(protos.iter()) {
1791 let v_i8: i8 = s.as_();
1792 *d = v_i8;
1793 }
1794 }
1795 } else {
1796 for (d, s) in dst.iter_mut().zip(protos.iter()) {
1797 let v_i8: i8 = s.as_();
1798 *d = v_i8;
1799 }
1800 }
1801 }
1802 let tensor_quant =
1803 edgefirst_tensor::Quantization::per_tensor(quant_protos.scale, quant_protos.zero_point);
1804 let protos_tensor = protos_tensor
1805 .with_quantization(tensor_quant)
1806 .expect("per-tensor quantization on new Tensor<i8>");
1807
1808 span.record("layout", tracing::field::debug(&proto_layout));
1809
1810 ProtoData {
1811 mask_coefficients,
1812 protos: protos_tensor.into(),
1813 layout: proto_layout,
1814 }
1815}
1816
1817pub trait FloatProtoElem: Copy + 'static {
1827 fn slice_into_tensor_dyn(
1830 values: &[Self],
1831 shape: &[usize],
1832 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn>;
1833
1834 fn arrayview3_into_tensor_dyn(
1837 view: ArrayView3<'_, Self>,
1838 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn>;
1839}
1840
1841impl FloatProtoElem for f32 {
1842 fn slice_into_tensor_dyn(
1843 values: &[f32],
1844 shape: &[usize],
1845 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1846 edgefirst_tensor::Tensor::<f32>::from_slice(values, shape).map(Into::into)
1847 }
1848 fn arrayview3_into_tensor_dyn(
1849 view: ArrayView3<'_, f32>,
1850 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1851 edgefirst_tensor::Tensor::<f32>::from_arrayview3(view).map(Into::into)
1852 }
1853}
1854
1855impl FloatProtoElem for half::f16 {
1856 fn slice_into_tensor_dyn(
1857 values: &[half::f16],
1858 shape: &[usize],
1859 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1860 edgefirst_tensor::Tensor::<half::f16>::from_slice(values, shape).map(Into::into)
1861 }
1862 fn arrayview3_into_tensor_dyn(
1863 view: ArrayView3<'_, half::f16>,
1864 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1865 edgefirst_tensor::Tensor::<half::f16>::from_arrayview3(view).map(Into::into)
1866 }
1867}
1868
1869impl FloatProtoElem for f64 {
1870 fn slice_into_tensor_dyn(
1871 values: &[f64],
1872 shape: &[usize],
1873 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1874 let narrowed: Vec<f32> = values.iter().map(|&v| v as f32).collect();
1876 edgefirst_tensor::Tensor::<f32>::from_slice(&narrowed, shape).map(Into::into)
1877 }
1878 fn arrayview3_into_tensor_dyn(
1879 view: ArrayView3<'_, f64>,
1880 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1881 let narrowed: ndarray::Array3<f32> = view.mapv(|v| v as f32);
1882 edgefirst_tensor::Tensor::<f32>::from_arrayview3(narrowed.view()).map(Into::into)
1883 }
1884}
1885
1886fn postprocess_yolo<'a, T>(
1887 output: &'a ArrayView2<'_, T>,
1888) -> (ArrayView2<'a, T>, ArrayView2<'a, T>) {
1889 let boxes_tensor = output.slice(s![..4, ..,]).reversed_axes();
1890 let scores_tensor = output.slice(s![4.., ..,]).reversed_axes();
1891 (boxes_tensor, scores_tensor)
1892}
1893
1894pub(crate) fn postprocess_yolo_seg<'a, T>(
1895 output: &'a ArrayView2<'_, T>,
1896 num_protos: usize,
1897) -> (ArrayView2<'a, T>, ArrayView2<'a, T>, ArrayView2<'a, T>) {
1898 assert!(
1899 output.shape()[0] > num_protos + 4,
1900 "Output shape is too short: {} <= {} + 4",
1901 output.shape()[0],
1902 num_protos
1903 );
1904 let num_classes = output.shape()[0] - 4 - num_protos;
1905 let boxes_tensor = output.slice(s![..4, ..,]).reversed_axes();
1906 let scores_tensor = output.slice(s![4..(num_classes + 4), ..,]).reversed_axes();
1907 let mask_tensor = output.slice(s![(num_classes + 4).., ..,]).reversed_axes();
1908 (boxes_tensor, scores_tensor, mask_tensor)
1909}
1910
1911pub(crate) fn postprocess_yolo_split_segdet<'a, 'b, 'c, BOX, SCORE, MASK>(
1912 boxes_tensor: ArrayView2<'a, BOX>,
1913 scores_tensor: ArrayView2<'b, SCORE>,
1914 mask_tensor: ArrayView2<'c, MASK>,
1915) -> (
1916 ArrayView2<'a, BOX>,
1917 ArrayView2<'b, SCORE>,
1918 ArrayView2<'c, MASK>,
1919) {
1920 let boxes_tensor = boxes_tensor.reversed_axes();
1921 let scores_tensor = scores_tensor.reversed_axes();
1922 let mask_tensor = mask_tensor.reversed_axes();
1923 (boxes_tensor, scores_tensor, mask_tensor)
1924}
1925
1926fn decode_segdet_f32<
1927 MASK: Float + AsPrimitive<f32> + Send + Sync,
1928 PROTO: Float + AsPrimitive<f32> + Send + Sync,
1929>(
1930 boxes: Vec<(DetectBox, usize)>,
1931 masks: ArrayView2<MASK>,
1932 protos: ArrayView3<PROTO>,
1933) -> Result<Vec<(DetectBox, BoundingBox, Array3<u8>)>, crate::DecoderError> {
1934 if boxes.is_empty() {
1935 return Ok(Vec::new());
1936 }
1937 if masks.shape()[1] != protos.shape()[2] {
1938 return Err(crate::DecoderError::InvalidShape(format!(
1939 "Mask coefficients count ({}) doesn't match protos channel count ({})",
1940 masks.shape()[1],
1941 protos.shape()[2],
1942 )));
1943 }
1944 boxes
1945 .into_par_iter()
1946 .map(|b| {
1947 let ind = b.1;
1948 let (protos, roi) = protobox(&protos, &b.0.bbox)?;
1953 Ok((b.0, roi, make_segmentation(masks.row(ind), protos.view())))
1954 })
1955 .collect()
1956}
1957
1958pub(crate) fn decode_segdet_quant<
1959 MASK: PrimInt + AsPrimitive<i64> + AsPrimitive<i128> + Send + Sync,
1960 PROTO: PrimInt + AsPrimitive<i64> + AsPrimitive<i128> + Send + Sync,
1961>(
1962 boxes: Vec<(DetectBox, usize)>,
1963 masks: ArrayView2<MASK>,
1964 protos: ArrayView3<PROTO>,
1965 quant_masks: Quantization,
1966 quant_protos: Quantization,
1967) -> Result<Vec<(DetectBox, BoundingBox, Array3<u8>)>, crate::DecoderError> {
1968 if boxes.is_empty() {
1969 return Ok(Vec::new());
1970 }
1971 if masks.shape()[1] != protos.shape()[2] {
1972 return Err(crate::DecoderError::InvalidShape(format!(
1973 "Mask coefficients count ({}) doesn't match protos channel count ({})",
1974 masks.shape()[1],
1975 protos.shape()[2],
1976 )));
1977 }
1978
1979 let total_bits = MASK::zero().count_zeros() + PROTO::zero().count_zeros() + 5; boxes
1981 .into_iter()
1982 .map(|b| {
1983 let i = b.1;
1984 let (protos, roi) = protobox(&protos, &b.0.bbox.to_canonical())?;
1988 let seg = match total_bits {
1989 0..=64 => make_segmentation_quant::<MASK, PROTO, i64>(
1990 masks.row(i),
1991 protos.view(),
1992 quant_masks,
1993 quant_protos,
1994 ),
1995 65..=128 => make_segmentation_quant::<MASK, PROTO, i128>(
1996 masks.row(i),
1997 protos.view(),
1998 quant_masks,
1999 quant_protos,
2000 ),
2001 _ => {
2002 return Err(crate::DecoderError::NotSupported(format!(
2003 "Unsupported bit width ({total_bits}) for segmentation computation"
2004 )));
2005 }
2006 };
2007 Ok((b.0, roi, seg))
2008 })
2009 .collect()
2010}
2011
2012fn protobox<'a, T>(
2013 protos: &'a ArrayView3<T>,
2014 roi: &BoundingBox,
2015) -> Result<(ArrayView3<'a, T>, BoundingBox), crate::DecoderError> {
2016 let width = protos.dim().1 as f32;
2017 let height = protos.dim().0 as f32;
2018
2019 const NORM_LIMIT: f32 = 2.0;
2031 if roi.xmin > NORM_LIMIT
2032 || roi.ymin > NORM_LIMIT
2033 || roi.xmax > NORM_LIMIT
2034 || roi.ymax > NORM_LIMIT
2035 {
2036 return Err(crate::DecoderError::InvalidShape(format!(
2037 "Bounding box coordinates appear un-normalized (pixel-space). \
2038 Got bbox=({:.2}, {:.2}, {:.2}, {:.2}) but expected values in [0, 1]. \
2039 Two ways to fix this: \
2040 (1) declare `Detection::normalized = false` in the model schema \
2041 AND make sure the schema's `input.shape` / `input.dshape` carries \
2042 the model input dims so the decoder can divide by (W, H) before NMS \
2043 (EDGEAI-1303 — verify with `Decoder::input_dims().is_some()`); or \
2044 (2) normalize the boxes in-graph before decode().",
2045 roi.xmin, roi.ymin, roi.xmax, roi.ymax,
2046 )));
2047 }
2048
2049 let roi = [
2050 (roi.xmin * width).clamp(0.0, width) as usize,
2051 (roi.ymin * height).clamp(0.0, height) as usize,
2052 (roi.xmax * width).clamp(0.0, width).ceil() as usize,
2053 (roi.ymax * height).clamp(0.0, height).ceil() as usize,
2054 ];
2055
2056 let roi_norm = [
2057 roi[0] as f32 / width,
2058 roi[1] as f32 / height,
2059 roi[2] as f32 / width,
2060 roi[3] as f32 / height,
2061 ]
2062 .into();
2063
2064 let cropped = protos.slice(s![roi[1]..roi[3], roi[0]..roi[2], ..]);
2065
2066 Ok((cropped, roi_norm))
2067}
2068
2069fn make_segmentation<
2075 MASK: Float + AsPrimitive<f32> + Send + Sync,
2076 PROTO: Float + AsPrimitive<f32> + Send + Sync,
2077>(
2078 mask: ArrayView1<MASK>,
2079 protos: ArrayView3<PROTO>,
2080) -> Array3<u8> {
2081 let shape = protos.shape();
2082
2083 let mask = mask.to_shape((1, mask.len())).unwrap();
2085 let protos = protos.to_shape([shape[0] * shape[1], shape[2]]).unwrap();
2086 let protos = protos.reversed_axes();
2087 let mask = mask.map(|x| x.as_());
2088 let protos = protos.map(|x| x.as_());
2089
2090 let mask = mask
2092 .dot(&protos)
2093 .into_shape_with_order((shape[0], shape[1], 1))
2094 .unwrap();
2095
2096 mask.map(|x| {
2097 let sigmoid = 1.0 / (1.0 + (-*x).exp());
2098 (sigmoid * 255.0).round() as u8
2099 })
2100}
2101
2102fn make_segmentation_quant<
2109 MASK: PrimInt + AsPrimitive<DEST> + Send + Sync,
2110 PROTO: PrimInt + AsPrimitive<DEST> + Send + Sync,
2111 DEST: PrimInt + 'static + Signed + AsPrimitive<f32> + Debug,
2112>(
2113 mask: ArrayView1<MASK>,
2114 protos: ArrayView3<PROTO>,
2115 quant_masks: Quantization,
2116 quant_protos: Quantization,
2117) -> Array3<u8>
2118where
2119 i32: AsPrimitive<DEST>,
2120 f32: AsPrimitive<DEST>,
2121{
2122 let shape = protos.shape();
2123
2124 let mask = mask.to_shape((1, mask.len())).unwrap();
2126
2127 let protos = protos.to_shape([shape[0] * shape[1], shape[2]]).unwrap();
2128 let protos = protos.reversed_axes();
2129
2130 let zp = quant_masks.zero_point.as_();
2131
2132 let mask = mask.mapv(|x| x.as_() - zp);
2133
2134 let zp = quant_protos.zero_point.as_();
2135 let protos = protos.mapv(|x| x.as_() - zp);
2136
2137 let segmentation = mask
2139 .dot(&protos)
2140 .into_shape_with_order((shape[0], shape[1], 1))
2141 .unwrap();
2142
2143 let combined_scale = quant_masks.scale * quant_protos.scale;
2144 segmentation.map(|x| {
2145 let val: f32 = (*x).as_() * combined_scale;
2146 let sigmoid = 1.0 / (1.0 + (-val).exp());
2147 (sigmoid * 255.0).round() as u8
2148 })
2149}
2150
2151pub(crate) fn yolo_segmentation_to_mask(
2163 segmentation: ArrayView3<u8>,
2164 threshold: u8,
2165) -> Result<Array2<u8>, crate::DecoderError> {
2166 if segmentation.shape()[2] != 1 {
2167 return Err(crate::DecoderError::InvalidShape(format!(
2168 "Yolo Instance Segmentation should have shape (H, W, 1), got (H, W, {})",
2169 segmentation.shape()[2]
2170 )));
2171 }
2172 Ok(segmentation
2173 .slice(s![.., .., 0])
2174 .map(|x| if *x >= threshold { 1 } else { 0 }))
2175}
2176
2177#[cfg(test)]
2178#[cfg_attr(coverage_nightly, coverage(off))]
2179mod tests {
2180 use super::*;
2181 use ndarray::Array2;
2182
2183 #[test]
2188 fn test_end_to_end_det_basic_filtering() {
2189 let data: Vec<f32> = vec![
2193 0.1, 0.2, 0.3, 0.1, 0.2, 0.3, 0.5, 0.6, 0.7, 0.5, 0.6, 0.7, 0.9, 0.1, 0.2, 0.0, 1.0, 2.0, ];
2201 let output = Array2::from_shape_vec((6, 3), data).unwrap();
2202
2203 let mut boxes = Vec::with_capacity(10);
2204 decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2205
2206 assert_eq!(boxes.len(), 1);
2208 assert_eq!(boxes[0].label, 0);
2209 assert!((boxes[0].score - 0.9).abs() < 0.01);
2210 assert!((boxes[0].bbox.xmin - 0.1).abs() < 0.01);
2211 assert!((boxes[0].bbox.ymin - 0.1).abs() < 0.01);
2212 assert!((boxes[0].bbox.xmax - 0.5).abs() < 0.01);
2213 assert!((boxes[0].bbox.ymax - 0.5).abs() < 0.01);
2214 }
2215
2216 #[test]
2217 fn test_end_to_end_det_all_pass_threshold() {
2218 let data: Vec<f32> = vec![
2220 10.0, 20.0, 10.0, 20.0, 50.0, 60.0, 50.0, 60.0, 0.8, 0.7, 1.0, 2.0, ];
2227 let output = Array2::from_shape_vec((6, 2), data).unwrap();
2228
2229 let mut boxes = Vec::with_capacity(10);
2230 decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2231
2232 assert_eq!(boxes.len(), 2);
2233 assert_eq!(boxes[0].label, 1);
2234 assert_eq!(boxes[1].label, 2);
2235 }
2236
2237 #[test]
2238 fn test_end_to_end_det_none_pass_threshold() {
2239 let data: Vec<f32> = vec![
2241 10.0, 20.0, 10.0, 20.0, 50.0, 60.0, 50.0, 60.0, 0.1, 0.2, 1.0, 2.0, ];
2248 let output = Array2::from_shape_vec((6, 2), data).unwrap();
2249
2250 let mut boxes = Vec::with_capacity(10);
2251 decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2252
2253 assert_eq!(boxes.len(), 0);
2254 }
2255
2256 #[test]
2257 fn test_end_to_end_det_capacity_limit() {
2258 let data: Vec<f32> = vec![
2260 0.1, 0.2, 0.3, 0.4, 0.5, 0.1, 0.2, 0.3, 0.4, 0.5, 0.5, 0.6, 0.7, 0.8, 0.9, 0.5, 0.6, 0.7, 0.8, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.0, 1.0, 2.0, 3.0, 4.0, ];
2267 let output = Array2::from_shape_vec((6, 5), data).unwrap();
2268
2269 let mut boxes = Vec::with_capacity(2); decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2271
2272 assert_eq!(boxes.len(), 2);
2273 }
2274
2275 #[test]
2276 fn test_end_to_end_det_empty_output() {
2277 let output = Array2::<f32>::zeros((6, 0));
2279
2280 let mut boxes = Vec::with_capacity(10);
2281 decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2282
2283 assert_eq!(boxes.len(), 0);
2284 }
2285
2286 #[test]
2287 fn test_end_to_end_det_pixel_coordinates() {
2288 let data: Vec<f32> = vec![
2290 100.0, 200.0, 300.0, 400.0, 0.95, 5.0, ];
2297 let output = Array2::from_shape_vec((6, 1), data).unwrap();
2298
2299 let mut boxes = Vec::with_capacity(10);
2300 decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2301
2302 assert_eq!(boxes.len(), 1);
2303 assert_eq!(boxes[0].label, 5);
2304 assert!((boxes[0].bbox.xmin - 100.0).abs() < 0.01);
2305 assert!((boxes[0].bbox.ymin - 200.0).abs() < 0.01);
2306 assert!((boxes[0].bbox.xmax - 300.0).abs() < 0.01);
2307 assert!((boxes[0].bbox.ymax - 400.0).abs() < 0.01);
2308 }
2309
2310 #[test]
2311 fn test_end_to_end_det_invalid_shape() {
2312 let output = Array2::<f32>::zeros((5, 3));
2314
2315 let mut boxes = Vec::with_capacity(10);
2316 let result = decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes);
2317
2318 assert!(result.is_err());
2319 assert!(matches!(
2320 result,
2321 Err(crate::DecoderError::InvalidShape(s)) if s.contains("at least 6 rows")
2322 ));
2323 }
2324
2325 #[test]
2330 fn test_end_to_end_segdet_basic() {
2331 let num_protos = 32;
2334 let num_detections = 2;
2335 let num_features = 6 + num_protos;
2336
2337 let mut data = vec![0.0f32; num_features * num_detections];
2339 data[0] = 0.1; data[1] = 0.5; data[num_detections] = 0.1; data[num_detections + 1] = 0.5; data[2 * num_detections] = 0.4; data[2 * num_detections + 1] = 0.9; data[3 * num_detections] = 0.4; data[3 * num_detections + 1] = 0.9; data[4 * num_detections] = 0.9; data[4 * num_detections + 1] = 0.3; data[5 * num_detections] = 1.0; data[5 * num_detections + 1] = 2.0; for i in 6..num_features {
2354 data[i * num_detections] = 0.1;
2355 data[i * num_detections + 1] = 0.1;
2356 }
2357
2358 let output = Array2::from_shape_vec((num_features, num_detections), data).unwrap();
2359
2360 let protos = Array3::<f32>::zeros((16, 16, num_protos));
2362
2363 let mut boxes = Vec::with_capacity(10);
2364 let mut masks = Vec::with_capacity(10);
2365 decode_yolo_end_to_end_segdet_float(
2366 output.view(),
2367 protos.view(),
2368 0.5,
2369 &mut boxes,
2370 &mut masks,
2371 )
2372 .unwrap();
2373
2374 assert_eq!(boxes.len(), 1);
2376 assert_eq!(masks.len(), 1);
2377 assert_eq!(boxes[0].label, 1);
2378 assert!((boxes[0].score - 0.9).abs() < 0.01);
2379 }
2380
2381 #[test]
2382 fn test_end_to_end_segdet_mask_coordinates() {
2383 let num_protos = 32;
2385 let num_features = 6 + num_protos;
2386
2387 let mut data = vec![0.0f32; num_features];
2388 data[0] = 0.2; data[1] = 0.2; data[2] = 0.8; data[3] = 0.8; data[4] = 0.95; data[5] = 3.0; let output = Array2::from_shape_vec((num_features, 1), data).unwrap();
2396 let protos = Array3::<f32>::zeros((16, 16, num_protos));
2397
2398 let mut boxes = Vec::with_capacity(10);
2399 let mut masks = Vec::with_capacity(10);
2400 decode_yolo_end_to_end_segdet_float(
2401 output.view(),
2402 protos.view(),
2403 0.5,
2404 &mut boxes,
2405 &mut masks,
2406 )
2407 .unwrap();
2408
2409 assert_eq!(boxes.len(), 1);
2410 assert_eq!(masks.len(), 1);
2411
2412 let step = 1.0 / 16.0;
2416 assert!(masks[0].xmin <= boxes[0].bbox.xmin);
2417 assert!(masks[0].ymin <= boxes[0].bbox.ymin);
2418 assert!(masks[0].xmax >= boxes[0].bbox.xmax);
2419 assert!(masks[0].ymax >= boxes[0].bbox.ymax);
2420 assert!((boxes[0].bbox.xmin - masks[0].xmin) < step);
2421 assert!((boxes[0].bbox.ymin - masks[0].ymin) < step);
2422 assert!((masks[0].xmax - boxes[0].bbox.xmax) < step);
2423 assert!((masks[0].ymax - boxes[0].bbox.ymax) < step);
2424 }
2425
2426 #[test]
2427 fn test_end_to_end_segdet_empty_output() {
2428 let num_protos = 32;
2429 let output = Array2::<f32>::zeros((6 + num_protos, 0));
2430 let protos = Array3::<f32>::zeros((16, 16, num_protos));
2431
2432 let mut boxes = Vec::with_capacity(10);
2433 let mut masks = Vec::with_capacity(10);
2434 decode_yolo_end_to_end_segdet_float(
2435 output.view(),
2436 protos.view(),
2437 0.5,
2438 &mut boxes,
2439 &mut masks,
2440 )
2441 .unwrap();
2442
2443 assert_eq!(boxes.len(), 0);
2444 assert_eq!(masks.len(), 0);
2445 }
2446
2447 #[test]
2448 fn test_end_to_end_segdet_capacity_limit() {
2449 let num_protos = 32;
2450 let num_detections = 5;
2451 let num_features = 6 + num_protos;
2452
2453 let mut data = vec![0.0f32; num_features * num_detections];
2454 for i in 0..num_detections {
2456 data[i] = 0.1 * (i as f32); data[num_detections + i] = 0.1 * (i as f32); data[2 * num_detections + i] = 0.1 * (i as f32) + 0.2; data[3 * num_detections + i] = 0.1 * (i as f32) + 0.2; data[4 * num_detections + i] = 0.9; data[5 * num_detections + i] = i as f32; }
2463
2464 let output = Array2::from_shape_vec((num_features, num_detections), data).unwrap();
2465 let protos = Array3::<f32>::zeros((16, 16, num_protos));
2466
2467 let mut boxes = Vec::with_capacity(2); let mut masks = Vec::with_capacity(2);
2469 decode_yolo_end_to_end_segdet_float(
2470 output.view(),
2471 protos.view(),
2472 0.5,
2473 &mut boxes,
2474 &mut masks,
2475 )
2476 .unwrap();
2477
2478 assert_eq!(boxes.len(), 2);
2479 assert_eq!(masks.len(), 2);
2480 }
2481
2482 #[test]
2483 fn test_end_to_end_segdet_invalid_shape_too_few_rows() {
2484 let output = Array2::<f32>::zeros((6, 3));
2486 let protos = Array3::<f32>::zeros((16, 16, 32));
2487
2488 let mut boxes = Vec::with_capacity(10);
2489 let mut masks = Vec::with_capacity(10);
2490 let result = decode_yolo_end_to_end_segdet_float(
2491 output.view(),
2492 protos.view(),
2493 0.5,
2494 &mut boxes,
2495 &mut masks,
2496 );
2497
2498 assert!(result.is_err());
2499 assert!(matches!(
2500 result,
2501 Err(crate::DecoderError::InvalidShape(s)) if s.contains("at least 7 rows")
2502 ));
2503 }
2504
2505 #[test]
2506 fn test_end_to_end_segdet_invalid_shape_protos_mismatch() {
2507 let num_protos = 32;
2509 let output = Array2::<f32>::zeros((6 + 16, 3)); let protos = Array3::<f32>::zeros((16, 16, num_protos)); let mut boxes = Vec::with_capacity(10);
2513 let mut masks = Vec::with_capacity(10);
2514 let result = decode_yolo_end_to_end_segdet_float(
2515 output.view(),
2516 protos.view(),
2517 0.5,
2518 &mut boxes,
2519 &mut masks,
2520 );
2521
2522 assert!(result.is_err());
2523 assert!(matches!(
2524 result,
2525 Err(crate::DecoderError::InvalidShape(s)) if s.contains("doesn't match protos count")
2526 ));
2527 }
2528
2529 #[test]
2534 fn test_split_end_to_end_segdet_basic() {
2535 let num_protos = 32;
2538 let num_detections = 2;
2539 let num_features = 6 + num_protos;
2540
2541 let mut data = vec![0.0f32; num_features * num_detections];
2543 data[0] = 0.1; data[1] = 0.5; data[num_detections] = 0.1; data[num_detections + 1] = 0.5; data[2 * num_detections] = 0.4; data[2 * num_detections + 1] = 0.9; data[3 * num_detections] = 0.4; data[3 * num_detections + 1] = 0.9; data[4 * num_detections] = 0.9; data[4 * num_detections + 1] = 0.3; data[5 * num_detections] = 1.0; data[5 * num_detections + 1] = 2.0; for i in 6..num_features {
2558 data[i * num_detections] = 0.1;
2559 data[i * num_detections + 1] = 0.1;
2560 }
2561
2562 let output = Array2::from_shape_vec((num_features, num_detections), data).unwrap();
2563 let box_coords = output.slice(s![..4, ..]);
2564 let scores = output.slice(s![4..5, ..]);
2565 let classes = output.slice(s![5..6, ..]);
2566 let mask_coeff = output.slice(s![6.., ..]);
2567 let protos = Array3::<f32>::zeros((16, 16, num_protos));
2569
2570 let mut boxes = Vec::with_capacity(10);
2571 let mut masks = Vec::with_capacity(10);
2572 decode_yolo_split_end_to_end_segdet_float(
2573 box_coords,
2574 scores,
2575 classes,
2576 mask_coeff,
2577 protos.view(),
2578 0.5,
2579 &mut boxes,
2580 &mut masks,
2581 )
2582 .unwrap();
2583
2584 assert_eq!(boxes.len(), 1);
2586 assert_eq!(masks.len(), 1);
2587 assert_eq!(boxes[0].label, 1);
2588 assert!((boxes[0].score - 0.9).abs() < 0.01);
2589 }
2590
2591 #[test]
2596 fn test_segmentation_to_mask_basic() {
2597 let data: Vec<u8> = vec![
2599 100, 200, 50, 150, 10, 255, 128, 64, 0, 127, 128, 255, 64, 64, 192, 192, ];
2604 let segmentation = Array3::from_shape_vec((4, 4, 1), data).unwrap();
2605
2606 let mask = yolo_segmentation_to_mask(segmentation.view(), 128).unwrap();
2607
2608 assert_eq!(mask[[0, 0]], 0); assert_eq!(mask[[0, 1]], 1); assert_eq!(mask[[0, 2]], 0); assert_eq!(mask[[0, 3]], 1); assert_eq!(mask[[1, 1]], 1); assert_eq!(mask[[1, 2]], 1); assert_eq!(mask[[2, 0]], 0); assert_eq!(mask[[2, 1]], 0); }
2618
2619 #[test]
2620 fn test_segmentation_to_mask_all_above() {
2621 let segmentation = Array3::from_elem((4, 4, 1), 255u8);
2622 let mask = yolo_segmentation_to_mask(segmentation.view(), 128).unwrap();
2623 assert!(mask.iter().all(|&x| x == 1));
2624 }
2625
2626 #[test]
2627 fn test_segmentation_to_mask_all_below() {
2628 let segmentation = Array3::from_elem((4, 4, 1), 64u8);
2629 let mask = yolo_segmentation_to_mask(segmentation.view(), 128).unwrap();
2630 assert!(mask.iter().all(|&x| x == 0));
2631 }
2632
2633 #[test]
2634 fn test_segmentation_to_mask_invalid_shape() {
2635 let segmentation = Array3::from_elem((4, 4, 3), 128u8);
2636 let result = yolo_segmentation_to_mask(segmentation.view(), 128);
2637
2638 assert!(result.is_err());
2639 assert!(matches!(
2640 result,
2641 Err(crate::DecoderError::InvalidShape(s)) if s.contains("(H, W, 1)")
2642 ));
2643 }
2644
2645 #[test]
2650 fn test_protobox_clamps_edge_coordinates() {
2651 let protos = Array3::<f32>::zeros((16, 16, 4));
2653 let view = protos.view();
2654 let roi = BoundingBox {
2655 xmin: 0.5,
2656 ymin: 0.5,
2657 xmax: 1.0,
2658 ymax: 1.0,
2659 };
2660 let result = protobox(&view, &roi);
2661 assert!(result.is_ok(), "protobox should accept xmax=1.0");
2662 let (cropped, _roi_norm) = result.unwrap();
2663 assert!(cropped.shape()[0] > 0);
2665 assert!(cropped.shape()[1] > 0);
2666 assert_eq!(cropped.shape()[2], 4);
2667 }
2668
2669 #[test]
2670 fn test_protobox_rejects_wildly_out_of_range() {
2671 let protos = Array3::<f32>::zeros((16, 16, 4));
2673 let view = protos.view();
2674 let roi = BoundingBox {
2675 xmin: 0.0,
2676 ymin: 0.0,
2677 xmax: 3.0,
2678 ymax: 3.0,
2679 };
2680 let result = protobox(&view, &roi);
2681 assert!(
2682 matches!(result, Err(crate::DecoderError::InvalidShape(s)) if s.contains("un-normalized")),
2683 "protobox should reject coords > NORM_LIMIT"
2684 );
2685 }
2686
2687 #[test]
2688 fn test_protobox_accepts_slightly_over_one() {
2689 let protos = Array3::<f32>::zeros((16, 16, 4));
2691 let view = protos.view();
2692 let roi = BoundingBox {
2693 xmin: 0.0,
2694 ymin: 0.0,
2695 xmax: 1.5,
2696 ymax: 1.5,
2697 };
2698 let result = protobox(&view, &roi);
2699 assert!(
2700 result.is_ok(),
2701 "protobox should accept coords <= NORM_LIMIT (2.0)"
2702 );
2703 let (cropped, _roi_norm) = result.unwrap();
2704 assert_eq!(cropped.shape()[0], 16);
2706 assert_eq!(cropped.shape()[1], 16);
2707 }
2708
2709 #[test]
2710 fn test_segdet_float_proto_no_panic() {
2711 let num_proposals = 100; let num_classes = 80;
2715 let num_mask_coeffs = 32;
2716 let rows = 4 + num_classes + num_mask_coeffs; let mut data = vec![0.0f32; rows * num_proposals];
2722 for i in 0..num_proposals {
2723 let row = |r: usize| r * num_proposals + i;
2724 data[row(0)] = 320.0; data[row(1)] = 320.0; data[row(2)] = 50.0; data[row(3)] = 50.0; data[row(4)] = 0.9; }
2730 let boxes = ndarray::Array2::from_shape_vec((rows, num_proposals), data).unwrap();
2731
2732 let protos = ndarray::Array3::<f32>::zeros((160, 160, num_mask_coeffs));
2737
2738 let mut output_boxes = Vec::with_capacity(300);
2739
2740 let proto_data = impl_yolo_segdet_float_proto::<XYWH, _, _>(
2742 boxes.view(),
2743 protos.view(),
2744 0.5,
2745 0.7,
2746 Some(Nms::default()),
2747 MAX_NMS_CANDIDATES,
2748 300,
2749 None,
2750 None,
2751 false, &mut output_boxes,
2753 );
2754
2755 assert!(!output_boxes.is_empty());
2757 let coeffs_shape = proto_data.mask_coefficients.shape();
2758 assert_eq!(coeffs_shape[0], output_boxes.len());
2759 assert_eq!(coeffs_shape[1], num_mask_coeffs);
2761 }
2762
2763 #[test]
2778 fn test_pre_nms_cap_truncates_excess_candidates() {
2779 let n: usize = 50_000;
2780 let num_classes = 1;
2781
2782 let mut boxes_data = Vec::with_capacity(n * 4);
2786 let mut scores_data = Vec::with_capacity(n * num_classes);
2787 for i in 0..n {
2788 boxes_data.extend_from_slice(&[0.1f32, 0.1, 0.5, 0.5]);
2789 scores_data.push(0.99 - (i as f32) * 1e-7);
2792 }
2793 let boxes = Array2::from_shape_vec((n, 4), boxes_data).unwrap();
2794 let scores = Array2::from_shape_vec((n, num_classes), scores_data).unwrap();
2795
2796 let result = impl_yolo_segdet_get_boxes::<XYXY, _, _>(
2797 boxes.view(),
2798 scores.view(),
2799 0.1,
2800 1.0, Some(Nms::ClassAgnostic), crate::yolo::MAX_NMS_CANDIDATES, usize::MAX, false, );
2806
2807 assert_eq!(
2808 result.len(),
2809 crate::yolo::MAX_NMS_CANDIDATES,
2810 "pre-NMS cap should truncate to MAX_NMS_CANDIDATES; got {}",
2811 result.len()
2812 );
2813 let top_score = result[0].0.score;
2816 assert!(
2817 top_score > 0.98,
2818 "highest-ranked survivor should have the largest score, got {top_score}"
2819 );
2820 }
2821
2822 #[test]
2827 fn test_pre_nms_cap_truncates_excess_candidates_quant() {
2828 use crate::Quantization;
2829 let n: usize = 50_000;
2830 let num_classes = 1;
2831
2832 let boxes_data = (0..n).flat_map(|_| [10i8, 10, 50, 50]).collect::<Vec<_>>();
2835 let boxes = Array2::from_shape_vec((n, 4), boxes_data).unwrap();
2836 let quant_boxes = Quantization {
2837 scale: 0.01,
2838 zero_point: 0,
2839 };
2840
2841 let scores_data: Vec<u8> = (0..n)
2846 .map(|i| 250u8.saturating_sub((i % 200) as u8))
2847 .collect();
2848 let scores = Array2::from_shape_vec((n, num_classes), scores_data).unwrap();
2849 let quant_scores = Quantization {
2850 scale: 0.00392,
2851 zero_point: 0,
2852 };
2853
2854 let result = impl_yolo_split_segdet_quant_get_boxes::<XYXY, _, _>(
2855 (boxes.view(), quant_boxes),
2856 (scores.view(), quant_scores),
2857 0.1,
2858 1.0, Some(Nms::ClassAgnostic), crate::yolo::MAX_NMS_CANDIDATES, usize::MAX, );
2863
2864 assert_eq!(
2865 result.len(),
2866 crate::yolo::MAX_NMS_CANDIDATES,
2867 "quant path pre-NMS cap should truncate to MAX_NMS_CANDIDATES; got {}",
2868 result.len()
2869 );
2870 }
2871
2872 #[test]
2886 fn segdet_combined_tensor_pairs_detection_with_matching_mask_row() {
2887 let nc = 2; let nm = 2; let n = 3; let feat = 4 + nc + nm; let mut data = vec![0.0f32; feat * n];
2910 let set = |d: &mut [f32], r: usize, c: usize, v: f32| d[r * n + c] = v;
2911 set(&mut data, 0, 0, 0.2);
2912 set(&mut data, 1, 0, 0.2);
2913 set(&mut data, 2, 0, 0.1);
2914 set(&mut data, 3, 0, 0.1);
2915 set(&mut data, 0, 1, 0.5);
2916 set(&mut data, 1, 1, 0.5);
2917 set(&mut data, 2, 1, 0.1);
2918 set(&mut data, 3, 1, 0.1);
2919 set(&mut data, 0, 2, 0.8);
2920 set(&mut data, 1, 2, 0.8);
2921 set(&mut data, 2, 2, 0.1);
2922 set(&mut data, 3, 2, 0.1);
2923 set(&mut data, 4, 0, 0.9);
2924 set(&mut data, 4, 2, 0.8);
2925 set(&mut data, 6, 0, 3.0);
2926 set(&mut data, 7, 0, 3.0);
2927 set(&mut data, 6, 2, -3.0);
2928 set(&mut data, 7, 2, -3.0);
2929
2930 let output = Array2::from_shape_vec((feat, n), data).unwrap();
2931 let protos = Array3::<f32>::from_elem((8, 8, nm), 1.0);
2932
2933 let mut boxes: Vec<DetectBox> = Vec::with_capacity(10);
2934 let mut masks: Vec<Segmentation> = Vec::with_capacity(10);
2935 decode_yolo_segdet_float(
2936 output.view(),
2937 protos.view(),
2938 0.5,
2939 0.5,
2940 Some(Nms::ClassAgnostic),
2941 &mut boxes,
2942 &mut masks,
2943 )
2944 .unwrap();
2945
2946 assert_eq!(
2947 boxes.len(),
2948 2,
2949 "two anchors above threshold should survive (a0 score=0.9, a2 score=0.8); got {}",
2950 boxes.len()
2951 );
2952
2953 for (b, m) in boxes.iter().zip(masks.iter()) {
2959 let cx = (b.bbox.xmin + b.bbox.xmax) * 0.5;
2960 let mean = {
2961 use edgefirst_tensor::{TensorMapTrait as _, TensorTrait as _};
2962 let t = m.segmentation.as_u8().expect("mask must be U8");
2963 let map = t.map_read().expect("map mask");
2964 let s = map.as_slice();
2965 let total: u32 = s.iter().map(|&v| v as u32).sum();
2966 total as f32 / s.len() as f32
2967 };
2968 if cx < 0.3 {
2969 assert!(
2971 mean > 200.0,
2972 "anchor 0 detection (centre {cx:.2}) should have high-value mask; got mean {mean}"
2973 );
2974 } else if cx > 0.7 {
2975 assert!(
2977 mean < 50.0,
2978 "anchor 2 detection (centre {cx:.2}) should have low-value mask; got mean {mean}"
2979 );
2980 } else {
2981 panic!("unexpected detection centre {cx:.2}");
2982 }
2983 }
2984 }
2985
2986 fn make_float_boxes(scores: &[f32]) -> Vec<(DetectBox, ())> {
2992 scores
2993 .iter()
2994 .enumerate()
2995 .map(|(i, &s)| {
2996 (
2997 DetectBox {
2998 bbox: BoundingBox {
2999 xmin: 0.0,
3000 ymin: 0.0,
3001 xmax: 1.0,
3002 ymax: 1.0,
3003 },
3004 score: s,
3005 label: i,
3006 },
3007 (),
3008 )
3009 })
3010 .collect()
3011 }
3012
3013 fn make_quant_boxes(scores: &[i8]) -> Vec<(DetectBoxQuantized<i8>, ())> {
3015 scores
3016 .iter()
3017 .enumerate()
3018 .map(|(i, &s)| {
3019 (
3020 DetectBoxQuantized {
3021 bbox: BoundingBox {
3022 xmin: 0.0,
3023 ymin: 0.0,
3024 xmax: 1.0,
3025 ymax: 1.0,
3026 },
3027 score: s,
3028 label: i,
3029 },
3030 (),
3031 )
3032 })
3033 .collect()
3034 }
3035
3036 #[test]
3037 fn truncate_float_top_k_zero_is_unbounded() {
3038 let mut boxes = make_float_boxes(&[0.9, 0.1, 0.5, 0.3, 0.7]);
3039 let original_len = boxes.len();
3040 truncate_to_top_k_by_score(&mut boxes, 0);
3041 assert_eq!(
3042 boxes.len(),
3043 original_len,
3044 "top_k=0 should keep all candidates (no-limit semantics)"
3045 );
3046 }
3047
3048 #[test]
3049 fn truncate_float_top_k_normal() {
3050 let mut boxes = make_float_boxes(&[0.9, 0.1, 0.5, 0.3, 0.7]);
3051 truncate_to_top_k_by_score(&mut boxes, 3);
3052 assert_eq!(boxes.len(), 3);
3053 let mut retained: Vec<f32> = boxes.iter().map(|(b, _)| b.score).collect();
3055 retained.sort_by(|a, b| b.total_cmp(a));
3056 assert_eq!(retained, vec![0.9, 0.7, 0.5]);
3057 }
3058
3059 #[test]
3060 fn truncate_float_top_k_noop_when_under_cap() {
3061 let mut boxes = make_float_boxes(&[0.9, 0.5]);
3062 truncate_to_top_k_by_score(&mut boxes, 10);
3063 assert_eq!(boxes.len(), 2, "should be no-op when len <= top_k");
3064 }
3065
3066 #[test]
3067 fn truncate_quant_top_k_zero_is_unbounded() {
3068 let mut boxes = make_quant_boxes(&[120, -50, 30, -10, 80]);
3069 let original_len = boxes.len();
3070 truncate_to_top_k_by_score_quant(&mut boxes, 0);
3071 assert_eq!(
3072 boxes.len(),
3073 original_len,
3074 "top_k=0 should keep all candidates (no-limit semantics)"
3075 );
3076 }
3077
3078 #[test]
3079 fn truncate_quant_top_k_normal() {
3080 let mut boxes = make_quant_boxes(&[120, -50, 30, -10, 80]);
3081 truncate_to_top_k_by_score_quant(&mut boxes, 3);
3082 assert_eq!(boxes.len(), 3);
3083 let mut retained: Vec<i8> = boxes.iter().map(|(b, _)| b.score).collect();
3084 retained.sort_by(|a, b| b.cmp(a));
3085 assert_eq!(retained, vec![120, 80, 30]);
3086 }
3087
3088 #[test]
3089 fn truncate_quant_top_k_noop_when_under_cap() {
3090 let mut boxes = make_quant_boxes(&[120, 80]);
3091 truncate_to_top_k_by_score_quant(&mut boxes, 10);
3092 assert_eq!(boxes.len(), 2, "should be no-op when len <= top_k");
3093 }
3094}