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
101impl ConfigOutput {
102    /// The kind of this output.
103    pub fn output_type(&self) -> configs::OutputType {
104        use configs::OutputType;
105        match self {
106            ConfigOutput::Detection(_) => OutputType::Detection,
107            ConfigOutput::Mask(_) => OutputType::Mask,
108            ConfigOutput::Segmentation(_) => OutputType::Segmentation,
109            ConfigOutput::Protos(_) => OutputType::Protos,
110            ConfigOutput::Scores(_) => OutputType::Scores,
111            ConfigOutput::Boxes(_) => OutputType::Boxes,
112            ConfigOutput::MaskCoefficients(_) => OutputType::MaskCoefficients,
113            ConfigOutput::Classes(_) => OutputType::Classes,
114        }
115    }
116}
117
118#[derive(Debug, PartialEq, Clone)]
119pub enum ConfigOutputRef<'a> {
120    Detection(&'a configs::Detection),
121    Mask(&'a configs::Mask),
122    Segmentation(&'a configs::Segmentation),
123    Protos(&'a configs::Protos),
124    Scores(&'a configs::Scores),
125    Boxes(&'a configs::Boxes),
126    MaskCoefficients(&'a configs::MaskCoefficients),
127    Classes(&'a configs::Classes),
128}
129
130impl<'a> ConfigOutputRef<'a> {
131    pub(super) fn decoder(&self) -> configs::DecoderType {
132        match self {
133            ConfigOutputRef::Detection(v) => v.decoder,
134            ConfigOutputRef::Mask(v) => v.decoder,
135            ConfigOutputRef::Segmentation(v) => v.decoder,
136            ConfigOutputRef::Protos(v) => v.decoder,
137            ConfigOutputRef::Scores(v) => v.decoder,
138            ConfigOutputRef::Boxes(v) => v.decoder,
139            ConfigOutputRef::MaskCoefficients(v) => v.decoder,
140            ConfigOutputRef::Classes(v) => v.decoder,
141        }
142    }
143
144    pub(super) fn dshape(&self) -> &[(DimName, usize)] {
145        match self {
146            ConfigOutputRef::Detection(v) => &v.dshape,
147            ConfigOutputRef::Mask(v) => &v.dshape,
148            ConfigOutputRef::Segmentation(v) => &v.dshape,
149            ConfigOutputRef::Protos(v) => &v.dshape,
150            ConfigOutputRef::Scores(v) => &v.dshape,
151            ConfigOutputRef::Boxes(v) => &v.dshape,
152            ConfigOutputRef::MaskCoefficients(v) => &v.dshape,
153            ConfigOutputRef::Classes(v) => &v.dshape,
154        }
155    }
156}
157
158impl<'a> From<&'a configs::Detection> for ConfigOutputRef<'a> {
159    /// Converts from references of config structs to ConfigOutputRef
160    /// # Examples
161    /// ```rust
162    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
163    /// let detection_config = configs::Detection {
164    ///     anchors: None,
165    ///     decoder: configs::DecoderType::Ultralytics,
166    ///     quantization: None,
167    ///     shape: vec![1, 84, 8400],
168    ///     dshape: Vec::new(),
169    ///     normalized: Some(true),
170    /// };
171    /// let output: ConfigOutputRef = (&detection_config).into();
172    /// ```
173    fn from(v: &'a configs::Detection) -> ConfigOutputRef<'a> {
174        ConfigOutputRef::Detection(v)
175    }
176}
177
178impl<'a> From<&'a configs::Mask> for ConfigOutputRef<'a> {
179    /// Converts from references of config structs to ConfigOutputRef
180    /// # Examples
181    /// ```rust
182    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
183    /// let mask = configs::Mask {
184    ///     decoder: configs::DecoderType::ModelPack,
185    ///     quantization: None,
186    ///     shape: vec![1, 160, 160, 1],
187    ///     dshape: Vec::new(),
188    /// };
189    /// let output: ConfigOutputRef = (&mask).into();
190    /// ```
191    fn from(v: &'a configs::Mask) -> ConfigOutputRef<'a> {
192        ConfigOutputRef::Mask(v)
193    }
194}
195
196impl<'a> From<&'a configs::Segmentation> for ConfigOutputRef<'a> {
197    /// Converts from references of config structs to ConfigOutputRef
198    /// # Examples
199    /// ```rust
200    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
201    /// let seg = configs::Segmentation {
202    ///     decoder: configs::DecoderType::ModelPack,
203    ///     quantization: None,
204    ///     shape: vec![1, 160, 160, 3],
205    ///     dshape: Vec::new(),
206    /// };
207    /// let output: ConfigOutputRef = (&seg).into();
208    /// ```
209    fn from(v: &'a configs::Segmentation) -> ConfigOutputRef<'a> {
210        ConfigOutputRef::Segmentation(v)
211    }
212}
213
214impl<'a> From<&'a configs::Protos> for ConfigOutputRef<'a> {
215    /// Converts from references of config structs to ConfigOutputRef
216    /// # Examples
217    /// ```rust
218    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
219    /// let protos = configs::Protos {
220    ///     decoder: configs::DecoderType::Ultralytics,
221    ///     quantization: None,
222    ///     shape: vec![1, 160, 160, 32],
223    ///     dshape: Vec::new(),
224    /// };
225    /// let output: ConfigOutputRef = (&protos).into();
226    /// ```
227    fn from(v: &'a configs::Protos) -> ConfigOutputRef<'a> {
228        ConfigOutputRef::Protos(v)
229    }
230}
231
232impl<'a> From<&'a configs::Scores> for ConfigOutputRef<'a> {
233    /// Converts from references of config structs to ConfigOutputRef
234    /// # Examples
235    /// ```rust
236    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
237    /// let scores = configs::Scores {
238    ///     decoder: configs::DecoderType::Ultralytics,
239    ///     quantization: None,
240    ///     shape: vec![1, 40, 8400],
241    ///     dshape: Vec::new(),
242    /// };
243    /// let output: ConfigOutputRef = (&scores).into();
244    /// ```
245    fn from(v: &'a configs::Scores) -> ConfigOutputRef<'a> {
246        ConfigOutputRef::Scores(v)
247    }
248}
249
250impl<'a> From<&'a configs::Boxes> for ConfigOutputRef<'a> {
251    /// Converts from references of config structs to ConfigOutputRef
252    /// # Examples
253    /// ```rust
254    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
255    /// let boxes = configs::Boxes {
256    ///     decoder: configs::DecoderType::Ultralytics,
257    ///     quantization: None,
258    ///     shape: vec![1, 4, 8400],
259    ///     dshape: Vec::new(),
260    ///     normalized: Some(true),
261    /// };
262    /// let output: ConfigOutputRef = (&boxes).into();
263    /// ```
264    fn from(v: &'a configs::Boxes) -> ConfigOutputRef<'a> {
265        ConfigOutputRef::Boxes(v)
266    }
267}
268
269impl<'a> From<&'a configs::MaskCoefficients> for ConfigOutputRef<'a> {
270    /// Converts from references of config structs to ConfigOutputRef
271    /// # Examples
272    /// ```rust
273    /// # use edgefirst_decoder::{configs, ConfigOutputRef};
274    /// let mask_coefficients = configs::MaskCoefficients {
275    ///     decoder: configs::DecoderType::Ultralytics,
276    ///     quantization: None,
277    ///     shape: vec![1, 32, 8400],
278    ///     dshape: Vec::new(),
279    /// };
280    /// let output: ConfigOutputRef = (&mask_coefficients).into();
281    /// ```
282    fn from(v: &'a configs::MaskCoefficients) -> ConfigOutputRef<'a> {
283        ConfigOutputRef::MaskCoefficients(v)
284    }
285}
286
287impl<'a> From<&'a configs::Classes> for ConfigOutputRef<'a> {
288    fn from(v: &'a configs::Classes) -> ConfigOutputRef<'a> {
289        ConfigOutputRef::Classes(v)
290    }
291}
292
293impl<'a> From<&'a ConfigOutput> for ConfigOutputRef<'a> {
294    fn from(v: &'a ConfigOutput) -> ConfigOutputRef<'a> {
295        match v {
296            ConfigOutput::Detection(c) => ConfigOutputRef::Detection(c),
297            ConfigOutput::Mask(c) => ConfigOutputRef::Mask(c),
298            ConfigOutput::Segmentation(c) => ConfigOutputRef::Segmentation(c),
299            ConfigOutput::Protos(c) => ConfigOutputRef::Protos(c),
300            ConfigOutput::Scores(c) => ConfigOutputRef::Scores(c),
301            ConfigOutput::Boxes(c) => ConfigOutputRef::Boxes(c),
302            ConfigOutput::MaskCoefficients(c) => ConfigOutputRef::MaskCoefficients(c),
303            ConfigOutput::Classes(c) => ConfigOutputRef::Classes(c),
304        }
305    }
306}
307
308impl ConfigOutput {
309    /// Returns the shape of the output.
310    ///
311    /// # Examples
312    /// ```rust
313    /// # use edgefirst_decoder::{configs, ConfigOutput};
314    /// let detection_config = configs::Detection {
315    ///     anchors: None,
316    ///     decoder: configs::DecoderType::Ultralytics,
317    ///     quantization: None,
318    ///     shape: vec![1, 84, 8400],
319    ///     dshape: Vec::new(),
320    ///     normalized: Some(true),
321    /// };
322    /// let output = ConfigOutput::Detection(detection_config);
323    /// assert_eq!(output.shape(), &[1, 84, 8400]);
324    /// ```
325    pub fn shape(&self) -> &[usize] {
326        match self {
327            ConfigOutput::Detection(detection) => &detection.shape,
328            ConfigOutput::Mask(mask) => &mask.shape,
329            ConfigOutput::Segmentation(segmentation) => &segmentation.shape,
330            ConfigOutput::Scores(scores) => &scores.shape,
331            ConfigOutput::Boxes(boxes) => &boxes.shape,
332            ConfigOutput::Protos(protos) => &protos.shape,
333            ConfigOutput::MaskCoefficients(mask_coefficients) => &mask_coefficients.shape,
334            ConfigOutput::Classes(classes) => &classes.shape,
335        }
336    }
337
338    /// Returns the decoder type of the output.
339    ///    
340    /// # Examples
341    /// ```rust
342    /// # use edgefirst_decoder::{configs, ConfigOutput};
343    /// let detection_config = configs::Detection {
344    ///     anchors: None,
345    ///     decoder: configs::DecoderType::Ultralytics,
346    ///     quantization: None,
347    ///     shape: vec![1, 84, 8400],
348    ///     dshape: Vec::new(),
349    ///     normalized: Some(true),
350    /// };
351    /// let output = ConfigOutput::Detection(detection_config);
352    /// assert_eq!(output.decoder(), &configs::DecoderType::Ultralytics);
353    /// ```
354    pub fn decoder(&self) -> &configs::DecoderType {
355        match self {
356            ConfigOutput::Detection(detection) => &detection.decoder,
357            ConfigOutput::Mask(mask) => &mask.decoder,
358            ConfigOutput::Segmentation(segmentation) => &segmentation.decoder,
359            ConfigOutput::Scores(scores) => &scores.decoder,
360            ConfigOutput::Boxes(boxes) => &boxes.decoder,
361            ConfigOutput::Protos(protos) => &protos.decoder,
362            ConfigOutput::MaskCoefficients(mask_coefficients) => &mask_coefficients.decoder,
363            ConfigOutput::Classes(classes) => &classes.decoder,
364        }
365    }
366
367    /// Returns the quantization of the output.
368    ///
369    /// # Examples
370    /// ```rust
371    /// # use edgefirst_decoder::{configs, ConfigOutput};
372    /// let detection_config = configs::Detection {
373    ///   anchors: None,
374    ///   decoder: configs::DecoderType::Ultralytics,
375    ///   quantization: Some(configs::QuantTuple(0.012345, 26)),
376    ///   shape: vec![1, 84, 8400],
377    ///   dshape: Vec::new(),
378    ///   normalized: Some(true),
379    /// };
380    /// let output = ConfigOutput::Detection(detection_config);
381    /// assert_eq!(output.quantization(), Some(configs::QuantTuple(0.012345, 26)));
382    /// ```
383    pub fn quantization(&self) -> Option<QuantTuple> {
384        match self {
385            ConfigOutput::Detection(detection) => detection.quantization,
386            ConfigOutput::Mask(mask) => mask.quantization,
387            ConfigOutput::Segmentation(segmentation) => segmentation.quantization,
388            ConfigOutput::Scores(scores) => scores.quantization,
389            ConfigOutput::Boxes(boxes) => boxes.quantization,
390            ConfigOutput::Protos(protos) => protos.quantization,
391            ConfigOutput::MaskCoefficients(mask_coefficients) => mask_coefficients.quantization,
392            ConfigOutput::Classes(classes) => classes.quantization,
393        }
394    }
395}
396
397#[cfg(test)]
398mod output_type_tests {
399    use super::*;
400    use configs::OutputType;
401
402    #[test]
403    fn output_type_strings_are_the_serialized_tags() {
404        let outputs = [
405            ConfigOutput::Detection(Default::default()),
406            ConfigOutput::Boxes(Default::default()),
407            ConfigOutput::Scores(Default::default()),
408            ConfigOutput::Protos(Default::default()),
409            ConfigOutput::Segmentation(Default::default()),
410            ConfigOutput::MaskCoefficients(Default::default()),
411            ConfigOutput::Mask(Default::default()),
412            ConfigOutput::Classes(Default::default()),
413        ];
414        assert_eq!(outputs.len(), OutputType::all().len());
415        for (output, &want) in outputs.iter().zip(OutputType::all()) {
416            assert_eq!(output.output_type(), want);
417            let json = serde_json::to_value(output).unwrap();
418            assert_eq!(json["type"], want.as_str(), "{want:?}");
419        }
420    }
421}