edgefirst_decoder/decoder/mod.rs
1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4use ndarray::{ArrayView, ArrayViewD, Dimension};
5use num_traits::{AsPrimitive, Float};
6
7use crate::{DecoderError, DetectBox, ProtoData, Segmentation};
8
9pub mod config;
10pub mod configs;
11
12use configs::ModelType;
13
14#[derive(Debug, Clone, PartialEq)]
15pub struct Decoder {
16 model_type: ModelType,
17 pub iou_threshold: f32,
18 pub score_threshold: f32,
19 /// NMS mode: Some(mode) applies NMS, None bypasses NMS (for end-to-end
20 /// models)
21 pub nms: Option<configs::Nms>,
22 /// Whether decoded boxes are in normalized [0,1] coordinates.
23 /// - `Some(true)`: Coordinates in [0,1] range
24 /// - `Some(false)`: Pixel coordinates
25 /// - `None`: Unknown, caller must infer (e.g., check if any coordinate >
26 /// 1.0)
27 normalized: Option<bool>,
28}
29
30#[derive(Debug)]
31pub enum ArrayViewDQuantized<'a> {
32 UInt8(ArrayViewD<'a, u8>),
33 Int8(ArrayViewD<'a, i8>),
34 UInt16(ArrayViewD<'a, u16>),
35 Int16(ArrayViewD<'a, i16>),
36 UInt32(ArrayViewD<'a, u32>),
37 Int32(ArrayViewD<'a, i32>),
38}
39
40impl<'a, D> From<ArrayView<'a, u8, D>> for ArrayViewDQuantized<'a>
41where
42 D: Dimension,
43{
44 fn from(arr: ArrayView<'a, u8, D>) -> Self {
45 Self::UInt8(arr.into_dyn())
46 }
47}
48
49impl<'a, D> From<ArrayView<'a, i8, D>> for ArrayViewDQuantized<'a>
50where
51 D: Dimension,
52{
53 fn from(arr: ArrayView<'a, i8, D>) -> Self {
54 Self::Int8(arr.into_dyn())
55 }
56}
57
58impl<'a, D> From<ArrayView<'a, u16, D>> for ArrayViewDQuantized<'a>
59where
60 D: Dimension,
61{
62 fn from(arr: ArrayView<'a, u16, D>) -> Self {
63 Self::UInt16(arr.into_dyn())
64 }
65}
66
67impl<'a, D> From<ArrayView<'a, i16, D>> for ArrayViewDQuantized<'a>
68where
69 D: Dimension,
70{
71 fn from(arr: ArrayView<'a, i16, D>) -> Self {
72 Self::Int16(arr.into_dyn())
73 }
74}
75
76impl<'a, D> From<ArrayView<'a, u32, D>> for ArrayViewDQuantized<'a>
77where
78 D: Dimension,
79{
80 fn from(arr: ArrayView<'a, u32, D>) -> Self {
81 Self::UInt32(arr.into_dyn())
82 }
83}
84
85impl<'a, D> From<ArrayView<'a, i32, D>> for ArrayViewDQuantized<'a>
86where
87 D: Dimension,
88{
89 fn from(arr: ArrayView<'a, i32, D>) -> Self {
90 Self::Int32(arr.into_dyn())
91 }
92}
93
94impl<'a> ArrayViewDQuantized<'a> {
95 /// Returns the shape of the underlying array.
96 ///
97 /// # Examples
98 /// ```rust
99 /// # use edgefirst_decoder::ArrayViewDQuantized;
100 /// # use ndarray::Array2;
101 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
102 /// let arr = Array2::from_shape_vec((2, 3), vec![1u8, 2, 3, 4, 5, 6])?;
103 /// let view = ArrayViewDQuantized::from(arr.view().into_dyn());
104 /// assert_eq!(view.shape(), &[2, 3]);
105 /// # Ok(())
106 /// # }
107 /// ```
108 pub fn shape(&self) -> &[usize] {
109 match self {
110 ArrayViewDQuantized::UInt8(a) => a.shape(),
111 ArrayViewDQuantized::Int8(a) => a.shape(),
112 ArrayViewDQuantized::UInt16(a) => a.shape(),
113 ArrayViewDQuantized::Int16(a) => a.shape(),
114 ArrayViewDQuantized::UInt32(a) => a.shape(),
115 ArrayViewDQuantized::Int32(a) => a.shape(),
116 }
117 }
118}
119
120/// WARNING: Do NOT nest `with_quantized!` calls. Each level multiplies
121/// monomorphized code paths by 6 (one per integer variant), so nesting
122/// N levels deep produces 6^N instantiations.
123///
124/// Instead, dequantize each tensor sequentially with `dequant_3d!`/`dequant_4d!`
125/// (6*N paths) or split into independent phases that each nest at most 2 levels.
126macro_rules! with_quantized {
127 ($x:expr, $var:ident, $body:expr) => {
128 match $x {
129 ArrayViewDQuantized::UInt8(x) => {
130 let $var = x;
131 $body
132 }
133 ArrayViewDQuantized::Int8(x) => {
134 let $var = x;
135 $body
136 }
137 ArrayViewDQuantized::UInt16(x) => {
138 let $var = x;
139 $body
140 }
141 ArrayViewDQuantized::Int16(x) => {
142 let $var = x;
143 $body
144 }
145 ArrayViewDQuantized::UInt32(x) => {
146 let $var = x;
147 $body
148 }
149 ArrayViewDQuantized::Int32(x) => {
150 let $var = x;
151 $body
152 }
153 }
154 };
155}
156
157mod builder;
158mod helpers;
159mod postprocess;
160mod tests;
161
162pub use builder::DecoderBuilder;
163pub use config::{ConfigOutput, ConfigOutputRef, ConfigOutputs};
164
165impl Decoder {
166 /// This function returns the parsed model type of the decoder.
167 ///
168 /// # Examples
169 ///
170 /// ```rust
171 /// # use edgefirst_decoder::{DecoderBuilder, DecoderResult, configs::ModelType};
172 /// # fn main() -> DecoderResult<()> {
173 /// # let config_yaml = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../testdata/modelpack_split.yaml")).to_string();
174 /// let decoder = DecoderBuilder::default()
175 /// .with_config_yaml_str(config_yaml)
176 /// .build()?;
177 /// assert!(matches!(
178 /// decoder.model_type(),
179 /// ModelType::ModelPackDetSplit { .. }
180 /// ));
181 /// # Ok(())
182 /// # }
183 /// ```
184 pub fn model_type(&self) -> &ModelType {
185 &self.model_type
186 }
187
188 /// Returns the box coordinate format if known from the model config.
189 ///
190 /// - `Some(true)`: Boxes are in normalized [0,1] coordinates
191 /// - `Some(false)`: Boxes are in pixel coordinates relative to model input
192 /// - `None`: Unknown, caller must infer (e.g., check if any coordinate >
193 /// 1.0)
194 ///
195 /// This is determined by the model config's `normalized` field, not the NMS
196 /// mode. When coordinates are in pixels or unknown, the caller may need
197 /// to normalize using the model input dimensions.
198 ///
199 /// # Examples
200 ///
201 /// ```rust
202 /// # use edgefirst_decoder::{DecoderBuilder, DecoderResult};
203 /// # fn main() -> DecoderResult<()> {
204 /// # let config_yaml = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../testdata/modelpack_split.yaml")).to_string();
205 /// let decoder = DecoderBuilder::default()
206 /// .with_config_yaml_str(config_yaml)
207 /// .build()?;
208 /// // Config doesn't specify normalized, so it's None
209 /// assert!(decoder.normalized_boxes().is_none());
210 /// # Ok(())
211 /// # }
212 /// ```
213 pub fn normalized_boxes(&self) -> Option<bool> {
214 self.normalized
215 }
216
217 /// This function decodes quantized model outputs into detection boxes and
218 /// segmentation masks. The quantized outputs can be of u8, i8, u16, i16,
219 /// u32, or i32 types. Up to `output_boxes.capacity()` boxes and masks
220 /// will be decoded. The function clears the provided output vectors
221 /// before populating them with the decoded results.
222 ///
223 /// This function returns a `DecoderError` if the the provided outputs don't
224 /// match the configuration provided by the user when building the decoder.
225 ///
226 /// # Examples
227 ///
228 /// ```rust
229 /// # use edgefirst_decoder::{BoundingBox, DecoderBuilder, DetectBox, DecoderResult};
230 /// # use ndarray::Array4;
231 /// # fn main() -> DecoderResult<()> {
232 /// # let detect0 = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../testdata/modelpack_split_9x15x18.bin"));
233 /// # let detect0 = ndarray::Array4::from_shape_vec((1, 9, 15, 18), detect0.to_vec())?;
234 /// #
235 /// # let detect1 = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../testdata/modelpack_split_17x30x18.bin"));
236 /// # let detect1 = ndarray::Array4::from_shape_vec((1, 17, 30, 18), detect1.to_vec())?;
237 /// # let model_output = vec![
238 /// # detect1.view().into_dyn().into(),
239 /// # detect0.view().into_dyn().into(),
240 /// # ];
241 /// let decoder = DecoderBuilder::default()
242 /// .with_config_yaml_str(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../testdata/modelpack_split.yaml")).to_string())
243 /// .with_score_threshold(0.45)
244 /// .with_iou_threshold(0.45)
245 /// .build()?;
246 ///
247 /// let mut output_boxes: Vec<_> = Vec::with_capacity(10);
248 /// let mut output_masks: Vec<_> = Vec::with_capacity(10);
249 /// decoder.decode_quantized(&model_output, &mut output_boxes, &mut output_masks)?;
250 /// assert!(output_boxes[0].equal_within_delta(
251 /// &DetectBox {
252 /// bbox: BoundingBox {
253 /// xmin: 0.43171933,
254 /// ymin: 0.68243736,
255 /// xmax: 0.5626645,
256 /// ymax: 0.808863,
257 /// },
258 /// score: 0.99240804,
259 /// label: 0
260 /// },
261 /// 1e-6
262 /// ));
263 /// # Ok(())
264 /// # }
265 /// ```
266 pub fn decode_quantized(
267 &self,
268 outputs: &[ArrayViewDQuantized],
269 output_boxes: &mut Vec<DetectBox>,
270 output_masks: &mut Vec<Segmentation>,
271 ) -> Result<(), DecoderError> {
272 output_boxes.clear();
273 output_masks.clear();
274 match &self.model_type {
275 ModelType::ModelPackSegDet {
276 boxes,
277 scores,
278 segmentation,
279 } => {
280 self.decode_modelpack_det_quantized(outputs, boxes, scores, output_boxes)?;
281 self.decode_modelpack_seg_quantized(outputs, segmentation, output_masks)
282 }
283 ModelType::ModelPackSegDetSplit {
284 detection,
285 segmentation,
286 } => {
287 self.decode_modelpack_det_split_quantized(outputs, detection, output_boxes)?;
288 self.decode_modelpack_seg_quantized(outputs, segmentation, output_masks)
289 }
290 ModelType::ModelPackDet { boxes, scores } => {
291 self.decode_modelpack_det_quantized(outputs, boxes, scores, output_boxes)
292 }
293 ModelType::ModelPackDetSplit { detection } => {
294 self.decode_modelpack_det_split_quantized(outputs, detection, output_boxes)
295 }
296 ModelType::ModelPackSeg { segmentation } => {
297 self.decode_modelpack_seg_quantized(outputs, segmentation, output_masks)
298 }
299 ModelType::YoloDet { boxes } => {
300 self.decode_yolo_det_quantized(outputs, boxes, output_boxes)
301 }
302 ModelType::YoloSegDet { boxes, protos } => self.decode_yolo_segdet_quantized(
303 outputs,
304 boxes,
305 protos,
306 output_boxes,
307 output_masks,
308 ),
309 ModelType::YoloSplitDet { boxes, scores } => {
310 self.decode_yolo_split_det_quantized(outputs, boxes, scores, output_boxes)
311 }
312 ModelType::YoloSplitSegDet {
313 boxes,
314 scores,
315 mask_coeff,
316 protos,
317 } => self.decode_yolo_split_segdet_quantized(
318 outputs,
319 boxes,
320 scores,
321 mask_coeff,
322 protos,
323 output_boxes,
324 output_masks,
325 ),
326 ModelType::YoloEndToEndDet { boxes } => {
327 self.decode_yolo_end_to_end_det_quantized(outputs, boxes, output_boxes)
328 }
329 ModelType::YoloEndToEndSegDet { boxes, protos } => self
330 .decode_yolo_end_to_end_segdet_quantized(
331 outputs,
332 boxes,
333 protos,
334 output_boxes,
335 output_masks,
336 ),
337 ModelType::YoloSplitEndToEndDet {
338 boxes,
339 scores,
340 classes,
341 } => self.decode_yolo_split_end_to_end_det_quantized(
342 outputs,
343 boxes,
344 scores,
345 classes,
346 output_boxes,
347 ),
348 ModelType::YoloSplitEndToEndSegDet {
349 boxes,
350 scores,
351 classes,
352 mask_coeff,
353 protos,
354 } => self.decode_yolo_split_end_to_end_segdet_quantized(
355 outputs,
356 boxes,
357 scores,
358 classes,
359 mask_coeff,
360 protos,
361 output_boxes,
362 output_masks,
363 ),
364 }
365 }
366
367 /// This function decodes floating point model outputs into detection boxes
368 /// and segmentation masks. Up to `output_boxes.capacity()` boxes and
369 /// masks will be decoded. The function clears the provided output
370 /// vectors before populating them with the decoded results.
371 ///
372 /// This function returns an `Error` if the the provided outputs don't
373 /// match the configuration provided by the user when building the decoder.
374 ///
375 /// Any quantization information in the configuration will be ignored.
376 ///
377 /// # Examples
378 ///
379 /// ```rust
380 /// # use edgefirst_decoder::{BoundingBox, DecoderBuilder, DetectBox, DecoderResult, configs, configs::{DecoderType, DecoderVersion}, dequantize_cpu, Quantization};
381 /// # use ndarray::Array3;
382 /// # fn main() -> DecoderResult<()> {
383 /// # let out = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../testdata/yolov8s_80_classes.bin"));
384 /// # let out = unsafe { std::slice::from_raw_parts(out.as_ptr() as *const i8, out.len()) };
385 /// # let mut out_dequant = vec![0.0_f64; 84 * 8400];
386 /// # let quant = Quantization::new(0.0040811873, -123);
387 /// # dequantize_cpu(out, quant, &mut out_dequant);
388 /// # let model_output_f64 = Array3::from_shape_vec((1, 84, 8400), out_dequant)?.into_dyn();
389 /// let decoder = DecoderBuilder::default()
390 /// .with_config_yolo_det(configs::Detection {
391 /// decoder: DecoderType::Ultralytics,
392 /// quantization: None,
393 /// shape: vec![1, 84, 8400],
394 /// anchors: None,
395 /// dshape: Vec::new(),
396 /// normalized: Some(true),
397 /// },
398 /// Some(DecoderVersion::Yolo11))
399 /// .with_score_threshold(0.25)
400 /// .with_iou_threshold(0.7)
401 /// .build()?;
402 ///
403 /// let mut output_boxes: Vec<_> = Vec::with_capacity(10);
404 /// let mut output_masks: Vec<_> = Vec::with_capacity(10);
405 /// let model_output_f64 = vec![model_output_f64.view().into()];
406 /// decoder.decode_float(&model_output_f64, &mut output_boxes, &mut output_masks)?;
407 /// assert!(output_boxes[0].equal_within_delta(
408 /// &DetectBox {
409 /// bbox: BoundingBox {
410 /// xmin: 0.5285137,
411 /// ymin: 0.05305544,
412 /// xmax: 0.87541467,
413 /// ymax: 0.9998909,
414 /// },
415 /// score: 0.5591227,
416 /// label: 0
417 /// },
418 /// 1e-6
419 /// ));
420 ///
421 /// # Ok(())
422 /// # }
423 pub fn decode_float<T>(
424 &self,
425 outputs: &[ArrayViewD<T>],
426 output_boxes: &mut Vec<DetectBox>,
427 output_masks: &mut Vec<Segmentation>,
428 ) -> Result<(), DecoderError>
429 where
430 T: Float + AsPrimitive<f32> + AsPrimitive<u8> + Send + Sync + 'static,
431 f32: AsPrimitive<T>,
432 {
433 output_boxes.clear();
434 output_masks.clear();
435 match &self.model_type {
436 ModelType::ModelPackSegDet {
437 boxes,
438 scores,
439 segmentation,
440 } => {
441 self.decode_modelpack_det_float(outputs, boxes, scores, output_boxes)?;
442 self.decode_modelpack_seg_float(outputs, segmentation, output_masks)?;
443 }
444 ModelType::ModelPackSegDetSplit {
445 detection,
446 segmentation,
447 } => {
448 self.decode_modelpack_det_split_float(outputs, detection, output_boxes)?;
449 self.decode_modelpack_seg_float(outputs, segmentation, output_masks)?;
450 }
451 ModelType::ModelPackDet { boxes, scores } => {
452 self.decode_modelpack_det_float(outputs, boxes, scores, output_boxes)?;
453 }
454 ModelType::ModelPackDetSplit { detection } => {
455 self.decode_modelpack_det_split_float(outputs, detection, output_boxes)?;
456 }
457 ModelType::ModelPackSeg { segmentation } => {
458 self.decode_modelpack_seg_float(outputs, segmentation, output_masks)?;
459 }
460 ModelType::YoloDet { boxes } => {
461 self.decode_yolo_det_float(outputs, boxes, output_boxes)?;
462 }
463 ModelType::YoloSegDet { boxes, protos } => {
464 self.decode_yolo_segdet_float(outputs, boxes, protos, output_boxes, output_masks)?;
465 }
466 ModelType::YoloSplitDet { boxes, scores } => {
467 self.decode_yolo_split_det_float(outputs, boxes, scores, output_boxes)?;
468 }
469 ModelType::YoloSplitSegDet {
470 boxes,
471 scores,
472 mask_coeff,
473 protos,
474 } => {
475 self.decode_yolo_split_segdet_float(
476 outputs,
477 boxes,
478 scores,
479 mask_coeff,
480 protos,
481 output_boxes,
482 output_masks,
483 )?;
484 }
485 ModelType::YoloEndToEndDet { boxes } => {
486 self.decode_yolo_end_to_end_det_float(outputs, boxes, output_boxes)?;
487 }
488 ModelType::YoloEndToEndSegDet { boxes, protos } => {
489 self.decode_yolo_end_to_end_segdet_float(
490 outputs,
491 boxes,
492 protos,
493 output_boxes,
494 output_masks,
495 )?;
496 }
497 ModelType::YoloSplitEndToEndDet {
498 boxes,
499 scores,
500 classes,
501 } => {
502 self.decode_yolo_split_end_to_end_det_float(
503 outputs,
504 boxes,
505 scores,
506 classes,
507 output_boxes,
508 )?;
509 }
510 ModelType::YoloSplitEndToEndSegDet {
511 boxes,
512 scores,
513 classes,
514 mask_coeff,
515 protos,
516 } => {
517 self.decode_yolo_split_end_to_end_segdet_float(
518 outputs,
519 boxes,
520 scores,
521 classes,
522 mask_coeff,
523 protos,
524 output_boxes,
525 output_masks,
526 )?;
527 }
528 }
529 Ok(())
530 }
531
532 /// Decodes quantized model outputs into detection boxes, returning raw
533 /// `ProtoData` for segmentation models instead of materialized masks.
534 ///
535 /// Returns `Ok(None)` for detection-only and ModelPack models (use
536 /// `decode_quantized` for those). Returns `Ok(Some(ProtoData))` for
537 /// YOLO segmentation models.
538 pub fn decode_quantized_proto(
539 &self,
540 outputs: &[ArrayViewDQuantized],
541 output_boxes: &mut Vec<DetectBox>,
542 ) -> Result<Option<ProtoData>, DecoderError> {
543 output_boxes.clear();
544 match &self.model_type {
545 // Detection-only and ModelPack variants: no proto data
546 ModelType::ModelPackSegDet { .. }
547 | ModelType::ModelPackSegDetSplit { .. }
548 | ModelType::ModelPackDet { .. }
549 | ModelType::ModelPackDetSplit { .. }
550 | ModelType::ModelPackSeg { .. }
551 | ModelType::YoloDet { .. }
552 | ModelType::YoloSplitDet { .. }
553 | ModelType::YoloEndToEndDet { .. }
554 | ModelType::YoloSplitEndToEndDet { .. } => Ok(None),
555
556 ModelType::YoloSegDet { boxes, protos } => {
557 let proto =
558 self.decode_yolo_segdet_quantized_proto(outputs, boxes, protos, output_boxes)?;
559 Ok(Some(proto))
560 }
561 ModelType::YoloSplitSegDet {
562 boxes,
563 scores,
564 mask_coeff,
565 protos,
566 } => {
567 let proto = self.decode_yolo_split_segdet_quantized_proto(
568 outputs,
569 boxes,
570 scores,
571 mask_coeff,
572 protos,
573 output_boxes,
574 )?;
575 Ok(Some(proto))
576 }
577 ModelType::YoloEndToEndSegDet { boxes, protos } => {
578 let proto = self.decode_yolo_end_to_end_segdet_quantized_proto(
579 outputs,
580 boxes,
581 protos,
582 output_boxes,
583 )?;
584 Ok(Some(proto))
585 }
586 ModelType::YoloSplitEndToEndSegDet {
587 boxes,
588 scores,
589 classes,
590 mask_coeff,
591 protos,
592 } => {
593 let proto = self.decode_yolo_split_end_to_end_segdet_quantized_proto(
594 outputs,
595 boxes,
596 scores,
597 classes,
598 mask_coeff,
599 protos,
600 output_boxes,
601 )?;
602 Ok(Some(proto))
603 }
604 }
605 }
606
607 /// Decodes floating-point model outputs into detection boxes, returning
608 /// raw `ProtoData` for segmentation models instead of materialized masks.
609 ///
610 /// Returns `Ok(None)` for detection-only and ModelPack models. Returns
611 /// `Ok(Some(ProtoData))` for YOLO segmentation models.
612 pub fn decode_float_proto<T>(
613 &self,
614 outputs: &[ArrayViewD<T>],
615 output_boxes: &mut Vec<DetectBox>,
616 ) -> Result<Option<ProtoData>, DecoderError>
617 where
618 T: Float + AsPrimitive<f32> + AsPrimitive<u8> + Send + Sync + 'static,
619 f32: AsPrimitive<T>,
620 {
621 output_boxes.clear();
622 match &self.model_type {
623 // Detection-only and ModelPack variants: no proto data
624 ModelType::ModelPackSegDet { .. }
625 | ModelType::ModelPackSegDetSplit { .. }
626 | ModelType::ModelPackDet { .. }
627 | ModelType::ModelPackDetSplit { .. }
628 | ModelType::ModelPackSeg { .. }
629 | ModelType::YoloDet { .. }
630 | ModelType::YoloSplitDet { .. }
631 | ModelType::YoloEndToEndDet { .. }
632 | ModelType::YoloSplitEndToEndDet { .. } => Ok(None),
633
634 ModelType::YoloSegDet { boxes, protos } => {
635 let proto =
636 self.decode_yolo_segdet_float_proto(outputs, boxes, protos, output_boxes)?;
637 Ok(Some(proto))
638 }
639 ModelType::YoloSplitSegDet {
640 boxes,
641 scores,
642 mask_coeff,
643 protos,
644 } => {
645 let proto = self.decode_yolo_split_segdet_float_proto(
646 outputs,
647 boxes,
648 scores,
649 mask_coeff,
650 protos,
651 output_boxes,
652 )?;
653 Ok(Some(proto))
654 }
655 ModelType::YoloEndToEndSegDet { boxes, protos } => {
656 let proto = self.decode_yolo_end_to_end_segdet_float_proto(
657 outputs,
658 boxes,
659 protos,
660 output_boxes,
661 )?;
662 Ok(Some(proto))
663 }
664 ModelType::YoloSplitEndToEndSegDet {
665 boxes,
666 scores,
667 classes,
668 mask_coeff,
669 protos,
670 } => {
671 let proto = self.decode_yolo_split_end_to_end_segdet_float_proto(
672 outputs,
673 boxes,
674 scores,
675 classes,
676 mask_coeff,
677 protos,
678 output_boxes,
679 )?;
680 Ok(Some(proto))
681 }
682 }
683 }
684}