use crate::{BoundingBox, Error, Region, Result};
crate::ef_vocabulary! {
#[derive(Default)]
#[non_exhaustive]
pub enum LetterboxPlacement {
#[default]
Center = 0, "center", CENTER,
TopLeft = 1, "topleft", TOP_LEFT,
}
#[doc(hidden)]
pub mod letterbox_placement_code;
}
crate::ef_vocabulary! {
#[derive(Default)]
#[non_exhaustive]
pub enum NormalizationPreset {
#[default]
Unsigned = 0, "unsigned", UNSIGNED,
Signed = 1, "signed", SIGNED,
Raw = 2, "raw", RAW,
Imagenet = 3, "imagenet", IMAGENET,
}
#[doc(hidden)]
pub mod normalization_code;
}
crate::ef_vocabulary! {
#[derive(Default)]
#[non_exhaustive]
pub enum FitMode {
#[default]
Stretch = 0, "stretch", STRETCH,
Letterbox = 1, "letterbox", LETTERBOX,
}
#[doc(hidden)]
pub mod fit_code;
}
crate::ef_vocabulary! {
#[derive(Default)]
#[non_exhaustive]
pub enum Interpolation {
#[default]
Bilinear = 0, "bilinear", BILINEAR,
Nearest = 1, "nearest", NEAREST,
Bicubic = 2, "bicubic", BICUBIC,
Area = 3, "area", AREA,
}
#[doc(hidden)]
pub mod interpolation_code;
}
crate::ef_vocabulary! {
#[derive(Default)]
#[non_exhaustive]
pub enum PadSpace {
#[default]
Pixel = 0, "pixel", PIXEL,
Normalized = 1, "normalized", NORMALIZED,
}
#[doc(hidden)]
pub mod pad_space_code;
}
impl Interpolation {
pub const fn supported(self) -> Self {
match self {
Self::Nearest => Self::Nearest,
Self::Bilinear | Self::Bicubic | Self::Area => Self::Bilinear,
}
}
}
macro_rules! serde_by_name {
($($ty:ident),+) => {$(
impl serde::Serialize for $ty {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
impl<'de> serde::Deserialize<'de> for $ty {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
let s = <std::borrow::Cow<'de, str> as serde::Deserialize>::deserialize(d)?;
Self::from_str_code(&s).ok_or_else(|| {
let names: Vec<&str> = Self::all().iter().map(|v| v.as_str()).collect();
serde::de::Error::custom(format!(
"unknown {} `{s}`, expected one of {names:?}",
stringify!($ty)
))
})
}
}
)+};
}
serde_by_name!(
LetterboxPlacement,
NormalizationPreset,
FitMode,
Interpolation,
PadSpace
);
const IMAGENET_MEAN: [f32; 3] = [123.675, 116.28, 103.53];
const IMAGENET_STD: [f32; 3] = [58.395, 57.12, 57.375];
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Normalization {
mean: [f32; 3],
std: [f32; 3],
}
impl Default for Normalization {
fn default() -> Self {
Self::preset(NormalizationPreset::Unsigned)
}
}
impl From<NormalizationPreset> for Normalization {
fn from(p: NormalizationPreset) -> Self {
Self::preset(p)
}
}
impl Normalization {
pub const fn preset(p: NormalizationPreset) -> Self {
let (mean, std) = match p {
NormalizationPreset::Unsigned => ([0.0; 3], [255.0; 3]),
NormalizationPreset::Signed => ([127.5; 3], [127.5; 3]),
NormalizationPreset::Raw => ([0.0; 3], [1.0; 3]),
NormalizationPreset::Imagenet => (IMAGENET_MEAN, IMAGENET_STD),
};
Self { mean, std }
}
pub const fn unsigned() -> Self {
Self::preset(NormalizationPreset::Unsigned)
}
pub const fn signed() -> Self {
Self::preset(NormalizationPreset::Signed)
}
pub const fn raw() -> Self {
Self::preset(NormalizationPreset::Raw)
}
pub const fn imagenet() -> Self {
Self::preset(NormalizationPreset::Imagenet)
}
pub fn mean_std(mean: &[f32], std: &[f32]) -> Result<Self> {
let count = |name: &str, v: &[f32]| {
broadcast_rgb(v).ok_or_else(|| {
Error::InvalidArgument(format!(
"normalization {name} needs 1 or 3 values, got {}",
v.len()
))
})
};
let mean = count("mean", mean)?;
let std = count("std", std)?;
if mean.iter().any(|m| !m.is_finite()) {
return Err(Error::InvalidArgument(format!(
"normalization mean {mean:?} must be finite"
)));
}
if std.iter().any(|s| !s.is_finite() || *s <= 0.0) {
return Err(Error::InvalidArgument(format!(
"normalization std {std:?} must be finite and positive"
)));
}
Ok(Self { mean, std })
}
pub fn unit_mean_std(mean: &[f32], std: &[f32], scale: f64) -> Result<Self> {
let scaled =
|v: &[f32]| -> Vec<f32> { v.iter().map(|&x| (f64::from(x) * scale) as f32).collect() };
Self::mean_std(&scaled(mean), &scaled(std))
}
pub fn mean(&self) -> [f32; 3] {
self.mean
}
pub fn std(&self) -> [f32; 3] {
self.std
}
pub fn as_preset(&self) -> Option<NormalizationPreset> {
NormalizationPreset::all()
.iter()
.copied()
.find(|&p| Self::preset(p) == *self)
}
}
pub fn broadcast_rgb<T: Copy>(v: &[T]) -> Option<[T; 3]> {
match *v {
[x] => Some([x; 3]),
[r, g, b] => Some([r, g, b]),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Fit {
#[default]
Stretch,
Letterbox {
pad: [u8; 4],
},
}
pub fn letterbox_rect(
src_w: usize,
src_h: usize,
dst_w: usize,
dst_h: usize,
placement: LetterboxPlacement,
) -> Region {
if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
return Region::new(0, 0, dst_w, dst_h);
}
let src_aspect = src_w as f64 / src_h as f64;
let dst_aspect = dst_w as f64 / dst_h as f64;
let (w, h) = if src_aspect > dst_aspect {
(dst_w, ((dst_w as f64 / src_aspect).round() as usize).max(1))
} else {
(((dst_h as f64 * src_aspect).round() as usize).max(1), dst_h)
};
let (x, y) = match placement {
LetterboxPlacement::Center => (dst_w.saturating_sub(w) / 2, dst_h.saturating_sub(h) / 2),
LetterboxPlacement::TopLeft => (0, 0),
};
Region::new(x, y, w, h)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ContentRegion {
pub pixels: Region,
pub normalized: [f32; 4],
}
impl ContentRegion {
pub fn new(pixels: Region, dst_w: usize, dst_h: usize) -> Self {
let (w, h) = (dst_w.max(1) as f32, dst_h.max(1) as f32);
Self {
pixels,
normalized: [
pixels.x as f32 / w,
pixels.y as f32 / h,
pixels.x.saturating_add(pixels.width) as f32 / w,
pixels.y.saturating_add(pixels.height) as f32 / h,
],
}
}
pub fn full(dst_w: usize, dst_h: usize) -> Self {
Self::new(Region::new(0, 0, dst_w, dst_h), dst_w, dst_h)
}
pub fn is_full(&self) -> bool {
self.normalized == [0.0, 0.0, 1.0, 1.0]
}
pub fn letterboxed(&self) -> Option<[f32; 4]> {
(!self.is_full()).then_some(self.normalized)
}
pub fn unletter(&self, b: BoundingBox) -> BoundingBox {
crate::unletter_norm(b, self.normalized)
}
}
pub trait ContentRegionSource {
fn content_region(
&self,
src_w: usize,
src_h: usize,
dst_w: usize,
dst_h: usize,
) -> Result<ContentRegion>;
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct PreprocessSpec {
pub normalization: Normalization,
pub fit: FitMode,
pub placement: LetterboxPlacement,
pub pad: [f32; 3],
pub pad_space: PadSpace,
pub interpolation: Interpolation,
pub size: Option<(usize, usize)>,
}
impl Default for PreprocessSpec {
fn default() -> Self {
Self {
normalization: Normalization::unsigned(),
fit: FitMode::Letterbox,
placement: LetterboxPlacement::Center,
pad: [114.0; 3],
pad_space: PadSpace::Pixel,
interpolation: Interpolation::Bilinear,
size: None,
}
}
}
impl PreprocessSpec {
pub fn new() -> Self {
Self::default()
}
}
impl ContentRegionSource for PreprocessSpec {
fn content_region(
&self,
src_w: usize,
src_h: usize,
dst_w: usize,
dst_h: usize,
) -> Result<ContentRegion> {
Ok(match self.fit {
FitMode::Stretch => ContentRegion::full(dst_w, dst_h),
FitMode::Letterbox => ContentRegion::new(
letterbox_rect(src_w, src_h, dst_w, dst_h, self.placement),
dst_w,
dst_h,
),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn letterbox_rect_rounds_and_places() {
let c = LetterboxPlacement::Center;
let t = LetterboxPlacement::TopLeft;
assert_eq!(
letterbox_rect(641, 479, 640, 640, c),
Region::new(0, 81, 640, 478)
);
assert_eq!(
letterbox_rect(641, 479, 640, 640, t),
Region::new(0, 0, 640, 478)
);
assert_eq!(
letterbox_rect(479, 641, 640, 640, c),
Region::new(81, 0, 478, 640)
);
assert_eq!(
letterbox_rect(1, 3, 320, 256, c),
Region::new(117, 0, 85, 256)
);
assert_eq!(
letterbox_rect(3, 1, 320, 256, c),
Region::new(0, 74, 320, 107)
);
assert_eq!(
letterbox_rect(640, 640, 320, 320, c),
Region::new(0, 0, 320, 320)
);
assert_eq!(letterbox_rect(0, 5, 32, 16, c), Region::new(0, 0, 32, 16));
assert_eq!(
letterbox_rect(10000, 1, 64, 64, t),
Region::new(0, 0, 64, 1)
);
}
#[test]
fn content_region_normalizes_pixels() {
let r = ContentRegion::new(Region::new(0, 140, 640, 360), 640, 640);
assert_eq!(r.normalized, [0.0, 0.21875, 1.0, 0.78125]);
assert!(!r.is_full());
assert_eq!(r.letterboxed(), Some(r.normalized));
assert!(ContentRegion::full(32, 16).is_full());
assert_eq!(ContentRegion::full(32, 16).letterboxed(), None);
}
#[test]
fn interpolation_codes_keep_c_values() {
assert_eq!(Interpolation::Bilinear.code(), 0);
assert_eq!(Interpolation::Nearest.code(), 1);
assert_eq!(Interpolation::Bicubic.supported(), Interpolation::Bilinear);
assert_eq!(Interpolation::Area.supported(), Interpolation::Bilinear);
assert_eq!(Interpolation::Nearest.supported(), Interpolation::Nearest);
}
#[test]
fn vocab_serde_round_trips() {
for &p in LetterboxPlacement::all() {
let s = serde_json::to_string(&p).unwrap();
assert_eq!(serde_json::from_str::<LetterboxPlacement>(&s).unwrap(), p);
}
assert_eq!(
serde_json::to_string(&PadSpace::Normalized).unwrap(),
"\"normalized\""
);
assert_eq!(
serde_json::to_string(&LetterboxPlacement::TopLeft).unwrap(),
"\"topleft\""
);
assert!(serde_json::from_str::<LetterboxPlacement>("\"top_left\"").is_err());
let e = serde_json::from_str::<Interpolation>("\"lanczos\"").unwrap_err();
assert!(e.to_string().contains("bicubic"), "{e}");
}
#[test]
fn spec_content_region_follows_fit() {
let mut spec = PreprocessSpec::new();
let lb = spec.content_region(1280, 720, 640, 640).unwrap();
assert_eq!(lb.pixels, Region::new(0, 140, 640, 360));
spec.fit = FitMode::Stretch;
assert!(spec.content_region(1280, 720, 640, 640).unwrap().is_full());
}
#[test]
fn normalization_finds_its_preset() {
assert_eq!(
Normalization::imagenet().as_preset(),
Some(NormalizationPreset::Imagenet)
);
let custom = Normalization::mean_std(&[1.0], &[2.0]).unwrap();
assert_eq!(custom.as_preset(), None);
assert!(Normalization::mean_std(&[0.0, 0.0], &[1.0]).is_err());
assert!(Normalization::mean_std(&[f32::NAN], &[1.0]).is_err());
}
}