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}