Skip to main content

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