Skip to main content

edgefirst_decoder/decoder/
config.rs

1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! [`ConfigOutputs`], the programmatic form of a model's output description.
5//!
6//! This is what `DecoderBuilder::with_config` takes, and what the JSON and
7//! YAML config strings deserialize into. Each [`ConfigOutput`] declares one
8//! output tensor's role, shape, and quantization; the builder reads the set of
9//! them to select a model-type variant.
10//!
11//! Declare `shape` and `dshape` in physical memory order, outermost axis
12//! first. See the output tensor physical-order contract in the crate's
13//! `ARCHITECTURE.md` — mis-declaring it makes every element access index the
14//! wrong byte, and nothing at runtime can detect it.
15
16use super::configs::{self, DimName, QuantTuple};
17use serde::{Deserialize, Serialize};
18
19/// Used to represent the outputs in the model configuration.
20/// # Examples
21/// ```rust,no_run
22/// # use edgefirst_decoder::{DecoderBuilder, ConfigOutputs};
23/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
24/// let config_json = std::fs::read_to_string("modelpack_split.json")?;
25/// let config: ConfigOutputs = serde_json::from_str(&config_json)?;
26/// let decoder = DecoderBuilder::new().with_config(config).build()?;
27///
28/// # Ok(())
29/// # }
30/// ```
31///
32/// The struct is `#[non_exhaustive]` so new optional settings can be added
33/// without a breaking change. Outside this crate, start from
34/// [`ConfigOutputs::new`] (or [`Default`], or a parser) and assign fields:
35///
36/// ```rust
37/// # use edgefirst_decoder::{configs::Nms, ConfigOutputs};
38/// let mut config = ConfigOutputs::new(Vec::new());
39/// config.nms = Some(Nms::ClassAgnostic);
40/// assert!(config.nms_multi_label.is_none());
41/// ```
42#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
43#[non_exhaustive]
44pub struct ConfigOutputs {
45    #[serde(default)]
46    pub outputs: Vec<ConfigOutput>,
47    /// NMS mode from config file. When present, overrides the builder's NMS
48    /// setting.
49    /// - `Some(Nms::ClassAgnostic)` — class-agnostic NMS: suppress overlapping
50    ///   boxes regardless of class
51    /// - `Some(Nms::ClassAware)` — class-aware NMS: only suppress boxes with
52    ///   the same class
53    /// - `None` — use the builder default (`Nms::Auto` resolves to class-aware)
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub nms: Option<configs::Nms>,
56    /// Emit one box per class above the score threshold (validation decode).
57    /// Tracked decode ignores it; an explicit decoder setting overrides it.
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub nms_multi_label: Option<bool>,
60    /// Decoder version for Ultralytics models. Determines the decoding
61    /// strategy.
62    /// - `Some(Yolo26)` — end-to-end model with embedded NMS
63    /// - `Some(Yolov5/Yolov8/Yolo11)` — traditional models requiring external
64    ///   NMS
65    /// - `None` — infer from other settings (legacy behavior)
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub decoder_version: Option<configs::DecoderVersion>,
68}
69
70impl ConfigOutputs {
71    /// A config with these outputs and every other setting unset.
72    pub fn new(outputs: Vec<ConfigOutput>) -> Self {
73        Self {
74            outputs,
75            ..Self::default()
76        }
77    }
78}
79
80#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
81#[serde(tag = "type")]
82pub enum ConfigOutput {
83    #[serde(rename = "detection")]
84    Detection(configs::Detection),
85    #[serde(rename = "masks")]
86    Mask(configs::Mask),
87    #[serde(rename = "segmentation")]
88    Segmentation(configs::Segmentation),
89    #[serde(rename = "protos")]
90    Protos(configs::Protos),
91    #[serde(rename = "scores")]
92    Scores(configs::Scores),
93    #[serde(rename = "boxes")]
94    Boxes(configs::Boxes),
95    #[serde(rename = "mask_coefs", alias = "mask_coefficients")]
96    MaskCoefficients(configs::MaskCoefficients),
97    #[serde(rename = "classes")]
98    Classes(configs::Classes),
99}
100
101#[derive(Debug, PartialEq, Clone)]
102pub enum ConfigOutputRef<'a> {
103    Detection(&'a configs::Detection),
104    Mask(&'a configs::Mask),
105    Segmentation(&'a configs::Segmentation),
106    Protos(&'a configs::Protos),
107    Scores(&'a configs::Scores),
108    Boxes(&'a configs::Boxes),
109    MaskCoefficients(&'a configs::MaskCoefficients),
110    Classes(&'a configs::Classes),
111}
112
113impl<'a> ConfigOutputRef<'a> {
114    pub(super) fn decoder(&self) -> configs::DecoderType {
115        match self {
116            ConfigOutputRef::Detection(v) => v.decoder,
117            ConfigOutputRef::Mask(v) => v.decoder,
118            ConfigOutputRef::Segmentation(v) => v.decoder,
119            ConfigOutputRef::Protos(v) => v.decoder,
120            ConfigOutputRef::Scores(v) => v.decoder,
121            ConfigOutputRef::Boxes(v) => v.decoder,
122            ConfigOutputRef::MaskCoefficients(v) => v.decoder,
123            ConfigOutputRef::Classes(v) => v.decoder,
124        }
125    }
126
127    pub(super) fn dshape(&self) -> &[(DimName, usize)] {
128        match self {
129            ConfigOutputRef::Detection(v) => &v.dshape,
130            ConfigOutputRef::Mask(v) => &v.dshape,
131            ConfigOutputRef::Segmentation(v) => &v.dshape,
132            ConfigOutputRef::Protos(v) => &v.dshape,
133            ConfigOutputRef::Scores(v) => &v.dshape,
134            ConfigOutputRef::Boxes(v) => &v.dshape,
135            ConfigOutputRef::MaskCoefficients(v) => &v.dshape,
136            ConfigOutputRef::Classes(v) => &v.dshape,
137        }
138    }
139}
140
141impl<'a> From<&'a configs::Detection> for ConfigOutputRef<'a> {
142    /// Converts from references of config structs to ConfigOutputRef
143    /// # Examples
144    /// ```rust
145    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
146    /// let detection_config = configs::Detection {
147    ///     anchors: None,
148    ///     decoder: configs::DecoderType::Ultralytics,
149    ///     quantization: None,
150    ///     shape: vec![1, 84, 8400],
151    ///     dshape: Vec::new(),
152    ///     normalized: Some(true),
153    /// };
154    /// let output: ConfigOutputRef = (&detection_config).into();
155    /// ```
156    fn from(v: &'a configs::Detection) -> ConfigOutputRef<'a> {
157        ConfigOutputRef::Detection(v)
158    }
159}
160
161impl<'a> From<&'a configs::Mask> for ConfigOutputRef<'a> {
162    /// Converts from references of config structs to ConfigOutputRef
163    /// # Examples
164    /// ```rust
165    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
166    /// let mask = configs::Mask {
167    ///     decoder: configs::DecoderType::ModelPack,
168    ///     quantization: None,
169    ///     shape: vec![1, 160, 160, 1],
170    ///     dshape: Vec::new(),
171    /// };
172    /// let output: ConfigOutputRef = (&mask).into();
173    /// ```
174    fn from(v: &'a configs::Mask) -> ConfigOutputRef<'a> {
175        ConfigOutputRef::Mask(v)
176    }
177}
178
179impl<'a> From<&'a configs::Segmentation> for ConfigOutputRef<'a> {
180    /// Converts from references of config structs to ConfigOutputRef
181    /// # Examples
182    /// ```rust
183    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
184    /// let seg = configs::Segmentation {
185    ///     decoder: configs::DecoderType::ModelPack,
186    ///     quantization: None,
187    ///     shape: vec![1, 160, 160, 3],
188    ///     dshape: Vec::new(),
189    /// };
190    /// let output: ConfigOutputRef = (&seg).into();
191    /// ```
192    fn from(v: &'a configs::Segmentation) -> ConfigOutputRef<'a> {
193        ConfigOutputRef::Segmentation(v)
194    }
195}
196
197impl<'a> From<&'a configs::Protos> for ConfigOutputRef<'a> {
198    /// Converts from references of config structs to ConfigOutputRef
199    /// # Examples
200    /// ```rust
201    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
202    /// let protos = configs::Protos {
203    ///     decoder: configs::DecoderType::Ultralytics,
204    ///     quantization: None,
205    ///     shape: vec![1, 160, 160, 32],
206    ///     dshape: Vec::new(),
207    /// };
208    /// let output: ConfigOutputRef = (&protos).into();
209    /// ```
210    fn from(v: &'a configs::Protos) -> ConfigOutputRef<'a> {
211        ConfigOutputRef::Protos(v)
212    }
213}
214
215impl<'a> From<&'a configs::Scores> for ConfigOutputRef<'a> {
216    /// Converts from references of config structs to ConfigOutputRef
217    /// # Examples
218    /// ```rust
219    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
220    /// let scores = configs::Scores {
221    ///     decoder: configs::DecoderType::Ultralytics,
222    ///     quantization: None,
223    ///     shape: vec![1, 40, 8400],
224    ///     dshape: Vec::new(),
225    /// };
226    /// let output: ConfigOutputRef = (&scores).into();
227    /// ```
228    fn from(v: &'a configs::Scores) -> ConfigOutputRef<'a> {
229        ConfigOutputRef::Scores(v)
230    }
231}
232
233impl<'a> From<&'a configs::Boxes> for ConfigOutputRef<'a> {
234    /// Converts from references of config structs to ConfigOutputRef
235    /// # Examples
236    /// ```rust
237    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
238    /// let boxes = configs::Boxes {
239    ///     decoder: configs::DecoderType::Ultralytics,
240    ///     quantization: None,
241    ///     shape: vec![1, 4, 8400],
242    ///     dshape: Vec::new(),
243    ///     normalized: Some(true),
244    /// };
245    /// let output: ConfigOutputRef = (&boxes).into();
246    /// ```
247    fn from(v: &'a configs::Boxes) -> ConfigOutputRef<'a> {
248        ConfigOutputRef::Boxes(v)
249    }
250}
251
252impl<'a> From<&'a configs::MaskCoefficients> for ConfigOutputRef<'a> {
253    /// Converts from references of config structs to ConfigOutputRef
254    /// # Examples
255    /// ```rust
256    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
257    /// let mask_coefficients = configs::MaskCoefficients {
258    ///     decoder: configs::DecoderType::Ultralytics,
259    ///     quantization: None,
260    ///     shape: vec![1, 32, 8400],
261    ///     dshape: Vec::new(),
262    /// };
263    /// let output: ConfigOutputRef = (&mask_coefficients).into();
264    /// ```
265    fn from(v: &'a configs::MaskCoefficients) -> ConfigOutputRef<'a> {
266        ConfigOutputRef::MaskCoefficients(v)
267    }
268}
269
270impl<'a> From<&'a configs::Classes> for ConfigOutputRef<'a> {
271    fn from(v: &'a configs::Classes) -> ConfigOutputRef<'a> {
272        ConfigOutputRef::Classes(v)
273    }
274}
275
276impl<'a> From<&'a ConfigOutput> for ConfigOutputRef<'a> {
277    fn from(v: &'a ConfigOutput) -> ConfigOutputRef<'a> {
278        match v {
279            ConfigOutput::Detection(c) => ConfigOutputRef::Detection(c),
280            ConfigOutput::Mask(c) => ConfigOutputRef::Mask(c),
281            ConfigOutput::Segmentation(c) => ConfigOutputRef::Segmentation(c),
282            ConfigOutput::Protos(c) => ConfigOutputRef::Protos(c),
283            ConfigOutput::Scores(c) => ConfigOutputRef::Scores(c),
284            ConfigOutput::Boxes(c) => ConfigOutputRef::Boxes(c),
285            ConfigOutput::MaskCoefficients(c) => ConfigOutputRef::MaskCoefficients(c),
286            ConfigOutput::Classes(c) => ConfigOutputRef::Classes(c),
287        }
288    }
289}
290
291impl ConfigOutput {
292    /// Returns the shape of the output.
293    ///
294    /// # Examples
295    /// ```rust
296    /// # use edgefirst_decoder::{configs, ConfigOutput};
297    /// let detection_config = configs::Detection {
298    ///     anchors: None,
299    ///     decoder: configs::DecoderType::Ultralytics,
300    ///     quantization: None,
301    ///     shape: vec![1, 84, 8400],
302    ///     dshape: Vec::new(),
303    ///     normalized: Some(true),
304    /// };
305    /// let output = ConfigOutput::Detection(detection_config);
306    /// assert_eq!(output.shape(), &[1, 84, 8400]);
307    /// ```
308    pub fn shape(&self) -> &[usize] {
309        match self {
310            ConfigOutput::Detection(detection) => &detection.shape,
311            ConfigOutput::Mask(mask) => &mask.shape,
312            ConfigOutput::Segmentation(segmentation) => &segmentation.shape,
313            ConfigOutput::Scores(scores) => &scores.shape,
314            ConfigOutput::Boxes(boxes) => &boxes.shape,
315            ConfigOutput::Protos(protos) => &protos.shape,
316            ConfigOutput::MaskCoefficients(mask_coefficients) => &mask_coefficients.shape,
317            ConfigOutput::Classes(classes) => &classes.shape,
318        }
319    }
320
321    /// Returns the decoder type of the output.
322    ///    
323    /// # Examples
324    /// ```rust
325    /// # use edgefirst_decoder::{configs, ConfigOutput};
326    /// let detection_config = configs::Detection {
327    ///     anchors: None,
328    ///     decoder: configs::DecoderType::Ultralytics,
329    ///     quantization: None,
330    ///     shape: vec![1, 84, 8400],
331    ///     dshape: Vec::new(),
332    ///     normalized: Some(true),
333    /// };
334    /// let output = ConfigOutput::Detection(detection_config);
335    /// assert_eq!(output.decoder(), &configs::DecoderType::Ultralytics);
336    /// ```
337    pub fn decoder(&self) -> &configs::DecoderType {
338        match self {
339            ConfigOutput::Detection(detection) => &detection.decoder,
340            ConfigOutput::Mask(mask) => &mask.decoder,
341            ConfigOutput::Segmentation(segmentation) => &segmentation.decoder,
342            ConfigOutput::Scores(scores) => &scores.decoder,
343            ConfigOutput::Boxes(boxes) => &boxes.decoder,
344            ConfigOutput::Protos(protos) => &protos.decoder,
345            ConfigOutput::MaskCoefficients(mask_coefficients) => &mask_coefficients.decoder,
346            ConfigOutput::Classes(classes) => &classes.decoder,
347        }
348    }
349
350    /// Returns the quantization of the output.
351    ///
352    /// # Examples
353    /// ```rust
354    /// # use edgefirst_decoder::{configs, ConfigOutput};
355    /// let detection_config = configs::Detection {
356    ///   anchors: None,
357    ///   decoder: configs::DecoderType::Ultralytics,
358    ///   quantization: Some(configs::QuantTuple(0.012345, 26)),
359    ///   shape: vec![1, 84, 8400],
360    ///   dshape: Vec::new(),
361    ///   normalized: Some(true),
362    /// };
363    /// let output = ConfigOutput::Detection(detection_config);
364    /// assert_eq!(output.quantization(), Some(configs::QuantTuple(0.012345, 26)));
365    /// ```
366    pub fn quantization(&self) -> Option<QuantTuple> {
367        match self {
368            ConfigOutput::Detection(detection) => detection.quantization,
369            ConfigOutput::Mask(mask) => mask.quantization,
370            ConfigOutput::Segmentation(segmentation) => segmentation.quantization,
371            ConfigOutput::Scores(scores) => scores.quantization,
372            ConfigOutput::Boxes(boxes) => boxes.quantization,
373            ConfigOutput::Protos(protos) => protos.quantization,
374            ConfigOutput::MaskCoefficients(mask_coefficients) => mask_coefficients.quantization,
375            ConfigOutput::Classes(classes) => classes.quantization,
376        }
377    }
378}