image_compare/
hybrid.rs

1use crate::prelude::*;
2use crate::squared_error::root_mean_squared_error_simple;
3use crate::ssim::ssim_simple;
4use crate::utils::{blend_alpha, split_rgba_to_yuva};
5use crate::Decompose;
6use image::{Rgba, RgbaImage};
7use itertools::izip;
8use std::borrow::Cow;
9
10fn merge_similarity_channels_yuva(
11    input: &[GraySimilarityImage; 4],
12    alpha: &GrayImage,
13    alpha_second: &GrayImage,
14) -> Similarity {
15    const ALPHA_VIS_MIN: f32 = 0.1;
16    const U8_MAX: f32 = u8::MAX as f32;
17    const A_BAR_NORM: f32 = 2. * U8_MAX;
18
19    let mut image = RGBASimilarityImage::new(input[0].width(), input[0].height());
20    let mut deviation = Vec::new();
21    deviation.resize((input[0].width() * input[0].height()) as usize, 0.0);
22    izip!(
23        image.pixels_mut(),
24        input[0].pixels(),
25        input[1].pixels(),
26        input[2].pixels(),
27        input[3].pixels(),
28        alpha.pixels(),
29        alpha_second.pixels(),
30        deviation.iter_mut()
31    )
32    .for_each(
33        |(rgba, y, u, v, a_d, alpha_source, alpha_source_second, deviation)| {
34            let y = y[0].clamp(0.0, 1.0);
35            let u = u[0].clamp(0.0, 1.0);
36            let v = v[0].clamp(0.0, 1.0);
37            let a_d = a_d[0].clamp(0.0, 1.0);
38            let alpha_bar = (alpha_source[0] as f32 + alpha_source_second[0] as f32) / A_BAR_NORM;
39            let alpha_bar = if alpha_bar.is_finite() {
40                alpha_bar
41            } else {
42                1.0
43            };
44
45            let color_diff = ((u).powi(2) + (v).powi(2)).sqrt().clamp(0.0, 1.0);
46            let min_sim = y.min(color_diff).min(a_d);
47            //the lower the alpha the fewer differences are visible in color and structure (and alpha)
48
49            let dev = if alpha_bar > 0. {
50                (min_sim / alpha_bar).clamp(0., 1.)
51            } else {
52                1.0
53            };
54            let alpha_vis = (ALPHA_VIS_MIN + a_d * (1.0 - ALPHA_VIS_MIN)).clamp(0., 1.);
55
56            *deviation = dev;
57            *rgba = Rgba([1. - y, 1. - u, 1. - v, alpha_vis]);
58        },
59    );
60
61    let score = deviation.iter().map(|s| *s as f64).sum::<f64>() / deviation.len() as f64;
62
63    Similarity {
64        image: image.into(),
65        score,
66    }
67}
68
69fn merge_similarity_channels_yuv(input: &[GraySimilarityImage; 3]) -> Similarity {
70    let mut image = RGBSimilarityImage::new(input[0].width(), input[0].height());
71    let mut deviation = Vec::new();
72    deviation.resize((input[0].width() * input[0].height()) as usize, 0.0);
73    izip!(
74        image.pixels_mut(),
75        input[0].pixels(),
76        input[1].pixels(),
77        input[2].pixels(),
78        deviation.iter_mut()
79    )
80    .for_each(|(rgb, y, u, v, deviation)| {
81        let y = y[0].clamp(0.0, 1.0);
82        let u = u[0].clamp(0.0, 1.0);
83        let v = v[0].clamp(0.0, 1.0);
84        let color_diff = ((u).powi(2) + (v).powi(2)).sqrt().clamp(0.0, 1.0);
85        //f32 for keeping numerical stability for hybrid compare in 0.2.-branch
86        *deviation += y.min(color_diff);
87        *rgb = Rgb([1. - y, 1. - u, 1. - v]);
88    });
89
90    let score = deviation.iter().map(|s| *s as f64).sum::<f64>() / deviation.len() as f64;
91    Similarity {
92        image: image.into(),
93        score,
94    }
95}
96
97/// Hybrid comparison for RGBA images.
98/// Will do MSSIM on luma, then RMS on U and V and alpha channels.
99/// The calculation of the score is then pixel-wise the minimum of each pixels similarity.
100/// To account for perceived indifference in lower alpha regions, this down-weights the difference
101/// linearly with mean alpha channel.
102pub fn rgba_hybrid_compare(
103    first: &RgbaImage,
104    second: &RgbaImage,
105) -> Result<Similarity, CompareError> {
106    if first.dimensions() != second.dimensions() {
107        return Err(CompareError::DimensionsDiffer);
108    }
109
110    let first = split_rgba_to_yuva(first);
111    let second = split_rgba_to_yuva(second);
112
113    let (_, mssim_result) = ssim_simple(&first[0], &second[0])?;
114    let (_, u_result) = root_mean_squared_error_simple(&first[1], &second[1])?;
115    let (_, v_result) = root_mean_squared_error_simple(&first[2], &second[2])?;
116
117    let (_, alpha_result) = root_mean_squared_error_simple(&first[3], &second[3])?;
118
119    let results = [mssim_result, u_result, v_result, alpha_result];
120
121    Ok(merge_similarity_channels_yuva(
122        &results, &first[3], &second[3],
123    ))
124}
125
126/// A wrapper class accepting both RgbaImage and RgbImage for the blended hybrid comparison
127pub enum BlendInput<'a> {
128    /// This variant means that the image is already alpha pre-blended and therefore RGB
129    PreBlended(&'a RgbImage),
130    /// This variant means that the image still needs to be blended with a certain background
131    RGBA(&'a RgbaImage),
132}
133
134impl<'a> BlendInput<'a> {
135    fn into_blended(self, background: Rgb<u8>) -> Cow<'a, RgbImage> {
136        match self {
137            BlendInput::PreBlended(image) => Cow::Borrowed(image),
138            BlendInput::RGBA(rgba) => Cow::Owned(blend_alpha(rgba, background)),
139        }
140    }
141}
142
143impl<'a> From<&'a RgbImage> for BlendInput<'a> {
144    fn from(value: &'a RgbImage) -> Self {
145        BlendInput::PreBlended(value)
146    }
147}
148
149impl<'a> From<&'a RgbaImage> for BlendInput<'a> {
150    fn from(value: &'a RgbaImage) -> Self {
151        BlendInput::RGBA(value)
152    }
153}
154
155/// This processes the RGBA images be pre-blending the colors with the desired background color.
156/// It's faster then the full RGBA similarity and more intuitive.
157pub fn rgba_blended_hybrid_compare(
158    first: BlendInput,
159    second: BlendInput,
160    background: Rgb<u8>,
161) -> Result<Similarity, CompareError> {
162    let first = first.into_blended(background);
163    let second = second.into_blended(background);
164    rgb_hybrid_compare(&first, &second)
165}
166
167/// Comparing structure via MSSIM on Y channel, comparing color-diff-vectors on U and V summing the squares
168/// Please mind that the RGBSimilarity-Image does _not_ contain plain RGB here
169/// - The red channel contains 1. - similarity(ssim, y)
170/// - The green channel contains 1. - similarity(rms, u)
171/// - The blue channel contains 1. - similarity(rms, v)
172/// This leads to a nice visualization of color and structure differences - with structural differences (meaning gray mssim diffs) leading to red rectangles
173/// and and the u and v color diffs leading to color-deviations in green, blue and cyan
174/// All-black meaning no differences
175pub fn rgb_hybrid_compare(first: &RgbImage, second: &RgbImage) -> Result<Similarity, CompareError> {
176    if first.dimensions() != second.dimensions() {
177        return Err(CompareError::DimensionsDiffer);
178    }
179
180    let first_channels = first.split_to_yuv();
181    let second_channels = second.split_to_yuv();
182
183    internal_yuv_hybrid_compare(&first_channels, &second_channels)
184}
185
186/// Comparing structure via MSSIM on Y channel, comparing color-diff-vectors on U and V summing the squares
187/// first_channels and second_channels are arrays, each containing 3 `GrayImage`s
188/// - The first GrayImage contains the Y values - similarity(ssim, y)
189/// - The second GrayImage contains the U values - similarity(rms, u)
190/// - The third GrayImage contains the V values - similarity(rms, v)
191/// Please mind that the SimilarityImage does _not_ contain plain RGB here
192/// - The red channel contains 1. - similarity(ssim, y)
193/// - The green channel contains 1. - similarity(rms, u)
194/// - The blue channel contains 1. - similarity(rms, v)
195/// This leads to a nice visualization of color and structure differences - with structural differences (meaning gray mssim diffs) leading to red rectangles
196/// and and the u and v color diffs leading to color-deviations in green, blue and cyan
197/// All-black meaning no differences
198#[cfg(feature = "yuv_compare")]
199pub fn yuv_hybrid_compare(first_channels: &[GrayImage; 3], second_channels: &[GrayImage; 3]) -> Result<Similarity, CompareError> {
200    if (first_channels[0].dimensions() != second_channels[0].dimensions())
201        || (first_channels[1].dimensions() != second_channels[1].dimensions())  // 5 checks needed, this ensures all channels are the same sizes.
202        || (first_channels[2].dimensions() != second_channels[2].dimensions())  // First 3 ensure the channels each have the same resolution across the 2 images
203        || (first_channels[0].dimensions() != first_channels[1].dimensions())   // Last 2 check if each channel is the same in the first image
204        || (first_channels[1].dimensions() != first_channels[2].dimensions()) { // If all checks pass this leaves all 6 being the same
205        return Err(CompareError::DimensionsDiffer);
206    }
207    internal_yuv_hybrid_compare(&first_channels, &second_channels)
208}
209
210pub(crate) fn internal_yuv_hybrid_compare(first_channels: &[GrayImage; 3], second_channels: &[GrayImage; 3]) -> Result<Similarity, CompareError> {
211    let (_, mssim_result) = ssim_simple(&first_channels[0], &second_channels[0])?;
212    let (_, u_result) = root_mean_squared_error_simple(&first_channels[1], &second_channels[1])?;
213    let (_, v_result) = root_mean_squared_error_simple(&first_channels[2], &second_channels[2])?;
214
215    let results = [mssim_result, u_result, v_result];
216
217    Ok(merge_similarity_channels_yuv(&results))
218}