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}