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.chunks_exact(3).map(|c| [c[0], c[1], c[2]]).collect();
91
92 let ref_img = ImgVec::new(ref_pixels, width, height);
93 let test_img = ImgVec::new(test_pixels, width, height);
94
95 // fast-ssim2 uses ImgRef, so convert ImgVec to ImgRef
96 compute_ssimulacra2(ref_img.as_ref(), test_img.as_ref()).map_err(|e| Error::MetricCalculation {
97 metric: "SSIMULACRA2".to_string(),
98 reason: format!("Failed to compute SSIMULACRA2: {e:?}"),
99 })
100}
101
102/// Calculate SSIMULACRA2 with ICC profile support.
103///
104/// This function transforms both images to sRGB before comparison, ensuring
105/// accurate results even when images have non-sRGB color profiles.
106///
107/// # Arguments
108///
109/// * `reference` - Reference image as RGB8 pixel data.
110/// * `reference_profile` - Color profile of the reference image.
111/// * `test` - Test image as RGB8 pixel data.
112/// * `test_profile` - Color profile of the test image.
113/// * `width` - Image width in pixels.
114/// * `height` - Image height in pixels.
115///
116/// # Returns
117///
118/// SSIMULACRA2 score where higher is better (100 = identical).
119///
120/// # Example
121///
122/// ```ignore
123/// use codec_eval::metrics::{ssimulacra2::calculate_ssimulacra2_icc, ColorProfile};
124///
125/// // For XYB JPEG with embedded ICC profile
126/// let score = calculate_ssimulacra2_icc(
127/// &reference_rgb,
128/// &ColorProfile::Srgb,
129/// &decoded_jpeg_rgb,
130/// &ColorProfile::Icc(jpeg_icc_data),
131/// width,
132/// height,
133/// )?;
134/// ```
135pub fn calculate_ssimulacra2_icc(
136 reference: &[u8],
137 reference_profile: &ColorProfile,
138 test: &[u8],
139 test_profile: &ColorProfile,
140 width: usize,
141 height: usize,
142) -> Result<f64> {
143 let (ref_srgb, test_srgb) =
144 super::icc::prepare_for_comparison(reference, reference_profile, test, test_profile)?;
145
146 calculate_ssimulacra2(&ref_srgb, &test_srgb, width, height)
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn test_identical_images() {
155 let data: Vec<u8> = (0..100 * 100 * 3).map(|i| (i % 256) as u8).collect();
156 let score = calculate_ssimulacra2(&data, &data, 100, 100).unwrap();
157 // Identical images should have score close to 100
158 assert!(
159 score > 99.0,
160 "Identical images should have score ~100, got {score}"
161 );
162 }
163
164 #[test]
165 fn test_different_images() {
166 let ref_data: Vec<u8> = vec![100u8; 100 * 100 * 3];
167 let test_data: Vec<u8> = vec![200u8; 100 * 100 * 3];
168 let score = calculate_ssimulacra2(&ref_data, &test_data, 100, 100).unwrap();
169 // Very different images should have low score
170 assert!(
171 score < 80.0,
172 "Very different images should have low score, got {score}"
173 );
174 }
175
176 #[test]
177 fn test_size_mismatch() {
178 let small: Vec<u8> = vec![128u8; 50 * 50 * 3];
179 let large: Vec<u8> = vec![128u8; 100 * 100 * 3];
180 let result = calculate_ssimulacra2(&small, &large, 100, 100);
181 assert!(result.is_err());
182 }
183}