use crate::configs::DimName;
use crate::infer::{ModelSignals, TensorInfo};
use crate::schema::{
DecodeMode, DecoderKind, InputSpec, LogicalType, SchemaV2, Stride, Upsample, UpsampleMethod,
};
use super::ModelConfigError;
const HEURISTIC_STRIDES: [usize; 4] = [1, 2, 4, 8];
#[derive(Debug, Clone)]
pub(crate) struct SemanticExport {
pub schema: SchemaV2,
pub num_classes: usize,
pub stride: usize,
pub size: (usize, usize),
}
fn nchw_image(t: &TensorInfo) -> Option<(usize, usize)> {
match t.shape.as_slice() {
[1, 3, h, w] => Some((*w, *h)),
_ => None,
}
}
fn score_layout(shape: &[usize], (w, h): (usize, usize)) -> Option<(usize, bool, usize)> {
let [1, a, b, c] = *shape else {
return None;
};
let stride = |sh: usize, sw: usize| {
(sh > 0 && sw > 0 && h % sh == 0 && w % sw == 0 && h / sh == w / sw).then(|| h / sh)
};
if let Some(s) = stride(b, c) {
return Some((a, true, s));
}
stride(a, b).map(|s| (c, false, s))
}
fn single<'a>(tensors: &'a [TensorInfo], name: &str) -> Option<&'a TensorInfo> {
tensors.iter().find(|t| t.name == name).or(match tensors {
[t] => Some(t),
_ => None,
})
}
pub(crate) fn semantic_schema(signals: &ModelSignals) -> Result<SemanticExport, ModelConfigError> {
let unresolved = |what: &str| ModelConfigError::Unresolved {
missing: format!("a semantic segmentation layout: {what}"),
overrides: "an edgefirst.json describing the outputs".into(),
};
let input = single(&signals.inputs, "pixel_values")
.ok_or_else(|| unresolved("no single image input"))?;
let (w, h, input_dshape) = match input.shape.as_slice() {
[1, 3, h, w] => (*w, *h, nchw_dims()),
[1, h, w, 3] => (*w, *h, nhwc_dims()),
s => {
return Err(unresolved(&format!(
"input shape {s:?} is not a 3-channel image"
)))
}
};
let rank4: Vec<&TensorInfo> = signals
.outputs
.iter()
.filter(|t| t.shape.len() == 4)
.collect();
let output = signals
.outputs
.iter()
.find(|t| t.name == "logits")
.or(match rank4.as_slice() {
[t] => Some(*t),
_ => None,
})
.ok_or_else(|| unresolved("no single rank-4 score output"))?;
let (classes, nchw, stride) = score_layout(&output.shape, (w, h)).ok_or_else(|| {
unresolved(&format!(
"output shape {:?} is not a score map of the {w}x{h} input",
output.shape
))
})?;
let dshape = if nchw {
vec![
(DimName::Batch, 1),
(DimName::NumClasses, classes),
(DimName::Height, h / stride),
(DimName::Width, w / stride),
]
} else {
vec![
(DimName::Batch, 1),
(DimName::Height, h / stride),
(DimName::Width, w / stride),
(DimName::NumClasses, classes),
]
};
let mut schema = SchemaV2::new();
schema.input = Some(InputSpec {
shape: input.shape.clone(),
dshape: input_dshape
.into_iter()
.zip(input.shape.iter().copied())
.collect(),
cameraadaptor: Some("rgb".into()),
preprocessing: None,
});
let mut out = super::resolve::empty_logical();
out.name = Some(output.name.clone());
out.type_ = Some(LogicalType::Segmentation);
out.decoder = Some(DecoderKind::Semantic);
out.shape = output.shape.clone();
out.dshape = dshape;
out.stride = Some(Stride::Square(stride as u32));
out.dtype = Some(output.dtype);
out.quantization = output.quantization.clone();
out.semantic.upsample = Some(Upsample {
method: UpsampleMethod::Bilinear,
align_corners: false,
});
out.semantic.decode_mode = Some(DecodeMode::Accurate);
schema.outputs.push(out);
schema.validate()?;
Ok(SemanticExport {
schema,
num_classes: classes,
stride,
size: (w, h),
})
}
fn nchw_dims() -> [DimName; 4] {
[
DimName::Batch,
DimName::NumFeatures,
DimName::Height,
DimName::Width,
]
}
fn nhwc_dims() -> [DimName; 4] {
[
DimName::Batch,
DimName::Height,
DimName::Width,
DimName::NumFeatures,
]
}
pub(crate) fn hf_family_name(stride: usize) -> &'static str {
if stride == 4 {
"SegFormer default"
} else {
"Hugging Face segmentation default"
}
}
pub(crate) fn hf_semantic_by_names(signals: &ModelSignals) -> Option<SemanticExport> {
let input = signals.inputs.iter().find(|t| t.name == "pixel_values")?;
let output = signals.outputs.iter().find(|t| t.name == "logits")?;
let size = nchw_image(input)?;
let (_, nchw, stride) = score_layout(&output.shape, size)?;
if !nchw || !HEURISTIC_STRIDES.contains(&stride) {
return None;
}
semantic_schema(signals).ok()
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::infer::ModelSource;
use crate::schema::DType;
pub(crate) fn tensor(name: &str, shape: &[usize]) -> TensorInfo {
TensorInfo {
name: name.into(),
shape: shape.to_vec(),
dtype: DType::Float32,
quantization: None,
}
}
pub(crate) fn segformer_signals(classes: usize) -> ModelSignals {
ModelSignals {
source: ModelSource::Onnx,
inputs: vec![tensor("pixel_values", &[1, 3, 512, 512])],
outputs: vec![tensor("logits", &[1, classes, 128, 128])],
metadata: Default::default(),
}
}
#[test]
fn segformer_names_are_recognised() {
let e = hf_semantic_by_names(&segformer_signals(150)).unwrap();
assert_eq!((e.num_classes, e.stride, e.size), (150, 4, (512, 512)));
let out = &e.schema.outputs[0];
assert_eq!(out.decoder, Some(DecoderKind::Semantic));
crate::semantic::SemanticConfig::from_logical(out).unwrap();
assert_eq!(hf_family_name(e.stride), "SegFormer default");
}
#[test]
fn other_names_or_strides_are_not_guessed() {
let mut s = segformer_signals(19);
s.outputs[0].name = "output".into();
assert!(hf_semantic_by_names(&s).is_none());
let mut s = segformer_signals(19);
s.outputs[0].shape = vec![1, 19, 32, 32];
assert!(
hf_semantic_by_names(&s).is_none(),
"stride 16 is not a guess"
);
assert_eq!(semantic_schema(&s).unwrap().stride, 16);
}
#[test]
fn nhwc_score_maps_are_laid_out() {
let s = ModelSignals {
source: ModelSource::TfLite,
inputs: vec![tensor("input", &[1, 256, 320, 3])],
outputs: vec![tensor("out", &[1, 64, 80, 21])],
metadata: Default::default(),
};
let e = semantic_schema(&s).unwrap();
assert_eq!((e.num_classes, e.stride, e.size), (21, 4, (320, 256)));
assert_eq!(e.schema.outputs[0].dshape[3], (DimName::NumClasses, 21));
}
}