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: m,
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: m,
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: TensorDyn::I8(coeff_tensor),
1653 protos: TensorDyn::I8(protos_tensor),
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 TensorDyn::I8(coeff_tensor)
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 TensorDyn::I16(coeff_tensor)
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 TensorDyn::F32(coeff_tensor)
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: TensorDyn::I8(protos_tensor),
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)
1847 .map(edgefirst_tensor::TensorDyn::F32)
1848 }
1849 fn arrayview3_into_tensor_dyn(
1850 view: ArrayView3<'_, f32>,
1851 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1852 edgefirst_tensor::Tensor::<f32>::from_arrayview3(view).map(edgefirst_tensor::TensorDyn::F32)
1853 }
1854}
1855
1856impl FloatProtoElem for half::f16 {
1857 fn slice_into_tensor_dyn(
1858 values: &[half::f16],
1859 shape: &[usize],
1860 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1861 edgefirst_tensor::Tensor::<half::f16>::from_slice(values, shape)
1862 .map(edgefirst_tensor::TensorDyn::F16)
1863 }
1864 fn arrayview3_into_tensor_dyn(
1865 view: ArrayView3<'_, half::f16>,
1866 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1867 edgefirst_tensor::Tensor::<half::f16>::from_arrayview3(view)
1868 .map(edgefirst_tensor::TensorDyn::F16)
1869 }
1870}
1871
1872impl FloatProtoElem for f64 {
1873 fn slice_into_tensor_dyn(
1874 values: &[f64],
1875 shape: &[usize],
1876 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1877 let narrowed: Vec<f32> = values.iter().map(|&v| v as f32).collect();
1879 edgefirst_tensor::Tensor::<f32>::from_slice(&narrowed, shape)
1880 .map(edgefirst_tensor::TensorDyn::F32)
1881 }
1882 fn arrayview3_into_tensor_dyn(
1883 view: ArrayView3<'_, f64>,
1884 ) -> edgefirst_tensor::Result<edgefirst_tensor::TensorDyn> {
1885 let narrowed: ndarray::Array3<f32> = view.mapv(|v| v as f32);
1886 edgefirst_tensor::Tensor::<f32>::from_arrayview3(narrowed.view())
1887 .map(edgefirst_tensor::TensorDyn::F32)
1888 }
1889}
1890
1891fn postprocess_yolo<'a, T>(
1892 output: &'a ArrayView2<'_, T>,
1893) -> (ArrayView2<'a, T>, ArrayView2<'a, T>) {
1894 let boxes_tensor = output.slice(s![..4, ..,]).reversed_axes();
1895 let scores_tensor = output.slice(s![4.., ..,]).reversed_axes();
1896 (boxes_tensor, scores_tensor)
1897}
1898
1899pub(crate) fn postprocess_yolo_seg<'a, T>(
1900 output: &'a ArrayView2<'_, T>,
1901 num_protos: usize,
1902) -> (ArrayView2<'a, T>, ArrayView2<'a, T>, ArrayView2<'a, T>) {
1903 assert!(
1904 output.shape()[0] > num_protos + 4,
1905 "Output shape is too short: {} <= {} + 4",
1906 output.shape()[0],
1907 num_protos
1908 );
1909 let num_classes = output.shape()[0] - 4 - num_protos;
1910 let boxes_tensor = output.slice(s![..4, ..,]).reversed_axes();
1911 let scores_tensor = output.slice(s![4..(num_classes + 4), ..,]).reversed_axes();
1912 let mask_tensor = output.slice(s![(num_classes + 4).., ..,]).reversed_axes();
1913 (boxes_tensor, scores_tensor, mask_tensor)
1914}
1915
1916pub(crate) fn postprocess_yolo_split_segdet<'a, 'b, 'c, BOX, SCORE, MASK>(
1917 boxes_tensor: ArrayView2<'a, BOX>,
1918 scores_tensor: ArrayView2<'b, SCORE>,
1919 mask_tensor: ArrayView2<'c, MASK>,
1920) -> (
1921 ArrayView2<'a, BOX>,
1922 ArrayView2<'b, SCORE>,
1923 ArrayView2<'c, MASK>,
1924) {
1925 let boxes_tensor = boxes_tensor.reversed_axes();
1926 let scores_tensor = scores_tensor.reversed_axes();
1927 let mask_tensor = mask_tensor.reversed_axes();
1928 (boxes_tensor, scores_tensor, mask_tensor)
1929}
1930
1931fn decode_segdet_f32<
1932 MASK: Float + AsPrimitive<f32> + Send + Sync,
1933 PROTO: Float + AsPrimitive<f32> + Send + Sync,
1934>(
1935 boxes: Vec<(DetectBox, usize)>,
1936 masks: ArrayView2<MASK>,
1937 protos: ArrayView3<PROTO>,
1938) -> Result<Vec<(DetectBox, BoundingBox, Array3<u8>)>, crate::DecoderError> {
1939 if boxes.is_empty() {
1940 return Ok(Vec::new());
1941 }
1942 if masks.shape()[1] != protos.shape()[2] {
1943 return Err(crate::DecoderError::InvalidShape(format!(
1944 "Mask coefficients count ({}) doesn't match protos channel count ({})",
1945 masks.shape()[1],
1946 protos.shape()[2],
1947 )));
1948 }
1949 boxes
1950 .into_par_iter()
1951 .map(|b| {
1952 let ind = b.1;
1953 let (protos, roi) = protobox(&protos, &b.0.bbox)?;
1958 Ok((b.0, roi, make_segmentation(masks.row(ind), protos.view())))
1959 })
1960 .collect()
1961}
1962
1963pub(crate) fn decode_segdet_quant<
1964 MASK: PrimInt + AsPrimitive<i64> + AsPrimitive<i128> + Send + Sync,
1965 PROTO: PrimInt + AsPrimitive<i64> + AsPrimitive<i128> + Send + Sync,
1966>(
1967 boxes: Vec<(DetectBox, usize)>,
1968 masks: ArrayView2<MASK>,
1969 protos: ArrayView3<PROTO>,
1970 quant_masks: Quantization,
1971 quant_protos: Quantization,
1972) -> Result<Vec<(DetectBox, BoundingBox, Array3<u8>)>, crate::DecoderError> {
1973 if boxes.is_empty() {
1974 return Ok(Vec::new());
1975 }
1976 if masks.shape()[1] != protos.shape()[2] {
1977 return Err(crate::DecoderError::InvalidShape(format!(
1978 "Mask coefficients count ({}) doesn't match protos channel count ({})",
1979 masks.shape()[1],
1980 protos.shape()[2],
1981 )));
1982 }
1983
1984 let total_bits = MASK::zero().count_zeros() + PROTO::zero().count_zeros() + 5; boxes
1986 .into_iter()
1987 .map(|b| {
1988 let i = b.1;
1989 let (protos, roi) = protobox(&protos, &b.0.bbox.to_canonical())?;
1993 let seg = match total_bits {
1994 0..=64 => make_segmentation_quant::<MASK, PROTO, i64>(
1995 masks.row(i),
1996 protos.view(),
1997 quant_masks,
1998 quant_protos,
1999 ),
2000 65..=128 => make_segmentation_quant::<MASK, PROTO, i128>(
2001 masks.row(i),
2002 protos.view(),
2003 quant_masks,
2004 quant_protos,
2005 ),
2006 _ => {
2007 return Err(crate::DecoderError::NotSupported(format!(
2008 "Unsupported bit width ({total_bits}) for segmentation computation"
2009 )));
2010 }
2011 };
2012 Ok((b.0, roi, seg))
2013 })
2014 .collect()
2015}
2016
2017fn protobox<'a, T>(
2018 protos: &'a ArrayView3<T>,
2019 roi: &BoundingBox,
2020) -> Result<(ArrayView3<'a, T>, BoundingBox), crate::DecoderError> {
2021 let width = protos.dim().1 as f32;
2022 let height = protos.dim().0 as f32;
2023
2024 const NORM_LIMIT: f32 = 2.0;
2036 if roi.xmin > NORM_LIMIT
2037 || roi.ymin > NORM_LIMIT
2038 || roi.xmax > NORM_LIMIT
2039 || roi.ymax > NORM_LIMIT
2040 {
2041 return Err(crate::DecoderError::InvalidShape(format!(
2042 "Bounding box coordinates appear un-normalized (pixel-space). \
2043 Got bbox=({:.2}, {:.2}, {:.2}, {:.2}) but expected values in [0, 1]. \
2044 Two ways to fix this: \
2045 (1) declare `Detection::normalized = false` in the model schema \
2046 AND make sure the schema's `input.shape` / `input.dshape` carries \
2047 the model input dims so the decoder can divide by (W, H) before NMS \
2048 (EDGEAI-1303 — verify with `Decoder::input_dims().is_some()`); or \
2049 (2) normalize the boxes in-graph before decode().",
2050 roi.xmin, roi.ymin, roi.xmax, roi.ymax,
2051 )));
2052 }
2053
2054 let roi = [
2055 (roi.xmin * width).clamp(0.0, width) as usize,
2056 (roi.ymin * height).clamp(0.0, height) as usize,
2057 (roi.xmax * width).clamp(0.0, width).ceil() as usize,
2058 (roi.ymax * height).clamp(0.0, height).ceil() as usize,
2059 ];
2060
2061 let roi_norm = [
2062 roi[0] as f32 / width,
2063 roi[1] as f32 / height,
2064 roi[2] as f32 / width,
2065 roi[3] as f32 / height,
2066 ]
2067 .into();
2068
2069 let cropped = protos.slice(s![roi[1]..roi[3], roi[0]..roi[2], ..]);
2070
2071 Ok((cropped, roi_norm))
2072}
2073
2074fn make_segmentation<
2080 MASK: Float + AsPrimitive<f32> + Send + Sync,
2081 PROTO: Float + AsPrimitive<f32> + Send + Sync,
2082>(
2083 mask: ArrayView1<MASK>,
2084 protos: ArrayView3<PROTO>,
2085) -> Array3<u8> {
2086 let shape = protos.shape();
2087
2088 let mask = mask.to_shape((1, mask.len())).unwrap();
2090 let protos = protos.to_shape([shape[0] * shape[1], shape[2]]).unwrap();
2091 let protos = protos.reversed_axes();
2092 let mask = mask.map(|x| x.as_());
2093 let protos = protos.map(|x| x.as_());
2094
2095 let mask = mask
2097 .dot(&protos)
2098 .into_shape_with_order((shape[0], shape[1], 1))
2099 .unwrap();
2100
2101 mask.map(|x| {
2102 let sigmoid = 1.0 / (1.0 + (-*x).exp());
2103 (sigmoid * 255.0).round() as u8
2104 })
2105}
2106
2107fn make_segmentation_quant<
2114 MASK: PrimInt + AsPrimitive<DEST> + Send + Sync,
2115 PROTO: PrimInt + AsPrimitive<DEST> + Send + Sync,
2116 DEST: PrimInt + 'static + Signed + AsPrimitive<f32> + Debug,
2117>(
2118 mask: ArrayView1<MASK>,
2119 protos: ArrayView3<PROTO>,
2120 quant_masks: Quantization,
2121 quant_protos: Quantization,
2122) -> Array3<u8>
2123where
2124 i32: AsPrimitive<DEST>,
2125 f32: AsPrimitive<DEST>,
2126{
2127 let shape = protos.shape();
2128
2129 let mask = mask.to_shape((1, mask.len())).unwrap();
2131
2132 let protos = protos.to_shape([shape[0] * shape[1], shape[2]]).unwrap();
2133 let protos = protos.reversed_axes();
2134
2135 let zp = quant_masks.zero_point.as_();
2136
2137 let mask = mask.mapv(|x| x.as_() - zp);
2138
2139 let zp = quant_protos.zero_point.as_();
2140 let protos = protos.mapv(|x| x.as_() - zp);
2141
2142 let segmentation = mask
2144 .dot(&protos)
2145 .into_shape_with_order((shape[0], shape[1], 1))
2146 .unwrap();
2147
2148 let combined_scale = quant_masks.scale * quant_protos.scale;
2149 segmentation.map(|x| {
2150 let val: f32 = (*x).as_() * combined_scale;
2151 let sigmoid = 1.0 / (1.0 + (-val).exp());
2152 (sigmoid * 255.0).round() as u8
2153 })
2154}
2155
2156pub(crate) fn yolo_segmentation_to_mask(
2168 segmentation: ArrayView3<u8>,
2169 threshold: u8,
2170) -> Result<Array2<u8>, crate::DecoderError> {
2171 if segmentation.shape()[2] != 1 {
2172 return Err(crate::DecoderError::InvalidShape(format!(
2173 "Yolo Instance Segmentation should have shape (H, W, 1), got (H, W, {})",
2174 segmentation.shape()[2]
2175 )));
2176 }
2177 Ok(segmentation
2178 .slice(s![.., .., 0])
2179 .map(|x| if *x >= threshold { 1 } else { 0 }))
2180}
2181
2182#[cfg(test)]
2183#[cfg_attr(coverage_nightly, coverage(off))]
2184mod tests {
2185 use super::*;
2186 use ndarray::Array2;
2187
2188 #[test]
2193 fn test_end_to_end_det_basic_filtering() {
2194 let data: Vec<f32> = vec![
2198 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, ];
2206 let output = Array2::from_shape_vec((6, 3), data).unwrap();
2207
2208 let mut boxes = Vec::with_capacity(10);
2209 decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2210
2211 assert_eq!(boxes.len(), 1);
2213 assert_eq!(boxes[0].label, 0);
2214 assert!((boxes[0].score - 0.9).abs() < 0.01);
2215 assert!((boxes[0].bbox.xmin - 0.1).abs() < 0.01);
2216 assert!((boxes[0].bbox.ymin - 0.1).abs() < 0.01);
2217 assert!((boxes[0].bbox.xmax - 0.5).abs() < 0.01);
2218 assert!((boxes[0].bbox.ymax - 0.5).abs() < 0.01);
2219 }
2220
2221 #[test]
2222 fn test_end_to_end_det_all_pass_threshold() {
2223 let data: Vec<f32> = vec![
2225 10.0, 20.0, 10.0, 20.0, 50.0, 60.0, 50.0, 60.0, 0.8, 0.7, 1.0, 2.0, ];
2232 let output = Array2::from_shape_vec((6, 2), data).unwrap();
2233
2234 let mut boxes = Vec::with_capacity(10);
2235 decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2236
2237 assert_eq!(boxes.len(), 2);
2238 assert_eq!(boxes[0].label, 1);
2239 assert_eq!(boxes[1].label, 2);
2240 }
2241
2242 #[test]
2243 fn test_end_to_end_det_none_pass_threshold() {
2244 let data: Vec<f32> = vec![
2246 10.0, 20.0, 10.0, 20.0, 50.0, 60.0, 50.0, 60.0, 0.1, 0.2, 1.0, 2.0, ];
2253 let output = Array2::from_shape_vec((6, 2), data).unwrap();
2254
2255 let mut boxes = Vec::with_capacity(10);
2256 decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2257
2258 assert_eq!(boxes.len(), 0);
2259 }
2260
2261 #[test]
2262 fn test_end_to_end_det_capacity_limit() {
2263 let data: Vec<f32> = vec![
2265 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, ];
2272 let output = Array2::from_shape_vec((6, 5), data).unwrap();
2273
2274 let mut boxes = Vec::with_capacity(2); decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2276
2277 assert_eq!(boxes.len(), 2);
2278 }
2279
2280 #[test]
2281 fn test_end_to_end_det_empty_output() {
2282 let output = Array2::<f32>::zeros((6, 0));
2284
2285 let mut boxes = Vec::with_capacity(10);
2286 decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2287
2288 assert_eq!(boxes.len(), 0);
2289 }
2290
2291 #[test]
2292 fn test_end_to_end_det_pixel_coordinates() {
2293 let data: Vec<f32> = vec![
2295 100.0, 200.0, 300.0, 400.0, 0.95, 5.0, ];
2302 let output = Array2::from_shape_vec((6, 1), data).unwrap();
2303
2304 let mut boxes = Vec::with_capacity(10);
2305 decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes).unwrap();
2306
2307 assert_eq!(boxes.len(), 1);
2308 assert_eq!(boxes[0].label, 5);
2309 assert!((boxes[0].bbox.xmin - 100.0).abs() < 0.01);
2310 assert!((boxes[0].bbox.ymin - 200.0).abs() < 0.01);
2311 assert!((boxes[0].bbox.xmax - 300.0).abs() < 0.01);
2312 assert!((boxes[0].bbox.ymax - 400.0).abs() < 0.01);
2313 }
2314
2315 #[test]
2316 fn test_end_to_end_det_invalid_shape() {
2317 let output = Array2::<f32>::zeros((5, 3));
2319
2320 let mut boxes = Vec::with_capacity(10);
2321 let result = decode_yolo_end_to_end_det_float(output.view(), 0.5, &mut boxes);
2322
2323 assert!(result.is_err());
2324 assert!(matches!(
2325 result,
2326 Err(crate::DecoderError::InvalidShape(s)) if s.contains("at least 6 rows")
2327 ));
2328 }
2329
2330 #[test]
2335 fn test_end_to_end_segdet_basic() {
2336 let num_protos = 32;
2339 let num_detections = 2;
2340 let num_features = 6 + num_protos;
2341
2342 let mut data = vec![0.0f32; num_features * num_detections];
2344 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 {
2359 data[i * num_detections] = 0.1;
2360 data[i * num_detections + 1] = 0.1;
2361 }
2362
2363 let output = Array2::from_shape_vec((num_features, num_detections), data).unwrap();
2364
2365 let protos = Array3::<f32>::zeros((16, 16, num_protos));
2367
2368 let mut boxes = Vec::with_capacity(10);
2369 let mut masks = Vec::with_capacity(10);
2370 decode_yolo_end_to_end_segdet_float(
2371 output.view(),
2372 protos.view(),
2373 0.5,
2374 &mut boxes,
2375 &mut masks,
2376 )
2377 .unwrap();
2378
2379 assert_eq!(boxes.len(), 1);
2381 assert_eq!(masks.len(), 1);
2382 assert_eq!(boxes[0].label, 1);
2383 assert!((boxes[0].score - 0.9).abs() < 0.01);
2384 }
2385
2386 #[test]
2387 fn test_end_to_end_segdet_mask_coordinates() {
2388 let num_protos = 32;
2390 let num_features = 6 + num_protos;
2391
2392 let mut data = vec![0.0f32; num_features];
2393 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();
2401 let protos = Array3::<f32>::zeros((16, 16, num_protos));
2402
2403 let mut boxes = Vec::with_capacity(10);
2404 let mut masks = Vec::with_capacity(10);
2405 decode_yolo_end_to_end_segdet_float(
2406 output.view(),
2407 protos.view(),
2408 0.5,
2409 &mut boxes,
2410 &mut masks,
2411 )
2412 .unwrap();
2413
2414 assert_eq!(boxes.len(), 1);
2415 assert_eq!(masks.len(), 1);
2416
2417 let step = 1.0 / 16.0;
2421 assert!(masks[0].xmin <= boxes[0].bbox.xmin);
2422 assert!(masks[0].ymin <= boxes[0].bbox.ymin);
2423 assert!(masks[0].xmax >= boxes[0].bbox.xmax);
2424 assert!(masks[0].ymax >= boxes[0].bbox.ymax);
2425 assert!((boxes[0].bbox.xmin - masks[0].xmin) < step);
2426 assert!((boxes[0].bbox.ymin - masks[0].ymin) < step);
2427 assert!((masks[0].xmax - boxes[0].bbox.xmax) < step);
2428 assert!((masks[0].ymax - boxes[0].bbox.ymax) < step);
2429 }
2430
2431 #[test]
2432 fn test_end_to_end_segdet_empty_output() {
2433 let num_protos = 32;
2434 let output = Array2::<f32>::zeros((6 + num_protos, 0));
2435 let protos = Array3::<f32>::zeros((16, 16, num_protos));
2436
2437 let mut boxes = Vec::with_capacity(10);
2438 let mut masks = Vec::with_capacity(10);
2439 decode_yolo_end_to_end_segdet_float(
2440 output.view(),
2441 protos.view(),
2442 0.5,
2443 &mut boxes,
2444 &mut masks,
2445 )
2446 .unwrap();
2447
2448 assert_eq!(boxes.len(), 0);
2449 assert_eq!(masks.len(), 0);
2450 }
2451
2452 #[test]
2453 fn test_end_to_end_segdet_capacity_limit() {
2454 let num_protos = 32;
2455 let num_detections = 5;
2456 let num_features = 6 + num_protos;
2457
2458 let mut data = vec![0.0f32; num_features * num_detections];
2459 for i in 0..num_detections {
2461 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; }
2468
2469 let output = Array2::from_shape_vec((num_features, num_detections), data).unwrap();
2470 let protos = Array3::<f32>::zeros((16, 16, num_protos));
2471
2472 let mut boxes = Vec::with_capacity(2); let mut masks = Vec::with_capacity(2);
2474 decode_yolo_end_to_end_segdet_float(
2475 output.view(),
2476 protos.view(),
2477 0.5,
2478 &mut boxes,
2479 &mut masks,
2480 )
2481 .unwrap();
2482
2483 assert_eq!(boxes.len(), 2);
2484 assert_eq!(masks.len(), 2);
2485 }
2486
2487 #[test]
2488 fn test_end_to_end_segdet_invalid_shape_too_few_rows() {
2489 let output = Array2::<f32>::zeros((6, 3));
2491 let protos = Array3::<f32>::zeros((16, 16, 32));
2492
2493 let mut boxes = Vec::with_capacity(10);
2494 let mut masks = Vec::with_capacity(10);
2495 let result = decode_yolo_end_to_end_segdet_float(
2496 output.view(),
2497 protos.view(),
2498 0.5,
2499 &mut boxes,
2500 &mut masks,
2501 );
2502
2503 assert!(result.is_err());
2504 assert!(matches!(
2505 result,
2506 Err(crate::DecoderError::InvalidShape(s)) if s.contains("at least 7 rows")
2507 ));
2508 }
2509
2510 #[test]
2511 fn test_end_to_end_segdet_invalid_shape_protos_mismatch() {
2512 let num_protos = 32;
2514 let output = Array2::<f32>::zeros((6 + 16, 3)); let protos = Array3::<f32>::zeros((16, 16, num_protos)); let mut boxes = Vec::with_capacity(10);
2518 let mut masks = Vec::with_capacity(10);
2519 let result = decode_yolo_end_to_end_segdet_float(
2520 output.view(),
2521 protos.view(),
2522 0.5,
2523 &mut boxes,
2524 &mut masks,
2525 );
2526
2527 assert!(result.is_err());
2528 assert!(matches!(
2529 result,
2530 Err(crate::DecoderError::InvalidShape(s)) if s.contains("doesn't match protos count")
2531 ));
2532 }
2533
2534 #[test]
2539 fn test_split_end_to_end_segdet_basic() {
2540 let num_protos = 32;
2543 let num_detections = 2;
2544 let num_features = 6 + num_protos;
2545
2546 let mut data = vec![0.0f32; num_features * num_detections];
2548 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 {
2563 data[i * num_detections] = 0.1;
2564 data[i * num_detections + 1] = 0.1;
2565 }
2566
2567 let output = Array2::from_shape_vec((num_features, num_detections), data).unwrap();
2568 let box_coords = output.slice(s![..4, ..]);
2569 let scores = output.slice(s![4..5, ..]);
2570 let classes = output.slice(s![5..6, ..]);
2571 let mask_coeff = output.slice(s![6.., ..]);
2572 let protos = Array3::<f32>::zeros((16, 16, num_protos));
2574
2575 let mut boxes = Vec::with_capacity(10);
2576 let mut masks = Vec::with_capacity(10);
2577 decode_yolo_split_end_to_end_segdet_float(
2578 box_coords,
2579 scores,
2580 classes,
2581 mask_coeff,
2582 protos.view(),
2583 0.5,
2584 &mut boxes,
2585 &mut masks,
2586 )
2587 .unwrap();
2588
2589 assert_eq!(boxes.len(), 1);
2591 assert_eq!(masks.len(), 1);
2592 assert_eq!(boxes[0].label, 1);
2593 assert!((boxes[0].score - 0.9).abs() < 0.01);
2594 }
2595
2596 #[test]
2601 fn test_segmentation_to_mask_basic() {
2602 let data: Vec<u8> = vec![
2604 100, 200, 50, 150, 10, 255, 128, 64, 0, 127, 128, 255, 64, 64, 192, 192, ];
2609 let segmentation = Array3::from_shape_vec((4, 4, 1), data).unwrap();
2610
2611 let mask = yolo_segmentation_to_mask(segmentation.view(), 128).unwrap();
2612
2613 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); }
2623
2624 #[test]
2625 fn test_segmentation_to_mask_all_above() {
2626 let segmentation = Array3::from_elem((4, 4, 1), 255u8);
2627 let mask = yolo_segmentation_to_mask(segmentation.view(), 128).unwrap();
2628 assert!(mask.iter().all(|&x| x == 1));
2629 }
2630
2631 #[test]
2632 fn test_segmentation_to_mask_all_below() {
2633 let segmentation = Array3::from_elem((4, 4, 1), 64u8);
2634 let mask = yolo_segmentation_to_mask(segmentation.view(), 128).unwrap();
2635 assert!(mask.iter().all(|&x| x == 0));
2636 }
2637
2638 #[test]
2639 fn test_segmentation_to_mask_invalid_shape() {
2640 let segmentation = Array3::from_elem((4, 4, 3), 128u8);
2641 let result = yolo_segmentation_to_mask(segmentation.view(), 128);
2642
2643 assert!(result.is_err());
2644 assert!(matches!(
2645 result,
2646 Err(crate::DecoderError::InvalidShape(s)) if s.contains("(H, W, 1)")
2647 ));
2648 }
2649
2650 #[test]
2655 fn test_protobox_clamps_edge_coordinates() {
2656 let protos = Array3::<f32>::zeros((16, 16, 4));
2658 let view = protos.view();
2659 let roi = BoundingBox {
2660 xmin: 0.5,
2661 ymin: 0.5,
2662 xmax: 1.0,
2663 ymax: 1.0,
2664 };
2665 let result = protobox(&view, &roi);
2666 assert!(result.is_ok(), "protobox should accept xmax=1.0");
2667 let (cropped, _roi_norm) = result.unwrap();
2668 assert!(cropped.shape()[0] > 0);
2670 assert!(cropped.shape()[1] > 0);
2671 assert_eq!(cropped.shape()[2], 4);
2672 }
2673
2674 #[test]
2675 fn test_protobox_rejects_wildly_out_of_range() {
2676 let protos = Array3::<f32>::zeros((16, 16, 4));
2678 let view = protos.view();
2679 let roi = BoundingBox {
2680 xmin: 0.0,
2681 ymin: 0.0,
2682 xmax: 3.0,
2683 ymax: 3.0,
2684 };
2685 let result = protobox(&view, &roi);
2686 assert!(
2687 matches!(result, Err(crate::DecoderError::InvalidShape(s)) if s.contains("un-normalized")),
2688 "protobox should reject coords > NORM_LIMIT"
2689 );
2690 }
2691
2692 #[test]
2693 fn test_protobox_accepts_slightly_over_one() {
2694 let protos = Array3::<f32>::zeros((16, 16, 4));
2696 let view = protos.view();
2697 let roi = BoundingBox {
2698 xmin: 0.0,
2699 ymin: 0.0,
2700 xmax: 1.5,
2701 ymax: 1.5,
2702 };
2703 let result = protobox(&view, &roi);
2704 assert!(
2705 result.is_ok(),
2706 "protobox should accept coords <= NORM_LIMIT (2.0)"
2707 );
2708 let (cropped, _roi_norm) = result.unwrap();
2709 assert_eq!(cropped.shape()[0], 16);
2711 assert_eq!(cropped.shape()[1], 16);
2712 }
2713
2714 #[test]
2715 fn test_segdet_float_proto_no_panic() {
2716 let num_proposals = 100; let num_classes = 80;
2720 let num_mask_coeffs = 32;
2721 let rows = 4 + num_classes + num_mask_coeffs; let mut data = vec![0.0f32; rows * num_proposals];
2727 for i in 0..num_proposals {
2728 let row = |r: usize| r * num_proposals + i;
2729 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; }
2735 let boxes = ndarray::Array2::from_shape_vec((rows, num_proposals), data).unwrap();
2736
2737 let protos = ndarray::Array3::<f32>::zeros((160, 160, num_mask_coeffs));
2742
2743 let mut output_boxes = Vec::with_capacity(300);
2744
2745 let proto_data = impl_yolo_segdet_float_proto::<XYWH, _, _>(
2747 boxes.view(),
2748 protos.view(),
2749 0.5,
2750 0.7,
2751 Some(Nms::default()),
2752 MAX_NMS_CANDIDATES,
2753 300,
2754 None,
2755 None,
2756 false, &mut output_boxes,
2758 );
2759
2760 assert!(!output_boxes.is_empty());
2762 let coeffs_shape = proto_data.mask_coefficients.shape();
2763 assert_eq!(coeffs_shape[0], output_boxes.len());
2764 assert_eq!(coeffs_shape[1], num_mask_coeffs);
2766 }
2767
2768 #[test]
2783 fn test_pre_nms_cap_truncates_excess_candidates() {
2784 let n: usize = 50_000;
2785 let num_classes = 1;
2786
2787 let mut boxes_data = Vec::with_capacity(n * 4);
2791 let mut scores_data = Vec::with_capacity(n * num_classes);
2792 for i in 0..n {
2793 boxes_data.extend_from_slice(&[0.1f32, 0.1, 0.5, 0.5]);
2794 scores_data.push(0.99 - (i as f32) * 1e-7);
2797 }
2798 let boxes = Array2::from_shape_vec((n, 4), boxes_data).unwrap();
2799 let scores = Array2::from_shape_vec((n, num_classes), scores_data).unwrap();
2800
2801 let result = impl_yolo_segdet_get_boxes::<XYXY, _, _>(
2802 boxes.view(),
2803 scores.view(),
2804 0.1,
2805 1.0, Some(Nms::ClassAgnostic), crate::yolo::MAX_NMS_CANDIDATES, usize::MAX, false, );
2811
2812 assert_eq!(
2813 result.len(),
2814 crate::yolo::MAX_NMS_CANDIDATES,
2815 "pre-NMS cap should truncate to MAX_NMS_CANDIDATES; got {}",
2816 result.len()
2817 );
2818 let top_score = result[0].0.score;
2821 assert!(
2822 top_score > 0.98,
2823 "highest-ranked survivor should have the largest score, got {top_score}"
2824 );
2825 }
2826
2827 #[test]
2832 fn test_pre_nms_cap_truncates_excess_candidates_quant() {
2833 use crate::Quantization;
2834 let n: usize = 50_000;
2835 let num_classes = 1;
2836
2837 let boxes_data = (0..n).flat_map(|_| [10i8, 10, 50, 50]).collect::<Vec<_>>();
2840 let boxes = Array2::from_shape_vec((n, 4), boxes_data).unwrap();
2841 let quant_boxes = Quantization {
2842 scale: 0.01,
2843 zero_point: 0,
2844 };
2845
2846 let scores_data: Vec<u8> = (0..n)
2851 .map(|i| 250u8.saturating_sub((i % 200) as u8))
2852 .collect();
2853 let scores = Array2::from_shape_vec((n, num_classes), scores_data).unwrap();
2854 let quant_scores = Quantization {
2855 scale: 0.00392,
2856 zero_point: 0,
2857 };
2858
2859 let result = impl_yolo_split_segdet_quant_get_boxes::<XYXY, _, _>(
2860 (boxes.view(), quant_boxes),
2861 (scores.view(), quant_scores),
2862 0.1,
2863 1.0, Some(Nms::ClassAgnostic), crate::yolo::MAX_NMS_CANDIDATES, usize::MAX, );
2868
2869 assert_eq!(
2870 result.len(),
2871 crate::yolo::MAX_NMS_CANDIDATES,
2872 "quant path pre-NMS cap should truncate to MAX_NMS_CANDIDATES; got {}",
2873 result.len()
2874 );
2875 }
2876
2877 #[test]
2891 fn segdet_combined_tensor_pairs_detection_with_matching_mask_row() {
2892 let nc = 2; let nm = 2; let n = 3; let feat = 4 + nc + nm; let mut data = vec![0.0f32; feat * n];
2915 let set = |d: &mut [f32], r: usize, c: usize, v: f32| d[r * n + c] = v;
2916 set(&mut data, 0, 0, 0.2);
2917 set(&mut data, 1, 0, 0.2);
2918 set(&mut data, 2, 0, 0.1);
2919 set(&mut data, 3, 0, 0.1);
2920 set(&mut data, 0, 1, 0.5);
2921 set(&mut data, 1, 1, 0.5);
2922 set(&mut data, 2, 1, 0.1);
2923 set(&mut data, 3, 1, 0.1);
2924 set(&mut data, 0, 2, 0.8);
2925 set(&mut data, 1, 2, 0.8);
2926 set(&mut data, 2, 2, 0.1);
2927 set(&mut data, 3, 2, 0.1);
2928 set(&mut data, 4, 0, 0.9);
2929 set(&mut data, 4, 2, 0.8);
2930 set(&mut data, 6, 0, 3.0);
2931 set(&mut data, 7, 0, 3.0);
2932 set(&mut data, 6, 2, -3.0);
2933 set(&mut data, 7, 2, -3.0);
2934
2935 let output = Array2::from_shape_vec((feat, n), data).unwrap();
2936 let protos = Array3::<f32>::from_elem((8, 8, nm), 1.0);
2937
2938 let mut boxes: Vec<DetectBox> = Vec::with_capacity(10);
2939 let mut masks: Vec<Segmentation> = Vec::with_capacity(10);
2940 decode_yolo_segdet_float(
2941 output.view(),
2942 protos.view(),
2943 0.5,
2944 0.5,
2945 Some(Nms::ClassAgnostic),
2946 &mut boxes,
2947 &mut masks,
2948 )
2949 .unwrap();
2950
2951 assert_eq!(
2952 boxes.len(),
2953 2,
2954 "two anchors above threshold should survive (a0 score=0.9, a2 score=0.8); got {}",
2955 boxes.len()
2956 );
2957
2958 for (b, m) in boxes.iter().zip(masks.iter()) {
2964 let cx = (b.bbox.xmin + b.bbox.xmax) * 0.5;
2965 let mean = {
2966 let s = &m.segmentation;
2967 let total: u32 = s.iter().map(|&v| v as u32).sum();
2968 total as f32 / s.len() as f32
2969 };
2970 if cx < 0.3 {
2971 assert!(
2973 mean > 200.0,
2974 "anchor 0 detection (centre {cx:.2}) should have high-value mask; got mean {mean}"
2975 );
2976 } else if cx > 0.7 {
2977 assert!(
2979 mean < 50.0,
2980 "anchor 2 detection (centre {cx:.2}) should have low-value mask; got mean {mean}"
2981 );
2982 } else {
2983 panic!("unexpected detection centre {cx:.2}");
2984 }
2985 }
2986 }
2987
2988 fn make_float_boxes(scores: &[f32]) -> Vec<(DetectBox, ())> {
2994 scores
2995 .iter()
2996 .enumerate()
2997 .map(|(i, &s)| {
2998 (
2999 DetectBox {
3000 bbox: BoundingBox {
3001 xmin: 0.0,
3002 ymin: 0.0,
3003 xmax: 1.0,
3004 ymax: 1.0,
3005 },
3006 score: s,
3007 label: i,
3008 },
3009 (),
3010 )
3011 })
3012 .collect()
3013 }
3014
3015 fn make_quant_boxes(scores: &[i8]) -> Vec<(DetectBoxQuantized<i8>, ())> {
3017 scores
3018 .iter()
3019 .enumerate()
3020 .map(|(i, &s)| {
3021 (
3022 DetectBoxQuantized {
3023 bbox: BoundingBox {
3024 xmin: 0.0,
3025 ymin: 0.0,
3026 xmax: 1.0,
3027 ymax: 1.0,
3028 },
3029 score: s,
3030 label: i,
3031 },
3032 (),
3033 )
3034 })
3035 .collect()
3036 }
3037
3038 #[test]
3039 fn truncate_float_top_k_zero_is_unbounded() {
3040 let mut boxes = make_float_boxes(&[0.9, 0.1, 0.5, 0.3, 0.7]);
3041 let original_len = boxes.len();
3042 truncate_to_top_k_by_score(&mut boxes, 0);
3043 assert_eq!(
3044 boxes.len(),
3045 original_len,
3046 "top_k=0 should keep all candidates (no-limit semantics)"
3047 );
3048 }
3049
3050 #[test]
3051 fn truncate_float_top_k_normal() {
3052 let mut boxes = make_float_boxes(&[0.9, 0.1, 0.5, 0.3, 0.7]);
3053 truncate_to_top_k_by_score(&mut boxes, 3);
3054 assert_eq!(boxes.len(), 3);
3055 let mut retained: Vec<f32> = boxes.iter().map(|(b, _)| b.score).collect();
3057 retained.sort_by(|a, b| b.total_cmp(a));
3058 assert_eq!(retained, vec![0.9, 0.7, 0.5]);
3059 }
3060
3061 #[test]
3062 fn truncate_float_top_k_noop_when_under_cap() {
3063 let mut boxes = make_float_boxes(&[0.9, 0.5]);
3064 truncate_to_top_k_by_score(&mut boxes, 10);
3065 assert_eq!(boxes.len(), 2, "should be no-op when len <= top_k");
3066 }
3067
3068 #[test]
3069 fn truncate_quant_top_k_zero_is_unbounded() {
3070 let mut boxes = make_quant_boxes(&[120, -50, 30, -10, 80]);
3071 let original_len = boxes.len();
3072 truncate_to_top_k_by_score_quant(&mut boxes, 0);
3073 assert_eq!(
3074 boxes.len(),
3075 original_len,
3076 "top_k=0 should keep all candidates (no-limit semantics)"
3077 );
3078 }
3079
3080 #[test]
3081 fn truncate_quant_top_k_normal() {
3082 let mut boxes = make_quant_boxes(&[120, -50, 30, -10, 80]);
3083 truncate_to_top_k_by_score_quant(&mut boxes, 3);
3084 assert_eq!(boxes.len(), 3);
3085 let mut retained: Vec<i8> = boxes.iter().map(|(b, _)| b.score).collect();
3086 retained.sort_by(|a, b| b.cmp(a));
3087 assert_eq!(retained, vec![120, 80, 30]);
3088 }
3089
3090 #[test]
3091 fn truncate_quant_top_k_noop_when_under_cap() {
3092 let mut boxes = make_quant_boxes(&[120, 80]);
3093 truncate_to_top_k_by_score_quant(&mut boxes, 10);
3094 assert_eq!(boxes.len(), 2, "should be no-op when len <= top_k");
3095 }
3096}