1use crate::{Crop, Error, Flip, Region, Result, Rotation};
9use edgefirst_tensor::{
10 ContentRegion, ContentRegionSource, DType, FitMode, Interpolation, LetterboxPlacement,
11 Normalization, PadSpace, PixelFormat, PreprocessSpec, QuantMode, Quantization,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Default)]
30pub struct ConvertOptions {
31 rotation: Rotation,
32 flip: Flip,
33 crop: Crop,
34 placement: LetterboxPlacement,
35 pad_output: Option<[f32; 4]>,
36 normalization: Normalization,
37 interpolation: Option<Interpolation>,
38}
39
40impl ConvertOptions {
41 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn rotation(mut self, rotation: Rotation) -> Self {
48 self.rotation = rotation;
49 self
50 }
51
52 pub fn flip(mut self, flip: Flip) -> Self {
54 self.flip = flip;
55 self
56 }
57
58 pub fn crop(mut self, crop: Crop) -> Self {
60 self.crop = crop;
61 self
62 }
63
64 pub fn source(mut self, source: Option<Region>) -> Self {
66 self.crop.source = source;
67 self
68 }
69
70 pub fn letterbox(mut self, pad: [u8; 4]) -> Self {
73 self.crop.fit = crate::Fit::Letterbox { pad };
74 self
75 }
76
77 pub fn placement(mut self, placement: LetterboxPlacement) -> Self {
79 self.placement = placement;
80 self
81 }
82
83 pub fn pad_output(mut self, pad: [f32; 4]) -> Self {
87 self.pad_output = Some(pad);
88 self
89 }
90
91 pub fn normalization(mut self, normalization: impl Into<Normalization>) -> Self {
93 self.normalization = normalization.into();
94 self
95 }
96
97 pub fn interpolation(mut self, interpolation: Interpolation) -> Self {
104 self.interpolation = Some(interpolation);
105 self
106 }
107
108 pub fn get_rotation(&self) -> Rotation {
110 self.rotation
111 }
112
113 pub fn get_flip(&self) -> Flip {
115 self.flip
116 }
117
118 pub fn get_crop(&self) -> Crop {
120 self.crop
121 }
122
123 pub fn get_placement(&self) -> LetterboxPlacement {
125 self.placement
126 }
127
128 pub fn get_pad_output(&self) -> Option<[f32; 4]> {
130 self.pad_output
131 }
132
133 pub fn get_normalization(&self) -> Normalization {
135 self.normalization
136 }
137
138 pub fn get_interpolation(&self) -> Option<Interpolation> {
140 self.interpolation
141 }
142
143 pub(crate) fn with_interpolation(mut self, mode: Option<Interpolation>) -> Self {
146 self.interpolation = mode;
147 self
148 }
149}
150
151impl ConvertOptions {
152 pub fn from_spec(spec: &PreprocessSpec) -> Result<Self> {
174 Self::new().with_spec(spec)
175 }
176
177 pub fn with_spec(mut self, spec: &PreprocessSpec) -> Result<Self> {
184 if spec.pad.iter().any(|v| !v.is_finite()) {
185 return Err(Error::InvalidOptions(format!(
186 "letterbox pad {:?} must be finite",
187 spec.pad
188 )));
189 }
190 self.normalization = spec.normalization;
191 self.interpolation = Some(spec.interpolation);
192 self.pad_output = None;
193 self.crop.fit = if spec.fit != FitMode::Letterbox {
195 crate::Fit::Stretch
196 } else if spec.pad_space == PadSpace::Normalized {
197 let [r, g, b] = spec.pad;
198 self.pad_output = Some([r, g, b, 1.0]);
199 crate::Fit::Letterbox {
200 pad: [0, 0, 0, 255],
201 }
202 } else {
203 if spec.pad.iter().any(|v| !(0.0..=255.0).contains(v)) {
204 return Err(Error::InvalidOptions(format!(
205 "pixel letterbox pad {:?} must be within 0-255",
206 spec.pad
207 )));
208 }
209 let [r, g, b] = spec.pad.map(|v| v.round() as u8);
210 crate::Fit::Letterbox {
211 pad: [r, g, b, 255],
212 }
213 };
214 if spec.fit == FitMode::Letterbox {
215 self.placement = spec.placement;
216 }
217 Ok(self)
218 }
219
220 pub fn content_region(
240 &self,
241 src_w: usize,
242 src_h: usize,
243 dst_w: usize,
244 dst_h: usize,
245 ) -> Result<ContentRegion> {
246 let resolved = self
247 .crop
248 .resolve(src_w, src_h, dst_w, dst_h, self.placement)?;
249 Ok(match resolved.dst_rect {
250 Some(r) => {
251 ContentRegion::new(Region::new(r.left, r.top, r.width, r.height), dst_w, dst_h)
252 }
253 None => ContentRegion::full(dst_w, dst_h),
254 })
255 }
256}
257
258impl ContentRegionSource for ConvertOptions {
259 fn content_region(
260 &self,
261 src_w: usize,
262 src_h: usize,
263 dst_w: usize,
264 dst_h: usize,
265 ) -> edgefirst_tensor::Result<ContentRegion> {
266 ConvertOptions::content_region(self, src_w, src_h, dst_w, dst_h)
267 .map_err(|e| edgefirst_tensor::Error::InvalidArgument(e.to_string()))
268 }
269}
270
271pub(crate) fn output_map_for(
275 options: &ConvertOptions,
276 dst: &edgefirst_tensor::TensorDyn,
277 letterboxed: bool,
278) -> Result<Option<(OutputMap, Option<[f32; 4]>)>> {
279 let fmt = dst.format().ok_or(Error::NotAnImage)?;
280 let mappable = matches!(
281 fmt.layout(),
282 edgefirst_tensor::PixelLayout::Packed | edgefirst_tensor::PixelLayout::Planar
283 ) && !fmt.is_yuv();
284 let pad_output = options.pad_output.filter(|_| letterboxed);
285 if !mappable {
286 if options.normalization != Normalization::default() || pad_output.is_some() {
287 return Err(Error::NotSupported(format!(
288 "normalization and output-domain padding do not apply to {fmt:?}"
289 )));
290 }
291 return Ok(None);
292 }
293 let map = OutputMap::resolve(&options.normalization, dst.dtype(), fmt, dst.quantization())?;
294 if map.is_default && pad_output.is_none() {
295 return Ok(None);
296 }
297 Ok(Some((map, pad_output)))
298}
299
300#[derive(Debug, Clone, Copy, PartialEq)]
311pub(crate) struct OutputMap {
312 pub(crate) scale: [f32; 4],
314 pub(crate) offset: [f32; 4],
315 pub(crate) zero_point: [f32; 4],
316 pub(crate) bounds: Option<[(f32, f32); 4]>,
318 pub(crate) quant_scale: [f32; 4],
321 pub(crate) is_default: bool,
323}
324
325impl OutputMap {
326 pub(crate) fn resolve(
331 normalization: &Normalization,
332 dtype: DType,
333 format: PixelFormat,
334 quantization: Option<&Quantization>,
335 ) -> Result<Self> {
336 let plain = *normalization == Normalization::default();
337 let order = channel_order(format);
338 let (lo, hi, implicit_zp) = match dtype {
339 DType::F32 | DType::F16 => {
340 let (scale, offset) = channel_affine(normalization, order, [1.0; 4]);
341 return Ok(Self {
342 scale,
343 offset,
344 zero_point: [0.0; 4],
345 bounds: None,
346 quant_scale: [1.0; 4],
347 is_default: plain,
348 });
349 }
350 DType::U8 => (0.0, 255.0, 0),
351 DType::I8 => (-128.0, 127.0, -128),
352 other => {
353 return Err(Error::NotSupported(format!(
354 "convert does not write {other:?} destinations"
355 )))
356 }
357 };
358 let (qscale, qzp) = match quantization {
359 None if plain => ([1.0 / 255.0; 4], [implicit_zp as f32; 4]),
360 None => {
361 return Err(Error::InvalidOptions(format!(
362 "a {dtype:?} destination needs quantization to apply a \
363 non-default normalization"
364 )))
365 }
366 Some(q) => quant_per_channel(q, format, format.channels())?,
367 };
368 let (mut scale, mut offset) = channel_affine(normalization, order, qscale);
369 let mut zero_point = qzp;
370 let mut quant_scale = qscale;
371 let mut bounds = [(lo, hi); 4];
372 for (c, &src) in order.iter().enumerate() {
373 if src == 3 {
374 (scale[c], offset[c], zero_point[c], quant_scale[c]) = (1.0, 0.0, 0.0, 1.0);
375 bounds[c] = (0.0, 255.0);
376 }
377 }
378 let is_default = plain
379 && qscale.iter().all(|s| *s == 1.0 / 255.0)
380 && qzp.iter().all(|z| *z == implicit_zp as f32);
381 Ok(Self {
382 scale,
383 offset,
384 zero_point,
385 bounds: Some(bounds),
386 quant_scale,
387 is_default,
388 })
389 }
390
391 pub(crate) fn apply(&self, c: usize, x: u8) -> f32 {
393 let y = x as f32 * self.scale[c] + self.offset[c];
394 match self.bounds {
395 Some(b) => ((y + 0.5).floor() + self.zero_point[c]).clamp(b[c].0, b[c].1),
396 None => y,
397 }
398 }
399
400 pub(crate) fn apply_output(&self, c: usize, v: f32) -> f32 {
402 match self.bounds {
403 Some(b) => {
404 ((v / self.quant_scale[c] + 0.5).floor() + self.zero_point[c]).clamp(b[c].0, b[c].1)
405 }
406 None => v,
407 }
408 }
409
410 #[cfg_attr(not(feature = "opengl"), allow(dead_code))]
414 pub(crate) fn sample_for(&self, c: usize, target: f32) -> Option<u8> {
415 self.bounds?;
416 (0..=255u8)
417 .filter(|&x| self.apply(c, x) == target)
418 .min_by(|&a, &b| {
419 let off = |x: u8| {
420 let y = x as f32 * self.scale[c] + self.offset[c];
421 ((y - y.floor()) - 0.5).abs()
422 };
423 off(b).total_cmp(&off(a))
424 })
425 }
426
427 pub(crate) fn pad_values(&self, pad: [u8; 4], pad_output: Option<[f32; 4]>) -> [f32; 4] {
430 std::array::from_fn(|c| match pad_output {
431 Some(p) => self.apply_output(c, p[c]),
432 None => self.apply(c, pad[c]),
433 })
434 }
435}
436
437fn channel_order(format: PixelFormat) -> [usize; 4] {
440 match format {
441 PixelFormat::Bgra => [2, 1, 0, 3],
442 PixelFormat::Grey => [0, 3, 3, 3],
443 _ => [0, 1, 2, 3],
444 }
445}
446
447fn channel_affine(n: &Normalization, order: [usize; 4], qscale: [f32; 4]) -> ([f32; 4], [f32; 4]) {
451 let mut scale = [0.0; 4];
452 let mut offset = [0.0; 4];
453 for (c, &src) in order.iter().enumerate() {
454 let (mean, std) = if src == 3 {
455 (0.0, 255.0)
456 } else {
457 (n.mean()[src] as f64, n.std()[src] as f64)
458 };
459 let qs = qscale[c] as f64;
460 scale[c] = (1.0 / (std * qs)) as f32;
461 offset[c] = (-mean / (std * qs)) as f32;
462 }
463 (scale, offset)
464}
465
466fn quant_per_channel(
468 q: &Quantization,
469 format: PixelFormat,
470 channels: usize,
471) -> Result<([f32; 4], [f32; 4])> {
472 let per_tensor = |s: f32, z: i32| ([s; 4], [z as f32; 4]);
473 let (s, z) = match q.mode() {
474 QuantMode::PerTensorSymmetric { scale } => per_tensor(scale, 0),
475 QuantMode::PerTensor { scale, zero_point } => per_tensor(scale, zero_point),
476 QuantMode::PerChannelSymmetric { scales, .. } if scales.len() == channels => {
477 let mut s = [scales[0]; 4];
478 s[..channels].copy_from_slice(scales);
479 (s, [0.0; 4])
480 }
481 QuantMode::PerChannel {
482 scales,
483 zero_points,
484 ..
485 } if scales.len() == channels => {
486 let mut s = [scales[0]; 4];
487 let mut z = [zero_points[0] as f32; 4];
488 s[..channels].copy_from_slice(scales);
489 for (d, zp) in z.iter_mut().zip(zero_points) {
490 *d = *zp as f32;
491 }
492 (s, z)
493 }
494 _ => {
495 return Err(Error::InvalidOptions(format!(
496 "{format:?} destination quantization must be per-tensor or \
497 have one scale per channel ({channels})"
498 )))
499 }
500 };
501 if s.iter().any(|v| !v.is_finite() || *v <= 0.0) {
502 return Err(Error::InvalidOptions(format!(
503 "destination quantization scale {s:?} must be positive and finite"
504 )));
505 }
506 Ok((s, z))
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
514 fn default_float_map_is_divide_by_255() {
515 let m = OutputMap::resolve(
516 &Normalization::default(),
517 DType::F32,
518 PixelFormat::Rgb,
519 None,
520 )
521 .unwrap();
522 assert!(m.is_default);
523 for x in [0u8, 1, 128, 255] {
524 assert_eq!(m.apply(0, x), x as f32 / 255.0);
525 }
526 }
527
528 #[test]
529 fn implicit_int_quantization_reproduces_plain_converts() {
530 let u = OutputMap::resolve(&Normalization::default(), DType::U8, PixelFormat::Rgb, None)
531 .unwrap();
532 let i = OutputMap::resolve(&Normalization::default(), DType::I8, PixelFormat::Rgb, None)
533 .unwrap();
534 assert!(u.is_default && i.is_default);
535 for x in 0..=255u8 {
536 assert_eq!(u.apply(1, x), x as f32);
537 assert_eq!(i.apply(1, x) as i8 as u8, x ^ 0x80);
538 }
539 }
540
541 #[test]
542 fn imagenet_matches_reference_in_bgra_order() {
543 let n = Normalization::imagenet();
544 let m = OutputMap::resolve(&n, DType::F32, PixelFormat::Bgra, None).unwrap();
545 assert!(!m.is_default);
546 let want_b = (200.0 - 103.53) / 57.375;
547 assert!((m.apply(0, 200) - want_b).abs() < 1e-5);
548 assert!((m.apply(3, 255) - 1.0).abs() < 1e-6, "alpha stays unsigned");
549 }
550
551 #[test]
552 fn int_normalization_needs_quantization() {
553 let e = OutputMap::resolve(&Normalization::signed(), DType::I8, PixelFormat::Rgb, None);
554 assert!(matches!(e, Err(Error::InvalidOptions(_))));
555 let q = Quantization::per_tensor(1.0 / 128.0, 0);
556 let m = OutputMap::resolve(
557 &Normalization::signed(),
558 DType::I8,
559 PixelFormat::Rgb,
560 Some(&q),
561 )
562 .unwrap();
563 assert_eq!(m.apply(0, 0), -128.0);
564 assert_eq!(m.apply(0, 255), 127.0, "128 clamps to the i8 range");
565 assert_eq!(m.apply(0, 191), 64.0);
566 }
567
568 #[test]
569 fn an_output_pad_has_an_equivalent_sample_when_reachable() {
570 let q = Quantization::per_tensor(0.0187, -14);
571 let m = OutputMap::resolve(
572 &Normalization::imagenet(),
573 DType::I8,
574 PixelFormat::Rgb,
575 Some(&q),
576 )
577 .unwrap();
578 let pad = m.pad_values([0; 4], Some([0.0; 4]));
579 for (c, &want) in pad.iter().enumerate().take(3) {
580 let x = m.sample_for(c, want).expect("0.0 is reachable");
581 assert_eq!(m.apply(c, x), want);
582 }
583 let q = Quantization::per_tensor(1.0 / 512.0, 0);
586 let m = OutputMap::resolve(
587 &Normalization::unsigned(),
588 DType::U8,
589 PixelFormat::Rgb,
590 Some(&q),
591 )
592 .unwrap();
593 assert_eq!(m.sample_for(0, 1.0), None);
594 }
595}