codec_eval/metrics/
dssim.rs

1//! DSSIM (Structural Dissimilarity) metric calculation.
2//!
3//! Wraps the `dssim-core` crate for perceptual image comparison.
4
5use dssim_core::Dssim;
6use imgref::ImgVec;
7use rgb::RGBA;
8
9use crate::error::{Error, Result};
10use crate::viewing::ViewingCondition;
11
12/// Calculate DSSIM between two images.
13///
14/// # Arguments
15///
16/// * `reference` - Reference image as RGBA f32 values (0.0-1.0).
17/// * `test` - Test image as RGBA f32 values (0.0-1.0).
18/// * `viewing` - Viewing condition for PPD-based adjustment (currently unused
19///   by dssim-core, but reserved for future use).
20///
21/// # Returns
22///
23/// DSSIM value where 0 = identical, higher = more different.
24/// Typical thresholds:
25/// - < 0.0003: Imperceptible
26/// - < 0.0007: Marginal
27/// - < 0.0015: Subtle
28/// - < 0.003: Noticeable
29/// - >= 0.003: Degraded
30///
31/// # Errors
32///
33/// Returns an error if the images have different dimensions or if DSSIM
34/// calculation fails.
35pub fn calculate_dssim(
36    reference: &ImgVec<RGBA<f32>>,
37    test: &ImgVec<RGBA<f32>>,
38    _viewing: &ViewingCondition,
39) -> Result<f64> {
40    if reference.width() != test.width() || reference.height() != test.height() {
41        return Err(Error::DimensionMismatch {
42            expected: (reference.width() as u32, reference.height() as u32),
43            actual: (test.width() as u32, test.height() as u32),
44        });
45    }
46
47    let dssim = Dssim::new();
48
49    let ref_image = dssim
50        .create_image(reference)
51        .ok_or_else(|| Error::MetricCalculation {
52            metric: "DSSIM".to_string(),
53            reason: "Failed to create reference image".to_string(),
54        })?;
55
56    let test_image = dssim
57        .create_image(test)
58        .ok_or_else(|| Error::MetricCalculation {
59            metric: "DSSIM".to_string(),
60            reason: "Failed to create test image".to_string(),
61        })?;
62
63    let (dssim_val, _ssim_maps) = dssim.compare(&ref_image, test_image);
64
65    Ok(f64::from(dssim_val))
66}
67
68/// Convert RGB8 image data to the format needed for DSSIM calculation.
69///
70/// # Arguments
71///
72/// * `data` - RGB8 pixel data in row-major order.
73/// * `width` - Image width in pixels.
74/// * `height` - Image height in pixels.
75///
76/// # Returns
77///
78/// An `ImgVec<RGBA<f32>>` suitable for DSSIM calculation.
79#[must_use]
80pub fn rgb8_to_dssim_image(data: &[u8], width: usize, height: usize) -> ImgVec<RGBA<f32>> {
81    let pixels: Vec<RGBA<f32>> = data
82        .chunks_exact(3)
83        .map(|rgb| RGBA {
84            r: f32::from(rgb[0]) / 255.0,
85            g: f32::from(rgb[1]) / 255.0,
86            b: f32::from(rgb[2]) / 255.0,
87            a: 1.0,
88        })
89        .collect();
90
91    ImgVec::new(pixels, width, height)
92}
93
94/// Convert RGBA8 image data to the format needed for DSSIM calculation.
95///
96/// # Arguments
97///
98/// * `data` - RGBA8 pixel data in row-major order.
99/// * `width` - Image width in pixels.
100/// * `height` - Image height in pixels.
101///
102/// # Returns
103///
104/// An `ImgVec<RGBA<f32>>` suitable for DSSIM calculation.
105#[must_use]
106pub fn rgba8_to_dssim_image(data: &[u8], width: usize, height: usize) -> ImgVec<RGBA<f32>> {
107    let pixels: Vec<RGBA<f32>> = data
108        .chunks_exact(4)
109        .map(|rgba| RGBA {
110            r: f32::from(rgba[0]) / 255.0,
111            g: f32::from(rgba[1]) / 255.0,
112            b: f32::from(rgba[2]) / 255.0,
113            a: f32::from(rgba[3]) / 255.0,
114        })
115        .collect();
116
117    ImgVec::new(pixels, width, height)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_identical_images() {
126        let pixels: Vec<RGBA<f32>> = (0..100 * 100)
127            .map(|_| RGBA {
128                r: 0.5,
129                g: 0.5,
130                b: 0.5,
131                a: 1.0,
132            })
133            .collect();
134        let img = ImgVec::new(pixels, 100, 100);
135
136        let dssim = calculate_dssim(&img, &img, &ViewingCondition::desktop()).unwrap();
137        assert!(
138            dssim < 0.0001,
139            "Identical images should have near-zero DSSIM"
140        );
141    }
142
143    #[test]
144    fn test_different_images() {
145        let ref_pixels: Vec<RGBA<f32>> = (0..100 * 100)
146            .map(|_| RGBA {
147                r: 0.3,
148                g: 0.3,
149                b: 0.3,
150                a: 1.0,
151            })
152            .collect();
153        let test_pixels: Vec<RGBA<f32>> = (0..100 * 100)
154            .map(|_| RGBA {
155                r: 0.7,
156                g: 0.7,
157                b: 0.7,
158                a: 1.0,
159            })
160            .collect();
161
162        let ref_img = ImgVec::new(ref_pixels, 100, 100);
163        let test_img = ImgVec::new(test_pixels, 100, 100);
164
165        let dssim = calculate_dssim(&ref_img, &test_img, &ViewingCondition::desktop()).unwrap();
166        assert!(dssim > 0.0, "Different images should have non-zero DSSIM");
167    }
168
169    #[test]
170    fn test_dimension_mismatch() {
171        let small: Vec<RGBA<f32>> = (0..50 * 50)
172            .map(|_| RGBA {
173                r: 0.5,
174                g: 0.5,
175                b: 0.5,
176                a: 1.0,
177            })
178            .collect();
179        let large: Vec<RGBA<f32>> = (0..100 * 100)
180            .map(|_| RGBA {
181                r: 0.5,
182                g: 0.5,
183                b: 0.5,
184                a: 1.0,
185            })
186            .collect();
187
188        let small_img = ImgVec::new(small, 50, 50);
189        let large_img = ImgVec::new(large, 100, 100);
190
191        let result = calculate_dssim(&small_img, &large_img, &ViewingCondition::desktop());
192        assert!(matches!(result, Err(Error::DimensionMismatch { .. })));
193    }
194
195    #[test]
196    fn test_rgb8_conversion() {
197        let rgb_data = vec![255u8, 0, 0, 0, 255, 0]; // Red, Green pixels
198        let img = rgb8_to_dssim_image(&rgb_data, 2, 1);
199
200        assert_eq!(img.width(), 2);
201        assert_eq!(img.height(), 1);
202        let pixels: Vec<_> = img.pixels().collect();
203        assert!((pixels[0].r - 1.0).abs() < 0.001);
204        assert!((pixels[1].g - 1.0).abs() < 0.001);
205    }
206
207    #[test]
208    fn test_rgba8_conversion() {
209        let rgba_data = vec![255u8, 0, 0, 128, 0, 255, 0, 255]; // Semi-transparent red, opaque green
210        let img = rgba8_to_dssim_image(&rgba_data, 2, 1);
211
212        assert_eq!(img.width(), 2);
213        assert_eq!(img.height(), 1);
214        let pixels: Vec<_> = img.pixels().collect();
215        assert!((pixels[0].a - 0.502).abs() < 0.01);
216        assert!((pixels[1].a - 1.0).abs() < 0.001);
217    }
218}