imageproc/
utils.rs

1//! Utils for testing and debugging.
2
3use image::{
4    open, DynamicImage, GenericImage, GenericImageView, GrayImage, Luma, Pixel, Rgb, RgbImage,
5};
6
7use itertools::Itertools;
8use std::cmp::{max, min};
9use std::collections::HashSet;
10use std::fmt;
11use std::fmt::Write;
12use std::path::Path;
13use std::u32;
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::{ImageBuffer, Luma};
93            ImageBuffer::<Luma<$channel_type>, Vec<$channel_type>>::new(0, 0)
94        }
95    };
96    // Non-empty image of default channel type u8
97    ($( $( $x: expr ),*);*) => {
98        gray_image!(type: u8, $( $( $x ),*);*)
99    };
100    // Non-empty image of given channel type
101    (type: $channel_type:ty, $( $( $x: expr ),*);*) => {
102        {
103            use image::{ImageBuffer, Luma};
104
105            let nested_array = [ $( [ $($x),* ] ),* ];
106            let height = nested_array.len() as u32;
107            let width = nested_array[0].len() as u32;
108
109            let flat_array: Vec<_> = nested_array.iter()
110                .flat_map(|row| row.into_iter())
111                .cloned()
112                .collect();
113
114            ImageBuffer::<Luma<$channel_type>, Vec<$channel_type>>::from_raw(width, height, flat_array)
115                .unwrap()
116        }
117    }
118}
119
120/// Helper for defining RGB images.
121///
122/// Pixels are delineated by square brackets, columns are
123/// separated by commas and rows are separated by semi-colons.
124/// By default a subpixel type of `u8` is used but this can be
125/// overridden, as shown in the examples.
126///
127/// # Examples
128/// ```
129/// # extern crate image;
130/// # #[macro_use]
131/// # extern crate imageproc;
132/// # fn main() {
133/// use image::{ImageBuffer, Rgb, RgbImage};
134///
135/// // An empty image with pixel type Rgb<u8>
136/// let empty = rgb_image!();
137///
138/// assert_pixels_eq!(
139///     empty,
140///     RgbImage::from_raw(0, 0, vec![]).unwrap()
141/// );
142///
143/// // A single pixel image with pixel type Rgb<u8>
144/// let single_pixel = rgb_image!([1, 2, 3]);
145///
146/// assert_pixels_eq!(
147///     single_pixel,
148///     RgbImage::from_raw(1, 1, vec![1, 2, 3]).unwrap()
149/// );
150///
151/// // A single row image with pixel type Rgb<u8>
152/// let single_row = rgb_image!([1, 2, 3], [4, 5, 6]);
153///
154/// assert_pixels_eq!(
155///     single_row,
156///     RgbImage::from_raw(2, 1, vec![1, 2, 3, 4, 5, 6]).unwrap()
157/// );
158///
159/// // An image with 2 rows and 2 columns
160/// let image = rgb_image!(
161///     [1,  2,  3], [ 4,  5,  6];
162///     [7,  8,  9], [10, 11, 12]);
163///
164/// let equivalent = RgbImage::from_raw(2, 2, vec![
165///     1,  2,  3,  4,  5,  6,
166///     7,  8,  9, 10, 11, 12
167/// ]).unwrap();
168///
169/// assert_pixels_eq!(image, equivalent);
170///
171/// // An empty image with pixel type Rgb<i16>.
172/// let empty_i16 = rgb_image!(type: i16);
173///
174/// // An image with 2 rows, 3 columns and pixel type Rgb<i16>
175/// let image_i16 = rgb_image!(type: i16,
176///     [1, 2, 3], [4, 5, 6];
177///     [7, 8, 9], [10, 11, 12]);
178///
179/// let expected_i16 = ImageBuffer::<Rgb<i16>, Vec<i16>>::from_raw(2, 2, vec![
180///     1, 2, 3, 4, 5, 6,
181///     7, 8, 9, 10, 11, 12],
182///     ).unwrap();
183/// # }
184/// ```
185#[macro_export]
186macro_rules! rgb_image {
187    // Empty image with default channel type u8
188    () => {
189        rgb_image!(type: u8)
190    };
191    // Empty image with the given channel type
192    (type: $channel_type:ty) => {
193        {
194            use image::{ImageBuffer, Rgb};
195            ImageBuffer::<Rgb<$channel_type>, Vec<$channel_type>>::new(0, 0)
196        }
197    };
198    // Non-empty image of default channel type u8
199    ($( $( [$r: expr, $g: expr, $b: expr]),*);*) => {
200        rgb_image!(type: u8, $( $( [$r, $g, $b]),*);*)
201    };
202    // Non-empty image of given channel type
203    (type: $channel_type:ty, $( $( [$r: expr, $g: expr, $b: expr]),*);*) => {
204        {
205            use image::{ImageBuffer, Rgb};
206            let nested_array = [$( [ $([$r, $g, $b]),*]),*];
207            let height = nested_array.len() as u32;
208            let width = nested_array[0].len() as u32;
209
210            let flat_array: Vec<_> = nested_array.iter()
211                .flat_map(|row| row.into_iter().flat_map(|p| p.into_iter()))
212                .cloned()
213                .collect();
214
215            ImageBuffer::<Rgb<$channel_type>, Vec<$channel_type>>::from_raw(width, height, flat_array)
216                .unwrap()
217        }
218    }
219}
220
221/// Helper for defining RGBA images.
222///
223/// Pixels are delineated by square brackets, columns are
224/// separated by commas and rows are separated by semi-colons.
225/// By default a subpixel type of `u8` is used but this can be
226/// overridden, as shown in the examples.
227///
228/// # Examples
229/// ```
230/// # extern crate image;
231/// # #[macro_use]
232/// # extern crate imageproc;
233/// # fn main() {
234/// use image::{ImageBuffer, Rgba, RgbaImage};
235///
236/// // An empty image with pixel type Rgba<u8>
237/// let empty = rgba_image!();
238///
239/// assert_pixels_eq!(
240///     empty,
241///     RgbaImage::from_raw(0, 0, vec![]).unwrap()
242/// );
243///
244/// // A single pixel image with pixel type Rgba<u8>
245/// let single_pixel = rgba_image!([1, 2, 3, 4]);
246///
247/// assert_pixels_eq!(
248///     single_pixel,
249///     RgbaImage::from_raw(1, 1, vec![1, 2, 3, 4]).unwrap()
250/// );
251///
252/// // A single row image with pixel type Rgba<u8>
253/// let single_row = rgba_image!([1, 2, 3, 10], [4, 5, 6, 20]);
254///
255/// assert_pixels_eq!(
256///     single_row,
257///     RgbaImage::from_raw(2, 1, vec![1, 2, 3, 10, 4, 5, 6, 20]).unwrap()
258/// );
259///
260/// // An image with 2 rows and 2 columns
261/// let image = rgba_image!(
262///     [1,  2,  3, 10], [ 4,  5,  6, 20];
263///     [7,  8,  9, 30], [10, 11, 12, 40]);
264///
265/// let equivalent = RgbaImage::from_raw(2, 2, vec![
266///     1,  2,  3, 10,  4,  5,  6, 20,
267///     7,  8,  9, 30, 10, 11, 12, 40
268/// ]).unwrap();
269///
270/// assert_pixels_eq!(image, equivalent);
271///
272/// // An empty image with pixel type Rgba<i16>.
273/// let empty_i16 = rgba_image!(type: i16);
274///
275/// // An image with 2 rows, 3 columns and pixel type Rgba<i16>
276/// let image_i16 = rgba_image!(type: i16,
277///     [1, 2, 3, 10], [ 4,  5,  6, 20];
278///     [7, 8, 9, 30], [10, 11, 12, 40]);
279///
280/// let expected_i16 = ImageBuffer::<Rgba<i16>, Vec<i16>>::from_raw(2, 2, vec![
281///     1, 2, 3, 10,  4,  5,  6, 20,
282///     7, 8, 9, 30, 10, 11, 12, 40],
283///     ).unwrap();
284/// # }
285/// ```
286#[macro_export]
287macro_rules! rgba_image {
288    // Empty image with default channel type u8
289    () => {
290        rgba_image!(type: u8)
291    };
292    // Empty image with the given channel type
293    (type: $channel_type:ty) => {
294        {
295            use image::{ImageBuffer, Rgba};
296            ImageBuffer::<Rgba<$channel_type>, Vec<$channel_type>>::new(0, 0)
297        }
298    };
299    // Non-empty image of default channel type u8
300    ($( $( [$r: expr, $g: expr, $b: expr, $a:expr]),*);*) => {
301        rgba_image!(type: u8, $( $( [$r, $g, $b, $a]),*);*)
302    };
303    // Non-empty image of given channel type
304    (type: $channel_type:ty, $( $( [$r: expr, $g: expr, $b: expr, $a: expr]),*);*) => {
305        {
306            use image::{ImageBuffer, Rgba};
307            let nested_array = [$( [ $([$r, $g, $b, $a]),*]),*];
308            let height = nested_array.len() as u32;
309            let width = nested_array[0].len() as u32;
310
311            let flat_array: Vec<_> = nested_array.iter()
312                .flat_map(|row| row.into_iter().flat_map(|p| p.into_iter()))
313                .cloned()
314                .collect();
315
316            ImageBuffer::<Rgba<$channel_type>, Vec<$channel_type>>::from_raw(width, height, flat_array)
317                .unwrap()
318        }
319    }
320}
321
322/// Human readable description of some of the pixels that differ
323/// between left and right, or None if all pixels match.
324pub fn pixel_diff_summary<I, J, P>(actual: &I, expected: &J) -> Option<String>
325where
326    P: Pixel + PartialEq,
327    P::Subpixel: fmt::Debug,
328    I: GenericImage<Pixel = P>,
329    J: GenericImage<Pixel = P>,
330{
331    significant_pixel_diff_summary(actual, expected, |p, q| p != q)
332}
333
334/// Human readable description of some of the pixels that differ
335/// signifcantly (according to provided function) between left
336/// and right, or None if all pixels match.
337pub fn significant_pixel_diff_summary<I, J, F, P>(
338    actual: &I,
339    expected: &J,
340    is_significant_diff: F,
341) -> Option<String>
342where
343    P: Pixel,
344    P::Subpixel: fmt::Debug,
345    I: GenericImage<Pixel = P>,
346    J: GenericImage<Pixel = P>,
347    F: Fn((u32, u32, I::Pixel), (u32, u32, J::Pixel)) -> bool,
348{
349    if actual.dimensions() != expected.dimensions() {
350        return Some(format!(
351            "dimensions do not match. \
352             actual: {:?}, expected: {:?}",
353            actual.dimensions(),
354            expected.dimensions()
355        ));
356    }
357    let diffs = pixel_diffs(actual, expected, is_significant_diff);
358    if diffs.is_empty() {
359        return None;
360    }
361    Some(describe_pixel_diffs(actual, expected, &diffs))
362}
363
364/// Panics if any pixels differ between the two input images.
365#[macro_export]
366macro_rules! assert_pixels_eq {
367    ($actual:expr, $expected:expr) => {{
368        $crate::assert_dimensions_match!($actual, $expected);
369        match $crate::utils::pixel_diff_summary(&$actual, &$expected) {
370            None => {}
371            Some(err) => panic!("{}", err),
372        };
373    }};
374}
375
376/// Panics if any pixels differ between the two images by more than the
377/// given tolerance in a single channel.
378#[macro_export]
379macro_rules! assert_pixels_eq_within {
380    ($actual:expr, $expected:expr, $channel_tolerance:expr) => {{
381        $crate::assert_dimensions_match!($actual, $expected);
382        let diffs = $crate::utils::pixel_diffs(&$actual, &$expected, |p, q| {
383            use image::Pixel;
384            let cp = p.2.channels();
385            let cq = q.2.channels();
386            if cp.len() != cq.len() {
387                panic!(
388                    "pixels have different channel counts. \
389                     actual: {:?}, expected: {:?}",
390                    cp.len(),
391                    cq.len()
392                )
393            }
394
395            let mut large_diff = false;
396            for i in 0..cp.len() {
397                let sp = cp[i];
398                let sq = cq[i];
399                // Handle unsigned subpixels
400                let diff = if sp > sq { sp - sq } else { sq - sp };
401                if diff > $channel_tolerance {
402                    large_diff = true;
403                    break;
404                }
405            }
406
407            large_diff
408        });
409        if !diffs.is_empty() {
410            panic!(
411                "{}",
412                $crate::utils::describe_pixel_diffs(&$actual, &$expected, &diffs,)
413            )
414        }
415    }};
416}
417
418/// Panics if image dimensions do not match.
419#[macro_export]
420macro_rules! assert_dimensions_match {
421    ($actual:expr, $expected:expr) => {{
422        let actual_dim = $actual.dimensions();
423        let expected_dim = $expected.dimensions();
424
425        if actual_dim != expected_dim {
426            panic!(
427                "dimensions do not match. \
428                 actual: {:?}, expected: {:?}",
429                actual_dim, expected_dim
430            )
431        }
432    }};
433}
434
435/// Lists pixels that differ between left and right images.
436pub fn pixel_diffs<I, J, F, P>(actual: &I, expected: &J, is_diff: F) -> Vec<Diff<I::Pixel>>
437where
438    P: Pixel,
439    I: GenericImage<Pixel = P>,
440    J: GenericImage<Pixel = P>,
441    F: Fn((u32, u32, I::Pixel), (u32, u32, J::Pixel)) -> bool,
442{
443    if is_empty(actual) || is_empty(expected) {
444        return vec![];
445    }
446
447    // Can't just call $image.pixels(), as that needn't hit the
448    // trait pixels method - ImageBuffer defines its own pixels
449    // method with a different signature
450    GenericImageView::pixels(actual)
451        .zip(GenericImageView::pixels(expected))
452        .filter(|&(p, q)| is_diff(p, q))
453        .map(|(p, q)| {
454            assert!(p.0 == q.0 && p.1 == q.1, "Pixel locations do not match");
455            Diff {
456                x: p.0,
457                y: p.1,
458                actual: p.2,
459                expected: q.2,
460            }
461        })
462        .collect::<Vec<_>>()
463}
464
465fn is_empty<I: GenericImage>(image: &I) -> bool {
466    image.width() == 0 || image.height() == 0
467}
468
469/// A difference between two images
470pub struct Diff<P> {
471    /// x-coordinate of diff.
472    pub x: u32,
473    /// y-coordinate of diff.
474    pub y: u32,
475    /// Pixel value in expected image.
476    pub expected: P,
477    /// Pixel value in actual image.
478    pub actual: P,
479}
480
481/// Gives a summary description of a list of pixel diffs for use in error messages.
482pub fn describe_pixel_diffs<I, J, P>(actual: &I, expected: &J, diffs: &[Diff<P>]) -> String
483where
484    P: Pixel,
485    P::Subpixel: fmt::Debug,
486    I: GenericImage<Pixel = P>,
487    J: GenericImage<Pixel = P>,
488{
489    let mut err = "pixels do not match.\n".to_owned();
490
491    // Find the boundaries of the region containing diffs
492    let top_left = diffs.iter().fold((u32::MAX, u32::MAX), |acc, d| {
493        (acc.0.min(d.x), acc.1.min(d.y))
494    });
495    let bottom_right = diffs
496        .iter()
497        .fold((0, 0), |acc, d| (acc.0.max(d.x), acc.1.max(d.y)));
498
499    // If all the diffs are contained in a small region of the image then render all of this
500    // region, with a small margin.
501    if max(bottom_right.0 - top_left.0, bottom_right.1 - top_left.1) < 6 {
502        let left = max(0, top_left.0 as i32 - 2) as u32;
503        let top = max(0, top_left.1 as i32 - 2) as u32;
504        let right = min(actual.width() as i32 - 1, bottom_right.0 as i32 + 2) as u32;
505        let bottom = min(actual.height() as i32 - 1, bottom_right.1 as i32 + 2) as u32;
506
507        let diff_locations = diffs.iter().map(|d| (d.x, d.y)).collect::<HashSet<_>>();
508
509        err.push_str(&colored("Actual:", Color::Red));
510        let actual_rendered = render_image_region(actual, left, top, right, bottom, |x, y| {
511            if diff_locations.contains(&(x, y)) {
512                Color::Red
513            } else {
514                Color::Cyan
515            }
516        });
517        err.push_str(&actual_rendered);
518
519        err.push_str(&colored("Expected:", Color::Green));
520        let expected_rendered = render_image_region(expected, left, top, right, bottom, |x, y| {
521            if diff_locations.contains(&(x, y)) {
522                Color::Green
523            } else {
524                Color::Cyan
525            }
526        });
527        err.push_str(&expected_rendered);
528
529        return err;
530    }
531
532    // Otherwise just list the first 5 diffs
533    err.push_str(
534        &(diffs
535            .iter()
536            .take(5)
537            .map(|d| {
538                format!(
539                    "\nlocation: {}, actual: {}, expected: {} ",
540                    colored(&format!("{:?}", (d.x, d.y)), Color::Yellow),
541                    colored(&render_pixel(d.actual), Color::Red),
542                    colored(&render_pixel(d.expected), Color::Green)
543                )
544            })
545            .collect::<Vec<_>>()
546            .join("")),
547    );
548    err
549}
550
551enum Color {
552    Red,
553    Green,
554    Cyan,
555    Yellow,
556}
557
558fn render_image_region<I, P, C>(
559    image: &I,
560    left: u32,
561    top: u32,
562    right: u32,
563    bottom: u32,
564    color: C,
565) -> String
566where
567    P: Pixel,
568    P::Subpixel: fmt::Debug,
569    I: GenericImage<Pixel = P>,
570    C: Fn(u32, u32) -> Color,
571{
572    let mut rendered = String::new();
573
574    // Render all the pixels first, so that we can determine the column width
575    let mut rendered_pixels = vec![];
576    for y in top..bottom + 1 {
577        for x in left..right + 1 {
578            let p = image.get_pixel(x, y);
579            rendered_pixels.push(render_pixel(p));
580        }
581    }
582
583    // Width of a column containing rendered pixels
584    let pixel_column_width = rendered_pixels.iter().map(|p| p.len()).max().unwrap() + 1;
585    // Maximum number of digits required to display a row or column number
586    let max_digits = (max(1, max(right, bottom)) as f64).log10().ceil() as usize;
587    // Each pixel column is labelled with its column number
588    let pixel_column_width = pixel_column_width.max(max_digits + 1);
589    let num_columns = (right - left + 1) as usize;
590
591    // First row contains the column numbers
592    write!(rendered, "\n{}", " ".repeat(max_digits + 4)).unwrap();
593    for x in left..right + 1 {
594        write!(rendered, "{x:>w$} ", x = x, w = pixel_column_width).unwrap();
595    }
596
597    // +--------------
598    write!(
599        rendered,
600        "\n  {}+{}",
601        " ".repeat(max_digits),
602        "-".repeat((pixel_column_width + 1) * num_columns + 1)
603    )
604    .unwrap();
605    // row_number |
606    write!(rendered, "\n  {y:>w$}| ", y = " ", w = max_digits).unwrap();
607
608    let mut count = 0;
609    for y in top..bottom + 1 {
610        // Empty row, except for leading | separating row numbers from pixels
611        write!(rendered, "\n  {y:>w$}| ", y = y, w = max_digits).unwrap();
612
613        for x in left..right + 1 {
614            // Pad pixel string to column width and right align
615            let padded = format!(
616                "{c:>w$}",
617                c = rendered_pixels[count],
618                w = pixel_column_width
619            );
620            write!(rendered, "{} ", &colored(&padded, color(x, y))).unwrap();
621            count += 1;
622        }
623        // Empty row, except for leading | separating row numbers from pixels
624        write!(rendered, "\n  {y:>w$}| ", y = " ", w = max_digits).unwrap();
625    }
626    rendered.push('\n');
627    rendered
628}
629
630fn render_pixel<P>(p: P) -> String
631where
632    P: Pixel,
633    P::Subpixel: fmt::Debug,
634{
635    let cs = p.channels();
636    match cs.len() {
637        1 => format!("{:?}", cs[0]),
638        _ => format!("[{}]", cs.iter().map(|c| format!("{:?}", c)).join(", ")),
639    }
640}
641
642fn colored(s: &str, c: Color) -> String {
643    let escape_sequence = match c {
644        Color::Red => "\x1b[31m",
645        Color::Green => "\x1b[32m",
646        Color::Cyan => "\x1b[36m",
647        Color::Yellow => "\x1b[33m",
648    };
649    format!("{}{}\x1b[0m", escape_sequence, s)
650}
651
652/// Loads image at given path, panicking on failure.
653pub fn load_image_or_panic<P: AsRef<Path> + fmt::Debug>(path: P) -> DynamicImage {
654    open(path.as_ref()).expect(&format!("Could not load image at {:?}", path.as_ref()))
655}
656
657/// Gray image to use in benchmarks. This is neither noise nor
658/// similar to natural images - it's just a convenience method
659/// to produce an image that's not constant.
660pub fn gray_bench_image(width: u32, height: u32) -> GrayImage {
661    let mut image = GrayImage::new(width, height);
662    for y in 0..image.height() {
663        for x in 0..image.width() {
664            let intensity = (x % 7 + y % 6) as u8;
665            image.put_pixel(x, y, Luma([intensity]));
666        }
667    }
668    image
669}
670
671/// RGB image to use in benchmarks. See comment on `gray_bench_image`.
672pub fn rgb_bench_image(width: u32, height: u32) -> RgbImage {
673    use std::cmp;
674    let mut image = RgbImage::new(width, height);
675    for y in 0..image.height() {
676        for x in 0..image.width() {
677            let r = (x % 7 + y % 6) as u8;
678            let g = 255u8 - r;
679            let b = cmp::min(r, g);
680            image.put_pixel(x, y, Rgb([r, g, b]));
681        }
682    }
683    image
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689
690    #[test]
691    fn test_assert_pixels_eq_passes() {
692        let image = gray_image!(
693            00, 01, 02;
694            10, 11, 12);
695
696        assert_pixels_eq!(image, image);
697    }
698
699    #[test]
700    #[should_panic]
701    fn test_assert_pixels_eq_fails() {
702        let image = gray_image!(
703            00, 01, 02;
704            10, 11, 12);
705
706        let diff = gray_image!(
707            00, 11, 02;
708            10, 11, 12);
709
710        assert_pixels_eq!(diff, image);
711    }
712
713    #[test]
714    fn test_assert_pixels_eq_within_passes() {
715        let image = gray_image!(
716            00, 01, 02;
717            10, 11, 12);
718
719        let diff = gray_image!(
720            00, 02, 02;
721            10, 11, 12);
722
723        assert_pixels_eq_within!(diff, image, 1);
724    }
725
726    #[test]
727    #[should_panic]
728    fn test_assert_pixels_eq_within_fails() {
729        let image = gray_image!(
730            00, 01, 02;
731            10, 11, 12);
732
733        let diff = gray_image!(
734            00, 03, 02;
735            10, 11, 12);
736
737        assert_pixels_eq_within!(diff, image, 1);
738    }
739
740    #[test]
741    fn test_pixel_diff_summary_handles_1x1_image() {
742        let summary = pixel_diff_summary(&gray_image!(1), &gray_image!(0));
743        assert_eq!(&summary.unwrap()[0..19], "pixels do not match");
744    }
745}