1use crate::{Crop, Error, Flip, Region, Result, Rotation};
8use edgefirst_tensor::{DType, PixelFormat, QuantMode, Quantization};
9
10edgefirst_tensor::ef_vocabulary! {
11 #[derive(Default)]
15 pub enum LetterboxPlacement {
16 #[default]
18 Center = 0, "center", CENTER,
19 TopLeft = 1, "top_left", TOP_LEFT,
21 }
22 #[doc(hidden)]
23 pub mod letterbox_placement_code;
24}
25
26edgefirst_tensor::ef_vocabulary! {
27 #[derive(Default)]
31 pub enum NormalizationPreset {
32 #[default]
34 Unsigned = 0, "unsigned", UNSIGNED,
35 Signed = 1, "signed", SIGNED,
37 Raw = 2, "raw", RAW,
39 Imagenet = 3, "imagenet", IMAGENET,
41 }
42 #[doc(hidden)]
43 pub mod normalization_code;
44}
45
46const IMAGENET_MEAN: [f32; 3] = [123.675, 116.28, 103.53];
47const IMAGENET_STD: [f32; 3] = [58.395, 57.12, 57.375];
48
49#[derive(Debug, Clone, Copy, PartialEq)]
55pub struct Normalization {
56 mean: [f32; 3],
57 std: [f32; 3],
58}
59
60impl Default for Normalization {
61 fn default() -> Self {
62 Self::preset(NormalizationPreset::Unsigned)
63 }
64}
65
66impl From<NormalizationPreset> for Normalization {
67 fn from(p: NormalizationPreset) -> Self {
68 Self::preset(p)
69 }
70}
71
72impl Normalization {
73 pub const fn preset(p: NormalizationPreset) -> Self {
75 let (mean, std) = match p {
76 NormalizationPreset::Unsigned => ([0.0; 3], [255.0; 3]),
77 NormalizationPreset::Signed => ([127.5; 3], [127.5; 3]),
78 NormalizationPreset::Raw => ([0.0; 3], [1.0; 3]),
79 NormalizationPreset::Imagenet => (IMAGENET_MEAN, IMAGENET_STD),
80 };
81 Self { mean, std }
82 }
83
84 pub const fn unsigned() -> Self {
86 Self::preset(NormalizationPreset::Unsigned)
87 }
88
89 pub const fn signed() -> Self {
91 Self::preset(NormalizationPreset::Signed)
92 }
93
94 pub const fn raw() -> Self {
96 Self::preset(NormalizationPreset::Raw)
97 }
98
99 pub const fn imagenet() -> Self {
101 Self::preset(NormalizationPreset::Imagenet)
102 }
103
104 pub fn mean_std(mean: &[f32], std: &[f32]) -> Result<Self> {
107 let mean = broadcast3("mean", mean)?;
108 let std = broadcast3("std", std)?;
109 if mean.iter().any(|m| !m.is_finite()) {
110 return Err(Error::InvalidOptions(format!(
111 "normalization mean {mean:?} must be finite"
112 )));
113 }
114 if std.iter().any(|s| !s.is_finite() || *s == 0.0) {
115 return Err(Error::InvalidOptions(format!(
116 "normalization std {std:?} must be finite and non-zero"
117 )));
118 }
119 Ok(Self { mean, std })
120 }
121
122 pub fn mean(&self) -> [f32; 3] {
124 self.mean
125 }
126
127 pub fn std(&self) -> [f32; 3] {
129 self.std
130 }
131}
132
133fn broadcast3(name: &str, v: &[f32]) -> Result<[f32; 3]> {
134 match v {
135 [x] => Ok([*x; 3]),
136 [r, g, b] => Ok([*r, *g, *b]),
137 _ => Err(Error::InvalidOptions(format!(
138 "normalization {name} needs 1 or 3 values, got {}",
139 v.len()
140 ))),
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Default)]
159pub struct ConvertOptions {
160 rotation: Rotation,
161 flip: Flip,
162 crop: Crop,
163 placement: LetterboxPlacement,
164 pad_output: Option<[f32; 4]>,
165 normalization: Normalization,
166}
167
168impl ConvertOptions {
169 pub fn new() -> Self {
171 Self::default()
172 }
173
174 pub fn rotation(mut self, rotation: Rotation) -> Self {
176 self.rotation = rotation;
177 self
178 }
179
180 pub fn flip(mut self, flip: Flip) -> Self {
182 self.flip = flip;
183 self
184 }
185
186 pub fn crop(mut self, crop: Crop) -> Self {
188 self.crop = crop;
189 self
190 }
191
192 pub fn source(mut self, source: Option<Region>) -> Self {
194 self.crop.source = source;
195 self
196 }
197
198 pub fn letterbox(mut self, pad: [u8; 4]) -> Self {
201 self.crop.fit = crate::Fit::Letterbox { pad };
202 self
203 }
204
205 pub fn placement(mut self, placement: LetterboxPlacement) -> Self {
207 self.placement = placement;
208 self
209 }
210
211 pub fn pad_output(mut self, pad: [f32; 4]) -> Self {
215 self.pad_output = Some(pad);
216 self
217 }
218
219 pub fn normalization(mut self, normalization: impl Into<Normalization>) -> Self {
221 self.normalization = normalization.into();
222 self
223 }
224
225 pub fn get_rotation(&self) -> Rotation {
227 self.rotation
228 }
229
230 pub fn get_flip(&self) -> Flip {
232 self.flip
233 }
234
235 pub fn get_crop(&self) -> Crop {
237 self.crop
238 }
239
240 pub fn get_placement(&self) -> LetterboxPlacement {
242 self.placement
243 }
244
245 pub fn get_pad_output(&self) -> Option<[f32; 4]> {
247 self.pad_output
248 }
249
250 pub fn get_normalization(&self) -> Normalization {
252 self.normalization
253 }
254}
255
256#[cfg(feature = "decode")]
257impl ConvertOptions {
258 pub fn with_preprocessing(
265 mut self,
266 pre: &edgefirst_decoder::schema::Preprocessing,
267 ) -> Result<Self> {
268 use edgefirst_decoder::schema::{LetterboxPlacement as P, PadSpace, ResizeMode};
269 if let Some(n) = &pre.normalization {
270 let (mean, std) = n
271 .mean_std(3)
272 .map_err(|e| Error::InvalidOptions(e.to_string()))?;
273 self.normalization = Normalization::mean_std(&mean, &std)?;
274 }
275 let letterbox = match pre.resize {
276 Some(ResizeMode::Stretch) => false,
277 Some(ResizeMode::Letterbox) => true,
278 Some(other) => {
279 return Err(Error::NotSupported(format!("resize mode {other:?}")));
280 }
281 None => pre.letterbox.is_some(),
282 };
283 if pre.resize == Some(ResizeMode::Stretch) {
284 self.crop.fit = crate::Fit::Stretch;
285 }
286 if !letterbox {
287 return Ok(self);
288 }
289 let lb = pre.letterbox.clone().unwrap_or_default();
290 self.placement = match lb.placement {
291 None | Some(P::Center) => LetterboxPlacement::Center,
292 Some(P::TopLeft) => LetterboxPlacement::TopLeft,
293 Some(other) => {
294 return Err(Error::NotSupported(format!(
295 "letterbox placement {other:?}"
296 )));
297 }
298 };
299 let pad: [f32; 3] = match lb.pad.as_deref() {
300 None => [114.0; 3],
301 Some(p) => broadcast3("letterbox pad", p)?,
302 };
303 match lb.pad_space {
304 None | Some(PadSpace::Pixel) => {
305 let [r, g, b] = pad.map(|v| v.round().clamp(0.0, 255.0) as u8);
306 self.crop.fit = crate::Fit::Letterbox {
307 pad: [r, g, b, 255],
308 };
309 self.pad_output = None;
310 }
311 Some(PadSpace::Normalized) => {
312 self.crop.fit = crate::Fit::Letterbox {
313 pad: [0, 0, 0, 255],
314 };
315 self.pad_output = Some([pad[0], pad[1], pad[2], 1.0]);
316 }
317 Some(other) => {
318 return Err(Error::NotSupported(format!(
319 "letterbox pad space {other:?}"
320 )));
321 }
322 }
323 Ok(self)
324 }
325}
326
327pub(crate) fn output_map_for(
331 options: &ConvertOptions,
332 dst: &edgefirst_tensor::TensorDyn,
333 letterboxed: bool,
334) -> Result<Option<(OutputMap, Option<[f32; 4]>)>> {
335 let fmt = dst.format().ok_or(Error::NotAnImage)?;
336 let mappable = matches!(
337 fmt.layout(),
338 edgefirst_tensor::PixelLayout::Packed | edgefirst_tensor::PixelLayout::Planar
339 ) && !fmt.is_yuv();
340 let pad_output = options.pad_output.filter(|_| letterboxed);
341 if !mappable {
342 if options.normalization != Normalization::default() || pad_output.is_some() {
343 return Err(Error::NotSupported(format!(
344 "normalization and output-domain padding do not apply to {fmt:?}"
345 )));
346 }
347 return Ok(None);
348 }
349 let map = OutputMap::resolve(&options.normalization, dst.dtype(), fmt, dst.quantization())?;
350 if map.is_default && pad_output.is_none() {
351 return Ok(None);
352 }
353 Ok(Some((map, pad_output)))
354}
355
356#[derive(Debug, Clone, Copy, PartialEq)]
367pub(crate) struct OutputMap {
368 pub(crate) scale: [f32; 4],
370 pub(crate) offset: [f32; 4],
371 pub(crate) zero_point: [f32; 4],
372 pub(crate) bounds: Option<[(f32, f32); 4]>,
374 pub(crate) quant_scale: [f32; 4],
377 pub(crate) is_default: bool,
379}
380
381impl OutputMap {
382 pub(crate) fn resolve(
387 normalization: &Normalization,
388 dtype: DType,
389 format: PixelFormat,
390 quantization: Option<&Quantization>,
391 ) -> Result<Self> {
392 let plain = *normalization == Normalization::default();
393 let order = channel_order(format);
394 let (lo, hi, implicit_zp) = match dtype {
395 DType::F32 | DType::F16 => {
396 let (scale, offset) = channel_affine(normalization, order, [1.0; 4]);
397 return Ok(Self {
398 scale,
399 offset,
400 zero_point: [0.0; 4],
401 bounds: None,
402 quant_scale: [1.0; 4],
403 is_default: plain,
404 });
405 }
406 DType::U8 => (0.0, 255.0, 0),
407 DType::I8 => (-128.0, 127.0, -128),
408 other => {
409 return Err(Error::NotSupported(format!(
410 "convert does not write {other:?} destinations"
411 )))
412 }
413 };
414 let (qscale, qzp) = match quantization {
415 None if plain => ([1.0 / 255.0; 4], [implicit_zp as f32; 4]),
416 None => {
417 return Err(Error::InvalidOptions(format!(
418 "a {dtype:?} destination needs quantization to apply a \
419 non-default normalization"
420 )))
421 }
422 Some(q) => quant_per_channel(q, format, format.channels())?,
423 };
424 let (mut scale, mut offset) = channel_affine(normalization, order, qscale);
425 let mut zero_point = qzp;
426 let mut quant_scale = qscale;
427 let mut bounds = [(lo, hi); 4];
428 for (c, &src) in order.iter().enumerate() {
429 if src == 3 {
430 (scale[c], offset[c], zero_point[c], quant_scale[c]) = (1.0, 0.0, 0.0, 1.0);
431 bounds[c] = (0.0, 255.0);
432 }
433 }
434 let is_default = plain
435 && qscale.iter().all(|s| *s == 1.0 / 255.0)
436 && qzp.iter().all(|z| *z == implicit_zp as f32);
437 Ok(Self {
438 scale,
439 offset,
440 zero_point,
441 bounds: Some(bounds),
442 quant_scale,
443 is_default,
444 })
445 }
446
447 pub(crate) fn apply(&self, c: usize, x: u8) -> f32 {
449 let y = x as f32 * self.scale[c] + self.offset[c];
450 match self.bounds {
451 Some(b) => ((y + 0.5).floor() + self.zero_point[c]).clamp(b[c].0, b[c].1),
452 None => y,
453 }
454 }
455
456 pub(crate) fn apply_output(&self, c: usize, v: f32) -> f32 {
458 match self.bounds {
459 Some(b) => {
460 ((v / self.quant_scale[c] + 0.5).floor() + self.zero_point[c]).clamp(b[c].0, b[c].1)
461 }
462 None => v,
463 }
464 }
465
466 #[cfg_attr(not(feature = "opengl"), allow(dead_code))]
470 pub(crate) fn sample_for(&self, c: usize, target: f32) -> Option<u8> {
471 self.bounds?;
472 (0..=255u8)
473 .filter(|&x| self.apply(c, x) == target)
474 .min_by(|&a, &b| {
475 let off = |x: u8| {
476 let y = x as f32 * self.scale[c] + self.offset[c];
477 ((y - y.floor()) - 0.5).abs()
478 };
479 off(b).total_cmp(&off(a))
480 })
481 }
482
483 pub(crate) fn pad_values(&self, pad: [u8; 4], pad_output: Option<[f32; 4]>) -> [f32; 4] {
486 std::array::from_fn(|c| match pad_output {
487 Some(p) => self.apply_output(c, p[c]),
488 None => self.apply(c, pad[c]),
489 })
490 }
491}
492
493fn channel_order(format: PixelFormat) -> [usize; 4] {
496 match format {
497 PixelFormat::Bgra => [2, 1, 0, 3],
498 PixelFormat::Grey => [0, 3, 3, 3],
499 _ => [0, 1, 2, 3],
500 }
501}
502
503fn channel_affine(n: &Normalization, order: [usize; 4], qscale: [f32; 4]) -> ([f32; 4], [f32; 4]) {
507 let mut scale = [0.0; 4];
508 let mut offset = [0.0; 4];
509 for (c, &src) in order.iter().enumerate() {
510 let (mean, std) = if src == 3 {
511 (0.0, 255.0)
512 } else {
513 (n.mean[src] as f64, n.std[src] as f64)
514 };
515 let qs = qscale[c] as f64;
516 scale[c] = (1.0 / (std * qs)) as f32;
517 offset[c] = (-mean / (std * qs)) as f32;
518 }
519 (scale, offset)
520}
521
522fn quant_per_channel(
524 q: &Quantization,
525 format: PixelFormat,
526 channels: usize,
527) -> Result<([f32; 4], [f32; 4])> {
528 let per_tensor = |s: f32, z: i32| ([s; 4], [z as f32; 4]);
529 let (s, z) = match q.mode() {
530 QuantMode::PerTensorSymmetric { scale } => per_tensor(scale, 0),
531 QuantMode::PerTensor { scale, zero_point } => per_tensor(scale, zero_point),
532 QuantMode::PerChannelSymmetric { scales, .. } if scales.len() == channels => {
533 let mut s = [scales[0]; 4];
534 s[..channels].copy_from_slice(scales);
535 (s, [0.0; 4])
536 }
537 QuantMode::PerChannel {
538 scales,
539 zero_points,
540 ..
541 } if scales.len() == channels => {
542 let mut s = [scales[0]; 4];
543 let mut z = [zero_points[0] as f32; 4];
544 s[..channels].copy_from_slice(scales);
545 for (d, zp) in z.iter_mut().zip(zero_points) {
546 *d = *zp as f32;
547 }
548 (s, z)
549 }
550 _ => {
551 return Err(Error::InvalidOptions(format!(
552 "{format:?} destination quantization must be per-tensor or \
553 have one scale per channel ({channels})"
554 )))
555 }
556 };
557 if s.iter().any(|v| !v.is_finite() || *v <= 0.0) {
558 return Err(Error::InvalidOptions(format!(
559 "destination quantization scale {s:?} must be positive and finite"
560 )));
561 }
562 Ok((s, z))
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
570 fn default_float_map_is_divide_by_255() {
571 let m = OutputMap::resolve(
572 &Normalization::default(),
573 DType::F32,
574 PixelFormat::Rgb,
575 None,
576 )
577 .unwrap();
578 assert!(m.is_default);
579 for x in [0u8, 1, 128, 255] {
580 assert_eq!(m.apply(0, x), x as f32 / 255.0);
581 }
582 }
583
584 #[test]
585 fn implicit_int_quantization_reproduces_plain_converts() {
586 let u = OutputMap::resolve(&Normalization::default(), DType::U8, PixelFormat::Rgb, None)
587 .unwrap();
588 let i = OutputMap::resolve(&Normalization::default(), DType::I8, PixelFormat::Rgb, None)
589 .unwrap();
590 assert!(u.is_default && i.is_default);
591 for x in 0..=255u8 {
592 assert_eq!(u.apply(1, x), x as f32);
593 assert_eq!(i.apply(1, x) as i8 as u8, x ^ 0x80);
594 }
595 }
596
597 #[test]
598 fn imagenet_matches_reference_in_bgra_order() {
599 let n = Normalization::imagenet();
600 let m = OutputMap::resolve(&n, DType::F32, PixelFormat::Bgra, None).unwrap();
601 assert!(!m.is_default);
602 let want_b = (200.0 - 103.53) / 57.375;
603 assert!((m.apply(0, 200) - want_b).abs() < 1e-5);
604 assert!((m.apply(3, 255) - 1.0).abs() < 1e-6, "alpha stays unsigned");
605 }
606
607 #[test]
608 fn int_normalization_needs_quantization() {
609 let e = OutputMap::resolve(&Normalization::signed(), DType::I8, PixelFormat::Rgb, None);
610 assert!(matches!(e, Err(Error::InvalidOptions(_))));
611 let q = Quantization::per_tensor(1.0 / 128.0, 0);
612 let m = OutputMap::resolve(
613 &Normalization::signed(),
614 DType::I8,
615 PixelFormat::Rgb,
616 Some(&q),
617 )
618 .unwrap();
619 assert_eq!(m.apply(0, 0), -128.0);
620 assert_eq!(m.apply(0, 255), 127.0, "128 clamps to the i8 range");
621 assert_eq!(m.apply(0, 191), 64.0);
622 }
623
624 #[test]
625 fn mean_std_broadcasts_and_validates() {
626 assert_eq!(
627 Normalization::mean_std(&[0.0], &[255.0]).unwrap(),
628 Normalization::unsigned()
629 );
630 assert!(Normalization::mean_std(&[0.0, 0.0], &[1.0]).is_err());
631 assert!(Normalization::mean_std(&[0.0], &[0.0]).is_err());
632 assert!(Normalization::mean_std(&[f32::NAN], &[1.0]).is_err());
633 }
634
635 #[test]
636 fn presets_parse_from_their_names() {
637 for &p in NormalizationPreset::all() {
638 assert_eq!(NormalizationPreset::from_str_code(p.as_str()), Some(p));
639 }
640 assert_eq!(
641 LetterboxPlacement::from_str_code("top_left"),
642 Some(LetterboxPlacement::TopLeft)
643 );
644 }
645
646 #[cfg(feature = "decode")]
647 #[test]
648 fn segformer_preprocessing_maps_to_options() {
649 let pre: edgefirst_decoder::schema::Preprocessing = serde_json::from_str(
650 r#"{"normalization": "imagenet", "resize": "letterbox",
651 "letterbox": {"placement": "top_left", "pad": [0.0], "pad_space": "normalized"}}"#,
652 )
653 .unwrap();
654 let o = ConvertOptions::new().with_preprocessing(&pre).unwrap();
655 assert_eq!(o.get_normalization(), Normalization::imagenet());
656 assert_eq!(o.get_placement(), LetterboxPlacement::TopLeft);
657 assert_eq!(o.get_pad_output(), Some([0.0, 0.0, 0.0, 1.0]));
658 assert!(matches!(o.get_crop().fit, crate::Fit::Letterbox { .. }));
659
660 let pre: edgefirst_decoder::schema::Preprocessing =
661 serde_json::from_str(r#"{"resize": "letterbox"}"#).unwrap();
662 let o = ConvertOptions::new().with_preprocessing(&pre).unwrap();
663 assert_eq!(
664 o.get_crop().fit,
665 crate::Fit::Letterbox {
666 pad: [114, 114, 114, 255]
667 }
668 );
669 assert_eq!(o.get_normalization(), Normalization::unsigned());
670 }
671
672 #[test]
673 fn an_output_pad_has_an_equivalent_sample_when_reachable() {
674 let q = Quantization::per_tensor(0.0187, -14);
675 let m = OutputMap::resolve(
676 &Normalization::imagenet(),
677 DType::I8,
678 PixelFormat::Rgb,
679 Some(&q),
680 )
681 .unwrap();
682 let pad = m.pad_values([0; 4], Some([0.0; 4]));
683 for (c, &want) in pad.iter().enumerate().take(3) {
684 let x = m.sample_for(c, want).expect("0.0 is reachable");
685 assert_eq!(m.apply(c, x), want);
686 }
687 let q = Quantization::per_tensor(1.0 / 512.0, 0);
690 let m = OutputMap::resolve(
691 &Normalization::unsigned(),
692 DType::U8,
693 PixelFormat::Rgb,
694 Some(&q),
695 )
696 .unwrap();
697 assert_eq!(m.sample_for(0, 1.0), None);
698 }
699}