1use crate::configs::{self, deserialize_dshape, DimName, QuantTuple};
47use crate::{ConfigOutput, ConfigOutputs, DecoderError, DecoderResult};
48use serde::{Deserialize, Serialize};
49
50pub const MAX_SUPPORTED_SCHEMA_VERSION: u32 = 2;
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74#[non_exhaustive]
75pub struct SchemaV2 {
76 pub schema_version: u32,
78
79 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub input: Option<InputSpec>,
86
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub outputs: Vec<LogicalOutput>,
91
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub nms: Option<NmsMode>,
95
96 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub nms_multi_label: Option<bool>,
100
101 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub decoder_version: Option<DecoderVersion>,
107}
108
109impl SchemaV2 {
110 pub fn new() -> Self {
112 Self::default()
113 }
114}
115
116impl Default for SchemaV2 {
117 fn default() -> Self {
118 Self {
119 schema_version: 2,
120 input: None,
121 outputs: Vec::new(),
122 nms: None,
123 nms_multi_label: None,
124 decoder_version: None,
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131pub struct InputSpec {
132 pub shape: Vec<usize>,
134
135 #[serde(
138 default,
139 deserialize_with = "deserialize_dshape",
140 skip_serializing_if = "Vec::is_empty"
141 )]
142 pub dshape: Vec<(DimName, usize)>,
143
144 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub cameraadaptor: Option<String>,
149
150 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub preprocessing: Option<Preprocessing>,
156}
157
158impl InputSpec {
159 pub fn channels(&self) -> Option<usize> {
175 if !self.dshape.is_empty() {
176 let mut rest = self.dshape.iter().enumerate().filter(|(_, (name, _))| {
177 !matches!(name, DimName::Batch | DimName::Height | DimName::Width)
178 });
179 return match (rest.next(), rest.next()) {
180 (Some((i, _)), None) => self.shape.get(i).copied(),
181 _ => None,
182 };
183 }
184 let [_, c1, _, c3] = self.shape.as_slice() else {
185 return None;
186 };
187 let is_channels = |c: usize| (1..=4).contains(&c);
188 match (is_channels(*c1), is_channels(*c3)) {
189 (true, false) => Some(*c1),
190 (false, true) => Some(*c3),
191 _ => None,
192 }
193 }
194}
195
196#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
216#[non_exhaustive]
217pub struct Preprocessing {
218 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub normalization: Option<Normalization>,
221
222 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub resize: Option<ResizeMode>,
225
226 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub interpolation: Option<Interpolation>,
230
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub letterbox: Option<Letterbox>,
234}
235
236#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
239#[serde(untagged)]
240pub enum Normalization {
241 Preset(NormalizationPreset),
243 MeanStd {
246 mean: Vec<f32>,
248 std: Vec<f32>,
250 },
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case")]
257#[non_exhaustive]
258pub enum NormalizationPreset {
259 Unsigned,
261 Signed,
263 Raw,
265 Imagenet,
267}
268
269const IMAGENET_MEAN: [f32; 3] = [123.675, 116.28, 103.53];
270const IMAGENET_STD: [f32; 3] = [58.395, 57.12, 57.375];
271
272impl Normalization {
273 pub fn mean_std(&self, channels: usize) -> DecoderResult<(Vec<f32>, Vec<f32>)> {
292 let (mean, std): (&[f32], &[f32]) = match self {
293 Normalization::Preset(NormalizationPreset::Unsigned) => (&[0.0], &[255.0]),
294 Normalization::Preset(NormalizationPreset::Signed) => (&[127.5], &[127.5]),
295 Normalization::Preset(NormalizationPreset::Raw) => (&[0.0], &[1.0]),
296 Normalization::Preset(NormalizationPreset::Imagenet) => {
297 if channels != 3 {
298 return Err(DecoderError::InvalidConfig(format!(
299 "imagenet normalization needs 3 channels, got {channels}"
300 )));
301 }
302 (&IMAGENET_MEAN, &IMAGENET_STD)
303 }
304 Normalization::MeanStd { mean, std } => (mean, std),
305 };
306 let mean = broadcast_channels("mean", mean, channels)?;
307 let std = broadcast_channels("std", std, channels)?;
308 if let Some(bad) = mean.iter().find(|m| !m.is_finite()) {
309 return Err(DecoderError::InvalidConfig(format!(
310 "normalization mean must be finite, got {bad}"
311 )));
312 }
313 if let Some(bad) = std.iter().find(|s| **s == 0.0 || !s.is_finite()) {
314 return Err(DecoderError::InvalidConfig(format!(
315 "normalization std must be finite and non-zero, got {bad}"
316 )));
317 }
318 Ok((mean, std))
319 }
320}
321
322fn broadcast_channels(field: &str, values: &[f32], channels: usize) -> DecoderResult<Vec<f32>> {
323 match values.len() {
324 1 => Ok(vec![values[0]; channels]),
325 n if n == channels => Ok(values.to_vec()),
326 n => Err(DecoderError::InvalidConfig(format!(
327 "normalization {field} has {n} values; expected 1 or {channels}"
328 ))),
329 }
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
334#[serde(rename_all = "snake_case")]
335#[non_exhaustive]
336pub enum ResizeMode {
337 Stretch,
339 Letterbox,
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
345#[serde(rename_all = "snake_case")]
346#[non_exhaustive]
347pub enum Interpolation {
348 Bilinear,
349 Nearest,
350 Bicubic,
351 Area,
352}
353
354#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
356#[non_exhaustive]
357pub struct Letterbox {
358 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub placement: Option<LetterboxPlacement>,
361
362 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub pad: Option<Vec<f32>>,
366
367 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub pad_space: Option<PadSpace>,
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
374#[serde(rename_all = "snake_case")]
375#[non_exhaustive]
376pub enum LetterboxPlacement {
377 Center,
379 TopLeft,
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
385#[serde(rename_all = "snake_case")]
386#[non_exhaustive]
387pub enum PadSpace {
388 Pixel,
390 Normalized,
392}
393
394#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
402pub struct LogicalOutput {
403 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub name: Option<String>,
406
407 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
414 pub type_: Option<LogicalType>,
415
416 pub shape: Vec<usize>,
419
420 #[serde(
422 default,
423 deserialize_with = "deserialize_dshape",
424 skip_serializing_if = "Vec::is_empty"
425 )]
426 pub dshape: Vec<(DimName, usize)>,
427
428 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub decoder: Option<DecoderKind>,
432
433 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub encoding: Option<BoxEncoding>,
436
437 #[serde(default, skip_serializing_if = "Option::is_none")]
439 pub score_format: Option<ScoreFormat>,
440
441 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub normalized: Option<bool>,
447
448 #[serde(default, skip_serializing_if = "Option::is_none")]
451 pub anchors: Option<Vec<[f32; 2]>>,
452
453 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub stride: Option<Stride>,
458
459 #[serde(default, skip_serializing_if = "Option::is_none")]
462 pub dtype: Option<DType>,
463
464 #[serde(default, skip_serializing_if = "Option::is_none")]
467 pub quantization: Option<Quantization>,
468
469 #[serde(default, skip_serializing_if = "Vec::is_empty")]
473 pub outputs: Vec<PhysicalOutput>,
474
475 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub activation_applied: Option<Activation>,
484
485 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub activation_required: Option<Activation>,
495
496 #[serde(flatten)]
499 pub semantic: SemanticParams,
500}
501
502#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
507#[non_exhaustive]
508pub struct SemanticParams {
509 #[serde(default, skip_serializing_if = "Option::is_none")]
511 pub upsample: Option<Upsample>,
512
513 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub decode_mode: Option<DecodeMode>,
516
517 #[serde(default, skip_serializing_if = "Option::is_none")]
521 pub threshold: Option<f32>,
522
523 #[serde(default, skip_serializing_if = "Option::is_none")]
525 pub background_index: Option<u32>,
526
527 #[serde(default, skip_serializing_if = "Option::is_none")]
530 pub no_object_index: Option<u32>,
531
532 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub mask_activation: Option<MaskActivation>,
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
539pub struct Upsample {
540 pub method: UpsampleMethod,
542 #[serde(default)]
544 pub align_corners: bool,
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
549#[serde(rename_all = "snake_case")]
550#[non_exhaustive]
551pub enum UpsampleMethod {
552 Bilinear,
553 Nearest,
554 Bicubic,
555}
556
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
559#[serde(rename_all = "snake_case")]
560#[non_exhaustive]
561pub enum DecodeMode {
562 Accurate,
564 Fast,
566}
567
568impl DecodeMode {
569 pub const fn code(self) -> u32 {
571 match self {
572 Self::Accurate => 0,
573 Self::Fast => 1,
574 }
575 }
576
577 pub const fn from_code(code: u32) -> Option<Self> {
579 match code {
580 0 => Some(Self::Accurate),
581 1 => Some(Self::Fast),
582 _ => None,
583 }
584 }
585}
586
587#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
589#[serde(rename_all = "snake_case")]
590#[non_exhaustive]
591pub enum MaskActivation {
592 Sigmoid,
594 SoftmaxOverQueries,
596}
597
598impl LogicalOutput {
599 pub fn is_split(&self) -> bool {
602 !self.outputs.is_empty()
603 }
604}
605
606#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
612pub struct PhysicalOutput {
613 pub name: String,
617
618 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
625 pub type_: Option<PhysicalType>,
626
627 pub shape: Vec<usize>,
629
630 #[serde(
633 default,
634 deserialize_with = "deserialize_dshape",
635 skip_serializing_if = "Vec::is_empty"
636 )]
637 pub dshape: Vec<(DimName, usize)>,
638
639 pub dtype: DType,
641
642 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub quantization: Option<Quantization>,
646
647 #[serde(default, skip_serializing_if = "Option::is_none")]
650 pub stride: Option<Stride>,
651
652 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub scale_index: Option<usize>,
656
657 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub activation_applied: Option<Activation>,
662
663 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub activation_required: Option<Activation>,
667}
668
669#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
675pub struct Quantization {
676 #[serde(deserialize_with = "deserialize_scalar_or_vec_f32")]
679 pub scale: Vec<f32>,
680
681 #[serde(
685 default,
686 deserialize_with = "deserialize_opt_scalar_or_vec_i32",
687 skip_serializing_if = "Option::is_none"
688 )]
689 pub zero_point: Option<Vec<i32>>,
690
691 #[serde(default, skip_serializing_if = "Option::is_none")]
694 pub axis: Option<usize>,
695
696 #[serde(default, skip_serializing_if = "Option::is_none")]
700 pub dtype: Option<DType>,
701}
702
703impl Quantization {
704 pub fn is_per_tensor(&self) -> bool {
706 self.scale.len() == 1
707 }
708
709 pub fn is_per_channel(&self) -> bool {
711 self.scale.len() > 1
712 }
713
714 pub fn is_symmetric(&self) -> bool {
716 match &self.zero_point {
717 None => true,
718 Some(zps) => zps.iter().all(|&z| z == 0),
719 }
720 }
721
722 pub fn zero_point_at(&self, channel: usize) -> i32 {
725 match &self.zero_point {
726 None => 0,
727 Some(zps) if zps.len() == 1 => zps[0],
728 Some(zps) => zps.get(channel).copied().unwrap_or(0),
729 }
730 }
731
732 pub fn scale_at(&self, channel: usize) -> f32 {
734 if self.scale.len() == 1 {
735 self.scale[0]
736 } else {
737 self.scale.get(channel).copied().unwrap_or(0.0)
738 }
739 }
740}
741
742impl TryFrom<&Quantization> for edgefirst_tensor::Quantization {
752 type Error = edgefirst_tensor::Error;
753
754 fn try_from(q: &Quantization) -> Result<Self, Self::Error> {
755 match (q.scale.as_slice(), q.zero_point.as_deref(), q.axis) {
756 ([scale], None, None) => Ok(Self::per_tensor_symmetric(*scale)),
758 ([scale], Some([zp]), None) => Ok(Self::per_tensor(*scale, *zp)),
760 ([scale], Some([zp]), Some(_)) => Ok(Self::per_tensor(*scale, *zp)),
762 ([scale], None, Some(_)) => Ok(Self::per_tensor_symmetric(*scale)),
763 (scales, None, Some(axis)) if scales.len() > 1 => {
765 Self::per_channel_symmetric(scales.to_vec(), axis)
766 }
767 (scales, Some(zps), Some(axis)) if scales.len() > 1 => {
768 Self::per_channel(scales.to_vec(), zps.to_vec(), axis)
769 }
770 (scales, _, None) if scales.len() > 1 => {
772 Err(edgefirst_tensor::Error::QuantizationInvalid {
773 field: "axis",
774 expected: "Some(axis) for per-channel".into(),
775 got: "None".into(),
776 })
777 }
778 _ => Err(edgefirst_tensor::Error::QuantizationInvalid {
779 field: "scale",
780 expected: "non-empty".into(),
781 got: format!("len={}", q.scale.len()),
782 }),
783 }
784 }
785}
786
787fn deserialize_scalar_or_vec_f32<'de, D>(de: D) -> Result<Vec<f32>, D::Error>
789where
790 D: serde::Deserializer<'de>,
791{
792 #[derive(Deserialize)]
793 #[serde(untagged)]
794 enum OneOrMany {
795 One(f32),
796 Many(Vec<f32>),
797 }
798 match OneOrMany::deserialize(de)? {
799 OneOrMany::One(v) => Ok(vec![v]),
800 OneOrMany::Many(vs) => Ok(vs),
801 }
802}
803
804fn deserialize_opt_scalar_or_vec_i32<'de, D>(de: D) -> Result<Option<Vec<i32>>, D::Error>
806where
807 D: serde::Deserializer<'de>,
808{
809 #[derive(Deserialize)]
810 #[serde(untagged)]
811 enum OneOrMany {
812 One(i32),
813 Many(Vec<i32>),
814 }
815 match Option::<OneOrMany>::deserialize(de)? {
816 None => Ok(None),
817 Some(OneOrMany::One(v)) => Ok(Some(vec![v])),
818 Some(OneOrMany::Many(vs)) => Ok(Some(vs)),
819 }
820}
821
822#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
825#[serde(untagged)]
826pub enum Stride {
827 Square(u32),
828 Rect([u32; 2]),
829}
830
831impl Stride {
832 pub fn x(self) -> u32 {
834 match self {
835 Stride::Square(s) => s,
836 Stride::Rect([sx, _]) => sx,
837 }
838 }
839
840 pub fn y(self) -> u32 {
842 match self {
843 Stride::Square(s) => s,
844 Stride::Rect([_, sy]) => sy,
845 }
846 }
847}
848
849#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
851#[serde(rename_all = "snake_case")]
852pub enum LogicalType {
853 Boxes,
855 Scores,
857 Objectness,
859 Classes,
861 MaskCoefs,
863 Protos,
865 Landmarks,
867 Detections,
869 Segmentation,
872 Masks,
875 Detection,
877 ClassLogits,
880 MaskLogits,
883}
884
885#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
891#[serde(rename_all = "snake_case")]
892pub enum PhysicalType {
893 Boxes,
894 Scores,
895 Objectness,
896 Classes,
897 MaskCoefs,
898 Protos,
899 Landmarks,
900 Detections,
901 Segmentation,
902 Masks,
903 Detection,
904 ClassLogits,
905 MaskLogits,
906 BoxesXy,
908 BoxesWh,
910}
911
912#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
914#[serde(rename_all = "snake_case")]
915pub enum BoxEncoding {
916 Dfl,
919 #[serde(alias = "ltrb")]
922 Direct,
923 Anchor,
926}
927
928#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
930#[serde(rename_all = "snake_case")]
931pub enum ScoreFormat {
932 PerClass,
935 ObjXClass,
939}
940
941#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
943#[serde(rename_all = "snake_case")]
944pub enum Activation {
945 Sigmoid,
946 Softmax,
947 Tanh,
948}
949
950#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
955#[repr(u32)]
956pub enum DecoderKind {
957 #[serde(rename = "modelpack")]
959 ModelPack = configs::decoder_type_code::MODELPACK,
960 #[serde(rename = "ultralytics")]
962 Ultralytics = configs::decoder_type_code::ULTRALYTICS,
963 #[serde(rename = "semantic")]
966 Semantic = configs::decoder_type_code::SEMANTIC,
967}
968
969impl DecoderKind {
970 pub const fn code(self) -> u32 {
972 self as u32
973 }
974}
975
976#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
981#[serde(rename_all = "snake_case")]
982#[repr(u32)]
983pub enum DecoderVersion {
984 Yolov5 = configs::decoder_version_code::YOLOV5,
985 Yolov8 = configs::decoder_version_code::YOLOV8,
986 Yolo11 = configs::decoder_version_code::YOLO11,
987 Yolo26 = configs::decoder_version_code::YOLO26,
988}
989
990impl DecoderVersion {
991 pub const fn code(self) -> u32 {
993 self as u32
994 }
995
996 pub fn is_end_to_end(self) -> bool {
998 matches!(self, DecoderVersion::Yolo26)
999 }
1000}
1001
1002#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1007#[serde(rename_all = "snake_case")]
1008#[repr(u32)]
1009pub enum NmsMode {
1010 ClassAgnostic = configs::nms_code::CLASS_AGNOSTIC,
1012 ClassAware = configs::nms_code::CLASS_AWARE,
1015}
1016
1017#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1025#[repr(u32)]
1026pub enum DType {
1027 Int8 = edgefirst_tensor::protocol::dtype::I8,
1028 Uint8 = edgefirst_tensor::protocol::dtype::U8,
1029 Int16 = edgefirst_tensor::protocol::dtype::I16,
1030 Uint16 = edgefirst_tensor::protocol::dtype::U16,
1031 Int32 = edgefirst_tensor::protocol::dtype::I32,
1032 Uint32 = edgefirst_tensor::protocol::dtype::U32,
1033 Float16 = edgefirst_tensor::protocol::dtype::F16,
1034 Float32 = edgefirst_tensor::protocol::dtype::F32,
1035 Int64 = edgefirst_tensor::protocol::dtype::I64,
1037}
1038
1039const DTYPE_LEGACY_NAMES: [(&str, DType); 9] = [
1041 ("int8", DType::Int8),
1042 ("uint8", DType::Uint8),
1043 ("int16", DType::Int16),
1044 ("uint16", DType::Uint16),
1045 ("int32", DType::Int32),
1046 ("uint32", DType::Uint32),
1047 ("float16", DType::Float16),
1048 ("float32", DType::Float32),
1049 ("int64", DType::Int64),
1050];
1051
1052impl DType {
1053 pub fn all() -> &'static [DType] {
1055 &[
1056 DType::Int8,
1057 DType::Uint8,
1058 DType::Int16,
1059 DType::Uint16,
1060 DType::Int32,
1061 DType::Uint32,
1062 DType::Float16,
1063 DType::Float32,
1064 DType::Int64,
1065 ]
1066 }
1067
1068 pub const fn code(self) -> u32 {
1070 self as u32
1071 }
1072
1073 pub fn to_tensor_dtype(self) -> edgefirst_tensor::DType {
1075 match self {
1076 DType::Int8 => edgefirst_tensor::DType::I8,
1077 DType::Uint8 => edgefirst_tensor::DType::U8,
1078 DType::Int16 => edgefirst_tensor::DType::I16,
1079 DType::Uint16 => edgefirst_tensor::DType::U16,
1080 DType::Int32 => edgefirst_tensor::DType::I32,
1081 DType::Uint32 => edgefirst_tensor::DType::U32,
1082 DType::Float16 => edgefirst_tensor::DType::F16,
1083 DType::Float32 => edgefirst_tensor::DType::F32,
1084 DType::Int64 => edgefirst_tensor::DType::I64,
1085 }
1086 }
1087
1088 pub fn from_tensor_dtype(dtype: edgefirst_tensor::DType) -> Option<Self> {
1091 Self::all()
1092 .iter()
1093 .copied()
1094 .find(|d| d.code() == dtype.code())
1095 }
1096
1097 pub fn from_code(code: u32) -> Option<Self> {
1100 edgefirst_tensor::DType::from_code(code).and_then(Self::from_tensor_dtype)
1101 }
1102
1103 pub fn as_str(self) -> &'static str {
1105 self.to_tensor_dtype().as_str()
1106 }
1107
1108 pub fn from_str_code(s: &str) -> Option<Self> {
1111 edgefirst_tensor::DType::from_str_code(s)
1112 .and_then(Self::from_tensor_dtype)
1113 .or_else(|| {
1114 DTYPE_LEGACY_NAMES
1115 .iter()
1116 .find(|(name, _)| *name == s)
1117 .map(|(_, d)| *d)
1118 })
1119 }
1120
1121 pub fn accepted_names() -> impl Iterator<Item = &'static str> {
1123 Self::all()
1124 .iter()
1125 .map(|d| d.as_str())
1126 .chain(DTYPE_LEGACY_NAMES.iter().map(|(name, _)| *name))
1127 }
1128}
1129
1130impl Serialize for DType {
1131 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
1132 serializer.serialize_str(self.as_str())
1133 }
1134}
1135
1136impl<'de> Deserialize<'de> for DType {
1137 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1138 let s = std::borrow::Cow::<'de, str>::deserialize(deserializer)?;
1139 DType::from_str_code(&s).ok_or_else(|| {
1140 let expected: Vec<&str> = DType::accepted_names().collect();
1141 serde::de::Error::custom(format!(
1142 "unknown dtype `{s}`, expected one of: {}",
1143 expected.join(", ")
1144 ))
1145 })
1146 }
1147}
1148
1149impl DType {
1150 pub fn size_bytes(self) -> usize {
1152 match self {
1153 DType::Int8 | DType::Uint8 => 1,
1154 DType::Int16 | DType::Uint16 | DType::Float16 => 2,
1155 DType::Int32 | DType::Uint32 | DType::Float32 => 4,
1156 DType::Int64 => 8,
1157 }
1158 }
1159
1160 pub fn is_integer(self) -> bool {
1162 matches!(
1163 self,
1164 DType::Int8
1165 | DType::Uint8
1166 | DType::Int16
1167 | DType::Uint16
1168 | DType::Int32
1169 | DType::Uint32
1170 | DType::Int64
1171 )
1172 }
1173
1174 pub fn is_float(self) -> bool {
1176 matches!(self, DType::Float16 | DType::Float32)
1177 }
1178}
1179
1180impl SchemaV2 {
1185 pub fn parse_json(s: &str) -> DecoderResult<Self> {
1193 let value: serde_json::Value = serde_json::from_str(s)?;
1194 Self::from_json_value(value)
1195 }
1196
1197 pub fn parse_yaml(s: &str) -> DecoderResult<Self> {
1201 let value: serde_yaml::Value = serde_yaml::from_str(s)?;
1202 let json = serde_json::to_value(value)
1203 .map_err(|e| DecoderError::InvalidConfig(format!("yaml→json bridge failed: {e}")))?;
1204 Self::from_json_value(json)
1205 }
1206
1207 pub fn parse_file(path: impl AsRef<std::path::Path>) -> DecoderResult<Self> {
1211 let path = path.as_ref();
1212 let content = std::fs::read_to_string(path)
1213 .map_err(|e| DecoderError::InvalidConfig(format!("read {}: {e}", path.display())))?;
1214 let ext = path
1215 .extension()
1216 .and_then(|e| e.to_str())
1217 .map(str::to_ascii_lowercase);
1218 match ext.as_deref() {
1219 Some("json") => Self::parse_json(&content),
1220 Some("yaml") | Some("yml") => Self::parse_yaml(&content),
1221 _ => Self::parse_json(&content).or_else(|_| Self::parse_yaml(&content)),
1222 }
1223 }
1224
1225 pub fn from_json_value(value: serde_json::Value) -> DecoderResult<Self> {
1228 let version = value
1229 .get("schema_version")
1230 .and_then(|v| v.as_u64())
1231 .map(|v| v as u32)
1232 .unwrap_or(1);
1233
1234 if version > MAX_SUPPORTED_SCHEMA_VERSION {
1235 return Err(DecoderError::NotSupported(format!(
1236 "schema_version {version} is not supported by this HAL \
1237 (maximum supported version is {MAX_SUPPORTED_SCHEMA_VERSION}); \
1238 upgrade the HAL or downgrade the metadata"
1239 )));
1240 }
1241
1242 if version >= 2 {
1243 serde_json::from_value(value).map_err(DecoderError::Json)
1244 } else {
1245 let v1: ConfigOutputs = serde_json::from_value(value).map_err(DecoderError::Json)?;
1246 Self::from_v1(&v1)
1247 }
1248 }
1249
1250 pub fn from_v1(v1: &ConfigOutputs) -> DecoderResult<Self> {
1266 let outputs = v1
1267 .outputs
1268 .iter()
1269 .map(logical_from_v1)
1270 .collect::<DecoderResult<Vec<_>>>()?;
1271 Ok(SchemaV2 {
1272 schema_version: 2,
1273 input: None,
1274 outputs,
1275 nms: v1.nms.as_ref().and_then(NmsMode::from_v1),
1276 nms_multi_label: v1.nms_multi_label,
1277 decoder_version: v1.decoder_version.as_ref().map(DecoderVersion::from_v1),
1278 })
1279 }
1280}
1281
1282impl SchemaV2 {
1283 pub fn to_legacy_config_outputs(&self) -> DecoderResult<ConfigOutputs> {
1310 let mut outputs = Vec::with_capacity(self.outputs.len());
1311 for logical in &self.outputs {
1312 if logical.type_.is_none() {
1318 continue;
1319 }
1320 if logical.type_ == Some(LogicalType::Boxes)
1327 && logical.encoding == Some(BoxEncoding::Dfl)
1328 && logical.outputs.is_empty()
1329 {
1330 return Err(DecoderError::NotSupported(format!(
1331 "`boxes` output `{}` has `encoding: dfl` on a flat \
1332 logical (no per-scale children); the HAL's DFL \
1333 decode kernel only runs inside the per-scale merge \
1334 path. Split the boxes output into per-FPN-level \
1335 children (Hailo convention) or pre-decode to 4 \
1336 channels in the model graph (TFLite convention).",
1337 logical.name.as_deref().unwrap_or("<anonymous>"),
1338 )));
1339 }
1340 if let Some(q) = &logical.quantization {
1341 if q.is_per_channel() {
1342 return Err(DecoderError::NotSupported(format!(
1343 "logical `{}` uses per-channel quantization \
1344 (axis {:?}, {} scales); the v1 decoder only \
1345 supports per-tensor quantization",
1346 logical.name.as_deref().unwrap_or("<anonymous>"),
1347 q.axis,
1348 q.scale.len(),
1349 )));
1350 }
1351 }
1352 outputs.push(logical_to_legacy_config_output(logical)?);
1353 }
1354 Ok(ConfigOutputs {
1355 outputs,
1356 nms: self.nms.map(NmsMode::to_v1),
1357 nms_multi_label: self.nms_multi_label,
1358 decoder_version: self.decoder_version.map(|v| v.to_v1()),
1359 })
1360 }
1361
1362 pub fn validate(&self) -> DecoderResult<()> {
1380 if self.schema_version == 0 || self.schema_version > MAX_SUPPORTED_SCHEMA_VERSION {
1381 return Err(DecoderError::InvalidConfig(format!(
1382 "schema_version {} outside supported range [1, {MAX_SUPPORTED_SCHEMA_VERSION}]",
1383 self.schema_version
1384 )));
1385 }
1386
1387 if let Some(input) = &self.input {
1388 if let Some(pre) = &input.preprocessing {
1389 validate_preprocessing(input, pre)?;
1390 }
1391 }
1392
1393 for logical in &self.outputs {
1394 validate_logical(logical)?;
1395 }
1396
1397 Ok(())
1398 }
1399}
1400
1401fn validate_preprocessing(input: &InputSpec, pre: &Preprocessing) -> DecoderResult<()> {
1402 let channels = input.channels();
1403 let mut vectors: Vec<(&str, &[f32])> = Vec::new();
1404 if let Some(Normalization::MeanStd { mean, std }) = &pre.normalization {
1405 vectors.push(("normalization mean", mean));
1406 vectors.push(("normalization std", std));
1407 }
1408 if let Some(pad) = pre.letterbox.as_ref().and_then(|l| l.pad.as_ref()) {
1409 vectors.push(("letterbox pad", pad));
1410 }
1411
1412 let mut expected = channels;
1415 for (field, values) in &vectors {
1416 if values.is_empty() || values.iter().any(|v| !v.is_finite()) {
1417 return Err(DecoderError::InvalidConfig(format!(
1418 "{field} {values:?} must hold one or more finite values"
1419 )));
1420 }
1421 if values.len() == 1 {
1422 continue;
1423 }
1424 match expected {
1425 Some(n) if n != values.len() => {
1426 return Err(DecoderError::InvalidConfig(format!(
1427 "{field} has {} values; expected 1 or {n} (one per input channel)",
1428 values.len()
1429 )));
1430 }
1431 Some(_) => {}
1432 None => expected = Some(values.len()),
1433 }
1434 }
1435
1436 if let Some(norm) = &pre.normalization {
1437 let n = match (norm, expected) {
1438 (_, Some(n)) => n,
1439 (Normalization::Preset(NormalizationPreset::Imagenet), None) => 3,
1442 (_, None) => 1,
1443 };
1444 norm.mean_std(n)?;
1445 }
1446 Ok(())
1447}
1448
1449fn validate_logical(logical: &LogicalOutput) -> DecoderResult<()> {
1450 if logical.outputs.is_empty() {
1451 return Ok(());
1452 }
1453
1454 for child in &logical.outputs {
1456 if child.name.is_empty() {
1457 return Err(DecoderError::InvalidConfig(format!(
1458 "physical child of logical `{}` is missing `name`; name is \
1459 required for tensor binding",
1460 logical.name.as_deref().unwrap_or("<anonymous>")
1461 )));
1462 }
1463 }
1464
1465 for (i, a) in logical.outputs.iter().enumerate() {
1474 for b in &logical.outputs[i + 1..] {
1475 let (Some(ta), Some(tb)) = (a.type_, b.type_) else {
1476 continue;
1477 };
1478 if a.shape == b.shape && ta == tb {
1479 return Err(DecoderError::InvalidConfig(format!(
1480 "physical children `{}` and `{}` share shape {:?} and \
1481 type; tensor binding cannot be resolved",
1482 a.name, b.name, a.shape
1483 )));
1484 }
1485 }
1486 }
1487
1488 let strided: Vec<_> = logical.outputs.iter().map(|c| c.stride.is_some()).collect();
1492 let all_strided = strided.iter().all(|&b| b);
1493 let none_strided = strided.iter().all(|&b| !b);
1494 if !(all_strided || none_strided) {
1495 return Err(DecoderError::InvalidConfig(format!(
1496 "logical `{}` mixes per-scale children (with stride) and \
1497 channel sub-split children (without stride); decomposition \
1498 must be uniform",
1499 logical.name.as_deref().unwrap_or("<anonymous>")
1500 )));
1501 }
1502
1503 if logical.type_ == Some(LogicalType::Boxes) && logical.encoding == Some(BoxEncoding::Dfl) {
1506 for child in &logical.outputs {
1507 if let Some(feat) = last_feature_axis(child) {
1508 if feat % 4 != 0 {
1509 return Err(DecoderError::InvalidConfig(format!(
1510 "DFL boxes child `{}` feature axis {feat} is not \
1511 divisible by 4 (reg_max×4)",
1512 child.name
1513 )));
1514 }
1515 }
1516 }
1517 }
1518
1519 Ok(())
1520}
1521
1522pub(crate) fn last_feature_axis(child: &PhysicalOutput) -> Option<usize> {
1525 for (name, size) in &child.dshape {
1528 if matches!(
1529 name,
1530 DimName::NumFeatures
1531 | DimName::NumClasses
1532 | DimName::NumProtos
1533 | DimName::BoxCoords
1534 | DimName::NumAnchorsXFeatures
1535 ) {
1536 return Some(*size);
1537 }
1538 }
1539 child.shape.last().copied()
1540}
1541
1542fn quantization_from_v1(q: Option<QuantTuple>) -> Option<Quantization> {
1543 q.map(|QuantTuple(scale, zp)| Quantization {
1544 scale: vec![scale],
1545 zero_point: Some(vec![zp]),
1546 axis: None,
1547 dtype: None,
1548 })
1549}
1550
1551fn logical_from_v1(v1: &ConfigOutput) -> DecoderResult<LogicalOutput> {
1552 match v1 {
1553 ConfigOutput::Detection(d) => {
1554 let encoding = match (d.decoder, d.anchors.is_some()) {
1560 (configs::DecoderType::ModelPack, true) => Some(BoxEncoding::Anchor),
1561 (configs::DecoderType::Ultralytics, _) => Some(BoxEncoding::Direct),
1562 (configs::DecoderType::ModelPack, false) => None,
1565 (configs::DecoderType::Semantic, _) => None,
1566 };
1567 Ok(LogicalOutput {
1568 name: None,
1569 type_: Some(LogicalType::Detection),
1570 shape: d.shape.clone(),
1571 dshape: d.dshape.clone(),
1572 decoder: Some(DecoderKind::from_v1(d.decoder)),
1573 encoding,
1574 score_format: None,
1575 normalized: d.normalized,
1576 anchors: d.anchors.clone(),
1577 stride: None,
1578 dtype: None,
1579 quantization: quantization_from_v1(d.quantization),
1580 outputs: Vec::new(),
1581 activation_applied: None,
1582 activation_required: None,
1583 semantic: Default::default(),
1584 })
1585 }
1586 ConfigOutput::Boxes(b) => Ok(LogicalOutput {
1587 name: None,
1588 type_: Some(LogicalType::Boxes),
1589 shape: b.shape.clone(),
1590 dshape: b.dshape.clone(),
1591 decoder: Some(DecoderKind::from_v1(b.decoder)),
1592 encoding: Some(BoxEncoding::Direct),
1596 score_format: None,
1597 normalized: b.normalized,
1598 anchors: None,
1599 stride: None,
1600 dtype: None,
1601 quantization: quantization_from_v1(b.quantization),
1602 outputs: Vec::new(),
1603 activation_applied: None,
1604 activation_required: None,
1605 semantic: Default::default(),
1606 }),
1607 ConfigOutput::Scores(s) => Ok(LogicalOutput {
1608 name: None,
1609 type_: Some(LogicalType::Scores),
1610 shape: s.shape.clone(),
1611 dshape: s.dshape.clone(),
1612 decoder: Some(DecoderKind::from_v1(s.decoder)),
1613 encoding: None,
1614 score_format: Some(ScoreFormat::PerClass),
1618 normalized: None,
1619 anchors: None,
1620 stride: None,
1621 dtype: None,
1622 quantization: quantization_from_v1(s.quantization),
1623 outputs: Vec::new(),
1624 activation_applied: None,
1625 activation_required: None,
1626 semantic: Default::default(),
1627 }),
1628 ConfigOutput::Protos(p) => Ok(LogicalOutput {
1629 name: None,
1630 type_: Some(LogicalType::Protos),
1631 shape: p.shape.clone(),
1632 dshape: p.dshape.clone(),
1633 decoder: Some(DecoderKind::from_v1(p.decoder)),
1635 encoding: None,
1636 score_format: None,
1637 normalized: None,
1638 anchors: None,
1639 stride: None,
1640 dtype: None,
1641 quantization: quantization_from_v1(p.quantization),
1642 outputs: Vec::new(),
1643 activation_applied: None,
1644 activation_required: None,
1645 semantic: Default::default(),
1646 }),
1647 ConfigOutput::MaskCoefficients(m) => Ok(LogicalOutput {
1648 name: None,
1649 type_: Some(LogicalType::MaskCoefs),
1650 shape: m.shape.clone(),
1651 dshape: m.dshape.clone(),
1652 decoder: Some(DecoderKind::from_v1(m.decoder)),
1653 encoding: None,
1654 score_format: None,
1655 normalized: None,
1656 anchors: None,
1657 stride: None,
1658 dtype: None,
1659 quantization: quantization_from_v1(m.quantization),
1660 outputs: Vec::new(),
1661 activation_applied: None,
1662 activation_required: None,
1663 semantic: Default::default(),
1664 }),
1665 ConfigOutput::Segmentation(seg) => Ok(LogicalOutput {
1666 name: None,
1667 type_: Some(LogicalType::Segmentation),
1668 shape: seg.shape.clone(),
1669 dshape: seg.dshape.clone(),
1670 decoder: Some(DecoderKind::from_v1(seg.decoder)),
1671 encoding: None,
1672 score_format: None,
1673 normalized: None,
1674 anchors: None,
1675 stride: None,
1676 dtype: None,
1677 quantization: quantization_from_v1(seg.quantization),
1678 outputs: Vec::new(),
1679 activation_applied: None,
1680 activation_required: None,
1681 semantic: Default::default(),
1682 }),
1683 ConfigOutput::Mask(m) => Ok(LogicalOutput {
1684 name: None,
1685 type_: Some(LogicalType::Masks),
1686 shape: m.shape.clone(),
1687 dshape: m.dshape.clone(),
1688 decoder: Some(DecoderKind::from_v1(m.decoder)),
1689 encoding: None,
1690 score_format: None,
1691 normalized: None,
1692 anchors: None,
1693 stride: None,
1694 dtype: None,
1695 quantization: quantization_from_v1(m.quantization),
1696 outputs: Vec::new(),
1697 activation_applied: None,
1698 activation_required: None,
1699 semantic: Default::default(),
1700 }),
1701 ConfigOutput::Classes(c) => Ok(LogicalOutput {
1702 name: None,
1703 type_: Some(LogicalType::Classes),
1704 shape: c.shape.clone(),
1705 dshape: c.dshape.clone(),
1706 decoder: Some(DecoderKind::from_v1(c.decoder)),
1707 encoding: None,
1708 score_format: None,
1709 normalized: None,
1710 anchors: None,
1711 stride: None,
1712 dtype: None,
1713 quantization: quantization_from_v1(c.quantization),
1714 outputs: Vec::new(),
1715 activation_applied: None,
1716 activation_required: None,
1717 semantic: Default::default(),
1718 }),
1719 }
1720}
1721
1722impl DecoderKind {
1723 pub fn from_v1(v: configs::DecoderType) -> Self {
1725 match v {
1726 configs::DecoderType::ModelPack => DecoderKind::ModelPack,
1727 configs::DecoderType::Ultralytics => DecoderKind::Ultralytics,
1728 configs::DecoderType::Semantic => DecoderKind::Semantic,
1729 }
1730 }
1731
1732 pub fn to_v1(self) -> configs::DecoderType {
1734 match self {
1735 DecoderKind::ModelPack => configs::DecoderType::ModelPack,
1736 DecoderKind::Ultralytics => configs::DecoderType::Ultralytics,
1737 DecoderKind::Semantic => configs::DecoderType::Semantic,
1738 }
1739 }
1740}
1741
1742impl DecoderVersion {
1743 pub fn from_v1(v: &configs::DecoderVersion) -> Self {
1745 match v {
1746 configs::DecoderVersion::Yolov5 => DecoderVersion::Yolov5,
1747 configs::DecoderVersion::Yolov8 => DecoderVersion::Yolov8,
1748 configs::DecoderVersion::Yolo11 => DecoderVersion::Yolo11,
1749 configs::DecoderVersion::Yolo26 => DecoderVersion::Yolo26,
1750 }
1751 }
1752
1753 pub fn to_v1(self) -> configs::DecoderVersion {
1755 match self {
1756 DecoderVersion::Yolov5 => configs::DecoderVersion::Yolov5,
1757 DecoderVersion::Yolov8 => configs::DecoderVersion::Yolov8,
1758 DecoderVersion::Yolo11 => configs::DecoderVersion::Yolo11,
1759 DecoderVersion::Yolo26 => configs::DecoderVersion::Yolo26,
1760 }
1761 }
1762}
1763
1764impl NmsMode {
1765 pub const fn code(self) -> u32 {
1767 self as u32
1768 }
1769
1770 pub fn from_v1(v: &configs::Nms) -> Option<Self> {
1772 match v {
1773 configs::Nms::Auto => None,
1774 configs::Nms::ClassAgnostic => Some(NmsMode::ClassAgnostic),
1775 configs::Nms::ClassAware => Some(NmsMode::ClassAware),
1776 }
1777 }
1778
1779 pub fn to_v1(self) -> configs::Nms {
1781 match self {
1782 NmsMode::ClassAgnostic => configs::Nms::ClassAgnostic,
1783 NmsMode::ClassAware => configs::Nms::ClassAware,
1784 }
1785 }
1786}
1787
1788fn quantization_to_legacy(q: &Quantization) -> DecoderResult<QuantTuple> {
1791 if q.is_per_channel() {
1792 return Err(DecoderError::NotSupported(
1793 "per-channel quantization cannot be expressed as a v1 QuantTuple".into(),
1794 ));
1795 }
1796 let scale = *q.scale.first().unwrap_or(&0.0);
1797 let zp = q.zero_point_at(0);
1798 Ok(QuantTuple(scale, zp))
1799}
1800
1801pub(crate) fn squeeze_padding_dims(
1807 shape: Vec<usize>,
1808 dshape: Vec<(DimName, usize)>,
1809) -> (Vec<usize>, Vec<(DimName, usize)>) {
1810 if dshape.is_empty() {
1814 return (shape, dshape);
1815 }
1816 let keep: Vec<bool> = dshape
1817 .iter()
1818 .map(|(n, _)| !matches!(n, DimName::Padding))
1819 .collect();
1820 let shape = shape
1821 .into_iter()
1822 .zip(keep.iter())
1823 .filter_map(|(s, &k)| k.then_some(s))
1824 .collect();
1825 let dshape = dshape
1826 .into_iter()
1827 .zip(keep.iter())
1828 .filter_map(|(d, &k)| k.then_some(d))
1829 .collect();
1830 (shape, dshape)
1831}
1832
1833pub(crate) fn padding_axes(dshape: &[(DimName, usize)]) -> Vec<usize> {
1838 let mut v: Vec<usize> = dshape
1839 .iter()
1840 .enumerate()
1841 .filter_map(|(i, (n, _))| matches!(n, DimName::Padding).then_some(i))
1842 .collect();
1843 v.sort_by(|a, b| b.cmp(a));
1844 v
1845}
1846
1847fn logical_to_legacy_config_output(logical: &LogicalOutput) -> DecoderResult<ConfigOutput> {
1848 let decoder = match logical.decoder {
1849 None => configs::DecoderType::Ultralytics,
1850 Some(DecoderKind::Semantic) => {
1851 return Err(DecoderError::NotSupported(format!(
1852 "logical `{}` uses decoder `Semantic`, which this HAL \
1853 version does not decode",
1854 logical.name.as_deref().unwrap_or("<anonymous>")
1855 )))
1856 }
1857 Some(kind) => kind.to_v1(),
1858 };
1859 let quantization = logical
1860 .quantization
1861 .as_ref()
1862 .map(quantization_to_legacy)
1863 .transpose()?;
1864 let (shape, dshape) = match logical.decoder {
1870 Some(DecoderKind::ModelPack) => (logical.shape.clone(), logical.dshape.clone()),
1871 _ => squeeze_padding_dims(logical.shape.clone(), logical.dshape.clone()),
1872 };
1873
1874 let ty = logical.type_.ok_or_else(|| {
1875 DecoderError::InvalidConfig(format!(
1879 "logical output `{}` has no type; typeless outputs should be \
1880 filtered before legacy conversion",
1881 logical.name.as_deref().unwrap_or("<anonymous>")
1882 ))
1883 })?;
1884
1885 let ctx = LegacyConvertCtx {
1886 decoder,
1887 quantization,
1888 shape,
1889 dshape,
1890 normalized: logical.normalized,
1891 anchors: logical.anchors.clone(),
1892 };
1893
1894 match ty {
1895 LogicalType::Boxes => Ok(boxes_to_legacy(&ctx)),
1896 LogicalType::Scores => Ok(scores_to_legacy(&ctx)),
1897 LogicalType::Protos => Ok(protos_to_legacy(&ctx)),
1898 LogicalType::MaskCoefs => Ok(mask_coefs_to_legacy(&ctx)),
1899 LogicalType::Segmentation => Ok(segmentation_to_legacy(&ctx)),
1900 LogicalType::Masks => Ok(masks_to_legacy(&ctx)),
1901 LogicalType::Classes => Ok(classes_to_legacy(&ctx)),
1902 LogicalType::Detection | LogicalType::Detections => Ok(detection_to_legacy(&ctx)),
1903 LogicalType::Objectness
1906 | LogicalType::Landmarks
1907 | LogicalType::ClassLogits
1908 | LogicalType::MaskLogits => Err(DecoderError::NotSupported(format!(
1909 "logical type {:?} has no legacy v1 equivalent; use the \
1910 native v2 decoder path",
1911 ty
1912 ))),
1913 }
1914}
1915
1916struct LegacyConvertCtx {
1918 decoder: configs::DecoderType,
1919 quantization: Option<QuantTuple>,
1920 shape: Vec<usize>,
1921 dshape: Vec<(DimName, usize)>,
1922 normalized: Option<bool>,
1923 anchors: Option<Vec<[f32; 2]>>,
1924}
1925
1926fn boxes_to_legacy(ctx: &LegacyConvertCtx) -> ConfigOutput {
1927 ConfigOutput::Boxes(configs::Boxes {
1928 decoder: ctx.decoder,
1929 quantization: ctx.quantization,
1930 shape: ctx.shape.clone(),
1931 dshape: ctx.dshape.clone(),
1932 normalized: ctx.normalized,
1933 })
1934}
1935
1936fn scores_to_legacy(ctx: &LegacyConvertCtx) -> ConfigOutput {
1937 ConfigOutput::Scores(configs::Scores {
1938 decoder: ctx.decoder,
1939 quantization: ctx.quantization,
1940 shape: ctx.shape.clone(),
1941 dshape: ctx.dshape.clone(),
1942 })
1943}
1944
1945fn protos_to_legacy(ctx: &LegacyConvertCtx) -> ConfigOutput {
1946 ConfigOutput::Protos(configs::Protos {
1947 decoder: ctx.decoder,
1948 quantization: ctx.quantization,
1949 shape: ctx.shape.clone(),
1950 dshape: ctx.dshape.clone(),
1951 })
1952}
1953
1954fn mask_coefs_to_legacy(ctx: &LegacyConvertCtx) -> ConfigOutput {
1955 ConfigOutput::MaskCoefficients(configs::MaskCoefficients {
1956 decoder: ctx.decoder,
1957 quantization: ctx.quantization,
1958 shape: ctx.shape.clone(),
1959 dshape: ctx.dshape.clone(),
1960 })
1961}
1962
1963fn segmentation_to_legacy(ctx: &LegacyConvertCtx) -> ConfigOutput {
1964 ConfigOutput::Segmentation(configs::Segmentation {
1965 decoder: ctx.decoder,
1966 quantization: ctx.quantization,
1967 shape: ctx.shape.clone(),
1968 dshape: ctx.dshape.clone(),
1969 })
1970}
1971
1972fn masks_to_legacy(ctx: &LegacyConvertCtx) -> ConfigOutput {
1973 ConfigOutput::Mask(configs::Mask {
1974 decoder: ctx.decoder,
1975 quantization: ctx.quantization,
1976 shape: ctx.shape.clone(),
1977 dshape: ctx.dshape.clone(),
1978 })
1979}
1980
1981fn classes_to_legacy(ctx: &LegacyConvertCtx) -> ConfigOutput {
1982 ConfigOutput::Classes(configs::Classes {
1983 decoder: ctx.decoder,
1984 quantization: ctx.quantization,
1985 shape: ctx.shape.clone(),
1986 dshape: ctx.dshape.clone(),
1987 })
1988}
1989
1990fn detection_to_legacy(ctx: &LegacyConvertCtx) -> ConfigOutput {
1991 ConfigOutput::Detection(configs::Detection {
1995 anchors: ctx.anchors.clone(),
1996 decoder: ctx.decoder,
1997 quantization: ctx.quantization,
1998 shape: ctx.shape.clone(),
1999 dshape: ctx.dshape.clone(),
2000 normalized: ctx.normalized,
2001 })
2002}
2003
2004#[cfg(test)]
2005#[cfg_attr(coverage_nightly, coverage(off))]
2006mod tests {
2007 use super::*;
2008
2009 #[test]
2010 fn nms_multi_label_true_round_trips_through_json() {
2011 let json = r#"{
2012 "schema_version": 2,
2013 "nms": "class_aware",
2014 "nms_multi_label": true,
2015 "outputs": [{
2016 "name": "output0", "type": "detection",
2017 "shape": [1, 84, 8400],
2018 "dshape": [{"batch": 1}, {"num_features": 84}, {"num_boxes": 8400}],
2019 "dtype": "float32", "decoder": "ultralytics", "encoding": "direct",
2020 "normalized": true
2021 }]
2022 }"#;
2023 let schema = SchemaV2::parse_json(json).unwrap();
2024 assert_eq!(schema.nms_multi_label, Some(true));
2025
2026 let text = serde_json::to_string(&schema).unwrap();
2027 let value: serde_json::Value = serde_json::from_str(&text).unwrap();
2028 assert_eq!(value["nms_multi_label"], serde_json::json!(true));
2029 let again = SchemaV2::parse_json(&text).unwrap();
2030 assert_eq!(again, schema);
2031
2032 let legacy = schema.to_legacy_config_outputs().unwrap();
2033 assert_eq!(legacy.nms_multi_label, Some(true));
2034 let back = SchemaV2::from_v1(&legacy).unwrap();
2035 assert_eq!(back.nms_multi_label, Some(true));
2036 }
2037
2038 #[test]
2039 fn schema_default_is_v2() {
2040 let s = SchemaV2::default();
2041 assert_eq!(s.schema_version, 2);
2042 assert!(s.outputs.is_empty());
2043 }
2044
2045 #[test]
2046 fn fixtures_round_trip_through_serde() {
2047 let yolov8 =
2048 edgefirst_bench::testdata::read_to_string("per_scale/synthetic_yolov8n_schema.json");
2049 let _: super::SchemaV2 = serde_json::from_str(&yolov8).expect("yolov8n fixture must parse");
2050
2051 let yolo26 =
2052 edgefirst_bench::testdata::read_to_string("per_scale/synthetic_yolo26n_schema.json");
2053 let _: super::SchemaV2 = serde_json::from_str(&yolo26).expect("yolo26n fixture must parse");
2054
2055 let flat =
2056 edgefirst_bench::testdata::read_to_string("per_scale/synthetic_flat_schema.json");
2057 let _: super::SchemaV2 = serde_json::from_str(&flat).expect("flat fixture must parse");
2058 }
2059
2060 #[test]
2061 fn box_encoding_accepts_ltrb_alias_for_direct() {
2062 let dfl: BoxEncoding = serde_json::from_str("\"dfl\"").unwrap();
2063 assert_eq!(dfl, BoxEncoding::Dfl);
2064
2065 let direct: BoxEncoding = serde_json::from_str("\"direct\"").unwrap();
2066 assert_eq!(direct, BoxEncoding::Direct);
2067
2068 let ltrb: BoxEncoding = serde_json::from_str("\"ltrb\"").unwrap();
2070 assert_eq!(ltrb, BoxEncoding::Direct);
2071 }
2072
2073 #[test]
2074 fn dtype_roundtrip() {
2075 for &d in DType::all() {
2076 let j = serde_json::to_string(&d).unwrap();
2077 let back: DType = serde_json::from_str(&j).unwrap();
2078 assert_eq!(back, d);
2079 }
2080 }
2081
2082 #[test]
2083 fn dtype_is_numbered_and_named_by_the_tensor_dtype() {
2084 for &d in DType::all() {
2085 let t = d.to_tensor_dtype();
2086 assert_eq!(d.code(), t.code(), "{d:?}");
2087 assert_eq!(d.as_str(), t.as_str(), "{d:?}");
2088 assert_eq!(DType::from_code(t.code()), Some(d));
2089 assert_eq!(DType::from_tensor_dtype(t), Some(d));
2090 assert_eq!(
2091 serde_json::to_string(&d).unwrap(),
2092 format!("\"{}\"", t.as_str())
2093 );
2094 }
2095 for t in [edgefirst_tensor::DType::U64, edgefirst_tensor::DType::F64] {
2097 assert_eq!(DType::from_tensor_dtype(t), None);
2098 assert_eq!(DType::from_code(t.code()), None);
2099 assert_eq!(DType::from_str_code(t.as_str()), None);
2100 }
2101 assert_eq!(DType::from_code(u32::MAX), None);
2102 }
2103
2104 #[test]
2105 fn dtype_accepts_every_legacy_name() {
2106 let legacy = [
2107 ("int8", DType::Int8),
2108 ("uint8", DType::Uint8),
2109 ("int16", DType::Int16),
2110 ("uint16", DType::Uint16),
2111 ("int32", DType::Int32),
2112 ("uint32", DType::Uint32),
2113 ("float16", DType::Float16),
2114 ("float32", DType::Float32),
2115 ("int64", DType::Int64),
2116 ];
2117 assert_eq!(legacy.len(), DType::all().len());
2118 for (name, d) in legacy {
2119 assert_eq!(DType::from_str_code(name), Some(d), "{name}");
2120 let back: DType = serde_json::from_str(&format!("\"{name}\"")).unwrap();
2121 assert_eq!(back, d, "{name}");
2122 assert_ne!(d.as_str(), name);
2124 }
2125 assert_eq!(DType::accepted_names().count(), 2 * DType::all().len());
2126 assert!(serde_json::from_str::<DType>("\"float64\"").is_err());
2127 assert!(serde_json::from_str::<DType>("\"Int8\"").is_err());
2128 }
2129
2130 #[test]
2131 fn schema_enums_take_their_codes_from_the_config_vocabularies() {
2132 for v in [
2133 DecoderVersion::Yolov5,
2134 DecoderVersion::Yolov8,
2135 DecoderVersion::Yolo11,
2136 DecoderVersion::Yolo26,
2137 ] {
2138 assert_eq!(v.code(), v.to_v1().code(), "{v:?}");
2139 }
2140 for m in [NmsMode::ClassAgnostic, NmsMode::ClassAware] {
2141 assert_eq!(m.code(), m.to_v1().code(), "{m:?}");
2142 }
2143 for &v in configs::DecoderType::all() {
2144 let k = DecoderKind::from_v1(v);
2145 assert_eq!(k.code(), v.code(), "{k:?}");
2146 assert_eq!(k.to_v1(), v, "{k:?}");
2147 }
2148 }
2149
2150 #[test]
2151 fn dtype_widths() {
2152 assert_eq!(DType::Int8.size_bytes(), 1);
2153 assert_eq!(DType::Float16.size_bytes(), 2);
2154 assert_eq!(DType::Float32.size_bytes(), 4);
2155 }
2156
2157 #[test]
2158 fn stride_accepts_scalar_or_pair() {
2159 let a: Stride = serde_json::from_str("8").unwrap();
2160 let b: Stride = serde_json::from_str("[8, 16]").unwrap();
2161 assert_eq!(a, Stride::Square(8));
2162 assert_eq!(b, Stride::Rect([8, 16]));
2163 assert_eq!(a.x(), 8);
2164 assert_eq!(a.y(), 8);
2165 assert_eq!(b.x(), 8);
2166 assert_eq!(b.y(), 16);
2167 }
2168
2169 #[test]
2170 fn quantization_scalar_scale() {
2171 let j = r#"{"scale": 0.00392, "zero_point": 0, "dtype": "int8"}"#;
2172 let q: Quantization = serde_json::from_str(j).unwrap();
2173 assert!(q.is_per_tensor());
2174 assert!(q.is_symmetric());
2175 assert_eq!(q.scale_at(0), 0.00392);
2176 assert_eq!(q.scale_at(5), 0.00392);
2177 assert_eq!(q.zero_point_at(0), 0);
2178 }
2179
2180 #[test]
2181 fn quantization_per_channel() {
2182 let j = r#"{"scale": [0.054, 0.089, 0.195], "axis": 0, "dtype": "int8"}"#;
2183 let q: Quantization = serde_json::from_str(j).unwrap();
2184 assert!(q.is_per_channel());
2185 assert!(q.is_symmetric());
2186 assert_eq!(q.axis, Some(0));
2187 assert_eq!(q.scale_at(0), 0.054);
2188 assert_eq!(q.scale_at(2), 0.195);
2189 }
2190
2191 #[test]
2192 fn quantization_asymmetric_per_tensor() {
2193 let j = r#"{"scale": 0.176, "zero_point": 198, "dtype": "uint8"}"#;
2194 let q: Quantization = serde_json::from_str(j).unwrap();
2195 assert!(!q.is_symmetric());
2196 assert_eq!(q.zero_point_at(0), 198);
2197 assert_eq!(q.zero_point_at(10), 198);
2198 }
2199
2200 #[test]
2201 fn quantization_symmetric_default_zero_point() {
2202 let j = r#"{"scale": 0.00392, "dtype": "int8"}"#;
2203 let q: Quantization = serde_json::from_str(j).unwrap();
2204 assert!(q.is_symmetric());
2205 assert_eq!(q.zero_point_at(0), 0);
2206 }
2207
2208 #[test]
2209 fn quantization_to_tensor_per_tensor_asymmetric() {
2210 let q = Quantization {
2211 scale: vec![0.1],
2212 zero_point: Some(vec![-5]),
2213 axis: None,
2214 dtype: Some(DType::Int8),
2215 };
2216 let t: edgefirst_tensor::Quantization = (&q).try_into().unwrap();
2217 assert!(t.is_per_tensor());
2218 assert!(!t.is_symmetric());
2219 assert_eq!(t.scale(), &[0.1]);
2220 assert_eq!(t.zero_point(), Some(&[-5][..]));
2221 }
2222
2223 #[test]
2224 fn quantization_to_tensor_per_tensor_symmetric() {
2225 let q = Quantization {
2226 scale: vec![0.05],
2227 zero_point: None,
2228 axis: None,
2229 dtype: Some(DType::Int8),
2230 };
2231 let t: edgefirst_tensor::Quantization = (&q).try_into().unwrap();
2232 assert!(t.is_per_tensor());
2233 assert!(t.is_symmetric());
2234 }
2235
2236 #[test]
2237 fn quantization_to_tensor_per_channel_asymmetric() {
2238 let q = Quantization {
2239 scale: vec![0.1, 0.2, 0.3],
2240 zero_point: Some(vec![-1, 0, 1]),
2241 axis: Some(2),
2242 dtype: Some(DType::Int8),
2243 };
2244 let t: edgefirst_tensor::Quantization = (&q).try_into().unwrap();
2245 assert!(t.is_per_channel());
2246 assert_eq!(t.axis(), Some(2));
2247 assert_eq!(t.scale().len(), 3);
2248 assert_eq!(t.zero_point().map(|z| z.len()), Some(3));
2249 }
2250
2251 #[test]
2252 fn quantization_to_tensor_per_channel_symmetric() {
2253 let q = Quantization {
2254 scale: vec![0.054, 0.089, 0.195],
2255 zero_point: None,
2256 axis: Some(0),
2257 dtype: Some(DType::Int8),
2258 };
2259 let t: edgefirst_tensor::Quantization = (&q).try_into().unwrap();
2260 assert!(t.is_per_channel());
2261 assert!(t.is_symmetric());
2262 assert_eq!(t.axis(), Some(0));
2263 }
2264
2265 #[test]
2266 fn quantization_to_tensor_per_channel_missing_axis_errors() {
2267 let q = Quantization {
2268 scale: vec![0.1, 0.2, 0.3],
2269 zero_point: None,
2270 axis: None,
2271 dtype: None,
2272 };
2273 let err = edgefirst_tensor::Quantization::try_from(&q).unwrap_err();
2274 assert!(matches!(
2278 err,
2279 edgefirst_tensor::Error::QuantizationInvalid { field: "axis", .. }
2280 ));
2281 }
2282
2283 #[test]
2287 fn quantization_to_tensor_per_tensor_ignores_a_redundant_axis() {
2288 let q = Quantization {
2289 scale: vec![0.5],
2290 zero_point: Some(vec![3]),
2291 axis: Some(0),
2292 dtype: Some(DType::Int8),
2293 };
2294 let t: edgefirst_tensor::Quantization = (&q).try_into().unwrap();
2295 assert!(t.is_per_tensor());
2296 assert_eq!(t.scale(), &[0.5][..]);
2297 assert_eq!(t.zero_point(), Some(&[3][..]));
2298 }
2299
2300 #[test]
2301 fn quantization_to_tensor_symmetric_ignores_a_redundant_axis() {
2302 let q = Quantization {
2303 scale: vec![0.5],
2304 zero_point: None,
2305 axis: Some(0),
2306 dtype: Some(DType::Int8),
2307 };
2308 let t: edgefirst_tensor::Quantization = (&q).try_into().unwrap();
2309 assert!(t.is_per_tensor());
2310 assert!(t.is_symmetric());
2311 assert_eq!(t.scale(), &[0.5][..]);
2312 }
2313
2314 #[test]
2318 fn quantization_to_tensor_empty_scale_errors_on_scale() {
2319 for (zero_point, axis) in [
2320 (None, None),
2321 (None, Some(0)),
2322 (Some(vec![]), Some(0)),
2323 (Some(vec![]), None),
2324 ] {
2325 let q = Quantization {
2326 scale: vec![],
2327 zero_point,
2328 axis,
2329 dtype: Some(DType::Int8),
2330 };
2331 let err = edgefirst_tensor::Quantization::try_from(&q).unwrap_err();
2332 assert!(
2333 matches!(
2334 err,
2335 edgefirst_tensor::Error::QuantizationInvalid { field: "scale", .. }
2336 ),
2337 "axis={axis:?} gave {err:?}"
2338 );
2339 }
2340 }
2341
2342 #[test]
2346 fn quantization_to_tensor_single_scale_with_many_zero_points_errors_on_scale() {
2347 for axis in [None, Some(0)] {
2348 let q = Quantization {
2349 scale: vec![0.5],
2350 zero_point: Some(vec![1, 2]),
2351 axis,
2352 dtype: Some(DType::Int8),
2353 };
2354 let err = edgefirst_tensor::Quantization::try_from(&q).unwrap_err();
2355 assert!(
2356 matches!(
2357 err,
2358 edgefirst_tensor::Error::QuantizationInvalid { field: "scale", .. }
2359 ),
2360 "axis={axis:?} gave {err:?}"
2361 );
2362 }
2363 }
2364
2365 #[test]
2366 fn per_channel_quantization_is_not_per_tensor() {
2367 let q = Quantization {
2368 scale: vec![0.1, 0.2, 0.3],
2369 zero_point: None,
2370 axis: Some(0),
2371 dtype: Some(DType::Int8),
2372 };
2373 assert!(q.is_per_channel());
2374 assert!(!q.is_per_tensor());
2375
2376 let per_tensor = Quantization {
2377 scale: vec![0.1],
2378 zero_point: None,
2379 axis: None,
2380 dtype: Some(DType::Int8),
2381 };
2382 assert!(per_tensor.is_per_tensor());
2383 assert!(!per_tensor.is_per_channel());
2384 }
2385
2386 #[test]
2390 fn zero_point_at_indexes_per_channel_zero_points() {
2391 let per_channel = Quantization {
2392 scale: vec![0.1, 0.2, 0.3],
2393 zero_point: Some(vec![10, 20, 30]),
2394 axis: Some(0),
2395 dtype: Some(DType::Int8),
2396 };
2397 assert_eq!(per_channel.zero_point_at(0), 10);
2398 assert_eq!(per_channel.zero_point_at(1), 20);
2399 assert_eq!(per_channel.zero_point_at(2), 30);
2400 assert_eq!(per_channel.zero_point_at(3), 0);
2402
2403 let shared = Quantization {
2404 scale: vec![0.1, 0.2, 0.3],
2405 zero_point: Some(vec![7]),
2406 axis: Some(0),
2407 dtype: Some(DType::Int8),
2408 };
2409 assert_eq!(shared.zero_point_at(0), 7);
2410 assert_eq!(shared.zero_point_at(2), 7);
2411
2412 let symmetric = Quantization {
2413 scale: vec![0.1],
2414 zero_point: None,
2415 axis: None,
2416 dtype: Some(DType::Int8),
2417 };
2418 assert_eq!(symmetric.zero_point_at(0), 0);
2419 }
2420
2421 #[test]
2422 fn logical_output_flat_tflite_boxes() {
2423 let j = r#"{
2425 "name": "boxes", "type": "boxes",
2426 "shape": [1, 64, 8400],
2427 "dshape": [{"batch": 1}, {"num_features": 64}, {"num_boxes": 8400}],
2428 "dtype": "int8",
2429 "quantization": {"scale": 0.00392, "zero_point": 0, "dtype": "int8"},
2430 "decoder": "ultralytics",
2431 "encoding": "dfl",
2432 "normalized": true
2433 }"#;
2434 let lo: LogicalOutput = serde_json::from_str(j).unwrap();
2435 assert_eq!(lo.type_, Some(LogicalType::Boxes));
2436 assert_eq!(lo.encoding, Some(BoxEncoding::Dfl));
2437 assert_eq!(lo.normalized, Some(true));
2438 assert!(!lo.is_split());
2439 assert_eq!(lo.dtype, Some(DType::Int8));
2440 }
2441
2442 #[test]
2443 fn logical_output_hailo_per_scale_split() {
2444 let j = r#"{
2446 "name": "boxes", "type": "boxes",
2447 "shape": [1, 64, 8400],
2448 "encoding": "dfl", "decoder": "ultralytics", "normalized": true,
2449 "outputs": [
2450 {
2451 "name": "boxes_0", "type": "boxes",
2452 "stride": 8, "scale_index": 0,
2453 "shape": [1, 80, 80, 64],
2454 "dshape": [{"batch": 1}, {"height": 80}, {"width": 80}, {"num_features": 64}],
2455 "dtype": "uint8",
2456 "quantization": {"scale": 0.0234, "zero_point": 128, "dtype": "uint8"}
2457 }
2458 ]
2459 }"#;
2460 let lo: LogicalOutput = serde_json::from_str(j).unwrap();
2461 assert!(lo.is_split());
2462 assert_eq!(lo.outputs.len(), 1);
2463 let child = &lo.outputs[0];
2464 assert_eq!(child.name, "boxes_0");
2465 assert_eq!(child.type_, Some(PhysicalType::Boxes));
2466 assert_eq!(child.stride, Some(Stride::Square(8)));
2467 assert_eq!(child.scale_index, Some(0));
2468 assert_eq!(child.dtype, DType::Uint8);
2469 }
2470
2471 #[test]
2472 fn logical_output_ara2_xy_wh_channel_split() {
2473 let j = r#"{
2475 "name": "boxes", "type": "boxes",
2476 "shape": [1, 4, 8400, 1],
2477 "encoding": "direct", "decoder": "ultralytics", "normalized": true,
2478 "outputs": [
2479 {
2480 "name": "_model_22_Div_1_output_0", "type": "boxes_xy",
2481 "shape": [1, 2, 8400, 1],
2482 "dshape": [{"batch": 1}, {"box_coords": 2}, {"num_boxes": 8400}, {"padding": 1}],
2483 "dtype": "int16",
2484 "quantization": {"scale": 3.129e-5, "zero_point": 0, "dtype": "int16"}
2485 },
2486 {
2487 "name": "_model_22_Sub_1_output_0", "type": "boxes_wh",
2488 "shape": [1, 2, 8400, 1],
2489 "dshape": [{"batch": 1}, {"box_coords": 2}, {"num_boxes": 8400}, {"padding": 1}],
2490 "dtype": "int16",
2491 "quantization": {"scale": 3.149e-5, "zero_point": 0, "dtype": "int16"}
2492 }
2493 ]
2494 }"#;
2495 let lo: LogicalOutput = serde_json::from_str(j).unwrap();
2496 assert_eq!(lo.encoding, Some(BoxEncoding::Direct));
2497 assert_eq!(lo.outputs.len(), 2);
2498 assert_eq!(lo.outputs[0].type_, Some(PhysicalType::BoxesXy));
2499 assert_eq!(lo.outputs[1].type_, Some(PhysicalType::BoxesWh));
2500 assert!(lo.outputs[0].stride.is_none());
2501 assert!(lo.outputs[1].stride.is_none());
2502 }
2503
2504 #[test]
2505 fn logical_output_hailo_scores_sigmoid_applied() {
2506 let j = r#"{
2507 "name": "scores", "type": "scores",
2508 "shape": [1, 80, 8400],
2509 "decoder": "ultralytics", "score_format": "per_class",
2510 "outputs": [
2511 {
2512 "name": "scores_0", "type": "scores",
2513 "stride": 8, "scale_index": 0,
2514 "shape": [1, 80, 80, 80],
2515 "dshape": [{"batch": 1}, {"height": 80}, {"width": 80}, {"num_classes": 80}],
2516 "dtype": "uint8",
2517 "quantization": {"scale": 0.003922, "dtype": "uint8"},
2518 "activation_applied": "sigmoid"
2519 }
2520 ]
2521 }"#;
2522 let lo: LogicalOutput = serde_json::from_str(j).unwrap();
2523 assert_eq!(lo.score_format, Some(ScoreFormat::PerClass));
2524 let child = &lo.outputs[0];
2525 assert_eq!(child.activation_applied, Some(Activation::Sigmoid));
2526 assert!(child.activation_required.is_none());
2527 }
2528
2529 #[test]
2530 fn yolo26_end_to_end_detections() {
2531 let j = r#"{
2532 "schema_version": 2,
2533 "decoder_version": "yolo26",
2534 "outputs": [{
2535 "name": "output0", "type": "detections",
2536 "shape": [1, 100, 6],
2537 "dshape": [{"batch": 1}, {"num_boxes": 100}, {"num_features": 6}],
2538 "dtype": "int8",
2539 "quantization": {"scale": 0.0078, "zero_point": 0, "dtype": "int8"},
2540 "normalized": false,
2541 "decoder": "ultralytics"
2542 }]
2543 }"#;
2544 let s: SchemaV2 = serde_json::from_str(j).unwrap();
2545 assert_eq!(s.decoder_version, Some(DecoderVersion::Yolo26));
2546 assert!(s.decoder_version.unwrap().is_end_to_end());
2547 assert_eq!(s.outputs[0].type_, Some(LogicalType::Detections));
2548 assert_eq!(s.outputs[0].normalized, Some(false));
2549 assert!(s.nms.is_none());
2550 }
2551
2552 #[test]
2553 fn modelpack_anchor_detection_with_rect_stride() {
2554 let j = r#"{
2555 "schema_version": 2,
2556 "outputs": [{
2557 "name": "output_0", "type": "detection",
2558 "shape": [1, 40, 40, 54],
2559 "dshape": [{"batch": 1}, {"height": 40}, {"width": 40}, {"num_anchors_x_features": 54}],
2560 "dtype": "uint8",
2561 "quantization": {"scale": 0.176, "zero_point": 198, "dtype": "uint8"},
2562 "decoder": "modelpack",
2563 "encoding": "anchor",
2564 "stride": [16, 16],
2565 "anchors": [[0.054, 0.065], [0.089, 0.139], [0.195, 0.196]]
2566 }]
2567 }"#;
2568 let s: SchemaV2 = serde_json::from_str(j).unwrap();
2569 let lo = &s.outputs[0];
2570 assert_eq!(lo.encoding, Some(BoxEncoding::Anchor));
2571 assert_eq!(lo.stride, Some(Stride::Rect([16, 16])));
2572 assert_eq!(lo.anchors.as_ref().map(|a| a.len()), Some(3));
2573 }
2574
2575 #[test]
2576 fn yolov5_obj_x_class_objectness_logical() {
2577 let j = r#"{
2578 "name": "objectness", "type": "objectness",
2579 "shape": [1, 3, 8400],
2580 "decoder": "ultralytics",
2581 "outputs": [{
2582 "name": "objectness_0", "type": "objectness",
2583 "stride": 8, "scale_index": 0,
2584 "shape": [1, 80, 80, 3],
2585 "dshape": [{"batch": 1}, {"height": 80}, {"width": 80}, {"num_features": 3}],
2586 "dtype": "uint8",
2587 "quantization": {"scale": 0.0039, "zero_point": 0, "dtype": "uint8"},
2588 "activation_applied": "sigmoid"
2589 }]
2590 }"#;
2591 let lo: LogicalOutput = serde_json::from_str(j).unwrap();
2592 assert_eq!(lo.type_, Some(LogicalType::Objectness));
2593 assert_eq!(lo.outputs[0].activation_applied, Some(Activation::Sigmoid));
2594 }
2595
2596 #[test]
2597 fn direct_protos_no_decoder() {
2598 let j = r#"{
2600 "name": "protos", "type": "protos",
2601 "shape": [1, 32, 160, 160],
2602 "dshape": [{"batch": 1}, {"num_protos": 32}, {"height": 160}, {"width": 160}],
2603 "dtype": "uint8",
2604 "quantization": {"scale": 0.0203, "zero_point": 45, "dtype": "uint8"},
2605 "stride": 4
2606 }"#;
2607 let lo: LogicalOutput = serde_json::from_str(j).unwrap();
2608 assert_eq!(lo.type_, Some(LogicalType::Protos));
2609 assert!(lo.decoder.is_none());
2610 assert_eq!(lo.stride, Some(Stride::Square(4)));
2611 }
2612
2613 #[test]
2614 fn full_yolov8_tflite_flat_detection() {
2615 let j = r#"{
2617 "schema_version": 2,
2618 "decoder_version": "yolov8",
2619 "nms": "class_agnostic",
2620 "input": { "shape": [1, 640, 640, 3], "cameraadaptor": "rgb" },
2621 "outputs": [
2622 {
2623 "name": "boxes", "type": "boxes",
2624 "shape": [1, 64, 8400],
2625 "dshape": [{"batch": 1}, {"num_features": 64}, {"num_boxes": 8400}],
2626 "dtype": "int8",
2627 "quantization": {"scale": 0.00392, "zero_point": 0, "dtype": "int8"},
2628 "decoder": "ultralytics",
2629 "encoding": "dfl",
2630 "normalized": true
2631 },
2632 {
2633 "name": "scores", "type": "scores",
2634 "shape": [1, 80, 8400],
2635 "dshape": [{"batch": 1}, {"num_classes": 80}, {"num_boxes": 8400}],
2636 "dtype": "int8",
2637 "quantization": {"scale": 0.00392, "zero_point": 0, "dtype": "int8"},
2638 "decoder": "ultralytics",
2639 "score_format": "per_class"
2640 }
2641 ]
2642 }"#;
2643 let s: SchemaV2 = serde_json::from_str(j).unwrap();
2644 assert_eq!(s.schema_version, 2);
2645 assert_eq!(s.decoder_version, Some(DecoderVersion::Yolov8));
2646 assert_eq!(s.nms, Some(NmsMode::ClassAgnostic));
2647 assert_eq!(s.input.as_ref().unwrap().shape, vec![1, 640, 640, 3]);
2648 assert_eq!(s.outputs.len(), 2);
2649 }
2650
2651 #[test]
2652 fn schema_unknown_version_parses_without_validation() {
2653 let j = r#"{"schema_version": 99, "outputs": []}"#;
2656 let s: SchemaV2 = serde_json::from_str(j).unwrap();
2657 assert_eq!(s.schema_version, 99);
2658 }
2659
2660 #[test]
2661 fn serde_roundtrip_preserves_fields() {
2662 let original = SchemaV2 {
2663 schema_version: 2,
2664 input: Some(InputSpec {
2665 shape: vec![1, 3, 640, 640],
2666 dshape: vec![],
2667 cameraadaptor: Some("rgb".into()),
2668 preprocessing: None,
2669 }),
2670 outputs: vec![LogicalOutput {
2671 name: Some("boxes".into()),
2672 type_: Some(LogicalType::Boxes),
2673 shape: vec![1, 4, 8400],
2674 dshape: vec![],
2675 decoder: Some(DecoderKind::Ultralytics),
2676 encoding: Some(BoxEncoding::Direct),
2677 score_format: None,
2678 normalized: Some(true),
2679 anchors: None,
2680 stride: None,
2681 dtype: Some(DType::Float32),
2682 quantization: None,
2683 outputs: vec![],
2684 activation_applied: None,
2685 activation_required: None,
2686 semantic: Default::default(),
2687 }],
2688 nms: Some(NmsMode::ClassAgnostic),
2689 nms_multi_label: None,
2690 decoder_version: Some(DecoderVersion::Yolov8),
2691 };
2692 let j = serde_json::to_string(&original).unwrap();
2693 let parsed: SchemaV2 = serde_json::from_str(&j).unwrap();
2694 assert_eq!(parsed, original);
2695 }
2696
2697 #[test]
2700 fn parse_v1_yaml_yolov8_seg_testdata() {
2701 let yaml = edgefirst_bench::testdata::read_to_string("yolov8_seg.yaml");
2702 let schema = SchemaV2::parse_yaml(&yaml).expect("parse v1 yaml");
2703 assert_eq!(schema.schema_version, 2);
2704 assert_eq!(schema.outputs.len(), 2);
2705 let det = &schema.outputs[0];
2707 assert_eq!(det.type_, Some(LogicalType::Detection));
2708 assert_eq!(det.shape, vec![1, 116, 8400]);
2709 assert_eq!(det.decoder, Some(DecoderKind::Ultralytics));
2710 assert_eq!(det.encoding, Some(BoxEncoding::Direct));
2711 let q = det.quantization.as_ref().unwrap();
2712 assert_eq!(q.scale.len(), 1);
2713 assert!((q.scale[0] - 0.021_287_762).abs() < 1e-6);
2714 assert_eq!(q.zero_point, Some(vec![31]));
2715 let protos = &schema.outputs[1];
2717 assert_eq!(protos.type_, Some(LogicalType::Protos));
2718 assert_eq!(protos.shape, vec![1, 160, 160, 32]);
2719 }
2720
2721 #[test]
2722 fn parse_v1_json_modelpack_split_testdata() {
2723 let json = edgefirst_bench::testdata::read_to_string("modelpack_split.json");
2724 let schema = SchemaV2::parse_json(&json).expect("parse v1 json");
2725 assert_eq!(schema.schema_version, 2);
2726 assert_eq!(schema.outputs.len(), 2);
2727 for out in &schema.outputs {
2729 assert_eq!(out.type_, Some(LogicalType::Detection));
2730 assert_eq!(out.decoder, Some(DecoderKind::ModelPack));
2731 assert_eq!(out.encoding, Some(BoxEncoding::Anchor));
2732 assert_eq!(out.anchors.as_ref().map(|a| a.len()), Some(3));
2733 }
2734 }
2735
2736 #[test]
2737 fn parse_v2_json_direct_when_schema_version_present() {
2738 let j = r#"{
2739 "schema_version": 2,
2740 "outputs": [{
2741 "name": "boxes", "type": "boxes",
2742 "shape": [1, 4, 8400],
2743 "dshape": [{"batch": 1}, {"box_coords": 4}, {"num_boxes": 8400}],
2744 "dtype": "float32",
2745 "decoder": "ultralytics",
2746 "encoding": "direct",
2747 "normalized": true
2748 }]
2749 }"#;
2750 let schema = SchemaV2::parse_json(j).unwrap();
2751 assert_eq!(schema.schema_version, 2);
2752 assert_eq!(schema.outputs[0].type_, Some(LogicalType::Boxes));
2753 }
2754
2755 #[test]
2756 fn parse_rejects_future_schema_version() {
2757 let j = r#"{"schema_version": 99, "outputs": []}"#;
2758 let err = SchemaV2::parse_json(j).unwrap_err();
2759 matches!(err, DecoderError::NotSupported(_));
2760 }
2761
2762 #[test]
2763 fn parse_absent_schema_version_treats_as_v1() {
2764 let j = r#"{
2766 "outputs": [
2767 {
2768 "type": "boxes", "decoder": "ultralytics",
2769 "shape": [1, 4, 8400],
2770 "quantization": [0.00392, 0]
2771 },
2772 {
2773 "type": "scores", "decoder": "ultralytics",
2774 "shape": [1, 80, 8400],
2775 "quantization": [0.00392, 0]
2776 }
2777 ]
2778 }"#;
2779 let schema = SchemaV2::parse_json(j).expect("v1 legacy parse");
2780 assert_eq!(schema.schema_version, 2); assert_eq!(schema.outputs.len(), 2);
2782 assert_eq!(schema.outputs[0].type_, Some(LogicalType::Boxes));
2783 assert_eq!(schema.outputs[1].type_, Some(LogicalType::Scores));
2784 assert_eq!(schema.outputs[1].score_format, Some(ScoreFormat::PerClass));
2786 }
2787
2788 #[test]
2789 fn from_v1_preserves_nms_and_decoder_version() {
2790 let v1 = ConfigOutputs {
2791 outputs: vec![ConfigOutput::Boxes(crate::configs::Boxes {
2792 decoder: crate::configs::DecoderType::Ultralytics,
2793 quantization: Some(crate::configs::QuantTuple(0.01, 5)),
2794 shape: vec![1, 4, 8400],
2795 dshape: vec![],
2796 normalized: Some(true),
2797 })],
2798 nms: Some(crate::configs::Nms::ClassAware),
2799 nms_multi_label: None,
2800 decoder_version: Some(crate::configs::DecoderVersion::Yolo11),
2801 };
2802 let v2 = SchemaV2::from_v1(&v1).unwrap();
2803 assert_eq!(v2.nms, Some(NmsMode::ClassAware));
2804 assert_eq!(v2.decoder_version, Some(DecoderVersion::Yolo11));
2805 assert_eq!(v2.outputs[0].normalized, Some(true));
2806 let q = v2.outputs[0].quantization.as_ref().unwrap();
2807 assert_eq!(q.scale, vec![0.01]);
2808 assert_eq!(q.zero_point, Some(vec![5]));
2809 assert_eq!(q.dtype, None); }
2811
2812 #[test]
2819 fn typeless_logical_output_parses_and_roundtrips() {
2820 let j = r#"{
2821 "schema_version": 2,
2822 "outputs": [
2823 {
2824 "name": "extra_telemetry",
2825 "shape": [1, 16]
2826 },
2827 {
2828 "name": "boxes",
2829 "type": "boxes",
2830 "shape": [1, 4, 8400]
2831 }
2832 ]
2833 }"#;
2834 let schema: SchemaV2 = serde_json::from_str(j).unwrap();
2835 assert_eq!(schema.outputs.len(), 2);
2836 assert_eq!(schema.outputs[0].type_, None);
2837 assert_eq!(schema.outputs[0].name.as_deref(), Some("extra_telemetry"));
2838 assert_eq!(schema.outputs[1].type_, Some(LogicalType::Boxes));
2839
2840 let round = serde_json::to_string(&schema).unwrap();
2842 let first_obj = round
2843 .split("\"outputs\":[")
2844 .nth(1)
2845 .and_then(|s| s.split("}").next())
2846 .expect("outputs array");
2847 assert!(
2848 !first_obj.contains("\"type\""),
2849 "typeless output must not serialize a `type` field, got: {first_obj}"
2850 );
2851 }
2852
2853 #[test]
2859 fn typeless_outputs_filtered_from_legacy_config() {
2860 let schema = SchemaV2 {
2861 schema_version: 2,
2862 input: None,
2863 outputs: vec![
2864 LogicalOutput {
2865 name: Some("diagnostic_histogram".into()),
2866 type_: None,
2867 shape: vec![1, 256],
2868 dshape: vec![],
2869 decoder: None,
2870 encoding: None,
2871 score_format: None,
2872 normalized: None,
2873 anchors: None,
2874 stride: None,
2875 dtype: None,
2876 quantization: None,
2877 outputs: vec![],
2878 activation_applied: None,
2879 activation_required: None,
2880 semantic: Default::default(),
2881 },
2882 LogicalOutput {
2883 name: Some("boxes".into()),
2884 type_: Some(LogicalType::Boxes),
2885 shape: vec![1, 4, 8400],
2886 dshape: vec![],
2887 decoder: Some(DecoderKind::Ultralytics),
2888 encoding: Some(BoxEncoding::Direct),
2889 score_format: None,
2890 normalized: Some(true),
2891 anchors: None,
2892 stride: None,
2893 dtype: None,
2894 quantization: None,
2895 outputs: vec![],
2896 activation_applied: None,
2897 activation_required: None,
2898 semantic: Default::default(),
2899 },
2900 ],
2901 nms: None,
2902 nms_multi_label: None,
2903 decoder_version: None,
2904 };
2905 let legacy = schema.to_legacy_config_outputs().unwrap();
2906 assert_eq!(
2907 legacy.outputs.len(),
2908 1,
2909 "typeless output must be filtered from legacy config"
2910 );
2911 assert!(
2912 matches!(legacy.outputs[0], ConfigOutput::Boxes(_)),
2913 "only the typed `boxes` output should survive lowering"
2914 );
2915 }
2916
2917 #[test]
2922 fn all_typeless_schema_produces_empty_legacy_config() {
2923 let schema = SchemaV2 {
2924 schema_version: 2,
2925 input: None,
2926 outputs: vec![LogicalOutput {
2927 name: Some("aux".into()),
2928 type_: None,
2929 shape: vec![1, 8],
2930 dshape: vec![],
2931 decoder: None,
2932 encoding: None,
2933 score_format: None,
2934 normalized: None,
2935 anchors: None,
2936 stride: None,
2937 dtype: None,
2938 quantization: None,
2939 outputs: vec![],
2940 activation_applied: None,
2941 activation_required: None,
2942 semantic: Default::default(),
2943 }],
2944 nms: None,
2945 nms_multi_label: None,
2946 decoder_version: None,
2947 };
2948 let legacy = schema.to_legacy_config_outputs().unwrap();
2949 assert!(legacy.outputs.is_empty());
2950 }
2951
2952 #[test]
2958 fn typeless_physical_child_parses_and_skips_uniqueness() {
2959 let j = r#"{
2960 "name": "boxes",
2961 "type": "boxes",
2962 "shape": [1, 8400, 4],
2963 "outputs": [
2964 {
2965 "name": "boxes_xy",
2966 "type": "boxes_xy",
2967 "shape": [1, 8400, 2],
2968 "dtype": "float32"
2969 },
2970 {
2971 "name": "aux_user_managed",
2972 "shape": [1, 8400, 2],
2973 "dtype": "float32"
2974 }
2975 ]
2976 }"#;
2977 let lo: LogicalOutput = serde_json::from_str(j).unwrap();
2978 assert_eq!(lo.outputs.len(), 2);
2979 assert_eq!(lo.outputs[0].type_, Some(PhysicalType::BoxesXy));
2980 assert_eq!(lo.outputs[1].type_, None);
2981
2982 let schema = SchemaV2 {
2986 schema_version: 2,
2987 input: None,
2988 outputs: vec![lo],
2989 nms: None,
2990 nms_multi_label: None,
2991 decoder_version: None,
2992 };
2993 schema.validate().expect(
2994 "typed + typeless children with equal shape must not trigger \
2995 uniqueness error",
2996 );
2997
2998 let s = serde_json::to_string(&schema).unwrap();
3000 assert!(
3001 s.contains("\"aux_user_managed\""),
3002 "typeless child must survive round-trip: {s}"
3003 );
3004 let aux_obj = s
3006 .split("\"aux_user_managed\"")
3007 .nth(1)
3008 .and_then(|s| s.split('}').next())
3009 .unwrap_or("");
3010 assert!(
3011 !aux_obj.contains("\"type\""),
3012 "typeless child must not serialize `type`, got: {aux_obj}"
3013 );
3014 }
3015
3016 #[test]
3017 fn from_v1_modelpack_anchor_detection_maps_encoding() {
3018 let v1 = ConfigOutputs {
3019 outputs: vec![ConfigOutput::Detection(crate::configs::Detection {
3020 anchors: Some(vec![[0.1, 0.2], [0.3, 0.4]]),
3021 decoder: crate::configs::DecoderType::ModelPack,
3022 quantization: Some(crate::configs::QuantTuple(0.176, 198)),
3023 shape: vec![1, 40, 40, 54],
3024 dshape: vec![],
3025 normalized: None,
3026 })],
3027 nms: None,
3028 nms_multi_label: None,
3029 decoder_version: None,
3030 };
3031 let v2 = SchemaV2::from_v1(&v1).unwrap();
3032 assert_eq!(v2.outputs[0].encoding, Some(BoxEncoding::Anchor));
3033 assert_eq!(v2.outputs[0].decoder, Some(DecoderKind::ModelPack));
3034 assert_eq!(v2.outputs[0].anchors.as_ref().map(|a| a.len()), Some(2));
3035 }
3036
3037 #[test]
3040 fn validate_accepts_flat_v2_yolov8_detection() {
3041 let j = r#"{
3042 "schema_version": 2,
3043 "outputs": [
3044 {"name":"boxes","type":"boxes","shape":[1,64,8400],
3045 "dtype":"int8","decoder":"ultralytics","encoding":"dfl"},
3046 {"name":"scores","type":"scores","shape":[1,80,8400],
3047 "dtype":"int8","decoder":"ultralytics","score_format":"per_class"}
3048 ]
3049 }"#;
3050 SchemaV2::parse_json(j).unwrap().validate().unwrap();
3051 }
3052
3053 #[test]
3054 fn validate_rejects_unnamed_physical_child() {
3055 let j = r#"{
3056 "schema_version": 2,
3057 "outputs": [{
3058 "name":"boxes","type":"boxes","shape":[1,64,8400],
3059 "encoding":"dfl","decoder":"ultralytics",
3060 "outputs": [{
3061 "name":"","type":"boxes","stride":8,
3062 "shape":[1,80,80,64],"dtype":"uint8"
3063 }]
3064 }]
3065 }"#;
3066 let err = SchemaV2::parse_json(j).unwrap().validate().unwrap_err();
3067 let msg = format!("{err}");
3068 assert!(msg.contains("missing `name`"), "got: {msg}");
3069 }
3070
3071 #[test]
3072 fn validate_rejects_duplicate_physical_shapes() {
3073 let j = r#"{
3074 "schema_version": 2,
3075 "outputs": [{
3076 "name":"boxes","type":"boxes","shape":[1,64,8400],
3077 "encoding":"dfl","decoder":"ultralytics",
3078 "outputs": [
3079 {"name":"a","type":"boxes","stride":8,"shape":[1,80,80,64],"dtype":"uint8"},
3080 {"name":"b","type":"boxes","stride":16,"shape":[1,80,80,64],"dtype":"uint8"}
3081 ]
3082 }]
3083 }"#;
3084 let err = SchemaV2::parse_json(j).unwrap().validate().unwrap_err();
3085 let msg = format!("{err}");
3086 assert!(msg.contains("share shape"), "got: {msg}");
3087 }
3088
3089 #[test]
3090 fn validate_rejects_mixed_decomposition() {
3091 let j = r#"{
3093 "schema_version": 2,
3094 "outputs": [{
3095 "name":"boxes","type":"boxes","shape":[1,4,8400,1],
3096 "encoding":"direct","decoder":"ultralytics",
3097 "outputs": [
3098 {"name":"xy","type":"boxes_xy","shape":[1,2,8400,1],"dtype":"int16"},
3099 {"name":"p0","type":"boxes","stride":8,"shape":[1,80,80,64],"dtype":"uint8"}
3100 ]
3101 }]
3102 }"#;
3103 let err = SchemaV2::parse_json(j).unwrap().validate().unwrap_err();
3104 let msg = format!("{err}");
3105 assert!(msg.contains("uniform"), "got: {msg}");
3106 }
3107
3108 #[test]
3109 fn validate_rejects_dfl_boxes_feature_not_divisible_by_4() {
3110 let j = r#"{
3111 "schema_version": 2,
3112 "outputs": [{
3113 "name":"boxes","type":"boxes","shape":[1,63,8400],
3114 "encoding":"dfl","decoder":"ultralytics",
3115 "outputs": [{
3116 "name":"b0","type":"boxes","stride":8,
3117 "shape":[1,80,80,63],
3118 "dshape":[{"batch":1},{"height":80},{"width":80},{"num_features":63}],
3119 "dtype":"uint8"
3120 }]
3121 }]
3122 }"#;
3123 let err = SchemaV2::parse_json(j).unwrap().validate().unwrap_err();
3124 let msg = format!("{err}");
3125 assert!(msg.contains("not"), "got: {msg}");
3126 assert!(msg.contains("divisible by 4"), "got: {msg}");
3127 }
3128
3129 #[test]
3130 fn validate_accepts_hailo_per_scale_yolov8() {
3131 let j = r#"{
3132 "schema_version": 2,
3133 "outputs": [{
3134 "name":"boxes","type":"boxes","shape":[1,64,8400],
3135 "encoding":"dfl","decoder":"ultralytics","normalized":true,
3136 "outputs": [
3137 {"name":"b0","type":"boxes","stride":8,
3138 "shape":[1,80,80,64],
3139 "dshape":[{"batch":1},{"height":80},{"width":80},{"num_features":64}],
3140 "dtype":"uint8",
3141 "quantization":{"scale":0.0234,"zero_point":128,"dtype":"uint8"}},
3142 {"name":"b1","type":"boxes","stride":16,
3143 "shape":[1,40,40,64],
3144 "dshape":[{"batch":1},{"height":40},{"width":40},{"num_features":64}],
3145 "dtype":"uint8",
3146 "quantization":{"scale":0.0198,"zero_point":130,"dtype":"uint8"}},
3147 {"name":"b2","type":"boxes","stride":32,
3148 "shape":[1,20,20,64],
3149 "dshape":[{"batch":1},{"height":20},{"width":20},{"num_features":64}],
3150 "dtype":"uint8",
3151 "quantization":{"scale":0.0312,"zero_point":125,"dtype":"uint8"}}
3152 ]
3153 }]
3154 }"#;
3155 let s = SchemaV2::parse_json(j).unwrap();
3156 s.validate().unwrap();
3157 }
3158
3159 #[test]
3160 fn validate_accepts_ara2_xy_wh() {
3161 let j = r#"{
3162 "schema_version": 2,
3163 "outputs": [{
3164 "name":"boxes","type":"boxes","shape":[1,4,8400,1],
3165 "encoding":"direct","decoder":"ultralytics","normalized":true,
3166 "outputs": [
3167 {"name":"xy","type":"boxes_xy","shape":[1,2,8400,1],
3168 "dshape":[{"batch":1},{"box_coords":2},{"num_boxes":8400},{"padding":1}],
3169 "dtype":"int16",
3170 "quantization":{"scale":3.1e-5,"zero_point":0,"dtype":"int16"}},
3171 {"name":"wh","type":"boxes_wh","shape":[1,2,8400,1],
3172 "dshape":[{"batch":1},{"box_coords":2},{"num_boxes":8400},{"padding":1}],
3173 "dtype":"int16",
3174 "quantization":{"scale":3.2e-5,"zero_point":0,"dtype":"int16"}}
3175 ]
3176 }]
3177 }"#;
3178 SchemaV2::parse_json(j).unwrap().validate().unwrap();
3179 }
3180
3181 #[test]
3182 fn parse_file_auto_detects_json() {
3183 let tmp = std::env::temp_dir().join(format!("schema_v2_test_{}.json", std::process::id()));
3184 std::fs::write(&tmp, r#"{"schema_version":2,"outputs":[]}"#).unwrap();
3185 let s = SchemaV2::parse_file(&tmp).unwrap();
3186 assert_eq!(s.schema_version, 2);
3187 let _ = std::fs::remove_file(&tmp);
3188 }
3189
3190 #[test]
3191 fn parse_file_auto_detects_yaml() {
3192 let tmp = std::env::temp_dir().join(format!("schema_v2_test_{}.yaml", std::process::id()));
3193 std::fs::write(&tmp, "schema_version: 2\noutputs: []\n").unwrap();
3194 let s = SchemaV2::parse_file(&tmp).unwrap();
3195 assert_eq!(s.schema_version, 2);
3196 let _ = std::fs::remove_file(&tmp);
3197 }
3198
3199 #[test]
3202 fn parse_real_ara2_int8_dvm_metadata() {
3203 let json = edgefirst_bench::testdata::read_to_string("ara2_int8_edgefirst.json");
3204 let schema = SchemaV2::parse_json(&json).expect("ARA-2 int8 parse");
3205 assert_eq!(schema.schema_version, 2);
3206 assert_eq!(schema.decoder_version, Some(DecoderVersion::Yolov8));
3207 assert_eq!(schema.nms, Some(NmsMode::ClassAgnostic));
3208 assert_eq!(schema.input.as_ref().unwrap().shape, vec![1, 3, 640, 640]);
3209
3210 assert_eq!(schema.outputs.len(), 4);
3212 let boxes = &schema.outputs[0];
3213 assert_eq!(boxes.type_, Some(LogicalType::Boxes));
3214 assert_eq!(boxes.encoding, Some(BoxEncoding::Direct));
3215 assert_eq!(boxes.normalized, Some(true));
3216 assert_eq!(boxes.shape, vec![1, 4, 8400, 1]); assert_eq!(boxes.outputs.len(), 2);
3218 assert_eq!(boxes.outputs[0].type_, Some(PhysicalType::BoxesXy));
3219 assert_eq!(boxes.outputs[1].type_, Some(PhysicalType::BoxesWh));
3220 let q_xy = boxes.outputs[0].quantization.as_ref().unwrap();
3222 assert_eq!(q_xy.dtype, Some(DType::Int8));
3223 assert!((q_xy.scale[0] - 0.004_177_792).abs() < 1e-6);
3224 assert_eq!(q_xy.zero_point_at(0), -122);
3225
3226 let scores = &schema.outputs[1];
3227 assert_eq!(scores.type_, Some(LogicalType::Scores));
3228 assert_eq!(scores.score_format, Some(ScoreFormat::PerClass));
3229 assert_eq!(scores.shape, vec![1, 80, 8400, 1]);
3230
3231 let mask_coefs = &schema.outputs[2];
3232 assert_eq!(mask_coefs.type_, Some(LogicalType::MaskCoefs));
3233 assert_eq!(mask_coefs.shape, vec![1, 32, 8400, 1]);
3234
3235 let protos = &schema.outputs[3];
3236 assert_eq!(protos.type_, Some(LogicalType::Protos));
3237 assert_eq!(protos.shape, vec![1, 32, 160, 160]);
3238
3239 schema.validate().expect("ARA-2 int8 validate");
3241 }
3242
3243 #[test]
3244 fn parse_real_ara2_int16_dvm_metadata() {
3245 let json = edgefirst_bench::testdata::read_to_string("ara2_int16_edgefirst.json");
3246 let schema = SchemaV2::parse_json(&json).expect("ARA-2 int16 parse");
3247 assert_eq!(schema.schema_version, 2);
3248 assert_eq!(schema.outputs.len(), 4);
3249 let boxes = &schema.outputs[0];
3250 assert_eq!(boxes.outputs.len(), 2);
3251 let q_xy = boxes.outputs[0].quantization.as_ref().unwrap();
3252 assert_eq!(q_xy.dtype, Some(DType::Int16));
3253 assert!((q_xy.scale[0] - 3.211_570_6e-5).abs() < 1e-10);
3254 assert_eq!(q_xy.zero_point_at(0), 0);
3255 let mc_q = schema.outputs[2].quantization.as_ref().unwrap();
3257 assert_eq!(mc_q.dtype, Some(DType::Int16));
3258 schema.validate().expect("ARA-2 int16 validate");
3259 }
3260
3261 #[test]
3262 fn parse_yaml_with_explicit_schema_version_2() {
3263 let yaml = r#"
3264schema_version: 2
3265outputs:
3266 - name: scores
3267 type: scores
3268 shape: [1, 80, 8400]
3269 dtype: int8
3270 quantization:
3271 scale: 0.00392
3272 dtype: int8
3273 decoder: ultralytics
3274 score_format: per_class
3275"#;
3276 let schema = SchemaV2::parse_yaml(yaml).unwrap();
3277 assert_eq!(schema.schema_version, 2);
3278 assert_eq!(schema.outputs[0].score_format, Some(ScoreFormat::PerClass));
3279 }
3280
3281 #[test]
3284 fn squeeze_padding_dims_preserves_shape_when_dshape_absent() {
3285 let (shape, dshape) = squeeze_padding_dims(vec![1, 4, 8400], vec![]);
3290 assert_eq!(shape, vec![1, 4, 8400]);
3291 assert!(dshape.is_empty());
3292 }
3293
3294 fn sample_logical(ty: LogicalType) -> LogicalOutput {
3295 LogicalOutput {
3296 name: Some("sample".into()),
3297 type_: Some(ty),
3298 shape: vec![1, 4, 100],
3299 dshape: vec![],
3300 decoder: None,
3301 encoding: None,
3302 score_format: None,
3303 normalized: Some(true),
3304 anchors: None,
3305 stride: None,
3306 dtype: None,
3307 quantization: None,
3308 outputs: vec![],
3309 activation_applied: None,
3310 activation_required: None,
3311 semantic: Default::default(),
3312 }
3313 }
3314
3315 #[test]
3316 fn logical_to_legacy_maps_each_supported_type() {
3317 let cases = [
3318 (LogicalType::Boxes, "Boxes"),
3319 (LogicalType::Scores, "Scores"),
3320 (LogicalType::Protos, "Protos"),
3321 (LogicalType::MaskCoefs, "MaskCoefficients"),
3322 (LogicalType::Segmentation, "Segmentation"),
3323 (LogicalType::Masks, "Mask"),
3324 (LogicalType::Classes, "Classes"),
3325 (LogicalType::Detection, "Detection"),
3326 (LogicalType::Detections, "Detection"),
3327 ];
3328 for (ty, expected) in cases {
3329 let out = logical_to_legacy_config_output(&sample_logical(ty)).unwrap();
3330 let name = match &out {
3331 ConfigOutput::Boxes(_) => "Boxes",
3332 ConfigOutput::Scores(_) => "Scores",
3333 ConfigOutput::Protos(_) => "Protos",
3334 ConfigOutput::MaskCoefficients(_) => "MaskCoefficients",
3335 ConfigOutput::Segmentation(_) => "Segmentation",
3336 ConfigOutput::Mask(_) => "Mask",
3337 ConfigOutput::Classes(_) => "Classes",
3338 ConfigOutput::Detection(_) => "Detection",
3339 };
3340 assert_eq!(name, expected, "type {ty:?}");
3341 }
3342 }
3343
3344 #[test]
3345 fn logical_to_legacy_rejects_objectness_and_landmarks() {
3346 for ty in [LogicalType::Objectness, LogicalType::Landmarks] {
3347 let err = logical_to_legacy_config_output(&sample_logical(ty)).unwrap_err();
3348 assert!(
3349 matches!(err, DecoderError::NotSupported(_)),
3350 "expected NotSupported for {ty:?}, got {err:?}"
3351 );
3352 }
3353 }
3354
3355 #[test]
3356 fn logical_to_legacy_rejects_typeless_output() {
3357 let mut logical = sample_logical(LogicalType::Boxes);
3358 logical.type_ = None;
3359 let err = logical_to_legacy_config_output(&logical).unwrap_err();
3360 assert!(matches!(err, DecoderError::InvalidConfig(_)));
3361 }
3362
3363 #[test]
3364 fn to_legacy_modelpack_boxes_preserves_padding_dim() {
3365 let j = r#"{
3370 "schema_version": 2,
3371 "outputs": [
3372 {"name":"boxes","type":"boxes",
3373 "shape":[1,1935,1,4],
3374 "dshape":[{"batch":1},{"num_boxes":1935},{"padding":1},{"box_coords":4}],
3375 "decoder":"modelpack"}
3376 ]
3377 }"#;
3378 let schema = SchemaV2::parse_json(j).unwrap();
3379 let legacy = schema.to_legacy_config_outputs().expect("lowers cleanly");
3380 let boxes = match &legacy.outputs[0] {
3381 crate::ConfigOutput::Boxes(b) => b,
3382 other => panic!("expected Boxes, got {other:?}"),
3383 };
3384 assert_eq!(boxes.shape, vec![1, 1935, 1, 4]);
3387 assert_eq!(
3388 boxes.dshape,
3389 vec![
3390 (DimName::Batch, 1),
3391 (DimName::NumBoxes, 1935),
3392 (DimName::Padding, 1),
3393 (DimName::BoxCoords, 4),
3394 ]
3395 );
3396 }
3397
3398 #[test]
3399 fn to_legacy_preserves_shape_for_v2_split_boxes_without_dshape() {
3400 let j = r#"{
3404 "schema_version": 2,
3405 "outputs": [
3406 {"name":"boxes","type":"boxes","shape":[1,4,8400],
3407 "dtype":"float32","decoder":"ultralytics","encoding":"direct"},
3408 {"name":"scores","type":"scores","shape":[1,80,8400],
3409 "dtype":"float32","decoder":"ultralytics","score_format":"per_class"}
3410 ]
3411 }"#;
3412 let schema = SchemaV2::parse_json(j).unwrap();
3413 let legacy = schema.to_legacy_config_outputs().expect("lowers cleanly");
3414 let boxes = match &legacy.outputs[0] {
3415 crate::ConfigOutput::Boxes(b) => b,
3416 other => panic!("expected Boxes, got {other:?}"),
3417 };
3418 assert_eq!(boxes.shape, vec![1, 4, 8400]);
3419 let scores = match &legacy.outputs[1] {
3420 crate::ConfigOutput::Scores(s) => s,
3421 other => panic!("expected Scores, got {other:?}"),
3422 };
3423 assert_eq!(scores.shape, vec![1, 80, 8400]);
3424 }
3425
3426 const SEGFORMER_B1: &str = r#"{
3427 "schema_version": 2,
3428 "input": {
3429 "shape": [1, 3, 512, 672],
3430 "dshape": [{"batch": 1}, {"channels": 3}, {"height": 512}, {"width": 672}],
3431 "cameraadaptor": "rgb",
3432 "preprocessing": {
3433 "normalization": {"mean": [123.675, 116.28, 103.53], "std": [58.395, 57.12, 57.375]},
3434 "resize": "letterbox",
3435 "interpolation": "bilinear",
3436 "letterbox": {"placement": "top_left", "pad": [0.0, 0.0, 0.0], "pad_space": "normalized"}
3437 }
3438 },
3439 "outputs": [{
3440 "name": "logits", "type": "segmentation", "decoder": "semantic",
3441 "shape": [1, 150, 128, 168],
3442 "dshape": [{"batch": 1}, {"num_classes": 150}, {"height": 128}, {"width": 168}],
3443 "dtype": "float32", "stride": 4,
3444 "upsample": {"method": "bilinear", "align_corners": false},
3445 "decode_mode": "accurate"
3446 }]
3447 }"#;
3448
3449 #[test]
3450 fn semantic_segformer_parses_and_validates() {
3451 let schema = SchemaV2::parse_json(SEGFORMER_B1).unwrap();
3452 schema.validate().unwrap();
3453
3454 let pre = schema
3455 .input
3456 .as_ref()
3457 .unwrap()
3458 .preprocessing
3459 .as_ref()
3460 .unwrap();
3461 assert_eq!(pre.resize, Some(ResizeMode::Letterbox));
3462 assert_eq!(pre.interpolation, Some(Interpolation::Bilinear));
3463 let lb = pre.letterbox.as_ref().unwrap();
3464 assert_eq!(lb.placement, Some(LetterboxPlacement::TopLeft));
3465 assert_eq!(lb.pad_space, Some(PadSpace::Normalized));
3466 let (mean, std) = pre.normalization.as_ref().unwrap().mean_std(3).unwrap();
3467 assert_eq!(mean, IMAGENET_MEAN.to_vec());
3468 assert_eq!(std, IMAGENET_STD.to_vec());
3469
3470 let out = &schema.outputs[0];
3471 assert_eq!(out.decoder, Some(DecoderKind::Semantic));
3472 assert_eq!(out.stride, Some(Stride::Square(4)));
3473 assert_eq!(
3474 out.semantic.upsample,
3475 Some(Upsample {
3476 method: UpsampleMethod::Bilinear,
3477 align_corners: false
3478 })
3479 );
3480 assert_eq!(out.semantic.decode_mode, Some(DecodeMode::Accurate));
3481 }
3482
3483 #[test]
3484 fn semantic_round_trips_with_flat_fields() {
3485 let schema = SchemaV2::parse_json(SEGFORMER_B1).unwrap();
3486 let value = serde_json::to_value(&schema).unwrap();
3487 let out = &value["outputs"][0];
3488 assert_eq!(out["decode_mode"], "accurate");
3489 assert_eq!(out["upsample"]["method"], "bilinear");
3490 assert!(out.get("semantic").is_none(), "fields must serialize flat");
3491 assert!(out.get("threshold").is_none(), "unset fields are omitted");
3492 let back: SchemaV2 = serde_json::from_value(value).unwrap();
3493 assert_eq!(back, schema);
3494 }
3495
3496 #[test]
3497 fn semantic_decoder_is_not_lowered_to_legacy() {
3498 let schema = SchemaV2::parse_json(SEGFORMER_B1).unwrap();
3499 let err = schema.to_legacy_config_outputs().unwrap_err();
3500 assert!(matches!(err, DecoderError::NotSupported(_)), "{err:?}");
3501 }
3502
3503 #[test]
3504 fn modelpack_segmentation_still_lowers_to_legacy() {
3505 let j = r#"{
3506 "schema_version": 2,
3507 "outputs": [{
3508 "name": "segmentation_output", "type": "segmentation",
3509 "shape": [1, 480, 640, 5],
3510 "dshape": [{"batch": 1}, {"height": 480}, {"width": 640}, {"num_classes": 5}],
3511 "dtype": "uint8",
3512 "quantization": {"scale": 0.00392, "zero_point": 0, "dtype": "uint8"},
3513 "decoder": "modelpack"
3514 }]
3515 }"#;
3516 let schema = SchemaV2::parse_json(j).unwrap();
3517 assert_eq!(schema.outputs[0].semantic, SemanticParams::default());
3518 let legacy = schema.to_legacy_config_outputs().unwrap();
3519 assert!(matches!(
3520 legacy.outputs[0],
3521 crate::ConfigOutput::Segmentation(_)
3522 ));
3523 }
3524
3525 #[test]
3526 fn normalization_presets_resolve() {
3527 let cases = [
3528 (NormalizationPreset::Unsigned, 0.0, 255.0),
3529 (NormalizationPreset::Signed, 127.5, 127.5),
3530 (NormalizationPreset::Raw, 0.0, 1.0),
3531 ];
3532 for (preset, m, s) in cases {
3533 let (mean, std) = Normalization::Preset(preset).mean_std(4).unwrap();
3534 assert_eq!(mean, vec![m; 4], "{preset:?}");
3535 assert_eq!(std, vec![s; 4], "{preset:?}");
3536 }
3537 let imagenet = Normalization::Preset(NormalizationPreset::Imagenet);
3538 assert!(imagenet.mean_std(3).is_ok());
3539 assert!(imagenet.mean_std(1).is_err());
3540 }
3541
3542 #[test]
3543 fn normalization_preset_parses_from_string() {
3544 for (text, preset) in [
3545 ("unsigned", NormalizationPreset::Unsigned),
3546 ("signed", NormalizationPreset::Signed),
3547 ("raw", NormalizationPreset::Raw),
3548 ("imagenet", NormalizationPreset::Imagenet),
3549 ] {
3550 let norm: Normalization = serde_json::from_value(serde_json::json!(text)).unwrap();
3551 assert_eq!(norm, Normalization::Preset(preset));
3552 }
3553 assert!(serde_json::from_value::<Normalization>(serde_json::json!("zscore")).is_err());
3554 }
3555
3556 #[test]
3557 fn mean_std_broadcasts_and_rejects_bad_input() {
3558 let one = Normalization::MeanStd {
3559 mean: vec![127.5],
3560 std: vec![127.5],
3561 };
3562 assert_eq!(one.mean_std(3).unwrap().0, vec![127.5; 3]);
3563
3564 let mismatched = Normalization::MeanStd {
3565 mean: vec![0.0, 0.0],
3566 std: vec![1.0; 3],
3567 };
3568 assert!(mismatched.mean_std(3).is_err());
3569
3570 let zero_std = Normalization::MeanStd {
3571 mean: vec![0.0; 3],
3572 std: vec![1.0, 0.0, 1.0],
3573 };
3574 assert!(zero_std.mean_std(3).is_err());
3575 }
3576
3577 #[test]
3578 fn validate_rejects_bad_preprocessing() {
3579 let bad_pad = r#"{
3580 "schema_version": 2,
3581 "input": {"shape": [1, 3, 8, 8], "preprocessing": {
3582 "normalization": {"mean": [0, 0, 0], "std": [1, 1, 1]},
3583 "letterbox": {"pad": [0, 0]}
3584 }}
3585 }"#;
3586 assert!(SchemaV2::parse_json(bad_pad).unwrap().validate().is_err());
3587
3588 let zero_std = r#"{
3589 "schema_version": 2,
3590 "input": {"shape": [1, 3, 8, 8], "preprocessing": {
3591 "normalization": {"mean": [0], "std": [0]}
3592 }}
3593 }"#;
3594 assert!(SchemaV2::parse_json(zero_std).unwrap().validate().is_err());
3595 }
3596
3597 #[test]
3598 fn non_finite_mean_is_rejected() {
3599 for bad in [f32::NAN, f32::INFINITY] {
3600 let norm = Normalization::MeanStd {
3601 mean: vec![bad],
3602 std: vec![1.0],
3603 };
3604 assert!(norm.mean_std(3).is_err(), "{bad}");
3605
3606 let mut schema =
3607 SchemaV2::parse_json(r#"{"schema_version": 2, "input": {"shape": [1, 3, 8, 8]}}"#)
3608 .unwrap();
3609 schema.input.as_mut().unwrap().preprocessing = Some(Preprocessing {
3610 normalization: Some(norm),
3611 ..Default::default()
3612 });
3613 assert!(schema.validate().is_err(), "{bad}");
3614 }
3615 }
3616
3617 fn validate_input(shape: &str, preprocessing: &str) -> DecoderResult<()> {
3618 let j = format!(
3619 r#"{{"schema_version": 2, "input": {{{shape}, "preprocessing": {preprocessing}}}}}"#
3620 );
3621 SchemaV2::parse_json(&j).unwrap().validate()
3622 }
3623
3624 #[test]
3625 fn scalar_coefficients_broadcast_with_per_channel_pad() {
3626 let pre = r#"{"normalization": {"mean": [0], "std": [255]},
3627 "letterbox": {"pad": [114, 114, 114]}}"#;
3628 validate_input(r#""shape": [1, 3, 512, 512]"#, pre).unwrap();
3629 validate_input(r#""shape": [1, 512, 512, 3]"#, pre).unwrap();
3630 validate_input(r#""shape": [1, 3, 4, 3]"#, pre).unwrap();
3632 }
3633
3634 #[test]
3635 fn coefficients_must_match_input_channels() {
3636 let two = r#"{"normalization": {"mean": [0, 0], "std": [1, 1]}}"#;
3637 let nchw = r#""shape": [1, 3, 512, 512]"#;
3638 let named = r#""shape": [1, 512, 512, 3],
3639 "dshape": [{"batch": 1}, {"height": 512}, {"width": 512}, {"channels": 3}]"#;
3640 assert!(validate_input(nchw, two).is_err());
3641 assert!(validate_input(named, two).is_err());
3642
3643 let pad_two = r#"{"normalization": "unsigned", "letterbox": {"pad": [0, 0]}}"#;
3644 assert!(validate_input(nchw, pad_two).is_err());
3645
3646 let imagenet = r#"{"normalization": "imagenet"}"#;
3647 assert!(validate_input(r#""shape": [1, 1, 64, 64]"#, imagenet).is_err());
3648 validate_input(nchw, imagenet).unwrap();
3649
3650 let mixed = r#"{"normalization": {"mean": [0, 0, 0], "std": [1, 1]}}"#;
3652 assert!(validate_input(r#""shape": [1, 3, 4, 3]"#, mixed).is_err());
3653 }
3654
3655 #[test]
3656 fn input_channels_resolve_from_dshape_or_shape() {
3657 let spec = |j: &str| serde_json::from_str::<InputSpec>(j).unwrap().channels();
3658 assert_eq!(spec(r#"{"shape": [1, 3, 512, 512]}"#), Some(3));
3659 assert_eq!(spec(r#"{"shape": [1, 512, 512, 4]}"#), Some(4));
3660 assert_eq!(spec(r#"{"shape": [1, 3, 4, 3]}"#), None);
3661 assert_eq!(spec(r#"{"shape": [1, 512, 512]}"#), None);
3662 assert_eq!(
3663 spec(
3664 r#"{"shape": [1, 3, 4, 3],
3665 "dshape": [{"batch": 1}, {"channels": 3}, {"height": 4}, {"width": 3}]}"#
3666 ),
3667 Some(3)
3668 );
3669 }
3670
3671 #[test]
3672 fn preprocessing_parses_from_yaml() {
3673 let y = r#"
3674schema_version: 2
3675input:
3676 shape: [1, 256, 256, 3]
3677 preprocessing:
3678 normalization: unsigned
3679 resize: stretch
3680"#;
3681 let schema = SchemaV2::parse_yaml(y).unwrap();
3682 let pre = schema.input.unwrap().preprocessing.unwrap();
3683 assert_eq!(
3684 pre.normalization,
3685 Some(Normalization::Preset(NormalizationPreset::Unsigned))
3686 );
3687 assert_eq!(pre.resize, Some(ResizeMode::Stretch));
3688 assert!(pre.letterbox.is_none());
3689 }
3690
3691 #[test]
3692 fn class_map_masks_accept_int64() {
3693 let j = r#"{
3694 "schema_version": 2,
3695 "outputs": [{
3696 "name": "ArgMax", "type": "masks", "decoder": "semantic",
3697 "shape": [1, 513, 513],
3698 "dshape": [{"batch": 1}, {"height": 513}, {"width": 513}],
3699 "dtype": "int64"
3700 }]
3701 }"#;
3702 let schema = SchemaV2::parse_json(j).unwrap();
3703 schema.validate().unwrap();
3704 assert_eq!(schema.outputs[0].dtype, Some(DType::Int64));
3705 assert_eq!(DType::Int64.size_bytes(), 8);
3706 assert!(DType::Int64.is_integer());
3707 }
3708
3709 #[test]
3710 fn single_channel_threshold_and_background_parse() {
3711 let j = r#"{
3712 "schema_version": 2,
3713 "outputs": [{
3714 "name": "segment", "type": "segmentation", "decoder": "semantic",
3715 "shape": [1, 256, 256, 1],
3716 "dshape": [{"batch": 1}, {"height": 256}, {"width": 256}, {"num_classes": 1}],
3717 "dtype": "float32", "activation_applied": "sigmoid",
3718 "threshold": 0.5, "background_index": 0
3719 }]
3720 }"#;
3721 let schema = SchemaV2::parse_json(j).unwrap();
3722 let out = &schema.outputs[0];
3723 assert_eq!(out.activation_applied, Some(Activation::Sigmoid));
3724 assert_eq!(out.semantic.threshold, Some(0.5));
3725 assert_eq!(out.semantic.background_index, Some(0));
3726 }
3727
3728 #[test]
3729 fn reserved_mask_classification_parses_but_does_not_lower() {
3730 let j = r#"{
3731 "schema_version": 2,
3732 "outputs": [
3733 {"name": "class_queries_logits", "type": "class_logits",
3734 "shape": [1, 100, 151],
3735 "dshape": [{"batch": 1}, {"num_queries": 100}, {"num_classes": 151}],
3736 "dtype": "float32", "no_object_index": 150},
3737 {"name": "masks_queries_logits", "type": "mask_logits",
3738 "shape": [1, 100, 128, 128],
3739 "dshape": [{"batch": 1}, {"num_queries": 100}, {"height": 128}, {"width": 128}],
3740 "dtype": "float32", "stride": 4, "mask_activation": "sigmoid"}
3741 ]
3742 }"#;
3743 let schema = SchemaV2::parse_json(j).unwrap();
3744 schema.validate().unwrap();
3745 assert_eq!(schema.outputs[0].type_, Some(LogicalType::ClassLogits));
3746 assert_eq!(schema.outputs[0].dshape[1].0, DimName::NumQueries);
3747 assert_eq!(schema.outputs[0].semantic.no_object_index, Some(150));
3748 assert_eq!(
3749 schema.outputs[1].semantic.mask_activation,
3750 Some(MaskActivation::Sigmoid)
3751 );
3752 assert!(matches!(
3753 schema.to_legacy_config_outputs(),
3754 Err(DecoderError::NotSupported(_))
3755 ));
3756 }
3757
3758 #[test]
3759 fn semantic_decoder_kind_shares_the_v1_code() {
3760 assert_eq!(
3761 DecoderKind::Semantic.to_v1(),
3762 configs::DecoderType::Semantic
3763 );
3764 let v: configs::DecoderType = serde_json::from_str("\"semantic\"").unwrap();
3765 assert_eq!(v, configs::DecoderType::Semantic);
3766 }
3767}