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}