use std::fmt;
use std::str::FromStr;
use edgefirst_tensor::{LetterboxPlacement, Normalization, NormalizationPreset};
use super::{ModelConfigError, ModelTask};
use crate::schema::DecodeMode;
fn bad(what: &str, value: &str, grammar: &str) -> ModelConfigError {
ModelConfigError::Override(format!("{what} `{value}` is not one of {grammar}"))
}
const NORMALIZE_GRAMMAR: &str = "unsigned | signed | raw | imagenet | R,G,B/R,G,B";
const RESIZE_GRAMMAR: &str = "stretch | letterbox | letterbox-center | letterbox-topleft";
const TASK_GRAMMAR: &str = "detect | segment | semantic";
const DECODE_GRAMMAR: &str = "accurate | fast";
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum NormalizeOverride {
Preset(NormalizationPreset),
MeanStd {
mean: [f32; 3],
std: [f32; 3],
},
}
impl NormalizeOverride {
pub fn to_normalization(&self) -> Result<Normalization, ModelConfigError> {
match self {
Self::Preset(p) => Ok(Normalization::preset(*p)),
Self::MeanStd { mean, std } => Normalization::unit_mean_std(mean, std, 255.0)
.map_err(|e| ModelConfigError::Override(format!("normalize: {e}"))),
}
}
}
fn triple(s: &str) -> Option<[f32; 3]> {
let mut it = s.split(',').map(|v| v.trim().parse::<f32>().ok());
let t = [it.next()??, it.next()??, it.next()??];
it.next().is_none().then_some(t)
}
impl FromStr for NormalizeOverride {
type Err = ModelConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if let Some(p) = NormalizationPreset::from_str_code(s) {
return Ok(Self::Preset(p));
}
let (mean, std) = s
.split_once('/')
.and_then(|(m, d)| Some((triple(m)?, triple(d)?)))
.ok_or_else(|| bad("normalize", s, NORMALIZE_GRAMMAR))?;
let n = Self::MeanStd { mean, std };
n.to_normalization()?;
Ok(n)
}
}
impl fmt::Display for NormalizeOverride {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Preset(p) => f.write_str(p.as_str()),
Self::MeanStd { mean, std } => write!(
f,
"{},{},{}/{},{},{}",
mean[0], mean[1], mean[2], std[0], std[1], std[2]
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResizeOverride {
Stretch,
Letterbox(Option<LetterboxPlacement>),
}
impl FromStr for ResizeOverride {
type Err = ModelConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
match s {
"stretch" => Ok(Self::Stretch),
"letterbox" => Ok(Self::Letterbox(None)),
_ => s
.strip_prefix("letterbox-")
.and_then(LetterboxPlacement::from_str_code)
.map(|p| Self::Letterbox(Some(p)))
.ok_or_else(|| bad("resize", s, RESIZE_GRAMMAR)),
}
}
}
impl fmt::Display for ResizeOverride {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Stretch => f.write_str("stretch"),
Self::Letterbox(None) => f.write_str("letterbox"),
Self::Letterbox(Some(p)) => write!(f, "letterbox-{}", p.as_str()),
}
}
}
impl FromStr for ModelTask {
type Err = ModelConfigError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_str_code(s.trim()).ok_or_else(|| bad("task", s, TASK_GRAMMAR))
}
}
impl fmt::Display for ModelTask {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
fn parse_labels(value: &str) -> Result<Vec<String>, ModelConfigError> {
let labels: Vec<String> = value.split(',').map(|l| l.trim().to_owned()).collect();
if labels.iter().any(String::is_empty) {
return Err(ModelConfigError::Override(format!(
"labels `{value}` has an empty name; expected comma-separated class names"
)));
}
Ok(labels)
}
#[derive(Debug, Clone, Default, PartialEq)]
#[non_exhaustive]
pub struct ModelOverrides {
pub task: Option<ModelTask>,
pub normalize: Option<NormalizeOverride>,
pub resize: Option<ResizeOverride>,
pub decode: Option<DecodeMode>,
pub labels: Option<Vec<String>>,
}
impl ModelOverrides {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
pub fn set(&mut self, key: &str, value: &str) -> Result<(), ModelConfigError> {
match key.trim() {
"task" => self.task = Some(value.parse()?),
"normalize" => self.normalize = Some(value.parse()?),
"resize" => self.resize = Some(value.parse()?),
"decode" => {
let v = value.trim();
self.decode = Some(
DecodeMode::from_str_code(v).ok_or_else(|| bad("decode", v, DECODE_GRAMMAR))?,
)
}
"labels" => self.labels = Some(parse_labels(value)?),
other => {
return Err(ModelConfigError::Override(format!(
"unknown override `{other}`; expected task, normalize, resize, decode or labels"
)))
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn override_grammar_round_trips() {
for s in [
"imagenet",
"unsigned",
"signed",
"raw",
"0.485,0.456,0.406/0.229,0.224,0.225",
"0.5,0.5,0.5/0.5,0.5,0.5",
] {
let n: NormalizeOverride = s.parse().unwrap();
assert_eq!(n.to_string(), s);
}
for s in [
"stretch",
"letterbox",
"letterbox-center",
"letterbox-topleft",
] {
assert_eq!(s.parse::<ResizeOverride>().unwrap().to_string(), s);
}
for s in ["detect", "segment", "semantic"] {
assert_eq!(s.parse::<ModelTask>().unwrap().to_string(), s);
}
let mut o = ModelOverrides::new();
for s in ["accurate", "fast"] {
o.set("decode", s).unwrap();
assert_eq!(o.decode.unwrap().as_str(), s);
}
}
#[test]
fn normalize_mean_std_is_scaled_to_255() {
let n: NormalizeOverride = "0.5,0.5,0.5/0.5,0.5,0.5".parse().unwrap();
assert_eq!(n.to_normalization().unwrap(), Normalization::signed());
}
#[test]
fn bad_override_lists_accepted_forms() {
let e = "fill".parse::<ResizeOverride>().unwrap_err().to_string();
assert!(e.contains("letterbox-topleft"), "{e}");
assert!("letterbox-top_left".parse::<ResizeOverride>().is_err());
assert!("unit".parse::<NormalizeOverride>().is_err());
let e = "0.1,0.2/0.3"
.parse::<NormalizeOverride>()
.unwrap_err()
.to_string();
assert!(e.contains("R,G,B/R,G,B"), "{e}");
assert!("1,2,3,4/1,2,3".parse::<NormalizeOverride>().is_err());
for bad_std in [
"0,0,0/0,1,1",
"0,0,0/-1,1,1",
"nan,0,0/1,1,1",
"0,0,0/inf,1,1",
] {
assert!(bad_std.parse::<NormalizeOverride>().is_err(), "{bad_std}");
}
assert!("classify".parse::<ModelTask>().is_err());
assert!(ModelOverrides::new().set("decode", "slow").is_err());
}
#[test]
fn set_by_key() {
let mut o = ModelOverrides::new();
assert!(o.is_empty());
o.set("task", "semantic").unwrap();
o.set("normalize", "imagenet").unwrap();
o.set("decode", " fast ").unwrap();
o.set("labels", "a, b").unwrap();
assert_eq!(o.task, Some(ModelTask::Semantic));
assert_eq!(o.decode, Some(DecodeMode::Fast));
assert_eq!(
o.labels.as_deref(),
Some(&["a".to_string(), "b".to_string()][..])
);
assert!(o.set("colour", "x").is_err());
for empty in ["", "a,,b", "a, "] {
assert!(o.set("labels", empty).is_err(), "{empty:?}");
}
}
}