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}