1use pulp::Arch;
2use rayon::prelude::*;
3use spatialrust_image::{
4 ColorSpace, Image, ImageMetadata, ImageRegion, ImageView, ImageViewMut, PlanarImage,
5};
6
7use crate::dispatch::{
8 should_parallelize, LIGHT_PARALLEL_COMPONENTS, ROWS_PER_TILE, ROW_PARALLEL_COMPONENTS,
9 TALL_IMAGE_ROWS,
10};
11use crate::{
12 resize, BilinearResizeU8Plan, Interpolation, PixelComponent, VisionError, VisionResult,
13};
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
17pub struct Padding {
18 pub left: usize,
20 pub right: usize,
22 pub top: usize,
24 pub bottom: usize,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct LetterboxTransform {
31 pub scale: f64,
33 pub pad_left: usize,
35 pub pad_top: usize,
37 pub content_width: usize,
39 pub content_height: usize,
41 pub output_width: usize,
43 pub output_height: usize,
45}
46
47impl LetterboxTransform {
48 #[must_use]
50 pub fn map_point(self, x: f64, y: f64) -> (f64, f64) {
51 (x.mul_add(self.scale, self.pad_left as f64), y.mul_add(self.scale, self.pad_top as f64))
52 }
53
54 #[must_use]
56 pub fn unmap_point(self, x: f64, y: f64) -> (f64, f64) {
57 ((x - self.pad_left as f64) / self.scale, (y - self.pad_top as f64) / self.scale)
58 }
59}
60
61pub fn crop<T: PixelComponent, const CHANNELS: usize>(
63 input: ImageView<'_, T, CHANNELS>,
64 region: ImageRegion,
65) -> VisionResult<Image<T, CHANNELS>> {
66 let view = input.subview(region)?;
67 let mut data = Vec::with_capacity(region.width * region.height * CHANNELS);
68 for y in 0..region.height {
69 data.extend_from_slice(view.row(y).expect("validated crop row"));
70 }
71 Ok(Image::try_new_with_metadata(region.width, region.height, data, input.metadata())?)
72}
73
74pub fn pad<T: PixelComponent, const CHANNELS: usize>(
76 input: ImageView<'_, T, CHANNELS>,
77 padding: Padding,
78 value: [T; CHANNELS],
79) -> VisionResult<Image<T, CHANNELS>> {
80 let width = input
81 .width()
82 .checked_add(padding.left)
83 .and_then(|value| value.checked_add(padding.right))
84 .ok_or_else(|| VisionError::InvalidDimensions("padding width overflow".to_owned()))?;
85 let height = input
86 .height()
87 .checked_add(padding.top)
88 .and_then(|value| value.checked_add(padding.bottom))
89 .ok_or_else(|| VisionError::InvalidDimensions("padding height overflow".to_owned()))?;
90 let mut output = Vec::with_capacity(width * height * CHANNELS);
91 for y in 0..height {
92 for x in 0..width {
93 if x >= padding.left
94 && x < padding.left + input.width()
95 && y >= padding.top
96 && y < padding.top + input.height()
97 {
98 output.extend_from_slice(
99 input.get(x - padding.left, y - padding.top).expect("validated pad coordinate"),
100 );
101 } else {
102 output.extend_from_slice(&value);
103 }
104 }
105 }
106 Ok(Image::try_new_with_metadata(width, height, output, input.metadata())?)
107}
108
109pub fn letterbox<T: PixelComponent, const CHANNELS: usize>(
111 input: ImageView<'_, T, CHANNELS>,
112 output_width: usize,
113 output_height: usize,
114 interpolation: Interpolation,
115 value: [T; CHANNELS],
116) -> VisionResult<(Image<T, CHANNELS>, LetterboxTransform)> {
117 if input.width() == 0 || input.height() == 0 || output_width == 0 || output_height == 0 {
118 return Err(VisionError::InvalidDimensions(
119 "letterbox requires non-empty input and output".to_owned(),
120 ));
121 }
122 let scale = (output_width as f64 / input.width() as f64)
123 .min(output_height as f64 / input.height() as f64);
124 let content_width = ((input.width() as f64 * scale).round() as usize).clamp(1, output_width);
125 let content_height = ((input.height() as f64 * scale).round() as usize).clamp(1, output_height);
126 let resized = resize(input, content_width, content_height, interpolation)?;
127 let remaining_x = output_width - content_width;
128 let remaining_y = output_height - content_height;
129 let padding = Padding {
130 left: remaining_x / 2,
131 right: remaining_x - remaining_x / 2,
132 top: remaining_y / 2,
133 bottom: remaining_y - remaining_y / 2,
134 };
135 let transform = LetterboxTransform {
136 scale,
137 pad_left: padding.left,
138 pad_top: padding.top,
139 content_width,
140 content_height,
141 output_width,
142 output_height,
143 };
144 Ok((pad(resized.view(), padding, value)?, transform))
145}
146
147pub fn normalize<T: PixelComponent, const CHANNELS: usize>(
150 input: ImageView<'_, T, CHANNELS>,
151 scale: f32,
152 mean: [f32; CHANNELS],
153 std: [f32; CHANNELS],
154) -> VisionResult<Image<f32, CHANNELS>> {
155 validate_normalization(scale, std)?;
156 let mut output = Vec::with_capacity(input.width() * input.height() * CHANNELS);
157 for y in 0..input.height() {
158 for pixel in input.row(y).expect("input row in bounds").chunks_exact(CHANNELS) {
159 for channel in 0..CHANNELS {
160 output
161 .push((pixel[channel].to_f64() as f32 * scale - mean[channel]) / std[channel]);
162 }
163 }
164 }
165 Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
166}
167
168pub fn normalize_into<T: PixelComponent, const CHANNELS: usize>(
170 input: ImageView<'_, T, CHANNELS>,
171 mut output: ImageViewMut<'_, f32, CHANNELS>,
172 scale: f32,
173 mean: [f32; CHANNELS],
174 std: [f32; CHANNELS],
175) -> VisionResult<()> {
176 validate_normalization(scale, std)?;
177 validate_output_dimensions(input, output.width(), output.height())?;
178 output.set_metadata(input.metadata())?;
179 for y in 0..input.height() {
180 let source = input.row(y).expect("input row in bounds");
181 let target = output.row_mut(y).expect("output row in bounds");
182 for (source_pixel, target_pixel) in
183 source.chunks_exact(CHANNELS).zip(target.chunks_exact_mut(CHANNELS))
184 {
185 for channel in 0..CHANNELS {
186 target_pixel[channel] =
187 (source_pixel[channel].to_f64() as f32 * scale - mean[channel]) / std[channel];
188 }
189 }
190 }
191 Ok(())
192}
193
194pub fn pack_chw<T: PixelComponent, const CHANNELS: usize>(
196 input: ImageView<'_, T, CHANNELS>,
197 scale: f32,
198 mean: [f32; CHANNELS],
199 std: [f32; CHANNELS],
200) -> VisionResult<PlanarImage<f32, CHANNELS>> {
201 let plane_len = input.width() * input.height();
202 let mut output = vec![0.0_f32; plane_len * CHANNELS];
203 pack_chw_into(input, scale, mean, std, &mut output)?;
204 Ok(PlanarImage::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
205}
206
207pub fn pack_chw_into<T: PixelComponent, const CHANNELS: usize>(
212 input: ImageView<'_, T, CHANNELS>,
213 scale: f32,
214 mean: [f32; CHANNELS],
215 std: [f32; CHANNELS],
216 output: &mut [f32],
217) -> VisionResult<()> {
218 validate_normalization(scale, std)?;
219 let plane_len = input.width().checked_mul(input.height()).ok_or_else(|| {
220 VisionError::InvalidDimensions("CHW plane dimensions overflow".to_owned())
221 })?;
222 let required = plane_len.checked_mul(CHANNELS).ok_or_else(|| {
223 VisionError::InvalidDimensions("CHW output dimensions overflow".to_owned())
224 })?;
225 if output.len() != required {
226 return Err(VisionError::ShapeMismatch(format!(
227 "CHW output needs {required} elements, found {}",
228 output.len()
229 )));
230 }
231 if plane_len == 0 {
232 return Ok(());
233 }
234 if CHANNELS > 1 && should_parallelize(plane_len, CHANNELS, ROW_PARALLEL_COMPONENTS) {
235 std::thread::scope(|scope| {
236 for (channel, plane) in output.chunks_exact_mut(plane_len).enumerate() {
237 scope.spawn(move || {
238 fill_chw_plane(input, channel, scale, mean[channel], std[channel], plane)
239 });
240 }
241 });
242 } else {
243 for (channel, plane) in output.chunks_exact_mut(plane_len).enumerate() {
244 fill_chw_plane(input, channel, scale, mean[channel], std[channel], plane);
245 }
246 }
247 Ok(())
248}
249
250pub fn resize_pack_chw(
252 input: ImageView<'_, u8, 3>,
253 output_width: usize,
254 output_height: usize,
255 scale: f32,
256 mean: [f32; 3],
257 std: [f32; 3],
258) -> VisionResult<PlanarImage<f32, 3>> {
259 BilinearResizeU8Plan::new(input.width(), input.height(), output_width, output_height)?
260 .resize_rgb_to_chw(input, scale, mean, std)
261}
262
263#[allow(clippy::too_many_arguments)]
265pub fn resize_pack_chw_into(
266 input: ImageView<'_, u8, 3>,
267 output_width: usize,
268 output_height: usize,
269 scale: f32,
270 mean: [f32; 3],
271 std: [f32; 3],
272 output: &mut [f32],
273) -> VisionResult<()> {
274 BilinearResizeU8Plan::new(input.width(), input.height(), output_width, output_height)?
275 .resize_rgb_to_chw_into(input, scale, mean, std, output)
276}
277
278fn fill_chw_plane<T: PixelComponent, const CHANNELS: usize>(
279 input: ImageView<'_, T, CHANNELS>,
280 channel: usize,
281 scale: f32,
282 mean: f32,
283 std: f32,
284 output: &mut [f32],
285) {
286 for y in 0..input.height() {
287 let row = input.row(y).expect("input row in bounds");
288 for (x, pixel) in row.chunks_exact(CHANNELS).enumerate() {
289 output[y * input.width() + x] = (pixel[channel].to_f64() as f32 * scale - mean) / std;
290 }
291 }
292}
293
294fn validate_normalization<const CHANNELS: usize>(
295 scale: f32,
296 std: [f32; CHANNELS],
297) -> VisionResult<()> {
298 if !scale.is_finite() || std.iter().any(|value| !value.is_finite() || *value == 0.0) {
299 return Err(VisionError::InvalidParameter(
300 "normalization scale/std must be finite and std non-zero".to_owned(),
301 ));
302 }
303 Ok(())
304}
305
306fn validate_output_dimensions<T: PixelComponent, const CHANNELS: usize>(
307 input: ImageView<'_, T, CHANNELS>,
308 output_width: usize,
309 output_height: usize,
310) -> VisionResult<()> {
311 if (input.width(), input.height()) != (output_width, output_height) {
312 return Err(VisionError::ShapeMismatch(format!(
313 "output dimensions {output_width}x{output_height} do not match input {}x{}",
314 input.width(),
315 input.height()
316 )));
317 }
318 Ok(())
319}
320
321pub fn swap_red_blue<T: PixelComponent>(input: ImageView<'_, T, 3>) -> VisionResult<Image<T, 3>> {
323 let mut data = Vec::with_capacity(input.width() * input.height() * 3);
324 for y in 0..input.height() {
325 for x in 0..input.width() {
326 let pixel = input.get(x, y).expect("image coordinate in bounds");
327 data.extend_from_slice(&[pixel[2], pixel[1], pixel[0]]);
328 }
329 }
330 let mut metadata = input.metadata();
331 metadata.color_space = match metadata.color_space {
332 ColorSpace::Rgb => ColorSpace::Bgr,
333 ColorSpace::Bgr => ColorSpace::Rgb,
334 other => other,
335 };
336 Ok(Image::try_new_with_metadata(input.width(), input.height(), data, metadata)?)
337}
338
339pub fn rgb_to_gray(input: ImageView<'_, u8, 3>) -> VisionResult<Image<u8, 1>> {
341 let metadata = ImageMetadata { color_space: ColorSpace::Gray, ..input.metadata() };
342 let len = input
343 .width()
344 .checked_mul(input.height())
345 .ok_or_else(|| VisionError::InvalidDimensions("gray output is too large".to_owned()))?;
346 let mut output =
347 Image::try_new_with_metadata(input.width(), input.height(), vec![0; len], metadata)?;
348 rgb_to_gray_into(input, output.view_mut())?;
349 Ok(output)
350}
351
352pub fn rgb_to_gray_into(
354 input: ImageView<'_, u8, 3>,
355 mut output: ImageViewMut<'_, u8, 1>,
356) -> VisionResult<()> {
357 validate_output_dimensions(input, output.width(), output.height())?;
358 output.set_metadata(ImageMetadata { color_space: ColorSpace::Gray, ..input.metadata() })?;
359 let output_stride = output.row_stride();
360 let pixels = input
361 .width()
362 .checked_mul(input.height())
363 .ok_or_else(|| VisionError::InvalidDimensions("gray output is too large".to_owned()))?;
364 let arch = Arch::new();
365 if should_parallelize(pixels, input.height(), LIGHT_PARALLEL_COMPONENTS) {
366 if input.height() >= TALL_IMAGE_ROWS {
367 let block_stride = output_stride.checked_mul(ROWS_PER_TILE).ok_or_else(|| {
368 VisionError::InvalidDimensions("gray row block is too large".to_owned())
369 })?;
370 output.as_mut_slice().par_chunks_mut(block_stride).enumerate().for_each(
371 |(block, rows)| {
372 arch.dispatch(|| {
373 for (row, target) in rows.chunks_mut(output_stride).enumerate() {
374 rgb_to_gray_row(input, block * ROWS_PER_TILE + row, target);
375 }
376 });
377 },
378 );
379 } else {
380 output
381 .as_mut_slice()
382 .par_chunks_mut(output_stride)
383 .enumerate()
384 .for_each(|(y, target)| arch.dispatch(|| rgb_to_gray_row(input, y, target)));
385 }
386 } else {
387 output
388 .as_mut_slice()
389 .chunks_mut(output_stride)
390 .enumerate()
391 .for_each(|(y, target)| arch.dispatch(|| rgb_to_gray_row(input, y, target)));
392 }
393 Ok(())
394}
395
396pub fn resize_rgb_to_gray(
398 input: ImageView<'_, u8, 3>,
399 output_width: usize,
400 output_height: usize,
401) -> VisionResult<Image<u8, 1>> {
402 BilinearResizeU8Plan::new(input.width(), input.height(), output_width, output_height)?
403 .resize_rgb_to_gray(input)
404}
405
406pub fn resize_rgb_to_gray_into(
408 input: ImageView<'_, u8, 3>,
409 output: ImageViewMut<'_, u8, 1>,
410) -> VisionResult<()> {
411 BilinearResizeU8Plan::new(input.width(), input.height(), output.width(), output.height())?
412 .resize_rgb_to_gray_into(input, output)
413}
414
415fn rgb_to_gray_row(input: ImageView<'_, u8, 3>, y: usize, target: &mut [u8]) {
416 let source = input.row(y).expect("input row in bounds");
417 for (pixel, target_value) in source.chunks_exact(3).zip(target.iter_mut()) {
418 *target_value = rgb_luma(pixel);
419 }
420}
421
422#[inline(always)]
423fn rgb_luma(pixel: &[u8]) -> u8 {
424 ((4_899_u32 * u32::from(pixel[0])
425 + 9_617_u32 * u32::from(pixel[1])
426 + 1_868_u32 * u32::from(pixel[2])
427 + 8_192)
428 >> 14) as u8
429}
430
431pub fn gray_to_rgb<T: PixelComponent>(input: ImageView<'_, T, 1>) -> VisionResult<Image<T, 3>> {
433 let mut data = Vec::with_capacity(input.width() * input.height() * 3);
434 for y in 0..input.height() {
435 for x in 0..input.width() {
436 let value = input.get(x, y).expect("image coordinate in bounds")[0];
437 data.extend_from_slice(&[value, value, value]);
438 }
439 }
440 let metadata = ImageMetadata { color_space: ColorSpace::Rgb, ..input.metadata() };
441 Ok(Image::try_new_with_metadata(input.width(), input.height(), data, metadata)?)
442}
443
444pub fn rgb_to_hsv(input: ImageView<'_, u8, 3>) -> VisionResult<Image<u8, 3>> {
446 let mut data = Vec::with_capacity(input.width() * input.height() * 3);
447 for y in 0..input.height() {
448 for x in 0..input.width() {
449 let [r, g, b] = *input.get(x, y).expect("image coordinate in bounds");
450 let rf = f64::from(r) / 255.0;
451 let gf = f64::from(g) / 255.0;
452 let bf = f64::from(b) / 255.0;
453 let max = rf.max(gf).max(bf);
454 let min = rf.min(gf).min(bf);
455 let delta = max - min;
456 let mut hue = if delta == 0.0 {
457 0.0
458 } else if max == rf {
459 60.0 * ((gf - bf) / delta).rem_euclid(6.0)
460 } else if max == gf {
461 60.0 * ((bf - rf) / delta + 2.0)
462 } else {
463 60.0 * ((rf - gf) / delta + 4.0)
464 };
465 if hue >= 360.0 {
466 hue = 0.0;
467 }
468 let saturation = if max == 0.0 { 0.0 } else { delta / max };
469 data.extend_from_slice(&[
470 (hue / 2.0).round().clamp(0.0, 179.0) as u8,
471 (saturation * 255.0).round() as u8,
472 (max * 255.0).round() as u8,
473 ]);
474 }
475 }
476 let metadata = ImageMetadata { color_space: ColorSpace::Hsv, ..input.metadata() };
477 Ok(Image::try_new_with_metadata(input.width(), input.height(), data, metadata)?)
478}
479
480#[cfg(test)]
481mod tests {
482 use super::{
483 crop, gray_to_rgb, letterbox, normalize, normalize_into, pack_chw, pack_chw_into, pad,
484 resize_pack_chw, resize_pack_chw_into, resize_rgb_to_gray, resize_rgb_to_gray_into,
485 rgb_to_gray, rgb_to_gray_into, rgb_to_hsv, Padding,
486 };
487 use crate::Interpolation;
488 use spatialrust_image::{
489 ColorSpace, Image, ImageMetadata, ImageRegion, ImageView, ImageViewMut,
490 };
491
492 #[test]
493 fn crop_and_pad_roundtrip_center() {
494 let image = Image::<u8, 1>::try_new(3, 2, vec![1, 2, 3, 4, 5, 6]).unwrap();
495 let cropped = crop(image.view(), ImageRegion::new(1, 0, 2, 2)).unwrap();
496 assert_eq!(cropped.as_slice(), &[2, 3, 5, 6]);
497 let padded =
498 pad(cropped.view(), Padding { left: 1, right: 0, top: 1, bottom: 0 }, [0]).unwrap();
499 assert_eq!(padded.as_slice(), &[0, 0, 0, 0, 2, 3, 0, 5, 6]);
500 }
501
502 #[test]
503 fn letterbox_preserves_aspect_and_maps_points() {
504 let image = Image::<u8, 1>::try_new(4, 2, vec![1; 8]).unwrap();
505 let (output, transform) =
506 letterbox(image.view(), 8, 8, Interpolation::Nearest, [0]).unwrap();
507 assert_eq!((output.width(), output.height()), (8, 8));
508 assert_eq!((transform.content_width, transform.content_height), (8, 4));
509 assert_eq!(transform.pad_top, 2);
510 let mapped = transform.map_point(1.0, 1.0);
511 assert_eq!(transform.unmap_point(mapped.0, mapped.1), (1.0, 1.0));
512 }
513
514 #[test]
515 fn normalize_and_chw_have_expected_layout() {
516 let image = Image::<u8, 3>::try_new(2, 1, vec![0, 10, 20, 30, 40, 50]).unwrap();
517 let normalized = normalize(image.view(), 0.1, [0.0; 3], [1.0; 3]).unwrap();
518 assert_eq!(normalized.as_slice(), &[0.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
519 let chw = pack_chw(image.view(), 0.1, [0.0; 3], [1.0; 3]).unwrap();
520 assert_eq!(chw.as_slice(), &[0.0, 3.0, 1.0, 4.0, 2.0, 5.0]);
521 }
522
523 #[test]
524 fn reusable_normalize_and_chw_match_owned_outputs() {
525 let image = Image::<u8, 3>::try_new(2, 1, vec![0, 10, 20, 30, 40, 50]).unwrap();
526 let mut interleaved = vec![-1.0_f32; 9];
527 let output = ImageViewMut::<f32, 3>::new(2, 1, 9, &mut interleaved).unwrap();
528 normalize_into(image.view(), output, 0.1, [0.0; 3], [1.0; 3]).unwrap();
529 assert_eq!(
530 &interleaved[..6],
531 normalize(image.view(), 0.1, [0.0; 3], [1.0; 3]).unwrap().as_slice()
532 );
533 assert_eq!(&interleaved[6..], &[-1.0; 3]);
534
535 let mut chw = vec![0.0; 6];
536 pack_chw_into(image.view(), 0.1, [0.0; 3], [1.0; 3], &mut chw).unwrap();
537 assert_eq!(chw, pack_chw(image.view(), 0.1, [0.0; 3], [1.0; 3]).unwrap().as_slice());
538 assert!(pack_chw_into(image.view(), 1.0, [0.0; 3], [1.0; 3], &mut chw[..5]).is_err());
539
540 let empty = Image::<u8, 3>::try_new(0, 0, Vec::new()).unwrap();
541 pack_chw_into(empty.view(), 1.0, [0.0; 3], [1.0; 3], &mut []).unwrap();
542 }
543
544 #[test]
545 fn large_chw_parallel_dispatch_matches_channel_layout() {
546 let image = Image::<u8, 3>::try_new(512, 512, vec![10; 512 * 512 * 3]).unwrap();
547 let mut chw = vec![0.0; 512 * 512 * 3];
548 pack_chw_into(image.view(), 0.1, [0.0, 0.5, 1.0], [1.0; 3], &mut chw).unwrap();
549 let plane = 512 * 512;
550 assert!(chw[..plane].iter().all(|&value| value == 1.0));
551 assert!(chw[plane..2 * plane].iter().all(|&value| value == 0.5));
552 assert!(chw[2 * plane..].iter().all(|&value| value == 0.0));
553 }
554
555 #[test]
556 fn fused_resize_chw_matches_unfused_general_and_preserves_metadata() {
557 let metadata = ImageMetadata {
558 color_space: ColorSpace::Rgb,
559 color_range: spatialrust_image::ColorRange::Full,
560 ..ImageMetadata::default()
561 };
562 let input = Image::<u8, 3>::try_new_with_metadata(
563 7,
564 5,
565 (0..105).map(|value| (value * 37 % 256) as u8).collect(),
566 metadata,
567 )
568 .unwrap();
569 let plan = crate::BilinearResizeU8Plan::new(7, 5, 11, 8).unwrap();
570 let resized = plan.resize(input.view()).unwrap();
571 let expected =
572 pack_chw(resized.view(), 1.0 / 255.0, [0.1, 0.2, 0.3], [0.5, 1.0, 2.0]).unwrap();
573 let actual =
574 resize_pack_chw(input.view(), 11, 8, 1.0 / 255.0, [0.1, 0.2, 0.3], [0.5, 1.0, 2.0])
575 .unwrap();
576 assert_eq!(actual, expected);
577 assert_eq!(actual.metadata(), metadata);
578 }
579
580 #[test]
581 fn fused_resize_chw_matches_unfused_half_scale_for_strided_input_and_reuse() {
582 let mut storage = vec![231_u8; 169];
583 for y in 0..6 {
584 for x in 0..24 {
585 storage[y * 29 + x] = (y * 41 + x * 13) as u8;
586 }
587 }
588 let input = ImageView::<u8, 3>::new(8, 6, 29, &storage).unwrap();
589 let plan = crate::BilinearResizeU8Plan::new(8, 6, 4, 3).unwrap();
590 let resized = plan.resize(input).unwrap();
591 let expected = pack_chw(resized.view(), 0.25, [1.0, 2.0, 3.0], [1.0; 3]).unwrap();
592 let mut output = vec![-1.0_f32; 36];
593 resize_pack_chw_into(input, 4, 3, 0.25, [1.0, 2.0, 3.0], [1.0; 3], &mut output).unwrap();
594 assert_eq!(output, expected.as_slice());
595 assert!(
596 resize_pack_chw_into(input, 4, 3, 0.25, [0.0; 3], [1.0; 3], &mut output[..35]).is_err()
597 );
598 }
599
600 #[test]
601 fn rgb_to_gray_into_accepts_strided_output() {
602 let image = Image::<u8, 3>::try_new(2, 1, vec![255, 0, 0, 0, 255, 0]).unwrap();
603 let mut storage = vec![200_u8; 4];
604 let output = ImageViewMut::<u8, 1>::new(2, 1, 4, &mut storage).unwrap();
605 rgb_to_gray_into(image.view(), output).unwrap();
606 assert_eq!(&storage[..2], rgb_to_gray(image.view()).unwrap().as_slice());
607 assert_eq!(&storage[2..], &[200, 200]);
608 }
609
610 #[test]
611 fn rgb_to_gray_accepts_strided_input_and_preserves_metadata() {
612 let storage = [255, 0, 0, 0, 255, 0, 91, 92, 0, 0, 255, 255, 255, 255];
613 let metadata = ImageMetadata { color_space: ColorSpace::Rgb, ..Default::default() };
614 let input = ImageView::<u8, 3>::new_with_metadata(2, 2, 8, &storage, metadata).unwrap();
615 let gray = rgb_to_gray(input).unwrap();
616 assert_eq!(gray.as_slice(), &[76, 150, 29, 255]);
617 assert_eq!(gray.metadata().color_space, ColorSpace::Gray);
618 }
619
620 #[test]
621 fn fused_resize_rgb_to_gray_matches_unfused_half_scale_exactly() {
622 let metadata = ImageMetadata {
623 color_space: ColorSpace::Rgb,
624 color_range: spatialrust_image::ColorRange::Full,
625 ..ImageMetadata::default()
626 };
627 let input = Image::<u8, 3>::try_new_with_metadata(
628 8,
629 6,
630 (0..144).map(|value| (value * 53 % 256) as u8).collect(),
631 metadata,
632 )
633 .unwrap();
634 let resized =
635 crate::BilinearResizeU8Plan::new(8, 6, 4, 3).unwrap().resize(input.view()).unwrap();
636 let expected = rgb_to_gray(resized.view()).unwrap();
637 let actual = resize_rgb_to_gray(input.view(), 4, 3).unwrap();
638 assert_eq!(actual, expected);
639 assert_eq!(actual.metadata().color_space, ColorSpace::Gray);
640 assert_eq!(actual.metadata().color_range, spatialrust_image::ColorRange::Full);
641 }
642
643 #[test]
644 fn fused_resize_rgb_to_gray_matches_unfused_general_and_strided_output() {
645 let mut input_storage = vec![231_u8; 83];
646 for y in 0..5 {
647 for x in 0..15 {
648 input_storage[y * 17 + x] = (y * 41 + x * 13) as u8;
649 }
650 }
651 let input = ImageView::<u8, 3>::new(5, 5, 17, &input_storage).unwrap();
652 let resized = crate::BilinearResizeU8Plan::new(5, 5, 7, 3).unwrap().resize(input).unwrap();
653 let expected = rgb_to_gray(resized.view()).unwrap();
654 let mut output_storage = vec![199_u8; 29];
655 let output = ImageViewMut::<u8, 1>::new(7, 3, 11, &mut output_storage).unwrap();
656 resize_rgb_to_gray_into(input, output).unwrap();
657 for y in 0..3 {
658 assert_eq!(&output_storage[y * 11..y * 11 + 7], expected.view().row(y).unwrap());
659 if y < 2 {
660 assert_eq!(&output_storage[y * 11 + 7..(y + 1) * 11], &[199; 4]);
661 }
662 }
663 }
664
665 #[test]
666 fn color_conversions_match_known_primaries() {
667 let metadata = ImageMetadata { color_space: ColorSpace::Rgb, ..Default::default() };
668 let image = Image::<u8, 3>::try_new_with_metadata(
669 3,
670 1,
671 vec![255, 0, 0, 0, 255, 0, 0, 0, 255],
672 metadata,
673 )
674 .unwrap();
675 assert_eq!(rgb_to_gray(image.view()).unwrap().as_slice(), &[76, 150, 29]);
676 assert_eq!(
677 rgb_to_hsv(image.view()).unwrap().as_slice(),
678 &[0, 255, 255, 60, 255, 255, 120, 255, 255]
679 );
680 let gray = Image::<u8, 1>::try_new(1, 1, vec![12]).unwrap();
681 assert_eq!(gray_to_rgb(gray.view()).unwrap().as_slice(), &[12, 12, 12]);
682 }
683}