image_compare/lib.rs
1#![crate_name = "image_compare"]
2//! # Comparing gray images using structure
3//! This crate allows to compare grayscale images using either structure or histogramming methods.
4//! The easiest use is loading two images, converting them to grayscale and running a comparison:
5//! ```no_run
6//! use image_compare::Algorithm;
7//! let image_one = image::open("image1.png").expect("Could not find test-image").into_luma8();
8//! let image_two = image::open("image2.png").expect("Could not find test-image").into_luma8();
9//! let result = image_compare::gray_similarity_structure(&Algorithm::MSSIMSimple, &image_one, &image_two).expect("Images had different dimensions");
10//! ```
11//! Check the [`Algorithm`] enum for implementation details
12//!
13//! # Comparing gray images using histogram
14//!
15//! Histogram comparisons are possible using the histogram comparison function
16//! ```no_run
17//! use image_compare::Metric;
18//! let image_one = image::open("image1.png").expect("Could not find test-image").into_luma8();
19//! let image_two = image::open("image2.png").expect("Could not find test-image").into_luma8();
20//! let result = image_compare::gray_similarity_histogram(Metric::Hellinger, &image_one, &image_two).expect("Images had different dimensions");
21//! ```
22//! Check the [`Metric`] enum for implementation details
23//!
24//! # Comparing rgb images using hybrid mode
25//!
26//! hybrid mode allows to decompose the image to structure and color channels (YUV) which
27//! are compared separately but then combined into a common result.
28//! ## Direct usage on two RGB8 images
29//! ```no_run
30//! let image_one = image::open("image1.png").expect("Could not find test-image").into_rgb8();
31//! let image_two = image::open("image2.png").expect("Could not find test-image").into_rgb8();
32//! let result = image_compare::rgb_hybrid_compare(&image_one, &image_two).expect("Images had different dimensions");
33//! ```
34//!
35//! ## Compare the similarity of two maybe-rgba images in front a given background color
36//! If an image is RGBA it will be blended with a background of the given color.
37//! RGB images will not be modified.
38//!
39//! ```no_run
40//! use image::Rgb;
41//! let image_one = image::open("image1.png").expect("Could not find test-image").into_rgba8();
42//! let image_two = image::open("image2.png").expect("Could not find test-image").into_rgb8();
43//! let white = Rgb([255,255,255]);
44//! let result = image_compare::rgba_blended_hybrid_compare((&image_one).into(), (&image_two).into(), white).expect("Images had different dimensions");
45//! ```
46//!
47//! # Comparing two RGBA8 images using hybrid mode
48//!
49//! hybrid mode allows to decompose the image to structure, color and alpha channels (YUVA) which
50//! are compared separately but then combined into a common result.
51//! ```no_run
52//! let image_one = image::open("image1.png").expect("Could not find test-image").into_rgba8();
53//! let image_two = image::open("image2.png").expect("Could not find test-image").into_rgba8();
54//! let result = image_compare::rgba_hybrid_compare(&image_one, &image_two).expect("Images had different dimensions");
55//! ```
56//!
57//! # Using structure results
58//! All structural comparisons return a result struct that contains the similarity score.
59//! For the score 1.0 is perfectly similar, 0.0 is dissimilar and some algorithms even provide up to -1.0 for inverse.
60//! Furthermore, the algorithm may produce a similarity map (MSSIM, RMS and hybrid compare do) that can be evaluated per pixel or converted to a visualization:
61//! ```no_run
62//! let image_one = image::open("image1.png").expect("Could not find test-image").into_rgba8();
63//! let image_two = image::open("image2.png").expect("Could not find test-image").into_rgba8();
64//! let result = image_compare::rgba_hybrid_compare(&image_one, &image_two).expect("Images had different dimensions");
65//! if result.score < 0.95 {
66//! let diff_img = result.image.to_color_map();
67//! diff_img.save("diff_image.png").expect("Could not save diff image");
68//! }
69//! ```
70
71#![warn(missing_docs)]
72#![warn(unused_qualifications)]
73#![deny(deprecated)]
74
75mod colorization;
76mod histogram;
77mod hybrid;
78mod squared_error;
79mod ssim;
80mod utils;
81
82#[doc(hidden)]
83pub mod prelude {
84 pub use image::{GrayImage, ImageBuffer, Luma, Rgb, RgbImage};
85 use thiserror::Error;
86 /// The enum for selecting a grayscale comparison implementation
87 pub enum Algorithm {
88 /// A simple RMSE implementation - will return: <img src="https://render.githubusercontent.com/render/math?math=1-\sqrt{\frac{(\sum_{x,y=0}^{x,y=w,h}\left(f(x,y)-g(x,y)\right)^2)}{w*h}}">
89 RootMeanSquared,
90 /// a simple MSSIM implementation - will run SSIM (implemented as on wikipedia: <img src="https://render.githubusercontent.com/render/math?math=\mathrm{SSIM}(x,y)={\frac {(2\mu _{x}\mu _{y}+c_{1})(2\sigma _{xy}+c_{2})}{(\mu _{x}^{2}+\mu _{y}^{2}+c_{1})(\sigma _{x}^{2}+\sigma _{y}^{2}+c_{2})}}"> ) over 8x8 px windows and average the results
91 MSSIMSimple,
92 }
93
94 #[derive(Error, Debug)]
95 /// The errors that can occur during comparison of the images
96 pub enum CompareError {
97 #[error("The dimensions of the input images are not identical")]
98 DimensionsDiffer,
99 #[error("Comparison calculation failed: {0}")]
100 CalculationFailed(String),
101 }
102
103 pub use crate::colorization::GraySimilarityImage;
104 pub use crate::colorization::RGBASimilarityImage;
105 pub use crate::colorization::RGBSimilarityImage;
106 pub use crate::colorization::Similarity;
107}
108
109#[doc(inline)]
110pub use histogram::Metric;
111#[doc(inline)]
112pub use prelude::Algorithm;
113#[doc(inline)]
114pub use prelude::CompareError;
115#[doc(inline)]
116pub use prelude::Similarity;
117
118use prelude::*;
119use utils::Decompose;
120
121/// Comparing gray images using structure.
122///
123/// # Arguments
124///
125/// * `algorithm` - The comparison algorithm to use
126///
127/// * `first` - The first of the images to compare
128///
129/// * `second` - The first of the images to compare
130pub fn gray_similarity_structure(
131 algorithm: &Algorithm,
132 first: &GrayImage,
133 second: &GrayImage,
134) -> Result<Similarity, CompareError> {
135 if first.dimensions() != second.dimensions() {
136 return Err(CompareError::DimensionsDiffer);
137 }
138 match algorithm {
139 Algorithm::RootMeanSquared => root_mean_squared_error_simple(first, second),
140 Algorithm::MSSIMSimple => ssim_simple(first, second),
141 }
142 .map(|(score, i)| Similarity {
143 image: i.into(),
144 score,
145 })
146}
147
148/// Comparing rgb images using structure.
149/// RGB structure similarity is performed by doing a channel split and taking the maximum deviation (minimum similarity) for the result.
150/// The image contains the complete deviations.
151/// # Arguments
152///
153/// * `algorithm` - The comparison algorithm to use
154///
155/// * `first` - The first of the images to compare
156///
157/// * `second` - The first of the images to compare
158///
159/// ### Experimental:
160/// As you can see from the pinning tests in cucumber - the differences are quite small, the runtime difference is rather large though.
161pub fn rgb_similarity_structure(
162 algorithm: &Algorithm,
163 first: &RgbImage,
164 second: &RgbImage,
165) -> Result<Similarity, CompareError> {
166 if first.dimensions() != second.dimensions() {
167 return Err(CompareError::DimensionsDiffer);
168 }
169
170 let first_channels = first.split_channels();
171 let second_channels = second.split_channels();
172 let mut results = Vec::new();
173
174 for channel in 0..3 {
175 match algorithm {
176 Algorithm::RootMeanSquared => {
177 results.push(root_mean_squared_error_simple(
178 &first_channels[channel],
179 &second_channels[channel],
180 )?);
181 }
182 Algorithm::MSSIMSimple => {
183 results.push(ssim_simple(
184 &first_channels[channel],
185 &second_channels[channel],
186 )?);
187 }
188 }
189 }
190 let input = results.iter().map(|(_, i)| i).collect::<Vec<_>>();
191 let image = utils::merge_similarity_channels(&input.try_into().unwrap());
192 let score = results.iter().map(|(s, _)| *s).fold(1., f64::min);
193 Ok(Similarity {
194 image: image.into(),
195 score,
196 })
197}
198
199/// Comparing gray images using histogram
200/// # Arguments
201///
202/// * `metric` - The distance metric to use
203///
204/// * `first` - The first of the images to compare
205///
206/// * `second` - The first of the images to compare
207pub fn gray_similarity_histogram(
208 metric: Metric,
209 first: &GrayImage,
210 second: &GrayImage,
211) -> Result<f64, CompareError> {
212 if first.dimensions() != second.dimensions() {
213 return Err(CompareError::DimensionsDiffer);
214 }
215 histogram::img_compare(first, second, metric)
216}
217
218#[doc(inline)]
219pub use hybrid::rgb_hybrid_compare;
220
221use crate::squared_error::root_mean_squared_error_simple;
222use crate::ssim::ssim_simple;
223#[doc(inline)]
224pub use hybrid::rgba_hybrid_compare;
225
226#[doc(inline)]
227pub use hybrid::rgba_blended_hybrid_compare;
228
229pub use hybrid::BlendInput;
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn dimensions_differ_test_gray_structure() {
237 let first = GrayImage::new(1, 1);
238 let second = GrayImage::new(2, 2);
239 let result = gray_similarity_structure(&Algorithm::RootMeanSquared, &first, &second);
240 assert!(result.is_err());
241 }
242
243 #[test]
244 fn dimensions_differ_test_rgb_structure() {
245 let first = RgbImage::new(1, 1);
246 let second = RgbImage::new(2, 2);
247 let result = rgb_similarity_structure(&Algorithm::RootMeanSquared, &first, &second);
248 assert!(result.is_err());
249 }
250
251 #[test]
252 fn dimensions_differ_test_gray_histos() {
253 let first = GrayImage::new(1, 1);
254 let second = GrayImage::new(2, 2);
255 let result = gray_similarity_histogram(Metric::Hellinger, &first, &second);
256 assert!(result.is_err());
257 }
258}