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;
80
81
82#[cfg(not(feature="yuv_compare"))] // Tests cannot be implemented to check the functionality of this feature gate, please be mindful of this.
83mod utils;
84
85/// Provides some utilities to make yuv image management and conversion easier.
86#[cfg(feature="yuv_compare")] // Exposes rgb/yuv conversions and split to yuv publicly, others were left to be pub crate or private
87pub mod utils;                // All exposed APIs have tests, and compilation will fail if they ever become non-public
88
89#[doc(hidden)]
90pub mod prelude {
91    pub use image::{GrayImage, ImageBuffer, Luma, Rgb, RgbImage};
92    use thiserror::Error;
93    /// The enum for selecting a grayscale comparison implementation
94    pub enum Algorithm {
95        /// 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}}">
96        RootMeanSquared,
97        /// 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
98        MSSIMSimple,
99    }
100
101    #[derive(Error, Debug)]
102    /// The errors that can occur during comparison of the images
103    pub enum CompareError {
104        #[error("The dimensions of the input images are not identical")]
105        DimensionsDiffer,
106        #[error("Comparison calculation failed: {0}")]
107        CalculationFailed(String),
108    }
109
110    pub use crate::colorization::GraySimilarityImage;
111    pub use crate::colorization::RGBASimilarityImage;
112    pub use crate::colorization::RGBSimilarityImage;
113    pub use crate::colorization::Similarity;
114}
115
116#[doc(inline)]
117pub use histogram::Metric;
118#[doc(inline)]
119pub use prelude::Algorithm;
120#[doc(inline)]
121pub use prelude::CompareError;
122#[doc(inline)]
123pub use prelude::Similarity;
124
125use prelude::*;
126use utils::Decompose;
127
128/// Comparing gray images using structure.
129///
130/// # Arguments
131///
132/// * `algorithm` - The comparison algorithm to use
133///
134/// * `first` - The first of the images to compare
135///
136/// * `second` - The first of the images to compare
137pub fn gray_similarity_structure(
138    algorithm: &Algorithm,
139    first: &GrayImage,
140    second: &GrayImage,
141) -> Result<Similarity, CompareError> {
142    if first.dimensions() != second.dimensions() {
143        return Err(CompareError::DimensionsDiffer);
144    }
145    match algorithm {
146        Algorithm::RootMeanSquared => root_mean_squared_error_simple(first, second),
147        Algorithm::MSSIMSimple => ssim_simple(first, second),
148    }
149    .map(|(score, i)| Similarity {
150        image: i.into(),
151        score,
152    })
153}
154
155/// Comparing rgb images using structure.
156/// RGB structure similarity is performed by doing a channel split and taking the maximum deviation (minimum similarity) for the result.
157/// The image contains the complete deviations.
158/// # Arguments
159///
160/// * `algorithm` - The comparison algorithm to use
161///
162/// * `first` - The first of the images to compare
163///
164/// * `second` - The first of the images to compare
165///
166/// ### Experimental:
167/// As you can see from the pinning tests in cucumber - the differences are quite small, the runtime difference is rather large though.
168pub fn rgb_similarity_structure(
169    algorithm: &Algorithm,
170    first: &RgbImage,
171    second: &RgbImage,
172) -> Result<Similarity, CompareError> {
173    if first.dimensions() != second.dimensions() {
174        return Err(CompareError::DimensionsDiffer);
175    }
176
177    let first_channels = first.split_channels();
178    let second_channels = second.split_channels();
179    let mut results = Vec::new();
180
181    for channel in 0..3 {
182        match algorithm {
183            Algorithm::RootMeanSquared => {
184                results.push(root_mean_squared_error_simple(
185                    &first_channels[channel],
186                    &second_channels[channel],
187                )?);
188            }
189            Algorithm::MSSIMSimple => {
190                results.push(ssim_simple(
191                    &first_channels[channel],
192                    &second_channels[channel],
193                )?);
194            }
195        }
196    }
197    let input = results.iter().map(|(_, i)| i).collect::<Vec<_>>();
198    let image = utils::merge_similarity_channels(&input.try_into().unwrap());
199    let score = results.iter().map(|(s, _)| *s).fold(1., f64::min);
200    Ok(Similarity {
201        image: image.into(),
202        score,
203    })
204}
205
206/// Comparing gray images using histogram
207/// # Arguments
208///
209/// * `metric` - The distance metric to use
210///
211/// * `first` - The first of the images to compare
212///
213/// * `second` - The first of the images to compare
214pub fn gray_similarity_histogram(
215    metric: Metric,
216    first: &GrayImage,
217    second: &GrayImage,
218) -> Result<f64, CompareError> {
219    if first.dimensions() != second.dimensions() {
220        return Err(CompareError::DimensionsDiffer);
221    }
222    histogram::img_compare(first, second, metric)
223}
224
225#[doc(inline)]
226pub use hybrid::rgb_hybrid_compare;
227
228use crate::squared_error::root_mean_squared_error_simple;
229use crate::ssim::ssim_simple;
230#[doc(inline)]
231pub use hybrid::rgba_hybrid_compare;
232
233#[doc(inline)]
234pub use hybrid::rgba_blended_hybrid_compare;
235
236#[doc(inline)]
237#[cfg(feature = "yuv_compare")]
238pub use hybrid::yuv_hybrid_compare;
239
240pub use hybrid::BlendInput;
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn dimensions_differ_test_gray_structure() {
248        let first = GrayImage::new(1, 1);
249        let second = GrayImage::new(2, 2);
250        let result = gray_similarity_structure(&Algorithm::RootMeanSquared, &first, &second);
251        assert!(result.is_err());
252    }
253
254    #[test]
255    fn dimensions_differ_test_rgb_structure() {
256        let first = RgbImage::new(1, 1);
257        let second = RgbImage::new(2, 2);
258        let result = rgb_similarity_structure(&Algorithm::RootMeanSquared, &first, &second);
259        assert!(result.is_err());
260    }
261
262    #[test]
263    fn dimensions_differ_test_gray_histos() {
264        let first = GrayImage::new(1, 1);
265        let second = GrayImage::new(2, 2);
266        let result = gray_similarity_histogram(Metric::Hellinger, &first, &second);
267        assert!(result.is_err());
268    }
269}