1use rayon::prelude::*;
2use spatialrust_image::{ColorSpace, Image, ImageMetadata, ImageView, ImageViewMut, PlanarImage};
3
4use crate::dispatch::{
5 is_packed_u8_fast_path, should_parallelize, LIGHT_PARALLEL_COMPONENTS, ROWS_PER_TILE,
6 TALL_IMAGE_ROWS,
7};
8use crate::{PixelComponent, VisionError, VisionResult};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
12pub enum Interpolation {
13 Nearest,
15 #[default]
17 Bilinear,
18 Bicubic,
20 Area,
22}
23
24const BILINEAR_WEIGHT_BITS: u32 = 11;
25const BILINEAR_WEIGHT_SCALE: u32 = 1 << BILINEAR_WEIGHT_BITS;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28struct BilinearAxisSample {
29 lower: usize,
30 upper: usize,
31 upper_weight: u16,
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct BilinearResizeU8Plan {
40 input_width: usize,
41 input_height: usize,
42 output_width: usize,
43 output_height: usize,
44 x_samples: Vec<BilinearAxisSample>,
45 y_samples: Vec<BilinearAxisSample>,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct NearestResizeU8Plan {
55 input_width: usize,
56 input_height: usize,
57 output_width: usize,
58 output_height: usize,
59 x_indices: Vec<usize>,
60 y_indices: Vec<usize>,
61}
62
63impl NearestResizeU8Plan {
64 pub fn new(
66 input_width: usize,
67 input_height: usize,
68 output_width: usize,
69 output_height: usize,
70 ) -> VisionResult<Self> {
71 validate_resize_dimensions(input_width, input_height, output_width, output_height)?;
72 Ok(Self {
73 input_width,
74 input_height,
75 output_width,
76 output_height,
77 x_indices: nearest_axis_indices(input_width, output_width),
78 y_indices: nearest_axis_indices(input_height, output_height),
79 })
80 }
81
82 #[must_use]
84 pub const fn input_dimensions(&self) -> (usize, usize) {
85 (self.input_width, self.input_height)
86 }
87
88 #[must_use]
90 pub const fn output_dimensions(&self) -> (usize, usize) {
91 (self.output_width, self.output_height)
92 }
93
94 pub fn resize<const CHANNELS: usize>(
96 &self,
97 input: ImageView<'_, u8, CHANNELS>,
98 ) -> VisionResult<Image<u8, CHANNELS>> {
99 let len = resize_output_len::<CHANNELS>(self.output_width, self.output_height)?;
100 let mut output = Image::try_new_with_metadata(
101 self.output_width,
102 self.output_height,
103 vec![0; len],
104 input.metadata(),
105 )?;
106 self.resize_into(input, output.view_mut())?;
107 Ok(output)
108 }
109
110 pub fn resize_into<const CHANNELS: usize>(
112 &self,
113 input: ImageView<'_, u8, CHANNELS>,
114 mut output: ImageViewMut<'_, u8, CHANNELS>,
115 ) -> VisionResult<()> {
116 validate_plan_views(self.input_dimensions(), self.output_dimensions(), input, &output)?;
117 if self.output_width == 0 || self.output_height == 0 {
118 output.set_metadata(input.metadata())?;
119 return Ok(());
120 }
121 if !is_packed_u8_fast_path::<CHANNELS>(
122 self.input_width,
123 input.row_stride(),
124 self.output_width,
125 output.row_stride(),
126 ) {
127 return resize_into(input, output, Interpolation::Nearest);
128 }
129
130 output.set_metadata(input.metadata())?;
131 let row_stride = output.row_stride();
132 let output_span = row_stride * self.output_height;
133 let run_row = |y: usize, row: &mut [u8]| {
134 let source = input.row(self.y_indices[y]).expect("planned source row");
135 for (x, &source_x) in self.x_indices.iter().enumerate() {
136 let source_offset = source_x * CHANNELS;
137 let destination = x * CHANNELS;
138 row[destination..destination + CHANNELS]
139 .copy_from_slice(&source[source_offset..source_offset + CHANNELS]);
140 }
141 };
142 if should_parallelize(
143 self.output_width * self.output_height * CHANNELS,
144 self.output_height,
145 LIGHT_PARALLEL_COMPONENTS,
146 ) {
147 output.as_mut_slice()[..output_span]
148 .par_chunks_mut(row_stride)
149 .enumerate()
150 .for_each(|(y, row)| run_row(y, row));
151 } else {
152 output.as_mut_slice()[..output_span]
153 .chunks_mut(row_stride)
154 .enumerate()
155 .for_each(|(y, row)| run_row(y, row));
156 }
157 Ok(())
158 }
159}
160
161#[derive(Clone, Debug, PartialEq)]
162struct AreaAxisSpan {
163 start: usize,
164 weights: Vec<f64>,
165}
166
167#[derive(Clone, Debug, PartialEq)]
174pub struct AreaResizeU8Plan {
175 input_width: usize,
176 input_height: usize,
177 output_width: usize,
178 output_height: usize,
179 integer_factor: Option<usize>,
180 x_spans: Vec<AreaAxisSpan>,
181 y_spans: Vec<AreaAxisSpan>,
182}
183
184impl AreaResizeU8Plan {
185 pub fn new(
187 input_width: usize,
188 input_height: usize,
189 output_width: usize,
190 output_height: usize,
191 ) -> VisionResult<Self> {
192 validate_resize_dimensions(input_width, input_height, output_width, output_height)?;
193 let downsample = output_width < input_width || output_height < input_height;
194 let integer_factor = [2, 4].into_iter().find(|&factor| {
195 input_width == output_width.saturating_mul(factor)
196 && input_height == output_height.saturating_mul(factor)
197 });
198 Ok(Self {
199 input_width,
200 input_height,
201 output_width,
202 output_height,
203 integer_factor,
204 x_spans: if downsample && integer_factor.is_none() {
205 area_axis_spans(input_width, output_width)
206 } else {
207 Vec::new()
208 },
209 y_spans: if downsample && integer_factor.is_none() {
210 area_axis_spans(input_height, output_height)
211 } else {
212 Vec::new()
213 },
214 })
215 }
216
217 #[must_use]
219 pub const fn input_dimensions(&self) -> (usize, usize) {
220 (self.input_width, self.input_height)
221 }
222
223 #[must_use]
225 pub const fn output_dimensions(&self) -> (usize, usize) {
226 (self.output_width, self.output_height)
227 }
228
229 pub fn resize<const CHANNELS: usize>(
231 &self,
232 input: ImageView<'_, u8, CHANNELS>,
233 ) -> VisionResult<Image<u8, CHANNELS>> {
234 let len = resize_output_len::<CHANNELS>(self.output_width, self.output_height)?;
235 let mut output = Image::try_new_with_metadata(
236 self.output_width,
237 self.output_height,
238 vec![0; len],
239 input.metadata(),
240 )?;
241 self.resize_into(input, output.view_mut())?;
242 Ok(output)
243 }
244
245 pub fn resize_into<const CHANNELS: usize>(
247 &self,
248 input: ImageView<'_, u8, CHANNELS>,
249 mut output: ImageViewMut<'_, u8, CHANNELS>,
250 ) -> VisionResult<()> {
251 validate_plan_views(self.input_dimensions(), self.output_dimensions(), input, &output)?;
252 if self.output_width == 0 || self.output_height == 0 {
253 output.set_metadata(input.metadata())?;
254 return Ok(());
255 }
256 let downsample =
257 self.output_width < self.input_width || self.output_height < self.input_height;
258 if !downsample
259 || !is_packed_u8_fast_path::<CHANNELS>(
260 self.input_width,
261 input.row_stride(),
262 self.output_width,
263 output.row_stride(),
264 )
265 {
266 return resize_into(input, output, Interpolation::Area);
267 }
268
269 output.set_metadata(input.metadata())?;
270 let row_stride = output.row_stride();
271 let output_span = row_stride * self.output_height;
272 let run_row = |y: usize, row: &mut [u8]| {
273 if let Some(factor) = self.integer_factor {
274 resize_integer_area_row(input, row, y, self.output_width, factor);
275 } else {
276 self.resize_weighted_area_row(input, row, y);
277 }
278 };
279 if should_parallelize(
280 self.output_width * self.output_height * CHANNELS,
281 self.output_height,
282 LIGHT_PARALLEL_COMPONENTS,
283 ) {
284 output.as_mut_slice()[..output_span]
285 .par_chunks_mut(row_stride)
286 .enumerate()
287 .for_each(|(y, row)| run_row(y, row));
288 } else {
289 output.as_mut_slice()[..output_span]
290 .chunks_mut(row_stride)
291 .enumerate()
292 .for_each(|(y, row)| run_row(y, row));
293 }
294 Ok(())
295 }
296
297 fn resize_weighted_area_row<const CHANNELS: usize>(
298 &self,
299 input: ImageView<'_, u8, CHANNELS>,
300 output: &mut [u8],
301 y: usize,
302 ) {
303 let y_span = &self.y_spans[y];
304 for (x, x_span) in self.x_spans.iter().enumerate() {
305 let mut sums = [0.0_f64; CHANNELS];
306 let mut total_weight = 0.0;
307 for (y_offset, &weight_y) in y_span.weights.iter().enumerate() {
308 let row = input.row(y_span.start + y_offset).expect("planned source row");
309 for (x_offset, &weight_x) in x_span.weights.iter().enumerate() {
310 let weight = weight_x * weight_y;
311 let source = (x_span.start + x_offset) * CHANNELS;
312 for channel in 0..CHANNELS {
313 sums[channel] += f64::from(row[source + channel]) * weight;
314 }
315 total_weight += weight;
316 }
317 }
318 let destination = x * CHANNELS;
319 for channel in 0..CHANNELS {
320 output[destination + channel] =
321 PixelComponent::from_f64(sums[channel] / total_weight);
322 }
323 }
324 }
325}
326
327impl BilinearResizeU8Plan {
328 pub fn new(
330 input_width: usize,
331 input_height: usize,
332 output_width: usize,
333 output_height: usize,
334 ) -> VisionResult<Self> {
335 if output_width != 0 && output_height != 0 && (input_width == 0 || input_height == 0) {
336 return Err(VisionError::InvalidDimensions(
337 "cannot resize an empty input to a non-empty output".to_owned(),
338 ));
339 }
340 let half_scale = input_width == output_width.saturating_mul(2)
341 && input_height == output_height.saturating_mul(2);
342 Ok(Self {
343 input_width,
344 input_height,
345 output_width,
346 output_height,
347 x_samples: if half_scale {
348 Vec::new()
349 } else {
350 bilinear_axis_samples(input_width, output_width)
351 },
352 y_samples: if half_scale {
353 Vec::new()
354 } else {
355 bilinear_axis_samples(input_height, output_height)
356 },
357 })
358 }
359
360 #[must_use]
362 pub const fn input_dimensions(&self) -> (usize, usize) {
363 (self.input_width, self.input_height)
364 }
365
366 #[must_use]
368 pub const fn output_dimensions(&self) -> (usize, usize) {
369 (self.output_width, self.output_height)
370 }
371
372 pub fn resize<const CHANNELS: usize>(
374 &self,
375 input: ImageView<'_, u8, CHANNELS>,
376 ) -> VisionResult<Image<u8, CHANNELS>> {
377 let len = self
378 .output_width
379 .checked_mul(self.output_height)
380 .and_then(|pixels| pixels.checked_mul(CHANNELS))
381 .ok_or_else(|| {
382 VisionError::InvalidDimensions("resize output is too large".to_owned())
383 })?;
384 let mut output = Image::try_new_with_metadata(
385 self.output_width,
386 self.output_height,
387 vec![0; len],
388 input.metadata(),
389 )?;
390 self.resize_into(input, output.view_mut())?;
391 Ok(output)
392 }
393
394 pub fn resize_into<const CHANNELS: usize>(
396 &self,
397 input: ImageView<'_, u8, CHANNELS>,
398 mut output: ImageViewMut<'_, u8, CHANNELS>,
399 ) -> VisionResult<()> {
400 if (input.width(), input.height()) != self.input_dimensions() {
401 return Err(VisionError::InvalidDimensions(format!(
402 "resize plan expects input {}x{}, found {}x{}",
403 self.input_width,
404 self.input_height,
405 input.width(),
406 input.height()
407 )));
408 }
409 if (output.width(), output.height()) != self.output_dimensions() {
410 return Err(VisionError::InvalidDimensions(format!(
411 "resize plan expects output {}x{}, found {}x{}",
412 self.output_width,
413 self.output_height,
414 output.width(),
415 output.height()
416 )));
417 }
418 output.set_metadata(input.metadata())?;
419 if self.output_width == 0 || self.output_height == 0 {
420 return Ok(());
421 }
422
423 let row_stride = output.row_stride();
424 let output_components = self.output_width * self.output_height * CHANNELS;
425 let rows = output.as_mut_slice().par_chunks_mut(row_stride).enumerate();
426 if self.input_width == self.output_width.saturating_mul(2)
427 && self.input_height == self.output_height.saturating_mul(2)
428 {
429 if should_parallelize(output_components, self.output_height, LIGHT_PARALLEL_COMPONENTS)
430 {
431 rows.for_each(|(y, row)| resize_half_row(input, row, y, self.output_width));
432 } else {
433 output
434 .as_mut_slice()
435 .chunks_mut(row_stride)
436 .enumerate()
437 .for_each(|(y, row)| resize_half_row(input, row, y, self.output_width));
438 }
439 } else if should_parallelize(
440 output_components,
441 self.output_height,
442 LIGHT_PARALLEL_COMPONENTS,
443 ) {
444 rows.for_each(|(y, row)| self.resize_bilinear_row(input, row, y));
445 } else {
446 output
447 .as_mut_slice()
448 .chunks_mut(row_stride)
449 .enumerate()
450 .for_each(|(y, row)| self.resize_bilinear_row(input, row, y));
451 }
452 Ok(())
453 }
454
455 pub fn resize_rgb_to_gray(&self, input: ImageView<'_, u8, 3>) -> VisionResult<Image<u8, 1>> {
461 let len = self.output_width.checked_mul(self.output_height).ok_or_else(|| {
462 VisionError::InvalidDimensions("fused resize-to-gray output is too large".to_owned())
463 })?;
464 let metadata = ImageMetadata { color_space: ColorSpace::Gray, ..input.metadata() };
465 let mut output = Image::try_new_with_metadata(
466 self.output_width,
467 self.output_height,
468 vec![0; len],
469 metadata,
470 )?;
471 self.resize_rgb_to_gray_into(input, output.view_mut())?;
472 Ok(output)
473 }
474
475 pub fn resize_rgb_to_gray_into(
477 &self,
478 input: ImageView<'_, u8, 3>,
479 mut output: ImageViewMut<'_, u8, 1>,
480 ) -> VisionResult<()> {
481 if (input.width(), input.height()) != self.input_dimensions() {
482 return Err(VisionError::InvalidDimensions(format!(
483 "resize plan expects input {}x{}, found {}x{}",
484 self.input_width,
485 self.input_height,
486 input.width(),
487 input.height()
488 )));
489 }
490 if (output.width(), output.height()) != self.output_dimensions() {
491 return Err(VisionError::InvalidDimensions(format!(
492 "resize plan expects output {}x{}, found {}x{}",
493 self.output_width,
494 self.output_height,
495 output.width(),
496 output.height()
497 )));
498 }
499 output.set_metadata(ImageMetadata { color_space: ColorSpace::Gray, ..input.metadata() })?;
500 if self.output_width == 0 || self.output_height == 0 {
501 return Ok(());
502 }
503
504 let row_stride = output.row_stride();
505 let half_scale = self.input_width == self.output_width.saturating_mul(2)
506 && self.input_height == self.output_height.saturating_mul(2);
507 let run_row = |y: usize, row: &mut [u8]| {
508 if half_scale {
509 resize_half_rgb_to_gray_row(input, row, y, self.output_width);
510 } else {
511 self.resize_bilinear_rgb_to_gray_row(input, row, y);
512 }
513 };
514 if should_parallelize(
515 self.output_width * self.output_height,
516 self.output_height,
517 LIGHT_PARALLEL_COMPONENTS,
518 ) {
519 if self.output_height >= TALL_IMAGE_ROWS {
520 let block_stride = row_stride.checked_mul(ROWS_PER_TILE).ok_or_else(|| {
521 VisionError::InvalidDimensions("fused resize row block is too large".to_owned())
522 })?;
523 output.as_mut_slice().par_chunks_mut(block_stride).enumerate().for_each(
524 |(block, rows)| {
525 for (row, target) in rows.chunks_mut(row_stride).enumerate() {
526 run_row(block * ROWS_PER_TILE + row, target);
527 }
528 },
529 );
530 } else {
531 output
532 .as_mut_slice()
533 .par_chunks_mut(row_stride)
534 .enumerate()
535 .for_each(|(y, row)| run_row(y, row));
536 }
537 } else {
538 output
539 .as_mut_slice()
540 .chunks_mut(row_stride)
541 .enumerate()
542 .for_each(|(y, row)| run_row(y, row));
543 }
544 Ok(())
545 }
546
547 pub fn resize_rgb_to_chw(
553 &self,
554 input: ImageView<'_, u8, 3>,
555 scale: f32,
556 mean: [f32; 3],
557 std: [f32; 3],
558 ) -> VisionResult<PlanarImage<f32, 3>> {
559 let plane_len = self.output_width.checked_mul(self.output_height).ok_or_else(|| {
560 VisionError::InvalidDimensions("fused resize-to-CHW plane is too large".to_owned())
561 })?;
562 let len = plane_len.checked_mul(3).ok_or_else(|| {
563 VisionError::InvalidDimensions("fused resize-to-CHW output is too large".to_owned())
564 })?;
565 let mut output = vec![0.0_f32; len];
566 self.resize_rgb_to_chw_into(input, scale, mean, std, &mut output)?;
567 Ok(PlanarImage::try_new_with_metadata(
568 self.output_width,
569 self.output_height,
570 output,
571 input.metadata(),
572 )?)
573 }
574
575 pub fn resize_rgb_to_chw_into(
577 &self,
578 input: ImageView<'_, u8, 3>,
579 scale: f32,
580 mean: [f32; 3],
581 std: [f32; 3],
582 output: &mut [f32],
583 ) -> VisionResult<()> {
584 validate_rgb_chw_normalization(scale, std)?;
585 if (input.width(), input.height()) != self.input_dimensions() {
586 return Err(VisionError::InvalidDimensions(format!(
587 "resize plan expects input {}x{}, found {}x{}",
588 self.input_width,
589 self.input_height,
590 input.width(),
591 input.height()
592 )));
593 }
594 let plane_len = self.output_width.checked_mul(self.output_height).ok_or_else(|| {
595 VisionError::InvalidDimensions("fused resize-to-CHW plane is too large".to_owned())
596 })?;
597 let required = plane_len.checked_mul(3).ok_or_else(|| {
598 VisionError::InvalidDimensions("fused resize-to-CHW output is too large".to_owned())
599 })?;
600 if output.len() != required {
601 return Err(VisionError::ShapeMismatch(format!(
602 "CHW output needs {required} elements, found {}",
603 output.len()
604 )));
605 }
606 if plane_len == 0 {
607 return Ok(());
608 }
609
610 let half_scale = self.input_width == self.output_width.saturating_mul(2)
611 && self.input_height == self.output_height.saturating_mul(2);
612 let run_row = |plane_row: usize, target: &mut [f32]| {
613 let channel = plane_row / self.output_height;
614 let y = plane_row % self.output_height;
615 if half_scale {
616 resize_half_rgb_to_chw_row(
617 input,
618 target,
619 y,
620 channel,
621 scale,
622 mean[channel],
623 std[channel],
624 );
625 } else {
626 self.resize_bilinear_rgb_to_chw_row(
627 input,
628 target,
629 y,
630 channel,
631 scale,
632 mean[channel],
633 std[channel],
634 );
635 }
636 };
637 if should_parallelize(required, self.output_height * 3, LIGHT_PARALLEL_COMPONENTS) {
638 output
639 .par_chunks_mut(self.output_width)
640 .enumerate()
641 .for_each(|(plane_row, target)| run_row(plane_row, target));
642 } else {
643 output
644 .chunks_mut(self.output_width)
645 .enumerate()
646 .for_each(|(plane_row, target)| run_row(plane_row, target));
647 }
648 Ok(())
649 }
650
651 fn resize_bilinear_row<const CHANNELS: usize>(
652 &self,
653 input: ImageView<'_, u8, CHANNELS>,
654 output: &mut [u8],
655 y: usize,
656 ) {
657 let y_sample = self.y_samples[y];
658 let top = input.row(y_sample.lower).expect("planned source row");
659 let bottom = input.row(y_sample.upper).expect("planned source row");
660 let wy = u32::from(y_sample.upper_weight);
661 let inv_wy = BILINEAR_WEIGHT_SCALE - wy;
662 let round = 1 << (BILINEAR_WEIGHT_BITS * 2 - 1);
663 for (x, x_sample) in self.x_samples.iter().copied().enumerate() {
664 let wx = u32::from(x_sample.upper_weight);
665 let inv_wx = BILINEAR_WEIGHT_SCALE - wx;
666 let lower = x_sample.lower * CHANNELS;
667 let upper = x_sample.upper * CHANNELS;
668 let destination = x * CHANNELS;
669 for channel in 0..CHANNELS {
670 let horizontal_top =
671 u32::from(top[lower + channel]) * inv_wx + u32::from(top[upper + channel]) * wx;
672 let horizontal_bottom = u32::from(bottom[lower + channel]) * inv_wx
673 + u32::from(bottom[upper + channel]) * wx;
674 output[destination + channel] =
675 ((horizontal_top * inv_wy + horizontal_bottom * wy + round)
676 >> (BILINEAR_WEIGHT_BITS * 2)) as u8;
677 }
678 }
679 }
680
681 fn resize_bilinear_rgb_to_gray_row(
682 &self,
683 input: ImageView<'_, u8, 3>,
684 output: &mut [u8],
685 y: usize,
686 ) {
687 let y_sample = self.y_samples[y];
688 let top = input.row(y_sample.lower).expect("planned source row");
689 let bottom = input.row(y_sample.upper).expect("planned source row");
690 let wy = u32::from(y_sample.upper_weight);
691 let inv_wy = BILINEAR_WEIGHT_SCALE - wy;
692 for (x, x_sample) in self.x_samples.iter().copied().enumerate() {
693 let wx = u32::from(x_sample.upper_weight);
694 let inv_wx = BILINEAR_WEIGHT_SCALE - wx;
695 let lower = x_sample.lower * 3;
696 let upper = x_sample.upper * 3;
697 let pixel = std::array::from_fn(|channel| {
698 bilinear_u8_component(
699 top[lower + channel],
700 top[upper + channel],
701 bottom[lower + channel],
702 bottom[upper + channel],
703 inv_wx,
704 wx,
705 inv_wy,
706 wy,
707 )
708 });
709 output[x] = rgb_luma_q14(pixel);
710 }
711 }
712
713 #[allow(clippy::too_many_arguments)]
714 fn resize_bilinear_rgb_to_chw_row(
715 &self,
716 input: ImageView<'_, u8, 3>,
717 output: &mut [f32],
718 y: usize,
719 channel: usize,
720 scale: f32,
721 mean: f32,
722 std: f32,
723 ) {
724 let y_sample = self.y_samples[y];
725 let top = input.row(y_sample.lower).expect("planned source row");
726 let bottom = input.row(y_sample.upper).expect("planned source row");
727 let wy = u32::from(y_sample.upper_weight);
728 let inv_wy = BILINEAR_WEIGHT_SCALE - wy;
729 for (x, x_sample) in self.x_samples.iter().copied().enumerate() {
730 let wx = u32::from(x_sample.upper_weight);
731 let inv_wx = BILINEAR_WEIGHT_SCALE - wx;
732 let lower = x_sample.lower * 3 + channel;
733 let upper = x_sample.upper * 3 + channel;
734 let value = bilinear_u8_component(
735 top[lower],
736 top[upper],
737 bottom[lower],
738 bottom[upper],
739 inv_wx,
740 wx,
741 inv_wy,
742 wy,
743 );
744 output[x] = (f32::from(value) * scale - mean) / std;
745 }
746 }
747}
748
749fn validate_resize_dimensions(
750 input_width: usize,
751 input_height: usize,
752 output_width: usize,
753 output_height: usize,
754) -> VisionResult<()> {
755 if output_width != 0 && output_height != 0 && (input_width == 0 || input_height == 0) {
756 return Err(VisionError::InvalidDimensions(
757 "cannot resize an empty input to a non-empty output".to_owned(),
758 ));
759 }
760 Ok(())
761}
762
763fn resize_output_len<const CHANNELS: usize>(width: usize, height: usize) -> VisionResult<usize> {
764 width
765 .checked_mul(height)
766 .and_then(|pixels| pixels.checked_mul(CHANNELS))
767 .ok_or_else(|| VisionError::InvalidDimensions("resize output is too large".to_owned()))
768}
769
770fn validate_plan_views<const CHANNELS: usize>(
771 input_dimensions: (usize, usize),
772 output_dimensions: (usize, usize),
773 input: ImageView<'_, u8, CHANNELS>,
774 output: &ImageViewMut<'_, u8, CHANNELS>,
775) -> VisionResult<()> {
776 if (input.width(), input.height()) != input_dimensions {
777 return Err(VisionError::InvalidDimensions(format!(
778 "resize plan expects input {}x{}, found {}x{}",
779 input_dimensions.0,
780 input_dimensions.1,
781 input.width(),
782 input.height()
783 )));
784 }
785 if (output.width(), output.height()) != output_dimensions {
786 return Err(VisionError::InvalidDimensions(format!(
787 "resize plan expects output {}x{}, found {}x{}",
788 output_dimensions.0,
789 output_dimensions.1,
790 output.width(),
791 output.height()
792 )));
793 }
794 Ok(())
795}
796
797fn nearest_axis_indices(input_len: usize, output_len: usize) -> Vec<usize> {
798 if input_len == 0 || output_len == 0 {
799 return Vec::new();
800 }
801 (0..output_len)
802 .map(|output| {
803 clamped_index(
804 half_pixel_coordinate(output, input_len, output_len).round() as isize,
805 input_len,
806 )
807 })
808 .collect()
809}
810
811fn area_axis_spans(input_len: usize, output_len: usize) -> Vec<AreaAxisSpan> {
812 if input_len == 0 || output_len == 0 {
813 return Vec::new();
814 }
815 let scale = input_len as f64 / output_len as f64;
816 (0..output_len)
817 .map(|output| {
818 let start_coordinate = output as f64 * scale;
819 let end_coordinate = (output + 1) as f64 * scale;
820 let start = start_coordinate.floor() as usize;
821 let end = end_coordinate.ceil().min(input_len as f64) as usize;
822 let weights = (start..end)
823 .map(|source| {
824 (end_coordinate.min(source as f64 + 1.0) - start_coordinate.max(source as f64))
825 .max(0.0)
826 })
827 .collect();
828 AreaAxisSpan { start, weights }
829 })
830 .collect()
831}
832
833fn resize_integer_area_row<const CHANNELS: usize>(
834 input: ImageView<'_, u8, CHANNELS>,
835 output: &mut [u8],
836 y: usize,
837 output_width: usize,
838 factor: usize,
839) {
840 let divisor = (factor * factor) as u32;
841 for x in 0..output_width {
842 let destination = x * CHANNELS;
843 for channel in 0..CHANNELS {
844 let mut sum = 0_u32;
845 for source_y in y * factor..(y + 1) * factor {
846 let row = input.row(source_y).expect("integer area source row");
847 for source_x in x * factor..(x + 1) * factor {
848 sum += u32::from(row[source_x * CHANNELS + channel]);
849 }
850 }
851 output[destination + channel] = ((sum + divisor / 2) / divisor) as u8;
852 }
853 }
854}
855
856fn validate_rgb_chw_normalization(scale: f32, std: [f32; 3]) -> VisionResult<()> {
857 if !scale.is_finite() || std.iter().any(|value| !value.is_finite() || *value == 0.0) {
858 return Err(VisionError::InvalidParameter(
859 "normalization scale/std must be finite and std non-zero".to_owned(),
860 ));
861 }
862 Ok(())
863}
864
865#[inline(always)]
866fn bilinear_u8_component(
867 top_left: u8,
868 top_right: u8,
869 bottom_left: u8,
870 bottom_right: u8,
871 inv_wx: u32,
872 wx: u32,
873 inv_wy: u32,
874 wy: u32,
875) -> u8 {
876 let top = u32::from(top_left) * inv_wx + u32::from(top_right) * wx;
877 let bottom = u32::from(bottom_left) * inv_wx + u32::from(bottom_right) * wx;
878 let round = 1 << (BILINEAR_WEIGHT_BITS * 2 - 1);
879 ((top * inv_wy + bottom * wy + round) >> (BILINEAR_WEIGHT_BITS * 2)) as u8
880}
881
882#[inline(always)]
883fn rgb_luma_q14(pixel: [u8; 3]) -> u8 {
884 ((4_899_u32 * u32::from(pixel[0])
885 + 9_617_u32 * u32::from(pixel[1])
886 + 1_868_u32 * u32::from(pixel[2])
887 + 8_192)
888 >> 14) as u8
889}
890
891fn bilinear_axis_samples(input_len: usize, output_len: usize) -> Vec<BilinearAxisSample> {
892 if input_len == 0 {
893 return Vec::new();
894 }
895 (0..output_len)
896 .map(|output| {
897 let coordinate = half_pixel_coordinate(output, input_len, output_len);
898 let base = coordinate.floor() as isize;
899 BilinearAxisSample {
900 lower: clamped_index(base, input_len),
901 upper: clamped_index(base + 1, input_len),
902 upper_weight: ((coordinate - coordinate.floor()) * f64::from(BILINEAR_WEIGHT_SCALE))
903 .round()
904 .clamp(0.0, f64::from(BILINEAR_WEIGHT_SCALE))
905 as u16,
906 }
907 })
908 .collect()
909}
910
911fn resize_half_row<const CHANNELS: usize>(
912 input: ImageView<'_, u8, CHANNELS>,
913 output: &mut [u8],
914 y: usize,
915 output_width: usize,
916) {
917 let top = input.row(y * 2).expect("half-scale source row");
918 let bottom = input.row(y * 2 + 1).expect("half-scale source row");
919 for x in 0..output_width {
920 let source = x * 2 * CHANNELS;
921 let destination = x * CHANNELS;
922 for channel in 0..CHANNELS {
923 let sum = u16::from(top[source + channel])
924 + u16::from(top[source + CHANNELS + channel])
925 + u16::from(bottom[source + channel])
926 + u16::from(bottom[source + CHANNELS + channel]);
927 output[destination + channel] = ((sum + 2) >> 2) as u8;
928 }
929 }
930}
931
932fn resize_half_rgb_to_gray_row(
933 input: ImageView<'_, u8, 3>,
934 output: &mut [u8],
935 y: usize,
936 output_width: usize,
937) {
938 let top = input.row(y * 2).expect("half-scale source row");
939 let bottom = input.row(y * 2 + 1).expect("half-scale source row");
940 for (x, target) in output.iter_mut().take(output_width).enumerate() {
941 let source = x * 6;
942 let pixel = std::array::from_fn(|channel| {
943 let sum = u16::from(top[source + channel])
944 + u16::from(top[source + 3 + channel])
945 + u16::from(bottom[source + channel])
946 + u16::from(bottom[source + 3 + channel]);
947 ((sum + 2) >> 2) as u8
948 });
949 *target = rgb_luma_q14(pixel);
950 }
951}
952
953#[allow(clippy::too_many_arguments)]
954fn resize_half_rgb_to_chw_row(
955 input: ImageView<'_, u8, 3>,
956 output: &mut [f32],
957 y: usize,
958 channel: usize,
959 scale: f32,
960 mean: f32,
961 std: f32,
962) {
963 let top = input.row(y * 2).expect("half-scale source row");
964 let bottom = input.row(y * 2 + 1).expect("half-scale source row");
965 for (x, target) in output.iter_mut().enumerate() {
966 let source = x * 6 + channel;
967 let sum = u16::from(top[source])
968 + u16::from(top[source + 3])
969 + u16::from(bottom[source])
970 + u16::from(bottom[source + 3]);
971 let value = ((sum + 2) >> 2) as u8;
972 *target = (f32::from(value) * scale - mean) / std;
973 }
974}
975
976pub fn resize<T: PixelComponent, const CHANNELS: usize>(
978 input: ImageView<'_, T, CHANNELS>,
979 output_width: usize,
980 output_height: usize,
981 interpolation: Interpolation,
982) -> VisionResult<Image<T, CHANNELS>> {
983 if output_width == 0 || output_height == 0 {
984 return Image::try_new_with_metadata(
985 output_width,
986 output_height,
987 Vec::new(),
988 input.metadata(),
989 )
990 .map_err(Into::into);
991 }
992 if input.width() == 0 || input.height() == 0 {
993 return Err(VisionError::InvalidDimensions(
994 "cannot resize an empty input to a non-empty output".to_owned(),
995 ));
996 }
997
998 let mut output = Vec::with_capacity(output_width * output_height * CHANNELS);
999 let area_downsample = interpolation == Interpolation::Area
1000 && (output_width < input.width() || output_height < input.height());
1001 for y in 0..output_height {
1002 for x in 0..output_width {
1003 let pixel = resized_pixel(
1004 input,
1005 x,
1006 y,
1007 output_width,
1008 output_height,
1009 interpolation,
1010 area_downsample,
1011 );
1012 output.extend_from_slice(&pixel);
1013 }
1014 }
1015 Ok(Image::try_new_with_metadata(output_width, output_height, output, input.metadata())?)
1016}
1017
1018pub fn resize_into<T: PixelComponent, const CHANNELS: usize>(
1024 input: ImageView<'_, T, CHANNELS>,
1025 mut output: ImageViewMut<'_, T, CHANNELS>,
1026 interpolation: Interpolation,
1027) -> VisionResult<()> {
1028 let output_width = output.width();
1029 let output_height = output.height();
1030 output.set_metadata(input.metadata())?;
1031 if output_width == 0 || output_height == 0 {
1032 return Ok(());
1033 }
1034 if input.width() == 0 || input.height() == 0 {
1035 return Err(VisionError::InvalidDimensions(
1036 "cannot resize an empty input to a non-empty output".to_owned(),
1037 ));
1038 }
1039 let area_downsample = interpolation == Interpolation::Area
1040 && (output_width < input.width() || output_height < input.height());
1041 for y in 0..output_height {
1042 let row = output.row_mut(y).expect("output row in bounds");
1043 for x in 0..output_width {
1044 let pixel = resized_pixel(
1045 input,
1046 x,
1047 y,
1048 output_width,
1049 output_height,
1050 interpolation,
1051 area_downsample,
1052 );
1053 row[x * CHANNELS..(x + 1) * CHANNELS].copy_from_slice(&pixel);
1054 }
1055 }
1056 Ok(())
1057}
1058
1059fn resized_pixel<T: PixelComponent, const CHANNELS: usize>(
1060 input: ImageView<'_, T, CHANNELS>,
1061 x: usize,
1062 y: usize,
1063 output_width: usize,
1064 output_height: usize,
1065 interpolation: Interpolation,
1066 area_downsample: bool,
1067) -> [T; CHANNELS] {
1068 if area_downsample {
1069 sample_area(input, x, y, output_width, output_height)
1070 } else {
1071 let sx = half_pixel_coordinate(x, input.width(), output_width);
1072 let sy = half_pixel_coordinate(y, input.height(), output_height);
1073 match interpolation {
1074 Interpolation::Nearest => sample_nearest(input, sx, sy),
1075 Interpolation::Bilinear | Interpolation::Area => sample_bilinear(input, sx, sy),
1076 Interpolation::Bicubic => sample_bicubic(input, sx, sy),
1077 }
1078 }
1079}
1080
1081fn half_pixel_coordinate(output: usize, input_len: usize, output_len: usize) -> f64 {
1082 (output as f64 + 0.5) * input_len as f64 / output_len as f64 - 0.5
1083}
1084
1085fn clamped_index(value: isize, length: usize) -> usize {
1086 value.clamp(0, length.saturating_sub(1) as isize) as usize
1087}
1088
1089pub(crate) fn sample_nearest<T: PixelComponent, const CHANNELS: usize>(
1090 input: ImageView<'_, T, CHANNELS>,
1091 x: f64,
1092 y: f64,
1093) -> [T; CHANNELS] {
1094 let ix = clamped_index(x.round() as isize, input.width());
1095 let iy = clamped_index(y.round() as isize, input.height());
1096 *input.get(ix, iy).expect("clamped resize coordinate")
1097}
1098
1099pub(crate) fn sample_bilinear<T: PixelComponent, const CHANNELS: usize>(
1100 input: ImageView<'_, T, CHANNELS>,
1101 x: f64,
1102 y: f64,
1103) -> [T; CHANNELS] {
1104 let x0_raw = x.floor() as isize;
1105 let y0_raw = y.floor() as isize;
1106 let x1_raw = x0_raw + 1;
1107 let y1_raw = y0_raw + 1;
1108 let wx = x - x.floor();
1109 let wy = y - y.floor();
1110 let x0 = clamped_index(x0_raw, input.width());
1111 let x1 = clamped_index(x1_raw, input.width());
1112 let y0 = clamped_index(y0_raw, input.height());
1113 let y1 = clamped_index(y1_raw, input.height());
1114 let p00 = input.get(x0, y0).expect("clamped resize coordinate");
1115 let p10 = input.get(x1, y0).expect("clamped resize coordinate");
1116 let p01 = input.get(x0, y1).expect("clamped resize coordinate");
1117 let p11 = input.get(x1, y1).expect("clamped resize coordinate");
1118 std::array::from_fn(|channel| {
1119 let top = p00[channel].to_f64().mul_add(1.0 - wx, p10[channel].to_f64() * wx);
1120 let bottom = p01[channel].to_f64().mul_add(1.0 - wx, p11[channel].to_f64() * wx);
1121 T::from_f64(top.mul_add(1.0 - wy, bottom * wy))
1122 })
1123}
1124
1125fn cubic_weight(distance: f64) -> f64 {
1126 let x = distance.abs();
1127 const A: f64 = -0.75;
1128 if x <= 1.0 {
1129 (A + 2.0) * x * x * x - (A + 3.0) * x * x + 1.0
1130 } else if x < 2.0 {
1131 A * x * x * x - 5.0 * A * x * x + 8.0 * A * x - 4.0 * A
1132 } else {
1133 0.0
1134 }
1135}
1136
1137fn sample_bicubic<T: PixelComponent, const CHANNELS: usize>(
1138 input: ImageView<'_, T, CHANNELS>,
1139 x: f64,
1140 y: f64,
1141) -> [T; CHANNELS] {
1142 let base_x = x.floor() as isize;
1143 let base_y = y.floor() as isize;
1144 let mut sums = [0.0_f64; CHANNELS];
1145 let mut total_weight = 0.0;
1146 for dy in -1..=2 {
1147 let wy = cubic_weight(y - (base_y + dy) as f64);
1148 let iy = clamped_index(base_y + dy, input.height());
1149 for dx in -1..=2 {
1150 let weight = wy * cubic_weight(x - (base_x + dx) as f64);
1151 let ix = clamped_index(base_x + dx, input.width());
1152 let pixel = input.get(ix, iy).expect("clamped resize coordinate");
1153 for channel in 0..CHANNELS {
1154 sums[channel] += pixel[channel].to_f64() * weight;
1155 }
1156 total_weight += weight;
1157 }
1158 }
1159 std::array::from_fn(|channel| T::from_f64(sums[channel] / total_weight))
1160}
1161
1162fn sample_area<T: PixelComponent, const CHANNELS: usize>(
1163 input: ImageView<'_, T, CHANNELS>,
1164 output_x: usize,
1165 output_y: usize,
1166 output_width: usize,
1167 output_height: usize,
1168) -> [T; CHANNELS] {
1169 let scale_x = input.width() as f64 / output_width as f64;
1170 let scale_y = input.height() as f64 / output_height as f64;
1171 let start_x = output_x as f64 * scale_x;
1172 let end_x = (output_x + 1) as f64 * scale_x;
1173 let start_y = output_y as f64 * scale_y;
1174 let end_y = (output_y + 1) as f64 * scale_y;
1175 let mut sums = [0.0_f64; CHANNELS];
1176 let mut total_weight = 0.0;
1177 for iy in start_y.floor() as usize..end_y.ceil().min(input.height() as f64) as usize {
1178 let overlap_y = (end_y.min(iy as f64 + 1.0) - start_y.max(iy as f64)).max(0.0);
1179 for ix in start_x.floor() as usize..end_x.ceil().min(input.width() as f64) as usize {
1180 let overlap_x = (end_x.min(ix as f64 + 1.0) - start_x.max(ix as f64)).max(0.0);
1181 let weight = overlap_x * overlap_y;
1182 let pixel = input.get(ix, iy).expect("area sample within image");
1183 for channel in 0..CHANNELS {
1184 sums[channel] += pixel[channel].to_f64() * weight;
1185 }
1186 total_weight += weight;
1187 }
1188 }
1189 std::array::from_fn(|channel| T::from_f64(sums[channel] / total_weight))
1190}
1191
1192#[cfg(test)]
1193mod tests {
1194 use super::{
1195 resize, resize_into, AreaResizeU8Plan, BilinearResizeU8Plan, Interpolation,
1196 NearestResizeU8Plan,
1197 };
1198 use proptest::prelude::*;
1199 use spatialrust_image::{Image, ImageView, ImageViewMut};
1200
1201 #[test]
1202 fn nearest_repeats_pixels() {
1203 let input = Image::<u8, 1>::try_new(2, 1, vec![10, 20]).unwrap();
1204 let output = resize(input.view(), 4, 1, Interpolation::Nearest).unwrap();
1205 assert_eq!(output.as_slice(), &[10, 10, 20, 20]);
1206 }
1207
1208 #[test]
1209 fn bilinear_interpolates_center() {
1210 let input = Image::<f32, 1>::try_new(2, 2, vec![0.0, 10.0, 20.0, 30.0]).unwrap();
1211 let output = resize(input.view(), 3, 3, Interpolation::Bilinear).unwrap();
1212 assert!((output[(1, 1)][0] - 15.0).abs() < 1e-6);
1213 }
1214
1215 #[test]
1216 fn area_computes_block_average() {
1217 let input = Image::<u8, 1>::try_new(2, 2, vec![0, 10, 20, 30]).unwrap();
1218 let output = resize(input.view(), 1, 1, Interpolation::Area).unwrap();
1219 assert_eq!(output.as_slice(), &[15]);
1220 }
1221
1222 #[test]
1223 fn identity_resize_is_exact_for_all_filters() {
1224 let input = Image::<u8, 3>::try_new(2, 2, (0..12).collect()).unwrap();
1225 for filter in [
1226 Interpolation::Nearest,
1227 Interpolation::Bilinear,
1228 Interpolation::Bicubic,
1229 Interpolation::Area,
1230 ] {
1231 assert_eq!(resize(input.view(), 2, 2, filter).unwrap(), input);
1232 }
1233 }
1234
1235 #[test]
1236 fn resize_into_reuses_strided_output_and_preserves_padding() {
1237 let input = Image::<u8, 1>::try_new(2, 2, vec![0, 10, 20, 30]).unwrap();
1238 let mut storage = vec![99_u8; 15];
1239 let output = ImageViewMut::<u8, 1>::new(3, 3, 6, &mut storage).unwrap();
1240 resize_into(input.view(), output, Interpolation::Bilinear).unwrap();
1241 let expected = resize(input.view(), 3, 3, Interpolation::Bilinear).unwrap();
1242 for y in 0..3 {
1243 assert_eq!(&storage[y * 6..y * 6 + 3], expected.view().row(y).unwrap());
1244 if y < 2 {
1245 assert_eq!(&storage[y * 6 + 3..(y + 1) * 6], &[99; 3]);
1246 }
1247 }
1248 }
1249
1250 #[test]
1251 fn bilinear_u8_plan_matches_generic_with_bounded_rounding() {
1252 let input =
1253 Image::<u8, 3>::try_new(7, 5, (0..105).map(|value| (value * 37 % 256) as u8).collect())
1254 .unwrap();
1255 let plan = BilinearResizeU8Plan::new(7, 5, 11, 8).unwrap();
1256 let actual = plan.resize(input.view()).unwrap();
1257 let expected = resize(input.view(), 11, 8, Interpolation::Bilinear).unwrap();
1258 assert!(actual
1259 .as_slice()
1260 .iter()
1261 .zip(expected.as_slice())
1262 .all(|(&left, &right)| left.abs_diff(right) <= 1));
1263 }
1264
1265 #[test]
1266 fn bilinear_u8_plan_half_scale_matches_generic_exactly() {
1267 let input =
1268 Image::<u8, 3>::try_new(8, 6, (0..144).map(|value| (value * 53 % 256) as u8).collect())
1269 .unwrap();
1270 let plan = BilinearResizeU8Plan::new(8, 6, 4, 3).unwrap();
1271 let actual = plan.resize(input.view()).unwrap();
1272 let expected = resize(input.view(), 4, 3, Interpolation::Bilinear).unwrap();
1273 assert_eq!(actual, expected);
1274 }
1275
1276 #[test]
1277 fn bilinear_u8_plan_preserves_strided_padding_and_validates_shape() {
1278 let input = Image::<u8, 1>::try_new(4, 4, (0..16).collect()).unwrap();
1279 let plan = BilinearResizeU8Plan::new(4, 4, 2, 2).unwrap();
1280 let mut storage = vec![99_u8; 7];
1281 let output = ImageViewMut::<u8, 1>::new(2, 2, 5, &mut storage).unwrap();
1282 plan.resize_into(input.view(), output).unwrap();
1283 assert_eq!(&storage, &[3, 5, 99, 99, 99, 11, 13]);
1284
1285 let wrong = BilinearResizeU8Plan::new(5, 4, 2, 2).unwrap();
1286 let mut output = Image::<u8, 1>::try_new(2, 2, vec![0; 4]).unwrap();
1287 assert!(wrong.resize_into(input.view(), output.view_mut()).is_err());
1288 }
1289
1290 #[test]
1291 fn bilinear_u8_plan_accepts_strided_rgb_input() {
1292 let mut storage = vec![231_u8; 54];
1293 for y in 0..4 {
1294 for x in 0..12 {
1295 storage[y * 14 + x] = (y * 41 + x * 13) as u8;
1296 }
1297 }
1298 let input = ImageView::<u8, 3>::new(4, 4, 14, &storage).unwrap();
1299 let plan = BilinearResizeU8Plan::new(4, 4, 2, 2).unwrap();
1300 let actual = plan.resize(input).unwrap();
1301 let expected = resize(input, 2, 2, Interpolation::Bilinear).unwrap();
1302 assert_eq!(actual, expected);
1303 }
1304
1305 proptest! {
1306 #![proptest_config(ProptestConfig::with_cases(300))]
1307
1308 #[test]
1309 fn packed_nearest_and_area_plans_are_bit_exact(
1310 input_width in 1_usize..25,
1311 input_height in 1_usize..25,
1312 output_width in 1_usize..25,
1313 output_height in 1_usize..25,
1314 seed in any::<u64>(),
1315 ) {
1316 let mut state = seed;
1317 let mut next_u8 = || {
1318 state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1319 (state >> 32) as u8
1320 };
1321 let rgb_data = (0..input_width * input_height * 3)
1322 .map(|_| next_u8())
1323 .collect::<Vec<_>>();
1324 let gray_data = (0..input_width * input_height)
1325 .map(|_| next_u8())
1326 .collect::<Vec<_>>();
1327 let rgb = Image::<u8, 3>::try_new(input_width, input_height, rgb_data).unwrap();
1328 let gray = Image::<u8, 1>::try_new(input_width, input_height, gray_data).unwrap();
1329
1330 let nearest = NearestResizeU8Plan::new(
1331 input_width,
1332 input_height,
1333 output_width,
1334 output_height,
1335 )
1336 .unwrap();
1337 prop_assert_eq!(
1338 nearest.resize(rgb.view()).unwrap(),
1339 resize(rgb.view(), output_width, output_height, Interpolation::Nearest).unwrap(),
1340 );
1341 prop_assert_eq!(
1342 nearest.resize(gray.view()).unwrap(),
1343 resize(gray.view(), output_width, output_height, Interpolation::Nearest).unwrap(),
1344 );
1345
1346 let area = AreaResizeU8Plan::new(
1347 input_width,
1348 input_height,
1349 output_width,
1350 output_height,
1351 )
1352 .unwrap();
1353 prop_assert_eq!(
1354 area.resize(rgb.view()).unwrap(),
1355 resize(rgb.view(), output_width, output_height, Interpolation::Area).unwrap(),
1356 );
1357 prop_assert_eq!(
1358 area.resize(gray.view()).unwrap(),
1359 resize(gray.view(), output_width, output_height, Interpolation::Area).unwrap(),
1360 );
1361 }
1362 }
1363
1364 #[test]
1365 fn nearest_and_area_plans_fall_back_for_strides_and_preserve_padding() {
1366 let mut input_storage = vec![211_u8; 47];
1367 for y in 0..4 {
1368 for x in 0..9 {
1369 input_storage[y * 11 + x] = (y * 67 + x * 19) as u8;
1370 }
1371 }
1372 let input = ImageView::<u8, 3>::new(3, 4, 11, &input_storage).unwrap();
1373 for (interpolation, nearest) in
1374 [(Interpolation::Nearest, true), (Interpolation::Area, false)]
1375 {
1376 let mut storage = vec![199_u8; 29];
1377 let output = ImageViewMut::<u8, 3>::new(2, 3, 10, &mut storage).unwrap();
1378 if nearest {
1379 NearestResizeU8Plan::new(3, 4, 2, 3).unwrap().resize_into(input, output).unwrap();
1380 } else {
1381 AreaResizeU8Plan::new(3, 4, 2, 3).unwrap().resize_into(input, output).unwrap();
1382 }
1383 let expected = resize(input, 2, 3, interpolation).unwrap();
1384 for y in 0..3 {
1385 assert_eq!(&storage[y * 10..y * 10 + 6], expected.view().row(y).unwrap());
1386 if y < 2 {
1387 assert_eq!(&storage[y * 10 + 6..(y + 1) * 10], &[199; 4]);
1388 }
1389 }
1390 }
1391 }
1392
1393 #[test]
1394 fn nearest_and_area_plans_reject_wrong_dimensions() {
1395 let input = Image::<u8, 1>::try_new(4, 4, (0..16).collect()).unwrap();
1396 let mut output = Image::<u8, 1>::try_new(2, 2, vec![0; 4]).unwrap();
1397 assert!(NearestResizeU8Plan::new(5, 4, 2, 2)
1398 .unwrap()
1399 .resize_into(input.view(), output.view_mut())
1400 .is_err());
1401 assert!(AreaResizeU8Plan::new(4, 4, 3, 2)
1402 .unwrap()
1403 .resize_into(input.view(), output.view_mut())
1404 .is_err());
1405 }
1406}