Skip to main content

imageproc/
utils.rs

1//! Utils for testing and debugging.
2
3use 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/// Helper for defining greyscale images.
16///
17/// Columns are separated by commas and rows by semi-colons.
18/// By default a subpixel type of `u8` is used but this can be
19/// overridden, as shown in the examples.
20///
21/// # Examples
22/// ```
23/// # extern crate image;
24/// # #[macro_use]
25/// # extern crate imageproc;
26/// # fn main() {
27/// use image::{GrayImage, ImageBuffer, Luma};
28///
29/// // An empty grayscale image with pixel type Luma<u8>
30/// let empty = gray_image!();
31///
32/// assert_pixels_eq!(
33///     empty,
34///     GrayImage::from_raw(0, 0, vec![]).unwrap()
35/// );
36///
37/// // A single pixel grayscale image with pixel type Luma<u8>
38/// let single_pixel = gray_image!(1);
39///
40/// assert_pixels_eq!(
41///     single_pixel,
42///     GrayImage::from_raw(1, 1, vec![1]).unwrap()
43/// );
44///
45/// // A single row grayscale image with pixel type Luma<u8>
46/// let single_row = gray_image!(1, 2, 3);
47///
48/// assert_pixels_eq!(
49///     single_row,
50///     GrayImage::from_raw(3, 1, vec![1, 2, 3]).unwrap()
51/// );
52///
53/// // A grayscale image with 2 rows and 3 columns
54/// let image = gray_image!(
55///     1, 2, 3;
56///     4, 5, 6);
57///
58/// let equivalent = GrayImage::from_raw(3, 2, vec![
59///     1, 2, 3,
60///     4, 5, 6
61/// ]).unwrap();
62///
63/// // An empty grayscale image with pixel type Luma<i16>.
64/// let empty_i16 = gray_image!(type: i16);
65///
66/// assert_pixels_eq!(
67///     empty_i16,
68///     ImageBuffer::<Luma<i16>, Vec<i16>>::from_raw(0, 0, vec![]).unwrap()
69/// );
70///
71/// // A grayscale image with 2 rows, 3 columns and pixel type Luma<i16>
72/// let image_i16 = gray_image!(type: i16,
73///     1, 2, 3;
74///     4, 5, 6);
75///
76/// let expected_i16 = ImageBuffer::<Luma<i16>, Vec<i16>>::from_raw(3, 2, vec![
77///     1, 2, 3,
78///     4, 5, 6]).unwrap();
79///
80/// assert_pixels_eq!(image_i16, expected_i16);
81/// # }
82/// ```
83#[macro_export]
84macro_rules! gray_image {
85    // Empty image with default channel type u8
86    () => {
87        gray_image!(type: u8)
88    };
89        // Empty image with the given channel type
90    (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    // Non-empty image of default channel type u8
99    ($( $( $x: expr ),*);*) => {
100        gray_image!(type: u8, $( $( $x ),*);*)
101    };
102    // Non-empty image of given channel type
103    (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/// Helper for defining RGB images.
124///
125/// Pixels are delineated by square brackets, columns are
126/// separated by commas and rows are separated by semi-colons.
127/// By default a subpixel type of `u8` is used but this can be
128/// overridden, as shown in the examples.
129///
130/// # Examples
131/// ```
132/// # extern crate image;
133/// # #[macro_use]
134/// # extern crate imageproc;
135/// # fn main() {
136/// use image::{ImageBuffer, Rgb, RgbImage};
137///
138/// // An empty image with pixel type Rgb<u8>
139/// let empty = rgb_image!();
140///
141/// assert_pixels_eq!(
142///     empty,
143///     RgbImage::from_raw(0, 0, vec![]).unwrap()
144/// );
145///
146/// // A single pixel image with pixel type Rgb<u8>
147/// let single_pixel = rgb_image!([1, 2, 3]);
148///
149/// assert_pixels_eq!(
150///     single_pixel,
151///     RgbImage::from_raw(1, 1, vec![1, 2, 3]).unwrap()
152/// );
153///
154/// // A single row image with pixel type Rgb<u8>
155/// let single_row = rgb_image!([1, 2, 3], [4, 5, 6]);
156///
157/// assert_pixels_eq!(
158///     single_row,
159///     RgbImage::from_raw(2, 1, vec![1, 2, 3, 4, 5, 6]).unwrap()
160/// );
161///
162/// // An image with 2 rows and 2 columns
163/// let image = rgb_image!(
164///     [1,  2,  3], [ 4,  5,  6];
165///     [7,  8,  9], [10, 11, 12]);
166///
167/// let equivalent = RgbImage::from_raw(2, 2, vec![
168///     1,  2,  3,  4,  5,  6,
169///     7,  8,  9, 10, 11, 12
170/// ]).unwrap();
171///
172/// assert_pixels_eq!(image, equivalent);
173///
174/// // An empty image with pixel type Rgb<i16>.
175/// let empty_i16 = rgb_image!(type: i16);
176///
177/// // An image with 2 rows, 3 columns and pixel type Rgb<i16>
178/// let image_i16 = rgb_image!(type: i16,
179///     [1, 2, 3], [4, 5, 6];
180///     [7, 8, 9], [10, 11, 12]);
181///
182/// let expected_i16 = ImageBuffer::<Rgb<i16>, Vec<i16>>::from_raw(2, 2, vec![
183///     1, 2, 3, 4, 5, 6,
184///     7, 8, 9, 10, 11, 12],
185///     ).unwrap();
186/// # }
187/// ```
188#[macro_export]
189macro_rules! rgb_image {
190    // Empty image with default channel type u8
191    () => {
192        rgb_image!(type: u8)
193    };
194    // Empty image with the given channel type
195    (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    // Non-empty image of default channel type u8
204    ($( $( [$r: expr, $g: expr, $b: expr]),*);*) => {
205        rgb_image!(type: u8, $( $( [$r, $g, $b]),*);*)
206    };
207    // Non-empty image of given channel type
208    (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/// Helper for defining RGBA images.
229///
230/// Pixels are delineated by square brackets, columns are
231/// separated by commas and rows are separated by semi-colons.
232/// By default a subpixel type of `u8` is used but this can be
233/// overridden, as shown in the examples.
234///
235/// # Examples
236/// ```
237/// # extern crate image;
238/// # #[macro_use]
239/// # extern crate imageproc;
240/// # fn main() {
241/// use image::{ImageBuffer, Rgba, RgbaImage};
242///
243/// // An empty image with pixel type Rgba<u8>
244/// let empty = rgba_image!();
245///
246/// assert_pixels_eq!(
247///     empty,
248///     RgbaImage::from_raw(0, 0, vec![]).unwrap()
249/// );
250///
251/// // A single pixel image with pixel type Rgba<u8>
252/// let single_pixel = rgba_image!([1, 2, 3, 4]);
253///
254/// assert_pixels_eq!(
255///     single_pixel,
256///     RgbaImage::from_raw(1, 1, vec![1, 2, 3, 4]).unwrap()
257/// );
258///
259/// // A single row image with pixel type Rgba<u8>
260/// let single_row = rgba_image!([1, 2, 3, 10], [4, 5, 6, 20]);
261///
262/// assert_pixels_eq!(
263///     single_row,
264///     RgbaImage::from_raw(2, 1, vec![1, 2, 3, 10, 4, 5, 6, 20]).unwrap()
265/// );
266///
267/// // An image with 2 rows and 2 columns
268/// let image = rgba_image!(
269///     [1,  2,  3, 10], [ 4,  5,  6, 20];
270///     [7,  8,  9, 30], [10, 11, 12, 40]);
271///
272/// let equivalent = RgbaImage::from_raw(2, 2, vec![
273///     1,  2,  3, 10,  4,  5,  6, 20,
274///     7,  8,  9, 30, 10, 11, 12, 40
275/// ]).unwrap();
276///
277/// assert_pixels_eq!(image, equivalent);
278///
279/// // An empty image with pixel type Rgba<i16>.
280/// let empty_i16 = rgba_image!(type: i16);
281///
282/// // An image with 2 rows, 3 columns and pixel type Rgba<i16>
283/// let image_i16 = rgba_image!(type: i16,
284///     [1, 2, 3, 10], [ 4,  5,  6, 20];
285///     [7, 8, 9, 30], [10, 11, 12, 40]);
286///
287/// let expected_i16 = ImageBuffer::<Rgba<i16>, Vec<i16>>::from_raw(2, 2, vec![
288///     1, 2, 3, 10,  4,  5,  6, 20,
289///     7, 8, 9, 30, 10, 11, 12, 40],
290///     ).unwrap();
291/// # }
292/// ```
293#[macro_export]
294macro_rules! rgba_image {
295    // Empty image with default channel type u8
296    () => {
297        rgba_image!(type: u8)
298    };
299    // Empty image with the given channel type
300    (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    // Non-empty image of default channel type u8
309    ($( $( [$r: expr, $g: expr, $b: expr, $a:expr]),*);*) => {
310        rgba_image!(type: u8, $( $( [$r, $g, $b, $a]),*);*)
311    };
312    // Non-empty image of given channel type
313    (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
333/// Human readable description of some of the pixels that differ
334/// between left and right, or None if all pixels match.
335pub 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
345/// Human readable description of some of the pixels that differ
346/// significantly (according to provided function) between left
347/// and right, or None if all pixels match.
348pub 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/// Panics if any pixels differ between the two input images.
376#[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/// Panics if any pixels differ between the two images by more than the
388/// given tolerance in a single channel.
389#[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                // Handle unsigned subpixels
411                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/// Panics if image dimensions do not match.
430#[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
446/// Lists pixels that differ between left and right images.
447pub 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    // Can't just call $image.pixels(), as that needn't hit the
459    // trait pixels method - ImageBuffer defines its own pixels
460    // method with a different signature
461    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
480/// A difference between two images
481pub struct Diff<P> {
482    /// x-coordinate of diff.
483    pub x: u32,
484    /// y-coordinate of diff.
485    pub y: u32,
486    /// Pixel value in expected image.
487    pub expected: P,
488    /// Pixel value in actual image.
489    pub actual: P,
490}
491
492/// Gives a summary description of a list of pixel diffs for use in error messages.
493pub 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    // Find the boundaries of the region containing diffs
503    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 all the diffs are contained in a small region of the image then render all of this
511    // region, with a small margin.
512    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    // Otherwise just list the first 5 diffs
544    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    // Render all the pixels first, so that we can determine the column width
586    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    // Width of a column containing rendered pixels
595    let pixel_column_width = rendered_pixels.iter().map(|p| p.len()).max().unwrap() + 1;
596    // Maximum number of digits required to display a row or column number
597    let max_digits = (max(1, max(right, bottom)) as f64).log10().ceil() as usize;
598    // Each pixel column is labelled with its column number
599    let pixel_column_width = pixel_column_width.max(max_digits + 1);
600    let num_columns = (right - left + 1) as usize;
601
602    // First row contains the column numbers
603    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    // +--------------
609    write!(
610        rendered,
611        "\n  {}+{}",
612        " ".repeat(max_digits),
613        "-".repeat((pixel_column_width + 1) * num_columns + 1)
614    )
615    .unwrap();
616    // row_number |
617    write!(rendered, "\n  {y:>w$}| ", y = " ", w = max_digits).unwrap();
618
619    let mut count = 0;
620    for y in top..bottom + 1 {
621        // Empty row, except for leading | separating row numbers from pixels
622        write!(rendered, "\n  {y:>w$}| ", y = y, w = max_digits).unwrap();
623
624        for x in left..right + 1 {
625            // Pad pixel string to column width and right align
626            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        // Empty row, except for leading | separating row numbers from pixels
635        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
663/// Loads image at given path, panicking on failure.
664pub 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
668/// Gray image to use in benchmarks. This is neither noise nor
669/// similar to natural images - it's just a convenience method
670/// to produce an image that's not constant.
671pub 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
682/// `Luma<f32>` image to use in benchmarks. See comment on `gray_bench_image`.
683pub 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
689/// RGB image to use in benchmarks. See comment on `gray_bench_image`.
690pub 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}