1use crate::{BoundingBox, Error, Region, Result};
15
16crate::ef_vocabulary! {
17 #[derive(Default)]
21 #[non_exhaustive]
22 pub enum LetterboxPlacement {
23 #[default]
26 Center = 0, "center", CENTER,
27 TopLeft = 1, "topleft", TOP_LEFT,
29 }
30 #[doc(hidden)]
31 pub mod letterbox_placement_code;
32}
33
34crate::ef_vocabulary! {
35 #[derive(Default)]
39 #[non_exhaustive]
40 pub enum NormalizationPreset {
41 #[default]
43 Unsigned = 0, "unsigned", UNSIGNED,
44 Signed = 1, "signed", SIGNED,
46 Raw = 2, "raw", RAW,
48 Imagenet = 3, "imagenet", IMAGENET,
50 }
51 #[doc(hidden)]
52 pub mod normalization_code;
53}
54
55crate::ef_vocabulary! {
56 #[derive(Default)]
60 #[non_exhaustive]
61 pub enum FitMode {
62 #[default]
65 Stretch = 0, "stretch", STRETCH,
66 Letterbox = 1, "letterbox", LETTERBOX,
68 }
69 #[doc(hidden)]
70 pub mod fit_code;
71}
72
73crate::ef_vocabulary! {
74 #[derive(Default)]
83 #[non_exhaustive]
84 pub enum Interpolation {
85 #[default]
87 Bilinear = 0, "bilinear", BILINEAR,
88 Nearest = 1, "nearest", NEAREST,
90 Bicubic = 2, "bicubic", BICUBIC,
92 Area = 3, "area", AREA,
94 }
95 #[doc(hidden)]
96 pub mod interpolation_code;
97}
98
99crate::ef_vocabulary! {
100 #[derive(Default)]
104 #[non_exhaustive]
105 pub enum PadSpace {
106 #[default]
108 Pixel = 0, "pixel", PIXEL,
109 Normalized = 1, "normalized", NORMALIZED,
112 }
113 #[doc(hidden)]
114 pub mod pad_space_code;
115}
116
117impl Interpolation {
118 pub const fn supported(self) -> Self {
122 match self {
123 Self::Nearest => Self::Nearest,
124 Self::Bilinear | Self::Bicubic | Self::Area => Self::Bilinear,
125 }
126 }
127}
128
129macro_rules! serde_by_name {
130 ($($ty:ident),+) => {$(
131 impl serde::Serialize for $ty {
132 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
133 s.serialize_str(self.as_str())
134 }
135 }
136
137 impl<'de> serde::Deserialize<'de> for $ty {
138 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
139 let s = <std::borrow::Cow<'de, str> as serde::Deserialize>::deserialize(d)?;
140 Self::from_str_code(&s).ok_or_else(|| {
141 let names: Vec<&str> = Self::all().iter().map(|v| v.as_str()).collect();
142 serde::de::Error::custom(format!(
143 "unknown {} `{s}`, expected one of {names:?}",
144 stringify!($ty)
145 ))
146 })
147 }
148 }
149 )+};
150}
151
152serde_by_name!(
153 LetterboxPlacement,
154 NormalizationPreset,
155 FitMode,
156 Interpolation,
157 PadSpace
158);
159
160const IMAGENET_MEAN: [f32; 3] = [123.675, 116.28, 103.53];
161const IMAGENET_STD: [f32; 3] = [58.395, 57.12, 57.375];
162
163#[derive(Debug, Clone, Copy, PartialEq)]
169pub struct Normalization {
170 mean: [f32; 3],
171 std: [f32; 3],
172}
173
174impl Default for Normalization {
175 fn default() -> Self {
176 Self::preset(NormalizationPreset::Unsigned)
177 }
178}
179
180impl From<NormalizationPreset> for Normalization {
181 fn from(p: NormalizationPreset) -> Self {
182 Self::preset(p)
183 }
184}
185
186impl Normalization {
187 pub const fn preset(p: NormalizationPreset) -> Self {
189 let (mean, std) = match p {
190 NormalizationPreset::Unsigned => ([0.0; 3], [255.0; 3]),
191 NormalizationPreset::Signed => ([127.5; 3], [127.5; 3]),
192 NormalizationPreset::Raw => ([0.0; 3], [1.0; 3]),
193 NormalizationPreset::Imagenet => (IMAGENET_MEAN, IMAGENET_STD),
194 };
195 Self { mean, std }
196 }
197
198 pub const fn unsigned() -> Self {
200 Self::preset(NormalizationPreset::Unsigned)
201 }
202
203 pub const fn signed() -> Self {
205 Self::preset(NormalizationPreset::Signed)
206 }
207
208 pub const fn raw() -> Self {
210 Self::preset(NormalizationPreset::Raw)
211 }
212
213 pub const fn imagenet() -> Self {
215 Self::preset(NormalizationPreset::Imagenet)
216 }
217
218 pub fn mean_std(mean: &[f32], std: &[f32]) -> Result<Self> {
234 let count = |name: &str, v: &[f32]| {
235 broadcast_rgb(v).ok_or_else(|| {
236 Error::InvalidArgument(format!(
237 "normalization {name} needs 1 or 3 values, got {}",
238 v.len()
239 ))
240 })
241 };
242 let mean = count("mean", mean)?;
243 let std = count("std", std)?;
244 if mean.iter().any(|m| !m.is_finite()) {
245 return Err(Error::InvalidArgument(format!(
246 "normalization mean {mean:?} must be finite"
247 )));
248 }
249 if std.iter().any(|s| !s.is_finite() || *s <= 0.0) {
250 return Err(Error::InvalidArgument(format!(
251 "normalization std {std:?} must be finite and positive"
252 )));
253 }
254 Ok(Self { mean, std })
255 }
256
257 pub fn unit_mean_std(mean: &[f32], std: &[f32], scale: f64) -> Result<Self> {
271 let scaled =
272 |v: &[f32]| -> Vec<f32> { v.iter().map(|&x| (f64::from(x) * scale) as f32).collect() };
273 Self::mean_std(&scaled(mean), &scaled(std))
274 }
275
276 pub fn mean(&self) -> [f32; 3] {
278 self.mean
279 }
280
281 pub fn std(&self) -> [f32; 3] {
283 self.std
284 }
285
286 pub fn as_preset(&self) -> Option<NormalizationPreset> {
288 NormalizationPreset::all()
289 .iter()
290 .copied()
291 .find(|&p| Self::preset(p) == *self)
292 }
293}
294
295pub fn broadcast_rgb<T: Copy>(v: &[T]) -> Option<[T; 3]> {
305 match *v {
306 [x] => Some([x; 3]),
307 [r, g, b] => Some([r, g, b]),
308 _ => None,
309 }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
314pub enum Fit {
315 #[default]
317 Stretch,
318 Letterbox {
322 pad: [u8; 4],
324 },
325}
326
327pub fn letterbox_rect(
344 src_w: usize,
345 src_h: usize,
346 dst_w: usize,
347 dst_h: usize,
348 placement: LetterboxPlacement,
349) -> Region {
350 if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
351 return Region::new(0, 0, dst_w, dst_h);
352 }
353 let src_aspect = src_w as f64 / src_h as f64;
354 let dst_aspect = dst_w as f64 / dst_h as f64;
355 let (w, h) = if src_aspect > dst_aspect {
356 (dst_w, ((dst_w as f64 / src_aspect).round() as usize).max(1))
357 } else {
358 (((dst_h as f64 * src_aspect).round() as usize).max(1), dst_h)
359 };
360 let (x, y) = match placement {
361 LetterboxPlacement::Center => (dst_w.saturating_sub(w) / 2, dst_h.saturating_sub(h) / 2),
362 LetterboxPlacement::TopLeft => (0, 0),
363 };
364 Region::new(x, y, w, h)
365}
366
367#[derive(Debug, Clone, Copy, PartialEq)]
371pub struct ContentRegion {
372 pub pixels: Region,
374 pub normalized: [f32; 4],
376}
377
378impl ContentRegion {
379 pub fn new(pixels: Region, dst_w: usize, dst_h: usize) -> Self {
381 let (w, h) = (dst_w.max(1) as f32, dst_h.max(1) as f32);
382 Self {
383 pixels,
384 normalized: [
385 pixels.x as f32 / w,
386 pixels.y as f32 / h,
387 pixels.x.saturating_add(pixels.width) as f32 / w,
388 pixels.y.saturating_add(pixels.height) as f32 / h,
389 ],
390 }
391 }
392
393 pub fn full(dst_w: usize, dst_h: usize) -> Self {
395 Self::new(Region::new(0, 0, dst_w, dst_h), dst_w, dst_h)
396 }
397
398 pub fn is_full(&self) -> bool {
400 self.normalized == [0.0, 0.0, 1.0, 1.0]
401 }
402
403 pub fn letterboxed(&self) -> Option<[f32; 4]> {
406 (!self.is_full()).then_some(self.normalized)
407 }
408
409 pub fn unletter(&self, b: BoundingBox) -> BoundingBox {
412 crate::unletter_norm(b, self.normalized)
413 }
414}
415
416pub trait ContentRegionSource {
421 fn content_region(
429 &self,
430 src_w: usize,
431 src_h: usize,
432 dst_w: usize,
433 dst_h: usize,
434 ) -> Result<ContentRegion>;
435}
436
437#[derive(Debug, Clone, Copy, PartialEq)]
452#[non_exhaustive]
453pub struct PreprocessSpec {
454 pub normalization: Normalization,
456 pub fit: FitMode,
458 pub placement: LetterboxPlacement,
460 pub pad: [f32; 3],
462 pub pad_space: PadSpace,
464 pub interpolation: Interpolation,
466 pub size: Option<(usize, usize)>,
468}
469
470impl Default for PreprocessSpec {
471 fn default() -> Self {
472 Self {
473 normalization: Normalization::unsigned(),
474 fit: FitMode::Letterbox,
475 placement: LetterboxPlacement::Center,
476 pad: [114.0; 3],
477 pad_space: PadSpace::Pixel,
478 interpolation: Interpolation::Bilinear,
479 size: None,
480 }
481 }
482}
483
484impl PreprocessSpec {
485 pub fn new() -> Self {
487 Self::default()
488 }
489}
490
491impl ContentRegionSource for PreprocessSpec {
492 fn content_region(
493 &self,
494 src_w: usize,
495 src_h: usize,
496 dst_w: usize,
497 dst_h: usize,
498 ) -> Result<ContentRegion> {
499 Ok(match self.fit {
500 FitMode::Stretch => ContentRegion::full(dst_w, dst_h),
501 FitMode::Letterbox => ContentRegion::new(
502 letterbox_rect(src_w, src_h, dst_w, dst_h, self.placement),
503 dst_w,
504 dst_h,
505 ),
506 })
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn letterbox_rect_rounds_and_places() {
516 let c = LetterboxPlacement::Center;
517 let t = LetterboxPlacement::TopLeft;
518 assert_eq!(
520 letterbox_rect(641, 479, 640, 640, c),
521 Region::new(0, 81, 640, 478)
522 );
523 assert_eq!(
524 letterbox_rect(641, 479, 640, 640, t),
525 Region::new(0, 0, 640, 478)
526 );
527 assert_eq!(
528 letterbox_rect(479, 641, 640, 640, c),
529 Region::new(81, 0, 478, 640)
530 );
531 assert_eq!(
532 letterbox_rect(1, 3, 320, 256, c),
533 Region::new(117, 0, 85, 256)
534 );
535 assert_eq!(
536 letterbox_rect(3, 1, 320, 256, c),
537 Region::new(0, 74, 320, 107)
538 );
539 assert_eq!(
540 letterbox_rect(640, 640, 320, 320, c),
541 Region::new(0, 0, 320, 320)
542 );
543 assert_eq!(letterbox_rect(0, 5, 32, 16, c), Region::new(0, 0, 32, 16));
544 assert_eq!(
546 letterbox_rect(10000, 1, 64, 64, t),
547 Region::new(0, 0, 64, 1)
548 );
549 }
550
551 #[test]
552 fn content_region_normalizes_pixels() {
553 let r = ContentRegion::new(Region::new(0, 140, 640, 360), 640, 640);
554 assert_eq!(r.normalized, [0.0, 0.21875, 1.0, 0.78125]);
555 assert!(!r.is_full());
556 assert_eq!(r.letterboxed(), Some(r.normalized));
557 assert!(ContentRegion::full(32, 16).is_full());
558 assert_eq!(ContentRegion::full(32, 16).letterboxed(), None);
559 }
560
561 #[test]
562 fn interpolation_codes_keep_c_values() {
563 assert_eq!(Interpolation::Bilinear.code(), 0);
564 assert_eq!(Interpolation::Nearest.code(), 1);
565 assert_eq!(Interpolation::Bicubic.supported(), Interpolation::Bilinear);
566 assert_eq!(Interpolation::Area.supported(), Interpolation::Bilinear);
567 assert_eq!(Interpolation::Nearest.supported(), Interpolation::Nearest);
568 }
569
570 #[test]
571 fn vocab_serde_round_trips() {
572 for &p in LetterboxPlacement::all() {
573 let s = serde_json::to_string(&p).unwrap();
574 assert_eq!(serde_json::from_str::<LetterboxPlacement>(&s).unwrap(), p);
575 }
576 assert_eq!(
577 serde_json::to_string(&PadSpace::Normalized).unwrap(),
578 "\"normalized\""
579 );
580 assert_eq!(
581 serde_json::to_string(&LetterboxPlacement::TopLeft).unwrap(),
582 "\"topleft\""
583 );
584 assert!(serde_json::from_str::<LetterboxPlacement>("\"top_left\"").is_err());
585 let e = serde_json::from_str::<Interpolation>("\"lanczos\"").unwrap_err();
586 assert!(e.to_string().contains("bicubic"), "{e}");
587 }
588
589 #[test]
590 fn spec_content_region_follows_fit() {
591 let mut spec = PreprocessSpec::new();
592 let lb = spec.content_region(1280, 720, 640, 640).unwrap();
593 assert_eq!(lb.pixels, Region::new(0, 140, 640, 360));
594 spec.fit = FitMode::Stretch;
595 assert!(spec.content_region(1280, 720, 640, 640).unwrap().is_full());
596 }
597
598 #[test]
599 fn normalization_finds_its_preset() {
600 assert_eq!(
601 Normalization::imagenet().as_preset(),
602 Some(NormalizationPreset::Imagenet)
603 );
604 let custom = Normalization::mean_std(&[1.0], &[2.0]).unwrap();
605 assert_eq!(custom.as_preset(), None);
606 assert!(Normalization::mean_std(&[0.0, 0.0], &[1.0]).is_err());
607 assert!(Normalization::mean_std(&[f32::NAN], &[1.0]).is_err());
608 }
609}