1#![allow(
9 unsafe_code,
10 clippy::similar_names,
11 clippy::cast_precision_loss,
12 clippy::cast_possible_wrap,
13 clippy::cast_sign_loss,
14 clippy::cast_possible_truncation,
15 clippy::wildcard_imports,
16 clippy::ptr_as_ptr,
17 clippy::cast_lossless,
18 clippy::single_match_else,
19 clippy::suboptimal_flops,
20 clippy::manual_div_ceil
21)]
22
23use std::cell::RefCell;
24use std::num::NonZeroUsize;
25use std::sync::Arc;
26
27use half::f16;
28use image::{DynamicImage, GenericImageView, RgbImage};
29use lru::LruCache;
30use ndarray::{Array3, Array4};
31
32pub const LETTERBOX_COLOR: [u8; 3] = [114, 114, 114];
34
35const SCALE_BITS: i32 = 11;
38const SCALE_INT: i32 = 1 << SCALE_BITS;
39
40const SCALE_BITS_2X: i32 = 2 * SCALE_BITS;
42
43const ROUND_BIAS: i32 = 1 << (SCALE_BITS_2X - 1);
46
47const LETTERBOX_NORM: f32 = 114.0 / 255.0;
49
50const INV_255: f32 = 1.0 / 255.0;
52
53const LUT_CACHE_SIZE: usize = 8;
55
56type XLutEntry = (usize, usize, i32, i32);
59type XLutKey = (u32, u32);
60
61thread_local! {
62 static X_LUT_CACHE: RefCell<LruCache<XLutKey, Arc<Vec<XLutEntry>>>> =
63 RefCell::new(LruCache::new(NonZeroUsize::new(LUT_CACHE_SIZE).unwrap()));
64}
65
66#[derive(Debug, Clone)]
68pub struct PreprocessResult {
69 pub tensor: Array4<f32>,
71 pub tensor_f16: Option<Array4<f16>>,
73 pub orig_shape: (u32, u32),
75 pub scale: (f32, f32),
77 pub padding: (f32, f32),
79}
80
81#[derive(Clone, Copy)]
83pub(crate) struct LetterboxGeometry {
84 pub(crate) new_w: u32,
85 pub(crate) new_h: u32,
86 pub(crate) pad_left: u32,
87 pub(crate) pad_top: u32,
88}
89
90impl LetterboxGeometry {
91 #[allow(
99 clippy::cast_precision_loss,
100 clippy::cast_possible_truncation,
101 clippy::cast_sign_loss
102 )]
103 pub(crate) fn compute(orig_w: u32, orig_h: u32, target: (usize, usize)) -> (Self, f32) {
104 let (target_h, target_w) = (target.0 as f32, target.1 as f32);
105 let (orig_hf, orig_wf) = (orig_h as f32, orig_w as f32);
106 let scale = (target_h / orig_hf).min(target_w / orig_wf);
107 let new_w = (orig_wf * scale).round() as u32;
108 let new_h = (orig_hf * scale).round() as u32;
109 let pad_left = (target.1 as u32).saturating_sub(new_w) / 2;
110 let pad_top = (target.0 as u32).saturating_sub(new_h) / 2;
111 (
112 Self {
113 new_w,
114 new_h,
115 pad_left,
116 pad_top,
117 },
118 scale,
119 )
120 }
121}
122
123#[allow(clippy::cast_precision_loss)]
128fn build_preprocess_result(
129 image: &DynamicImage,
130 target_size: (usize, usize),
131 geom: LetterboxGeometry,
132 scale: (f32, f32),
133 orig_shape: (u32, u32),
134 half: bool,
135) -> PreprocessResult {
136 let (orig_width, orig_height) = image.dimensions();
137
138 let tensor = match image {
139 DynamicImage::ImageRgb8(rgb) => {
140 fused_zerocopy_preprocess(rgb.as_raw(), orig_width, orig_height, target_size, &geom)
141 }
142 _ => {
143 let src_rgb = image.to_rgb8();
144 fused_zerocopy_preprocess(
145 src_rgb.as_raw(),
146 orig_width,
147 orig_height,
148 target_size,
149 &geom,
150 )
151 }
152 };
153
154 let tensor_f16 = if half {
155 Some(tensor_f32_to_f16(&tensor))
156 } else {
157 None
158 };
159
160 PreprocessResult {
161 tensor,
162 tensor_f16,
163 orig_shape,
164 scale,
165 padding: (geom.pad_top as f32, geom.pad_left as f32),
166 }
167}
168
169#[must_use]
184pub fn preprocess_image(
185 image: &DynamicImage,
186 target_size: (usize, usize),
187 stride: u32,
188) -> PreprocessResult {
189 preprocess_image_with_precision(image, target_size, stride, false)
190}
191
192#[must_use]
205pub fn preprocess_image_with_precision(
206 image: &DynamicImage,
207 target_size: (usize, usize),
208 stride: u32,
209 half: bool,
210) -> PreprocessResult {
211 let (orig_width, orig_height) = image.dimensions();
212 let orig_shape = (orig_height, orig_width);
213
214 let (geom, scale) = calculate_letterbox_params(orig_width, orig_height, target_size, stride);
215 build_preprocess_result(image, target_size, geom, scale, orig_shape, half)
216}
217
218fn get_or_compute_x_lut(src_w: u32, dst_w: u32) -> Arc<Vec<XLutEntry>> {
230 let key = (src_w, dst_w);
231
232 X_LUT_CACHE.with(|cache| {
233 let mut cache = cache.borrow_mut();
234
235 if let Some(lut) = cache.get(&key) {
236 return Arc::clone(lut);
237 }
238
239 let scale_x = src_w as f32 / dst_w as f32;
240 let src_w_max = (src_w - 1) as i32;
241
242 let lut: Arc<Vec<XLutEntry>> = Arc::new(
243 (0..dst_w)
244 .map(|dx| {
245 let sx = ((dx as f32 + 0.5) * scale_x - 0.5).max(0.0);
246 let x0 = sx.floor() as i32;
247 let fx_f = sx - x0 as f32;
250 let fx_inv = ((1.0 - fx_f) * SCALE_INT as f32 + 0.5) as i32;
251 let fx = SCALE_INT - fx_inv;
252 let x0c = x0.clamp(0, src_w_max) as usize * 3;
253 let x1c = (x0 + 1).clamp(0, src_w_max) as usize * 3;
254 (x0c, x1c, fx_inv, fx)
255 })
256 .collect(),
257 );
258
259 cache.put(key, Arc::clone(&lut));
260 lut
261 })
262}
263
264fn fused_zerocopy_preprocess(
269 src_raw: &[u8],
270 src_w: u32,
271 src_h: u32,
272 target_size: (usize, usize),
273 geom: &LetterboxGeometry,
274) -> Array4<f32> {
275 #[allow(clippy::wildcard_imports)] use crate::parallel::*;
277 use std::mem::MaybeUninit;
278 use std::sync::atomic::{AtomicPtr, Ordering};
279
280 let LetterboxGeometry {
281 new_w: new_width,
282 new_h: new_height,
283 pad_left,
284 pad_top,
285 } = *geom;
286
287 let (dst_h, dst_w) = target_size;
288 let channel_size = dst_h * dst_w;
289 let src_stride = (src_w * 3) as usize;
290
291 let mut tensor: Array4<MaybeUninit<f32>> = Array4::uninit((1, 3, dst_h, dst_w));
293 let out_ptr = tensor.as_mut_ptr() as *mut f32;
294
295 let atomic_ptr = AtomicPtr::new(out_ptr);
297
298 let x_lut = get_or_compute_x_lut(src_w, new_width);
299 let scale_y = src_h as f32 / new_height as f32;
300 let src_h_max = (src_h - 1) as i32;
301
302 let pad_top_usize = pad_top as usize;
303 let pad_left_usize = pad_left as usize;
304 let new_height_usize = new_height as usize;
305 let new_width_usize = new_width as usize;
306
307 (0..dst_h).into_par_iter().for_each(|dy| {
309 let data_ptr = atomic_ptr.load(Ordering::Relaxed);
310 unsafe {
311 let r_row = data_ptr.add(dy * dst_w);
314 let g_row = data_ptr.add(channel_size + dy * dst_w);
315 let b_row = data_ptr.add(2 * channel_size + dy * dst_w);
316
317 if dy < pad_top_usize || dy >= pad_top_usize + new_height_usize {
319 for dx in 0..dst_w {
320 *r_row.add(dx) = LETTERBOX_NORM;
321 *g_row.add(dx) = LETTERBOX_NORM;
322 *b_row.add(dx) = LETTERBOX_NORM;
323 }
324 return;
325 }
326
327 let img_dy = dy - pad_top_usize;
330 let sy = ((img_dy as f32 + 0.5) * scale_y - 0.5).max(0.0);
331 let y0 = sy.floor() as i32;
332 let fy_f = sy - y0 as f32;
333 let fy_inv = ((1.0 - fy_f) * SCALE_INT as f32 + 0.5) as i32;
334 let fy = SCALE_INT - fy_inv;
335
336 let y0c = y0.clamp(0, src_h_max) as usize;
337 let y1c = (y0 + 1).clamp(0, src_h_max) as usize;
338 let row0_off = y0c * src_stride;
339 let row1_off = y1c * src_stride;
340
341 for dx in 0..pad_left_usize {
343 *r_row.add(dx) = LETTERBOX_NORM;
344 *g_row.add(dx) = LETTERBOX_NORM;
345 *b_row.add(dx) = LETTERBOX_NORM;
346 }
347
348 let mut img_dx = 0usize;
354 let src_ptr = src_raw.as_ptr();
355
356 while img_dx < new_width_usize {
357 let (x0_off, x1_off, fx_inv, fx) = *x_lut.get_unchecked(img_dx);
358 let w00 = fx_inv * fy_inv;
359 let w10 = fx * fy_inv;
360 let w01 = fx_inv * fy;
361 let w11 = fx * fy;
362
363 let p00 = src_ptr.add(row0_off + x0_off);
364 let p10 = src_ptr.add(row0_off + x1_off);
365 let p01 = src_ptr.add(row1_off + x0_off);
366 let p11 = src_ptr.add(row1_off + x1_off);
367
368 let out_x = pad_left_usize + img_dx;
369 *r_row.add(out_x) = ((*p00 as i32 * w00
370 + *p10 as i32 * w10
371 + *p01 as i32 * w01
372 + *p11 as i32 * w11
373 + ROUND_BIAS)
374 >> SCALE_BITS_2X) as f32
375 * INV_255;
376 *g_row.add(out_x) = ((*p00.add(1) as i32 * w00
377 + *p10.add(1) as i32 * w10
378 + *p01.add(1) as i32 * w01
379 + *p11.add(1) as i32 * w11
380 + ROUND_BIAS)
381 >> SCALE_BITS_2X) as f32
382 * INV_255;
383 *b_row.add(out_x) = ((*p00.add(2) as i32 * w00
384 + *p10.add(2) as i32 * w10
385 + *p01.add(2) as i32 * w01
386 + *p11.add(2) as i32 * w11
387 + ROUND_BIAS)
388 >> SCALE_BITS_2X) as f32
389 * INV_255;
390
391 img_dx += 1;
392 }
393
394 for dx in (pad_left_usize + new_width_usize)..dst_w {
396 *r_row.add(dx) = LETTERBOX_NORM;
397 *g_row.add(dx) = LETTERBOX_NORM;
398 *b_row.add(dx) = LETTERBOX_NORM;
399 }
400 }
401 });
402
403 unsafe { tensor.assume_init() }
405}
406
407fn tensor_f32_to_f16(tensor: &Array4<f32>) -> Array4<half::f16> {
409 use half::slice::HalfFloatSliceExt;
410 let Some(src) = tensor.as_slice() else {
413 return tensor.mapv(half::f16::from_f32);
414 };
415 let mut out = vec![half::f16::ZERO; src.len()];
416 out.convert_from_f32_slice(src);
417 Array4::from_shape_vec(tensor.raw_dim(), out).expect("shape matches the source tensor")
418}
419
420#[must_use]
436pub fn calculate_rect_size(
437 orig_width: u32,
438 orig_height: u32,
439 target_size: (usize, usize),
440 stride: u32,
441) -> (usize, usize) {
442 let (target_h, target_w) = target_size;
443
444 #[allow(clippy::cast_precision_loss)]
445 let orig_h = orig_height as f32;
446 #[allow(clippy::cast_precision_loss)]
447 let orig_w = orig_width as f32;
448 #[allow(clippy::cast_precision_loss)]
449 let target_h_f = target_h as f32;
450 #[allow(clippy::cast_precision_loss)]
451 let target_w_f = target_w as f32;
452
453 let scale = (target_h_f / orig_h).min(target_w_f / orig_w);
455
456 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
458 let new_h = (orig_h * scale).round() as usize;
459 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
460 let new_w = (orig_w * scale).round() as usize;
461
462 let stride = stride as usize;
464 let rect_h = ((new_h + stride - 1) / stride) * stride;
465 let rect_w = ((new_w + stride - 1) / stride) * stride;
466
467 (rect_h, rect_w)
468}
469
470fn calculate_letterbox_params(
490 orig_width: u32,
491 orig_height: u32,
492 target_size: (usize, usize),
493 _stride: u32,
494) -> (LetterboxGeometry, (f32, f32)) {
495 let (geom, scale) = LetterboxGeometry::compute(orig_width, orig_height, target_size);
498 (geom, (scale, scale))
499}
500
501fn image_to_tensor<T: Clone>(
508 image: &RgbImage,
509 zero: T,
510 mut convert: impl FnMut(u8) -> T,
511) -> Array4<T> {
512 let (width, height) = image.dimensions();
513 let (w, h) = (width as usize, height as usize);
514 let pixels = image.as_raw();
515
516 let mut tensor = Array4::from_elem((1, 3, h, w), zero);
517
518 let (r_slice, rest) = tensor.as_slice_mut().unwrap().split_at_mut(h * w);
520 let (g_slice, b_slice) = rest.split_at_mut(h * w);
521
522 for (i, chunk) in pixels.as_chunks::<3>().0.iter().enumerate() {
523 r_slice[i] = convert(chunk[0]);
524 g_slice[i] = convert(chunk[1]);
525 b_slice[i] = convert(chunk[2]);
526 }
527
528 tensor
529}
530
531#[must_use]
537pub fn image_to_array(image: &DynamicImage) -> Array3<u8> {
538 let rgb = image.to_rgb8();
539 let (width, height) = rgb.dimensions();
540 let pixels = rgb.into_raw();
541
542 Array3::from_shape_vec((height as usize, width as usize, 3), pixels)
543 .expect("Failed to create array from image pixels")
544}
545
546#[must_use]
558pub fn scale_coords(coords: &[f32; 4], scale: (f32, f32), padding: (f32, f32)) -> [f32; 4] {
559 let (scale_y, scale_x) = scale;
560 let (pad_top, pad_left) = padding;
561
562 [
563 (coords[0] - pad_left) / scale_x, (coords[1] - pad_top) / scale_y, (coords[2] - pad_left) / scale_x, (coords[3] - pad_top) / scale_y, ]
568}
569
570#[must_use]
581pub const fn clip_coords(coords: &[f32; 4], shape: (u32, u32)) -> [f32; 4] {
582 #[allow(clippy::cast_precision_loss)]
583 let (h, w) = (shape.0 as f32, shape.1 as f32);
584 [
585 coords[0].clamp(0.0, w),
586 coords[1].clamp(0.0, h),
587 coords[2].clamp(0.0, w),
588 coords[3].clamp(0.0, h),
589 ]
590}
591
592#[must_use]
606pub fn preprocess_image_center_crop(
607 image: &DynamicImage,
608 target_size: (usize, usize),
609 half: bool,
610) -> PreprocessResult {
611 let (orig_width, orig_height) = image.dimensions();
612 let orig_shape = (orig_height, orig_width);
613
614 let (cropped, scale) = center_crop_image(image, target_size);
616
617 let tensor = image_to_tensor(&cropped, 0.0, |v| f32::from(v) / 255.0);
619
620 let tensor_f16 = half.then(|| {
622 let scale = f16::from_f32(1.0 / 255.0);
623 image_to_tensor(&cropped, f16::ZERO, move |v| {
624 f16::from_f32(f32::from(v)) * scale
625 })
626 });
627
628 let padding = (0.0, 0.0);
632
633 PreprocessResult {
634 tensor,
635 tensor_f16,
636 orig_shape,
637 scale,
638 padding,
639 }
640}
641
642#[allow(clippy::similar_names)]
658fn center_crop_image(image: &DynamicImage, target_size: (usize, usize)) -> (RgbImage, (f32, f32)) {
659 use fast_image_resize::{
660 PixelType, ResizeAlg, ResizeOptions, Resizer,
661 images::{Image, ImageRef},
662 };
663
664 let (src_w, src_h) = image.dimensions();
665 #[allow(clippy::cast_possible_truncation)]
666 let (target_h, target_w) = (target_size.0 as u32, target_size.1 as u32);
667
668 #[allow(clippy::cast_precision_loss)]
671 let scale_x = target_w as f32 / src_w as f32;
672 #[allow(clippy::cast_precision_loss)]
673 let scale_y = target_h as f32 / src_h as f32;
674 let scale = scale_x.max(scale_y);
675
676 let (new_w, new_h) = if scale_x >= scale_y {
677 #[allow(
678 clippy::cast_possible_truncation,
679 clippy::cast_sign_loss,
680 clippy::cast_precision_loss
681 )]
682 (target_w, (src_h as f32 * scale_x) as u32)
683 } else {
684 #[allow(
685 clippy::cast_possible_truncation,
686 clippy::cast_sign_loss,
687 clippy::cast_precision_loss
688 )]
689 ((src_w as f32 * scale_y) as u32, target_h)
690 };
691
692 let owned_rgb;
695 let src_bytes: &[u8] = match image {
696 DynamicImage::ImageRgb8(rgb) => rgb.as_raw(),
697 other => {
698 owned_rgb = other.to_rgb8();
699 owned_rgb.as_raw()
700 }
701 };
702 let src_image = ImageRef::new(src_w, src_h, src_bytes, PixelType::U8x3)
703 .expect("Failed to create source image");
704
705 let safe_new_w = new_w.max(1);
707 let safe_new_h = new_h.max(1);
708
709 let mut dst_image = Image::new(safe_new_w, safe_new_h, PixelType::U8x3);
710
711 let mut resizer = Resizer::new();
712 let options = ResizeOptions::new().resize_alg(ResizeAlg::Convolution(
713 fast_image_resize::FilterType::Bilinear,
714 ));
715 resizer
716 .resize(&src_image, &mut dst_image, Some(&options))
717 .expect("Failed to resize image");
718
719 let resized_buffer = dst_image.into_vec();
721 let resized_rgb = RgbImage::from_raw(safe_new_w, safe_new_h, resized_buffer)
722 .expect("Failed to create resized buffer");
723
724 #[allow(clippy::cast_precision_loss)]
726 let crop_x_float = (new_w.saturating_sub(target_w)) as f32 / 2.0;
727 #[allow(clippy::cast_precision_loss)]
728 let crop_y_float = (new_h.saturating_sub(target_h)) as f32 / 2.0;
729
730 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
731 let crop_x = bankers_round(crop_x_float) as u32;
732 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
733 let crop_y = bankers_round(crop_y_float) as u32;
734
735 let cropped =
736 image::imageops::crop_imm(&resized_rgb, crop_x, crop_y, target_w, target_h).to_image();
737
738 (cropped, (scale, scale))
739}
740
741fn bankers_round(v: f32) -> f32 {
743 let n = v.floor();
744 let d = v - n;
745 if (d - 0.5).abs() < 1e-6 {
746 if n % 2.0 == 0.0 { n } else { n + 1.0 }
747 } else {
748 v.round()
749 }
750}
751
752#[allow(clippy::similar_names)]
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 #[test]
758 fn test_letterbox_params() {
759 let (geom, _scale) = calculate_letterbox_params(640, 640, (640, 640), 32);
761 assert_eq!((geom.new_w, geom.new_h), (640, 640));
762 assert_eq!((geom.pad_left, geom.pad_top), (0, 0));
763
764 let (geom, _) = calculate_letterbox_params(1280, 720, (640, 640), 32);
766 assert!(geom.new_w <= 640 && geom.new_h <= 640);
767 assert_eq!(geom.pad_left, 0);
768
769 let (geom, _) = calculate_letterbox_params(480, 640, (640, 640), 32);
771 assert!(geom.pad_left > 0);
772 assert_eq!(geom.pad_top, 0);
773 }
774
775 #[test]
776 fn test_scale_coords() {
777 let coords = [100.0, 100.0, 200.0, 200.0];
778 let scale = (1.0, 1.0);
779 let padding = (10.0, 10.0);
780
781 let scaled = scale_coords(&coords, scale, padding);
782
783 assert!((scaled[0] - 90.0).abs() < 1e-6);
784 assert!((scaled[1] - 90.0).abs() < 1e-6);
785 assert!((scaled[2] - 190.0).abs() < 1e-6);
786 assert!((scaled[3] - 190.0).abs() < 1e-6);
787 }
788
789 #[test]
790 fn test_clip_coords() {
791 let coords = [-10.0, -20.0, 700.0, 500.0];
792 let clipped = clip_coords(&coords, (480, 640));
793
794 assert!((clipped[0] - 0.0).abs() < 1e-6);
795 assert!((clipped[1] - 0.0).abs() < 1e-6);
796 assert!((clipped[2] - 640.0).abs() < 1e-6);
797 assert!((clipped[3] - 480.0).abs() < 1e-6);
798 }
799
800 #[test]
801 fn test_preprocess_image_center_crop() {
802 let img = image::DynamicImage::new_rgb8(400, 300);
805 for half in [false, true] {
806 let res = preprocess_image_center_crop(&img, (224, 224), half);
807 assert_eq!(res.tensor.dim(), (1, 3, 224, 224));
808 assert_eq!(res.orig_shape, (300, 400));
809 assert_eq!(res.padding, (0.0, 0.0));
810 assert!(res.tensor.iter().all(|v| (0.0..=1.0).contains(v)));
811 assert_eq!(res.tensor_f16.is_some(), half);
812 if let Some(t16) = &res.tensor_f16 {
813 assert_eq!(t16.dim(), res.tensor.dim());
814 }
815 }
816 }
817
818 #[test]
819 fn test_preprocess_image_static_centered_letterbox() {
820 let img = image::DynamicImage::new_rgb8(640, 480);
823 let res = preprocess_image_with_precision(&img, (1024, 1024), 32, false);
824 let (_, _, h, w) = res.tensor.dim();
825 assert_eq!(h, 1024);
826 assert_eq!(w, 1024);
827 assert!(res.padding.1.abs() < 1e-6, "wide image: no left padding");
829 assert!(res.padding.0 > 0.0, "wide image: top padding expected");
830 }
831
832 #[test]
833 fn test_preprocess_image_rect_uses_centered_letterbox() {
834 let img = image::DynamicImage::new_rgb8(640, 333);
837 let rect_size = calculate_rect_size(640, 333, (1024, 1024), 32);
838 assert_eq!(rect_size, (544, 1024));
839 let res = preprocess_image_with_precision(&img, rect_size, 32, false);
840 let (_, _, h, w) = res.tensor.dim();
841 assert_eq!((h, w), rect_size);
842 assert_eq!(res.padding, (5.0, 0.0));
843 }
844
845 #[test]
846 fn test_preprocess_image_public_wrapper() {
847 let img = image::DynamicImage::new_rgb8(320, 240);
848 let res = preprocess_image(&img, (640, 640), 32);
849 let (_, c, h, w) = res.tensor.dim();
850 assert_eq!((c, h, w), (3, 640, 640));
851 assert!(res.tensor_f16.is_none());
852 }
853
854 #[test]
855 fn test_preprocess_image_fp16_path() {
856 let img = image::DynamicImage::new_rgb8(320, 240);
857 let res = preprocess_image_with_precision(&img, (640, 640), 32, true);
858 let f16 = res.tensor_f16.expect("fp16 tensor present");
860 assert_eq!(f16.dim(), res.tensor.dim());
861 }
862
863 #[test]
864 fn test_preprocess_various_aspect_ratios() {
865 for (w, h) in [(100u32, 400u32), (400, 100), (1, 1), (640, 640)] {
867 let img = image::DynamicImage::new_rgb8(w, h);
868 let res = preprocess_image(&img, (320, 320), 32);
869 let (_, c, th, tw) = res.tensor.dim();
870 assert_eq!((c, th, tw), (3, 320, 320));
871 assert_eq!(res.orig_shape, (h, w));
872 }
873 }
874
875 #[test]
876 fn test_x_lut_cache_reuse() {
877 let img = image::DynamicImage::new_rgb8(200, 150);
879 let a = preprocess_image(&img, (320, 320), 32);
880 let b = preprocess_image(&img, (320, 320), 32);
881 assert_eq!(a.tensor.dim(), b.tensor.dim());
882 }
883
884 #[test]
885 fn test_calculate_rect_size() {
886 assert_eq!(calculate_rect_size(640, 640, (640, 640), 32), (640, 640));
888
889 for (w, h) in [(400u32, 1000u32), (1000, 400), (800, 600)] {
892 let (rh, rw) = calculate_rect_size(w, h, (640, 640), 32);
893 assert_eq!((rh % 32, rw % 32), (0, 0));
894 assert!(rh <= 640 && rw <= 640);
895 }
896 }
897}