rendiff/
lib.rs

1//! Image comparison (diffing) for computer graphics renderer test cases.
2//!
3//! The algorithm implemented in this library is intended to allow comparing images
4//! of the same scene which were rendered using different algorithms, or different
5//! hardware, causing small “rounding errors” in either color or spatial position.
6//!
7//! To use it, call [`diff()`] on your images, and test the result against a [`Threshold`].
8//!
9//! ## When to use this library
10//!
11//! `rendiff` is *not* a “perceptual” difference algorithm; it does not attempt to calculate
12//! how visible a difference is to the eye.
13//! Rather, it is intended to allow for various kinds of numerical error or arbitrary
14//! choice in rendering, which happen to also be perceptually insignificant:
15//!
16//! * Spatial: A line or curve being considered to fall on one side or another of a pixel center.
17//! * Color: A color value being rounded up or down.
18//!
19//! This algorithm will always ignore spatial errors which meet the following criteria:
20//!
21//! * the spatial offset is at most 1 pixel,
22//! * there are no 1-pixel-sized shapes that vanish entirely
23//!   (e.g. imagine a very narrow, pointy, non-axis-aligned triangle;
24//!   its sharp end turns into a series of disconnected dots), and
25//! * there isn't antialiasing which introduces miscellaneous intermediate shades.
26//!
27//! Therefore, `rendiff` is ideal for comparing non-antialiased renderings of “vector” graphics.
28//! In other situations, and for color rounding differences, you must tune the
29//! [`Threshold`] for allowed differences.
30//!
31//! `rendiff` is unsuitable for comparing images which have strong noise (e.g. halftone)
32//! or spatial displacements of more than one pixel.
33//!
34//! ## Example output
35//!
36//! These two cartoon robot head images are the inputs.
37//! The third image is the visual output of the diff function;
38//! red comes from the input and cyan marks differences.
39//!
40//! <div style="background: gray; text-align: center; padding: 0.7em;">
41//!
42//! ![robot-exp]
43//! ![robot-actual]
44//! ![robot-diff]
45//!
46//! </div>
47//!
48//! Note that the eyes’ shapes differ slightly, and this is ignored, but the gaps at the bottom
49//! and between the teeth are highlighted, because they are places where colors are present in
50//! one image but entirely absent in the other.
51//! These are examples of the type of rendering bug which `rendiff` is designed to catch.
52//!
53//! ## Example usage
54//!
55//! ```
56//! use rendiff::Threshold;
57//!
58//! // In a real application, you would load the expected image from disk and
59//! // the actual image from the output of your renderer or other image processor.
60//! // For this example, we'll embed some very simple images as text.
61//!
62//! fn ascii_image(s: &str) -> Vec<[u8; 4]> {
63//!     s.chars().map(|ch| {
64//!         let gray = u8::try_from(ch.to_digit(10).unwrap()).unwrap();
65//!         [gray, gray, gray, 255]
66//!     }).collect()
67//! }
68//!
69//! let expected_image = imgref::ImgVec::new(
70//!     ascii_image("\
71//!         00000000\
72//!         00000000\
73//!         00229900\
74//!         00229900\
75//!         00229900\
76//!         00000000\
77//!         00000000\
78//!     "),
79//!     8, 6
80//! );
81//! let actual_image = imgref::ImgVec::new(
82//!     ascii_image("\
83//!         00000000\
84//!         00000000\
85//!         00449990\
86//!         00449990\
87//!         00449990\
88//!         00000000\
89//!         00000000\
90//!     "),
91//!     8, 6
92//! );
93//!
94//! let difference = rendiff::diff(actual_image.as_ref(), expected_image.as_ref());
95//!
96//! // `difference` describes the differences found but does not define success or failure.
97//! // To do that, you must use a `Threshold`, or examine the `histogram()` yourself.
98//!
99//! assert!(Threshold::no_bigger_than(2).allows(difference.histogram()));
100//! assert!(!Threshold::no_bigger_than(1).allows(difference.histogram()));
101//!
102//! let diff_image = difference.diff_image();
103//! // You can put `diff_image` in your test report.
104//! ```
105//!
106//! ## Principle of operation
107//!
108//! Suppose we are comparing two images, A and B.
109//! For each pixel in A (except for the perimeter),
110//! a neighborhood around the corresponding pixel in B is compared, and the _smallest_
111//! color difference is taken to be the difference value for that pixel in A.
112//! Then, the same process is repeated, swapping the roles of the two images, and the
113//! final difference value for each pixel is the maximum of those two.
114//!
115//! The pixel differences are then compiled into a difference image (for user viewing)
116//! and a histogram (for pass/fail conditions).
117//!
118//! The effect of this strategy is that any feature in the image, such as the edge of a
119//! shape, can be displaced by up to the neighborhood size (currently fixed to 1 pixel
120//! radius, i.e. a 3×3 neighborhood) in any direction, thus
121//! tolerating different choices of rounding into the pixel grid, as long as the color is
122//! the same.
123//!
124//! This algorithm does not inherently accept differences in antialiased images, because
125//! depending on how an edge lands with respect to the pixel grid, the color may be
126//! different. A future version of this library may solve that problem by accepting any
127//! color which is a blend of colors found in the neighborhood.
128//!
129#![doc = ::embed_doc_image::embed_image!("robot-actual", "example-comparisons/robot-actual.png")]
130#![doc = ::embed_doc_image::embed_image!("robot-diff", "example-comparisons/robot-diff.png")]
131#![doc = ::embed_doc_image::embed_image!("robot-exp", "example-comparisons/robot-exp.png")]
132//!
133
134// This list is sorted.
135#![forbid(rust_2018_idioms)]
136#![forbid(unsafe_code)]
137#![warn(clippy::exhaustive_enums)]
138#![warn(clippy::exhaustive_structs)]
139#![warn(clippy::modulo_arithmetic)]
140#![warn(clippy::pedantic)]
141#![warn(clippy::unnecessary_self_imports)]
142#![warn(missing_debug_implementations)]
143#![warn(missing_docs)]
144#![warn(noop_method_call)]
145#![warn(trivial_numeric_casts)]
146#![warn(unreachable_pub)]
147#![warn(unused_lifetimes)]
148
149/// `rendiff` uses image types from `imgref`.
150pub use ::imgref;
151
152type RgbaPixel = [u8; 4];
153
154mod image;
155
156mod diff;
157pub use diff::*;
158
159mod histogram;
160pub use histogram::*;
161
162mod threshold;
163pub use threshold::*;
164
165mod visualize;