image_compare/
colorization.rs

1use image::{DynamicImage, GrayImage, ImageBuffer, Luma, Rgb, RgbImage, Rgba, RgbaImage};
2
3/// a single-channel f32 typed image containing a result-score for each pixel
4pub type GraySimilarityImage = ImageBuffer<Luma<f32>, Vec<f32>>;
5/// a three-channel f32 typed image containing a result-score per color channel for each pixel
6pub type RGBSimilarityImage = ImageBuffer<Rgb<f32>, Vec<f32>>;
7/// a four-channel f32 typed image containing a result-score per color channel for each pixel
8pub type RGBASimilarityImage = ImageBuffer<Rgba<f32>, Vec<f32>>;
9
10#[derive(Debug)]
11#[allow(clippy::upper_case_acronyms)]
12pub enum SimilarityImage {
13    Gray(GraySimilarityImage),
14    RGB(RGBSimilarityImage),
15    RGBA(RGBASimilarityImage),
16}
17
18impl From<GraySimilarityImage> for SimilarityImage {
19    fn from(value: GraySimilarityImage) -> Self {
20        SimilarityImage::Gray(value)
21    }
22}
23impl From<RGBASimilarityImage> for SimilarityImage {
24    fn from(value: RGBASimilarityImage) -> Self {
25        SimilarityImage::RGBA(value)
26    }
27}
28impl From<RGBSimilarityImage> for SimilarityImage {
29    fn from(value: RGBSimilarityImage) -> Self {
30        SimilarityImage::RGB(value)
31    }
32}
33
34fn gray_map(img: &GraySimilarityImage) -> DynamicImage {
35    let mut img_gray = GrayImage::new(img.width(), img.height());
36    for row in 0..img.height() {
37        for col in 0..img.width() {
38            let new_val = img.get_pixel(col, row)[0].clamp(0., 1.) * 255.;
39            img_gray.put_pixel(col, row, Luma([new_val as u8]));
40        }
41    }
42    img_gray.into()
43}
44
45fn to_color_map(img: &RGBSimilarityImage) -> DynamicImage {
46    let mut img_rgb = RgbImage::new(img.width(), img.height());
47    for row in 0..img.height() {
48        for col in 0..img.width() {
49            let pixel = img.get_pixel(col, row);
50            let mut new_pixel = [0u8; 3];
51            for channel in 0..3 {
52                new_pixel[channel] = (pixel[channel].clamp(0., 1.) * 255.) as u8;
53            }
54            img_rgb.put_pixel(col, row, Rgb(new_pixel));
55        }
56    }
57    img_rgb.into()
58}
59
60fn to_color_map_rgba(img: &RGBASimilarityImage) -> DynamicImage {
61    let mut img_rgba = RgbaImage::new(img.width(), img.height());
62    for row in 0..img.height() {
63        for col in 0..img.width() {
64            let pixel = img.get_pixel(col, row);
65            let mut new_pixel = [0u8; 4];
66            for channel in 0..4 {
67                new_pixel[channel] = (pixel[channel].clamp(0., 1.) * 255.) as u8;
68            }
69            img_rgba.put_pixel(col, row, Rgba(new_pixel));
70        }
71    }
72    img_rgba.into()
73}
74
75impl SimilarityImage {
76    pub fn to_color_map(&self) -> DynamicImage {
77        match self {
78            SimilarityImage::Gray(gray) => gray_map(gray),
79            SimilarityImage::RGB(rgb) => to_color_map(rgb),
80            SimilarityImage::RGBA(rgba) => to_color_map_rgba(rgba),
81        }
82    }
83}
84
85#[derive(Debug)]
86/// the resulting struct containing both an image of per pixel diffs as well as an average score
87pub struct Similarity {
88    /// Contains the resulting differences per pixel if applicable
89    /// The buffer will contain the resulting values of the respective algorithms:
90    /// - RMS will be between 0. for all-white vs all-black and 1.0 for identical
91    /// - SSIM usually is near 1. for similar, near 0. for different but can take on negative values for negative covariances
92    /// - Hybrid mode will be inverse: 0. means no difference, 1.0 is maximum difference. For details see [`crate::hybrid::rgb_hybrid_compare`]    
93    pub image: SimilarityImage,
94    /// the average score of the image
95    pub score: f64,
96}