Skip to main content

codec_eval/metrics/
ssimulacra2.rs

1//! SSIMULACRA2 metric calculation.
2//!
3//! SSIMULACRA2 is a perceptual image quality metric that correlates well with
4//! human visual perception. Higher scores indicate better quality.
5//!
6//! Score interpretation:
7//! - 100: Identical
8//! - > 90: Imperceptible difference
9//! - > 80: Marginal difference
10//! - > 70: Subtle difference
11//! - > 50: Noticeable difference
12//! - <= 50: Degraded
13//!
14//! # Implementation
15//!
16//! This module uses `fast-ssim2` for SIMD-accelerated SSIMULACRA2 calculation.
17//! It provides significantly better performance than the reference `ssimulacra2`
18//! implementation while producing identical results.
19//!
20//! # ICC Profile Support
21//!
22//! When comparing images with embedded ICC profiles, use [`calculate_ssimulacra2_icc`]
23//! to ensure accurate color space conversion before comparison. This is critical for:
24//!
25//! - XYB JPEGs from jpegli (which embed custom ICC profiles)
26//! - Wide-gamut images (Display P3, Rec.2020)
27//! - Any image with non-sRGB color space
28//!
29//! Without proper ICC handling, scores can be off by 1-2 points at high quality levels.
30
31use fast_ssim2::compute_ssimulacra2;
32use imgref::ImgVec;
33
34use super::icc::ColorProfile;
35use crate::error::{Error, Result};
36
37/// Calculate SSIMULACRA2 between two images.
38///
39/// # Arguments
40///
41/// * `reference` - Reference image as RGB8 pixel data (row-major, 3 bytes per pixel).
42/// * `test` - Test image as RGB8 pixel data (row-major, 3 bytes per pixel).
43/// * `width` - Image width in pixels.
44/// * `height` - Image height in pixels.
45///
46/// # Returns
47///
48/// SSIMULACRA2 score where higher is better (100 = identical).
49///
50/// # Errors
51///
52/// Returns an error if the images have different sizes or if calculation fails.
53///
54/// # Performance
55///
56/// This function uses `fast-ssim2` with SIMD acceleration. For the fastest
57/// performance, enable the `unsafe-simd` feature on fast-ssim2 (requires
58/// unsafe code, but significantly faster on modern CPUs).
59pub fn calculate_ssimulacra2(
60    reference: &[u8],
61    test: &[u8],
62    width: usize,
63    height: usize,
64) -> Result<f64> {
65    if reference.len() != test.len() {
66        return Err(Error::DimensionMismatch {
67            expected: (width, height),
68            actual: (test.len() / 3 / height, height),
69        });
70    }
71
72    let expected_len = width * height * 3;
73    if reference.len() != expected_len {
74        return Err(Error::MetricCalculation {
75            metric: "SSIMULACRA2".to_string(),
76            reason: format!(
77                "Invalid image size: expected {} bytes, got {}",
78                expected_len,
79                reference.len()
80            ),
81        });
82    }
83
84    // Convert flat RGB8 buffer to [u8; 3] array for fast-ssim2
85    let ref_pixels: Vec<[u8; 3]> = reference
86        .chunks_exact(3)
87        .map(|c| [c[0], c[1], c[2]])
88        .collect();
89
90    let test_pixels: Vec<[u8; 3]> = test
91        .chunks_exact(3)
92        .map(|c| [c[0], c[1], c[2]])
93        .collect();
94
95    let ref_img = ImgVec::new(ref_pixels, width, height);
96    let test_img = ImgVec::new(test_pixels, width, height);
97
98    // fast-ssim2 uses ImgRef, so convert ImgVec to ImgRef
99    compute_ssimulacra2(ref_img.as_ref(), test_img.as_ref()).map_err(|e| {
100        Error::MetricCalculation {
101            metric: "SSIMULACRA2".to_string(),
102            reason: format!("Failed to compute SSIMULACRA2: {e:?}"),
103        }
104    })
105}
106
107/// Calculate SSIMULACRA2 with ICC profile support.
108///
109/// This function transforms both images to sRGB before comparison, ensuring
110/// accurate results even when images have non-sRGB color profiles.
111///
112/// # Arguments
113///
114/// * `reference` - Reference image as RGB8 pixel data.
115/// * `reference_profile` - Color profile of the reference image.
116/// * `test` - Test image as RGB8 pixel data.
117/// * `test_profile` - Color profile of the test image.
118/// * `width` - Image width in pixels.
119/// * `height` - Image height in pixels.
120///
121/// # Returns
122///
123/// SSIMULACRA2 score where higher is better (100 = identical).
124///
125/// # Example
126///
127/// ```ignore
128/// use codec_eval::metrics::{ssimulacra2::calculate_ssimulacra2_icc, ColorProfile};
129///
130/// // For XYB JPEG with embedded ICC profile
131/// let score = calculate_ssimulacra2_icc(
132///     &reference_rgb,
133///     &ColorProfile::Srgb,
134///     &decoded_jpeg_rgb,
135///     &ColorProfile::Icc(jpeg_icc_data),
136///     width,
137///     height,
138/// )?;
139/// ```
140pub fn calculate_ssimulacra2_icc(
141    reference: &[u8],
142    reference_profile: &ColorProfile,
143    test: &[u8],
144    test_profile: &ColorProfile,
145    width: usize,
146    height: usize,
147) -> Result<f64> {
148    let (ref_srgb, test_srgb) =
149        super::icc::prepare_for_comparison(reference, reference_profile, test, test_profile)?;
150
151    calculate_ssimulacra2(&ref_srgb, &test_srgb, width, height)
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_identical_images() {
160        let data: Vec<u8> = (0..100 * 100 * 3).map(|i| (i % 256) as u8).collect();
161        let score = calculate_ssimulacra2(&data, &data, 100, 100).unwrap();
162        // Identical images should have score close to 100
163        assert!(
164            score > 99.0,
165            "Identical images should have score ~100, got {score}"
166        );
167    }
168
169    #[test]
170    fn test_different_images() {
171        let ref_data: Vec<u8> = vec![100u8; 100 * 100 * 3];
172        let test_data: Vec<u8> = vec![200u8; 100 * 100 * 3];
173        let score = calculate_ssimulacra2(&ref_data, &test_data, 100, 100).unwrap();
174        // Very different images should have low score
175        assert!(
176            score < 80.0,
177            "Very different images should have low score, got {score}"
178        );
179    }
180
181    #[test]
182    fn test_size_mismatch() {
183        let small: Vec<u8> = vec![128u8; 50 * 50 * 3];
184        let large: Vec<u8> = vec![128u8; 100 * 100 * 3];
185        let result = calculate_ssimulacra2(&small, &large, 100, 100);
186        assert!(result.is_err());
187    }
188}