1use crate::definitions::Image;
4use image::{
5 DynamicImage, GenericImage, GenericImageView, GrayImage, Luma, Pixel, Rgb, RgbImage, open,
6};
7use itertools::Itertools;
8
9use std::cmp::{max, min};
10use std::collections::HashSet;
11use std::hint::black_box;
12use std::path::Path;
13use std::{fmt, fmt::Write};
14
15#[macro_export]
84macro_rules! gray_image {
85 () => {
87 gray_image!(type: u8)
88 };
89 (type: $channel_type:ty) => {
91 {
92 use image::Luma;
93 use $crate::definitions::Image;
94
95 Image::<Luma<$channel_type>>::new(0, 0)
96 }
97 };
98 ($( $( $x: expr ),*);*) => {
100 gray_image!(type: u8, $( $( $x ),*);*)
101 };
102 (type: $channel_type:ty, $( $( $x: expr ),*);*) => {
104 {
105 use image::Luma;
106 use $crate::definitions::Image;
107
108 let nested_array = [ $( [ $($x),* ] ),* ];
109 let height = nested_array.len() as u32;
110 let width = nested_array[0].len() as u32;
111
112 let flat_array: Vec<_> = nested_array.iter()
113 .flat_map(|row| row.into_iter())
114 .cloned()
115 .collect();
116
117 Image::<Luma<$channel_type>>::from_raw(width, height, flat_array)
118 .unwrap()
119 }
120 }
121}
122
123#[macro_export]
189macro_rules! rgb_image {
190 () => {
192 rgb_image!(type: u8)
193 };
194 (type: $channel_type:ty) => {
196 {
197 use image::Rgb;
198 use $crate::definitions::Image;
199
200 Image::<Rgb<$channel_type>>::new(0, 0)
201 }
202 };
203 ($( $( [$r: expr, $g: expr, $b: expr]),*);*) => {
205 rgb_image!(type: u8, $( $( [$r, $g, $b]),*);*)
206 };
207 (type: $channel_type:ty, $( $( [$r: expr, $g: expr, $b: expr]),*);*) => {
209 {
210 use image::Rgb;
211 use $crate::definitions::Image;
212
213 let nested_array = [$( [ $([$r, $g, $b]),*]),*];
214 let height = nested_array.len() as u32;
215 let width = nested_array[0].len() as u32;
216
217 let flat_array: Vec<_> = nested_array.iter()
218 .flat_map(|row| row.into_iter().flat_map(|p| p.into_iter()))
219 .cloned()
220 .collect();
221
222 Image::<Rgb<$channel_type>>::from_raw(width, height, flat_array)
223 .unwrap()
224 }
225 }
226}
227
228#[macro_export]
294macro_rules! rgba_image {
295 () => {
297 rgba_image!(type: u8)
298 };
299 (type: $channel_type:ty) => {
301 {
302 use image::Rgba;
303 use $crate::definitions::Image;
304
305 Image::<Rgba<$channel_type>>::new(0, 0)
306 }
307 };
308 ($( $( [$r: expr, $g: expr, $b: expr, $a:expr]),*);*) => {
310 rgba_image!(type: u8, $( $( [$r, $g, $b, $a]),*);*)
311 };
312 (type: $channel_type:ty, $( $( [$r: expr, $g: expr, $b: expr, $a: expr]),*);*) => {
314 {
315 use image::Rgba;
316 use $crate::definitions::Image;
317
318 let nested_array = [$( [ $([$r, $g, $b, $a]),*]),*];
319 let height = nested_array.len() as u32;
320 let width = nested_array[0].len() as u32;
321
322 let flat_array: Vec<_> = nested_array.iter()
323 .flat_map(|row| row.into_iter().flat_map(|p| p.into_iter()))
324 .cloned()
325 .collect();
326
327 Image::<Rgba<$channel_type>>::from_raw(width, height, flat_array)
328 .unwrap()
329 }
330 }
331}
332
333pub fn pixel_diff_summary<I, J, P>(actual: &I, expected: &J) -> Option<String>
336where
337 P: Pixel + PartialEq,
338 P::Subpixel: fmt::Debug,
339 I: GenericImage<Pixel = P>,
340 J: GenericImage<Pixel = P>,
341{
342 significant_pixel_diff_summary(actual, expected, |p, q| p != q)
343}
344
345pub fn significant_pixel_diff_summary<I, J, F, P>(
349 actual: &I,
350 expected: &J,
351 is_significant_diff: F,
352) -> Option<String>
353where
354 P: Pixel,
355 P::Subpixel: fmt::Debug,
356 I: GenericImage<Pixel = P>,
357 J: GenericImage<Pixel = P>,
358 F: Fn((u32, u32, I::Pixel), (u32, u32, J::Pixel)) -> bool,
359{
360 if actual.dimensions() != expected.dimensions() {
361 return Some(format!(
362 "dimensions do not match. \
363 actual: {:?}, expected: {:?}",
364 actual.dimensions(),
365 expected.dimensions()
366 ));
367 }
368 let diffs = pixel_diffs(actual, expected, is_significant_diff);
369 if diffs.is_empty() {
370 return None;
371 }
372 Some(describe_pixel_diffs(actual, expected, &diffs))
373}
374
375#[macro_export]
377macro_rules! assert_pixels_eq {
378 ($actual:expr, $expected:expr) => {{
379 $crate::assert_dimensions_match!($actual, $expected);
380 match $crate::utils::pixel_diff_summary(&$actual, &$expected) {
381 None => {}
382 Some(err) => panic!("{}", err),
383 };
384 }};
385}
386
387#[macro_export]
390macro_rules! assert_pixels_eq_within {
391 ($actual:expr, $expected:expr, $channel_tolerance:expr) => {{
392 $crate::assert_dimensions_match!($actual, $expected);
393 let diffs = $crate::utils::pixel_diffs(&$actual, &$expected, |p, q| {
394 use image::Pixel;
395 let cp = p.2.channels();
396 let cq = q.2.channels();
397 if cp.len() != cq.len() {
398 panic!(
399 "pixels have different channel counts. \
400 actual: {:?}, expected: {:?}",
401 cp.len(),
402 cq.len()
403 )
404 }
405
406 let mut large_diff = false;
407 for i in 0..cp.len() {
408 let sp = cp[i];
409 let sq = cq[i];
410 let diff = if sp > sq { sp - sq } else { sq - sp };
412 if diff > $channel_tolerance {
413 large_diff = true;
414 break;
415 }
416 }
417
418 large_diff
419 });
420 if !diffs.is_empty() {
421 panic!(
422 "{}",
423 $crate::utils::describe_pixel_diffs(&$actual, &$expected, &diffs,)
424 )
425 }
426 }};
427}
428
429#[macro_export]
431macro_rules! assert_dimensions_match {
432 ($actual:expr, $expected:expr) => {{
433 let actual_dim = $actual.dimensions();
434 let expected_dim = $expected.dimensions();
435
436 if actual_dim != expected_dim {
437 panic!(
438 "dimensions do not match. \
439 actual: {:?}, expected: {:?}",
440 actual_dim, expected_dim
441 )
442 }
443 }};
444}
445
446pub fn pixel_diffs<I, J, F, P>(actual: &I, expected: &J, is_diff: F) -> Vec<Diff<I::Pixel>>
448where
449 P: Pixel,
450 I: GenericImage<Pixel = P>,
451 J: GenericImage<Pixel = P>,
452 F: Fn((u32, u32, I::Pixel), (u32, u32, J::Pixel)) -> bool,
453{
454 if is_empty(actual) || is_empty(expected) {
455 return vec![];
456 }
457
458 GenericImageView::pixels(actual)
462 .zip(GenericImageView::pixels(expected))
463 .filter(|&(p, q)| is_diff(p, q))
464 .map(|(p, q)| {
465 assert!(p.0 == q.0 && p.1 == q.1, "Pixel locations do not match");
466 Diff {
467 x: p.0,
468 y: p.1,
469 actual: p.2,
470 expected: q.2,
471 }
472 })
473 .collect::<Vec<_>>()
474}
475
476fn is_empty<I: GenericImage>(image: &I) -> bool {
477 image.width() == 0 || image.height() == 0
478}
479
480pub struct Diff<P> {
482 pub x: u32,
484 pub y: u32,
486 pub expected: P,
488 pub actual: P,
490}
491
492pub fn describe_pixel_diffs<I, J, P>(actual: &I, expected: &J, diffs: &[Diff<P>]) -> String
494where
495 P: Pixel,
496 P::Subpixel: fmt::Debug,
497 I: GenericImage<Pixel = P>,
498 J: GenericImage<Pixel = P>,
499{
500 let mut err = "pixels do not match.\n".to_owned();
501
502 let top_left = diffs.iter().fold((u32::MAX, u32::MAX), |acc, d| {
504 (acc.0.min(d.x), acc.1.min(d.y))
505 });
506 let bottom_right = diffs
507 .iter()
508 .fold((0, 0), |acc, d| (acc.0.max(d.x), acc.1.max(d.y)));
509
510 if max(bottom_right.0 - top_left.0, bottom_right.1 - top_left.1) < 6 {
513 let left = max(0, top_left.0 as i32 - 2) as u32;
514 let top = max(0, top_left.1 as i32 - 2) as u32;
515 let right = min(actual.width() as i32 - 1, bottom_right.0 as i32 + 2) as u32;
516 let bottom = min(actual.height() as i32 - 1, bottom_right.1 as i32 + 2) as u32;
517
518 let diff_locations = diffs.iter().map(|d| (d.x, d.y)).collect::<HashSet<_>>();
519
520 err.push_str(&colored("Actual:", Color::Red));
521 let actual_rendered = render_image_region(actual, left, top, right, bottom, |x, y| {
522 if diff_locations.contains(&(x, y)) {
523 Color::Red
524 } else {
525 Color::Cyan
526 }
527 });
528 err.push_str(&actual_rendered);
529
530 err.push_str(&colored("Expected:", Color::Green));
531 let expected_rendered = render_image_region(expected, left, top, right, bottom, |x, y| {
532 if diff_locations.contains(&(x, y)) {
533 Color::Green
534 } else {
535 Color::Cyan
536 }
537 });
538 err.push_str(&expected_rendered);
539
540 return err;
541 }
542
543 err.push_str(
545 &(diffs
546 .iter()
547 .take(5)
548 .map(|d| {
549 format!(
550 "\nlocation: {}, actual: {}, expected: {} ",
551 colored(&format!("{:?}", (d.x, d.y)), Color::Yellow),
552 colored(&render_pixel(d.actual), Color::Red),
553 colored(&render_pixel(d.expected), Color::Green)
554 )
555 })
556 .collect::<Vec<_>>()
557 .join("")),
558 );
559 err
560}
561
562enum Color {
563 Red,
564 Green,
565 Cyan,
566 Yellow,
567}
568
569fn render_image_region<I, P, C>(
570 image: &I,
571 left: u32,
572 top: u32,
573 right: u32,
574 bottom: u32,
575 color: C,
576) -> String
577where
578 P: Pixel,
579 P::Subpixel: fmt::Debug,
580 I: GenericImage<Pixel = P>,
581 C: Fn(u32, u32) -> Color,
582{
583 let mut rendered = String::new();
584
585 let mut rendered_pixels = vec![];
587 for y in top..bottom + 1 {
588 for x in left..right + 1 {
589 let p = image.get_pixel(x, y);
590 rendered_pixels.push(render_pixel(p));
591 }
592 }
593
594 let pixel_column_width = rendered_pixels.iter().map(|p| p.len()).max().unwrap() + 1;
596 let max_digits = (max(1, max(right, bottom)) as f64).log10().ceil() as usize;
598 let pixel_column_width = pixel_column_width.max(max_digits + 1);
600 let num_columns = (right - left + 1) as usize;
601
602 write!(rendered, "\n{}", " ".repeat(max_digits + 4)).unwrap();
604 for x in left..right + 1 {
605 write!(rendered, "{x:>w$} ", x = x, w = pixel_column_width).unwrap();
606 }
607
608 write!(
610 rendered,
611 "\n {}+{}",
612 " ".repeat(max_digits),
613 "-".repeat((pixel_column_width + 1) * num_columns + 1)
614 )
615 .unwrap();
616 write!(rendered, "\n {y:>w$}| ", y = " ", w = max_digits).unwrap();
618
619 let mut count = 0;
620 for y in top..bottom + 1 {
621 write!(rendered, "\n {y:>w$}| ", y = y, w = max_digits).unwrap();
623
624 for x in left..right + 1 {
625 let padded = format!(
627 "{c:>w$}",
628 c = rendered_pixels[count],
629 w = pixel_column_width
630 );
631 write!(rendered, "{} ", &colored(&padded, color(x, y))).unwrap();
632 count += 1;
633 }
634 write!(rendered, "\n {y:>w$}| ", y = " ", w = max_digits).unwrap();
636 }
637 rendered.push('\n');
638 rendered
639}
640
641fn render_pixel<P>(p: P) -> String
642where
643 P: Pixel,
644 P::Subpixel: fmt::Debug,
645{
646 let cs = p.channels();
647 match cs.len() {
648 1 => format!("{:?}", cs[0]),
649 _ => format!("[{}]", cs.iter().map(|c| format!("{:?}", c)).join(", ")),
650 }
651}
652
653fn colored(s: &str, c: Color) -> String {
654 let escape_sequence = match c {
655 Color::Red => "\x1b[31m",
656 Color::Green => "\x1b[32m",
657 Color::Cyan => "\x1b[36m",
658 Color::Yellow => "\x1b[33m",
659 };
660 format!("{}{}\x1b[0m", escape_sequence, s)
661}
662
663pub fn load_image_or_panic<P: AsRef<Path> + fmt::Debug>(path: P) -> DynamicImage {
665 open(path.as_ref()).unwrap_or_else(|_| panic!("Could not load image at {:?}", path.as_ref()))
666}
667
668pub fn gray_bench_image(width: u32, height: u32) -> GrayImage {
672 let mut image = GrayImage::new(width, height);
673 for y in 0..image.height() {
674 for x in 0..image.width() {
675 let intensity = (x % 7 + y % 6) as u8;
676 image.put_pixel(x, y, Luma([intensity]));
677 }
678 }
679 black_box(image)
680}
681
682pub fn luma32f_bench_image(width: u32, height: u32) -> Image<Luma<f32>> {
684 use image::DynamicImage;
685 let img = gray_bench_image(width, height);
686 DynamicImage::ImageLuma8(img).to_luma32f()
687}
688
689pub fn rgb_bench_image(width: u32, height: u32) -> RgbImage {
691 use std::cmp;
692 let mut image = RgbImage::new(width, height);
693 for y in 0..image.height() {
694 for x in 0..image.width() {
695 let r = (x % 7 + y % 6) as u8;
696 let g = 255u8 - r;
697 let b = cmp::min(r, g);
698 image.put_pixel(x, y, Rgb([r, g, b]));
699 }
700 }
701 black_box(image)
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707
708 #[test]
709 fn test_assert_pixels_eq_passes() {
710 let image = gray_image!(
711 00, 01, 02;
712 10, 11, 12);
713
714 assert_pixels_eq!(image, image);
715 }
716
717 #[test]
718 #[should_panic]
719 fn test_assert_pixels_eq_fails() {
720 let image = gray_image!(
721 00, 01, 02;
722 10, 11, 12);
723
724 let diff = gray_image!(
725 00, 11, 02;
726 10, 11, 12);
727
728 assert_pixels_eq!(diff, image);
729 }
730
731 #[test]
732 fn test_assert_pixels_eq_within_passes() {
733 let image = gray_image!(
734 00, 01, 02;
735 10, 11, 12);
736
737 let diff = gray_image!(
738 00, 02, 02;
739 10, 11, 12);
740
741 assert_pixels_eq_within!(diff, image, 1);
742 }
743
744 #[test]
745 #[should_panic]
746 fn test_assert_pixels_eq_within_fails() {
747 let image = gray_image!(
748 00, 01, 02;
749 10, 11, 12);
750
751 let diff = gray_image!(
752 00, 03, 02;
753 10, 11, 12);
754
755 assert_pixels_eq_within!(diff, image, 1);
756 }
757
758 #[test]
759 fn test_pixel_diff_summary_handles_1x1_image() {
760 let summary = pixel_diff_summary(&gray_image!(1), &gray_image!(0));
761 assert_eq!(&summary.unwrap()[0..19], "pixels do not match");
762 }
763}