use std::collections::HashMap;
use std::fmt::Display;
use serde::{Deserialize, Serialize};
pub fn deserialize_dshape<'de, D>(deserializer: D) -> Result<Vec<(DimName, usize)>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum DShapeItem {
Tuple(DimName, usize),
Map(HashMap<DimName, usize>),
}
let items: Vec<DShapeItem> = Vec::deserialize(deserializer)?;
items
.into_iter()
.map(|item| match item {
DShapeItem::Tuple(name, size) => Ok((name, size)),
DShapeItem::Map(map) => {
if map.len() != 1 {
return Err(serde::de::Error::custom(
"dshape map entry must have exactly one key",
));
}
let (name, size) = map.into_iter().next().unwrap();
Ok((name, size))
}
})
.collect()
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Copy)]
pub struct QuantTuple(pub f32, pub i32);
impl From<QuantTuple> for (f32, i32) {
fn from(value: QuantTuple) -> Self {
(value.0, value.1)
}
}
impl From<(f32, i32)> for QuantTuple {
fn from(value: (f32, i32)) -> Self {
QuantTuple(value.0, value.1)
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
pub struct Segmentation {
#[serde(default)]
pub decoder: DecoderType,
#[serde(default)]
pub quantization: Option<QuantTuple>,
#[serde(default)]
pub shape: Vec<usize>,
#[serde(default, deserialize_with = "deserialize_dshape")]
pub dshape: Vec<(DimName, usize)>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
pub struct Protos {
#[serde(default)]
pub decoder: DecoderType,
#[serde(default)]
pub quantization: Option<QuantTuple>,
#[serde(default)]
pub shape: Vec<usize>,
#[serde(default, deserialize_with = "deserialize_dshape")]
pub dshape: Vec<(DimName, usize)>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
pub struct MaskCoefficients {
#[serde(default)]
pub decoder: DecoderType,
#[serde(default)]
pub quantization: Option<QuantTuple>,
#[serde(default)]
pub shape: Vec<usize>,
#[serde(default, deserialize_with = "deserialize_dshape")]
pub dshape: Vec<(DimName, usize)>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
pub struct Mask {
#[serde(default)]
pub decoder: DecoderType,
#[serde(default)]
pub quantization: Option<QuantTuple>,
#[serde(default)]
pub shape: Vec<usize>,
#[serde(default, deserialize_with = "deserialize_dshape")]
pub dshape: Vec<(DimName, usize)>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
pub struct Detection {
#[serde(default)]
pub anchors: Option<Vec<[f32; 2]>>,
#[serde(default)]
pub decoder: DecoderType,
#[serde(default)]
pub quantization: Option<QuantTuple>,
#[serde(default)]
pub shape: Vec<usize>,
#[serde(default, deserialize_with = "deserialize_dshape")]
pub dshape: Vec<(DimName, usize)>,
#[serde(default)]
pub normalized: Option<bool>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
pub struct Scores {
#[serde(default)]
pub decoder: DecoderType,
#[serde(default)]
pub quantization: Option<QuantTuple>,
#[serde(default)]
pub shape: Vec<usize>,
#[serde(default, deserialize_with = "deserialize_dshape")]
pub dshape: Vec<(DimName, usize)>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
pub struct Boxes {
#[serde(default)]
pub decoder: DecoderType,
#[serde(default)]
pub quantization: Option<QuantTuple>,
#[serde(default)]
pub shape: Vec<usize>,
#[serde(default, deserialize_with = "deserialize_dshape")]
pub dshape: Vec<(DimName, usize)>,
#[serde(default)]
pub normalized: Option<bool>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
pub struct Classes {
#[serde(default)]
pub decoder: DecoderType,
#[serde(default)]
pub quantization: Option<QuantTuple>,
#[serde(default)]
pub shape: Vec<usize>,
#[serde(default, deserialize_with = "deserialize_dshape")]
pub dshape: Vec<(DimName, usize)>,
}
edgefirst_tensor::ef_vocabulary! {
#[derive(Serialize, Deserialize)]
pub enum DimName {
#[serde(rename = "batch")]
Batch = 0, "batch", BATCH,
#[serde(rename = "height")]
Height = 1, "height", HEIGHT,
#[serde(rename = "width")]
Width = 2, "width", WIDTH,
#[serde(rename = "num_classes")]
NumClasses = 3, "num_classes", NUM_CLASSES,
#[serde(rename = "num_features")]
NumFeatures = 4, "num_features", NUM_FEATURES,
#[serde(rename = "num_boxes")]
NumBoxes = 5, "num_boxes", NUM_BOXES,
#[serde(rename = "num_protos")]
NumProtos = 6, "num_protos", NUM_PROTOS,
#[serde(rename = "num_anchors_x_features")]
NumAnchorsXFeatures = 7, "num_anchors_x_features", NUM_ANCHORS_X_FEATURES,
#[serde(rename = "padding")]
Padding = 8, "padding", PADDING,
#[serde(rename = "box_coords")]
BoxCoords = 9, "box_coords", BOX_COORDS,
#[serde(rename = "num_queries")]
NumQueries = 11, "num_queries", NUM_QUERIES,
#[serde(other)]
Unknown = 10, "unknown", UNKNOWN,
}
#[doc(hidden)]
pub mod dim_name_code;
}
impl Display for DimName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
edgefirst_tensor::ef_vocabulary! {
#[derive(Serialize, Deserialize, Default)]
pub enum DecoderType {
#[default]
#[serde(rename = "ultralytics", alias = "yolov8")]
Ultralytics = 0, "ultralytics", ULTRALYTICS,
#[serde(rename = "modelpack")]
ModelPack = 1, "modelpack", MODELPACK,
#[serde(rename = "semantic")]
Semantic = 2, "semantic", SEMANTIC,
}
#[doc(hidden)]
pub mod decoder_type_code;
}
edgefirst_tensor::ef_vocabulary! {
#[derive(Serialize, Deserialize)]
pub enum DecoderVersion {
#[serde(rename = "yolov5")]
Yolov5 = 0, "yolov5", YOLOV5,
#[serde(rename = "yolov8")]
Yolov8 = 1, "yolov8", YOLOV8,
#[serde(rename = "yolo11")]
Yolo11 = 2, "yolo11", YOLO11,
#[serde(rename = "yolo26")]
Yolo26 = 3, "yolo26", YOLO26,
}
#[doc(hidden)]
pub mod decoder_version_code;
}
impl DecoderVersion {
pub fn is_end_to_end(&self) -> bool {
matches!(self, DecoderVersion::Yolo26)
}
}
edgefirst_tensor::ef_vocabulary! {
#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum Nms {
Auto = 1, "auto", AUTO,
#[default]
ClassAware = 2, "class_aware", CLASS_AWARE,
ClassAgnostic = 3, "class_agnostic", CLASS_AGNOSTIC,
}
#[doc(hidden)]
pub mod nms_code;
}
edgefirst_tensor::ef_vocabulary! {
pub enum OutputType {
Detection = 0, "detection", DETECTION,
Boxes = 1, "boxes", BOXES,
Scores = 2, "scores", SCORES,
Protos = 3, "protos", PROTOS,
Segmentation = 4, "segmentation", SEGMENTATION,
MaskCoefficients = 5, "mask_coefs", MASK_COEFFICIENTS,
Mask = 6, "masks", MASK,
Classes = 7, "classes", CLASSES,
}
#[doc(hidden)]
pub mod output_type_code;
}
pub const NMS_OFF_CODE: u32 = 0;
impl Nms {
pub const fn option_code(nms: Option<Nms>) -> u32 {
match nms {
Some(n) => n.code(),
None => NMS_OFF_CODE,
}
}
pub fn from_option_code(code: u32) -> Option<Option<Nms>> {
if code == NMS_OFF_CODE {
Some(None)
} else {
Nms::from_code(code).map(Some)
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ModelType {
ModelPackSegDet {
boxes: Boxes,
scores: Scores,
segmentation: Segmentation,
},
ModelPackSegDetSplit {
detection: Vec<Detection>,
segmentation: Segmentation,
},
ModelPackDet {
boxes: Boxes,
scores: Scores,
},
ModelPackDetSplit {
detection: Vec<Detection>,
},
ModelPackSeg {
segmentation: Segmentation,
},
YoloDet {
boxes: Detection,
},
YoloSegDet {
boxes: Detection,
protos: Protos,
},
YoloSplitDet {
boxes: Boxes,
scores: Scores,
},
YoloSplitSegDet {
boxes: Boxes,
scores: Scores,
mask_coeff: MaskCoefficients,
protos: Protos,
},
YoloSegDet2Way {
boxes: Detection,
mask_coeff: MaskCoefficients,
protos: Protos,
},
YoloEndToEndDet {
boxes: Detection,
},
YoloEndToEndSegDet {
boxes: Detection,
protos: Protos,
},
YoloSplitEndToEndDet {
boxes: Boxes,
scores: Scores,
classes: Classes,
},
YoloSplitEndToEndSegDet {
boxes: Boxes,
scores: Scores,
classes: Classes,
mask_coeff: MaskCoefficients,
protos: Protos,
},
PerScale,
Semantic,
}
pub(crate) fn validate_dshape(
role_name: &str,
shape: &[usize],
dshape: &[(DimName, usize)],
) -> Result<(), crate::DecoderError> {
use crate::DecoderError;
if dshape.is_empty() {
return Ok(());
}
if dshape.len() != shape.len() {
return Err(DecoderError::InvalidConfig(format!(
"{role_name} output: dshape has {} entries but shape has {} entries \
— shape and dshape must describe the same axes in the same order",
dshape.len(),
shape.len()
)));
}
for (i, (name, size)) in dshape.iter().enumerate() {
if *size != shape[i] {
return Err(DecoderError::InvalidConfig(format!(
"{role_name} output: dshape[{i}] = ({name}, {size}) does not \
match shape[{i}] = {} — declare shape and dshape in the \
same physical order (outermost axis first)",
shape[i]
)));
}
if *name == DimName::Padding && *size != 1 {
return Err(DecoderError::InvalidConfig(format!(
"{role_name} output: `padding` axis must have size 1, got {size}"
)));
}
if *name == DimName::BoxCoords && *size != 4 {
return Err(DecoderError::InvalidConfig(format!(
"{role_name} output: `box_coords` axis must have size 4, got {size}"
)));
}
}
for i in 0..dshape.len() {
for j in (i + 1)..dshape.len() {
if dshape[i].0 == dshape[j].0 {
return Err(DecoderError::InvalidConfig(format!(
"{role_name} output: dshape axis `{}` appears at \
both index {i} and {j}",
dshape[i].0
)));
}
}
}
Ok(())
}
#[cfg(test)]
mod vocabulary_tests {
use super::*;
#[test]
fn nms_off_is_code_zero_and_no_variant_claims_it() {
assert_eq!(Nms::from_code(NMS_OFF_CODE), None);
assert_eq!(Nms::option_code(None), NMS_OFF_CODE);
assert_eq!(Nms::from_option_code(NMS_OFF_CODE), Some(None));
for &n in Nms::all() {
assert_ne!(n.code(), NMS_OFF_CODE);
assert_eq!(Nms::from_option_code(n.code()), Some(Some(n)));
}
assert_eq!(Nms::from_option_code(99), None);
}
#[test]
fn serde_names_match_the_vocabulary_strings() {
for &n in Nms::all() {
assert_eq!(serde_json::to_value(n).unwrap(), n.as_str());
}
for &d in DecoderType::all() {
assert_eq!(serde_json::to_value(d).unwrap(), d.as_str());
}
for &v in DecoderVersion::all() {
assert_eq!(serde_json::to_value(v).unwrap(), v.as_str());
}
for &d in DimName::all() {
assert_eq!(d.to_string(), d.as_str());
if d != DimName::Unknown {
assert_eq!(serde_json::to_value(d).unwrap(), d.as_str());
}
}
}
}