1use crate::blur::Blur;
40use crate::input::ToLinearRgb;
41use crate::{
42 LinearRgb, Msssim, MsssimScale, NUM_SCALES, SimdImpl, Ssimulacra2Error, downscale_by_2,
43 edge_diff_map, image_multiply, linear_rgb_to_xyb_simd, make_positive_xyb, ssim_map,
44 xyb_to_planar,
45};
46
47#[derive(Clone, Debug)]
49struct ScaleData {
50 img1_planar: [Vec<f32>; 3],
52 mu1: [Vec<f32>; 3],
54 sigma1_sq: [Vec<f32>; 3],
56}
57
58#[derive(Clone, Debug)]
67pub struct Ssimulacra2Reference {
68 scales: Vec<ScaleData>,
69 original_width: usize,
70 original_height: usize,
71}
72
73impl Ssimulacra2Reference {
74 pub fn new<T: ToLinearRgb>(source: T) -> Result<Self, Ssimulacra2Error> {
84 let mut img1: LinearRgb = source.into_linear_rgb().into();
85 if img1.width().get() < 8 || img1.height().get() < 8 {
86 return Err(Ssimulacra2Error::InvalidImageSize);
87 }
88
89 let original_width = img1.width().get();
90 let original_height = img1.height().get();
91 let mut width = original_width;
92 let mut height = original_height;
93
94 let mut mul = [
95 vec![0.0f32; width * height],
96 vec![0.0f32; width * height],
97 vec![0.0f32; width * height],
98 ];
99 let mut blur = Blur::new(width, height);
100 let mut scales = Vec::with_capacity(NUM_SCALES);
101
102 for scale in 0..NUM_SCALES {
103 if width < 8 || height < 8 {
104 break;
105 }
106
107 if scale > 0 {
108 img1 = downscale_by_2(&img1);
109 width = img1.width().get();
110 height = img1.height().get();
111 }
112
113 for c in &mut mul {
114 c.truncate(width * height);
115 }
116 blur.shrink_to(width, height);
117
118 let mut img1_xyb = linear_rgb_to_xyb_simd(img1.clone());
119 make_positive_xyb(&mut img1_xyb);
120
121 let img1_planar = xyb_to_planar(&img1_xyb);
122
123 let mu1 = blur.blur(&img1_planar);
125
126 image_multiply(&img1_planar, &img1_planar, &mut mul, SimdImpl::default());
128 let sigma1_sq = blur.blur(&mul);
129
130 scales.push(ScaleData {
131 img1_planar,
132 mu1,
133 sigma1_sq,
134 });
135 }
136
137 Ok(Self {
138 scales,
139 original_width,
140 original_height,
141 })
142 }
143
144 pub fn compare<T: ToLinearRgb>(&self, distorted: T) -> Result<f64, Ssimulacra2Error> {
152 let mut img2: LinearRgb = distorted.into_linear_rgb().into();
153 if img2.width().get() != self.original_width || img2.height().get() != self.original_height
154 {
155 return Err(Ssimulacra2Error::NonMatchingImageDimensions);
156 }
157
158 let mut width = img2.width().get();
159 let mut height = img2.height().get();
160
161 let mut mul = [
162 vec![0.0f32; width * height],
163 vec![0.0f32; width * height],
164 vec![0.0f32; width * height],
165 ];
166 let mut blur = Blur::new(width, height);
167 let mut msssim = Msssim::default();
168
169 for (scale_idx, scale_data) in self.scales.iter().enumerate() {
170 if width < 8 || height < 8 {
171 break;
172 }
173
174 if scale_idx > 0 {
175 img2 = downscale_by_2(&img2);
176 width = img2.width().get();
177 height = img2.height().get();
178 }
179
180 for c in &mut mul {
181 c.truncate(width * height);
182 }
183 blur.shrink_to(width, height);
184
185 let mut img2_xyb = linear_rgb_to_xyb_simd(img2.clone());
186 make_positive_xyb(&mut img2_xyb);
187
188 let img2_planar = xyb_to_planar(&img2_xyb);
189
190 let mu2 = blur.blur(&img2_planar);
192
193 image_multiply(&img2_planar, &img2_planar, &mut mul, SimdImpl::default());
195 let sigma2_sq = blur.blur(&mul);
196
197 image_multiply(
199 &scale_data.img1_planar,
200 &img2_planar,
201 &mut mul,
202 SimdImpl::default(),
203 );
204 let sigma12 = blur.blur(&mul);
205
206 let avg_ssim = ssim_map(
208 width,
209 height,
210 &scale_data.mu1,
211 &mu2,
212 &scale_data.sigma1_sq,
213 &sigma2_sq,
214 &sigma12,
215 SimdImpl::default(),
216 );
217
218 let avg_edgediff = edge_diff_map(
219 width,
220 height,
221 &scale_data.img1_planar,
222 &scale_data.mu1,
223 &img2_planar,
224 &mu2,
225 SimdImpl::default(),
226 );
227
228 msssim.scales.push(MsssimScale {
229 avg_ssim,
230 avg_edgediff,
231 });
232 }
233
234 Ok(msssim.score())
235 }
236
237 #[must_use]
239 pub fn width(&self) -> usize {
240 self.original_width
241 }
242
243 #[must_use]
245 pub fn height(&self) -> usize {
246 self.original_height
247 }
248
249 #[must_use]
251 pub fn num_scales(&self) -> usize {
252 self.scales.len()
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use crate::compute_ssimulacra2;
260 use std::num::NonZeroUsize;
261 use yuvxyb::{ColorPrimaries, Rgb, TransferCharacteristic};
262
263 #[test]
264 fn test_precompute_matches_full_compute() {
265 let width = 64usize;
267 let height = 64usize;
268 let nz_width = NonZeroUsize::new(width).unwrap();
269 let nz_height = NonZeroUsize::new(height).unwrap();
270 let source_data: Vec<[f32; 3]> = (0..width * height)
271 .map(|i| {
272 let x = (i % width) as f32 / width as f32;
273 let y = (i / width) as f32 / height as f32;
274 [x, y, 0.5]
275 })
276 .collect();
277
278 let distorted_data: Vec<[f32; 3]> = source_data
279 .iter()
280 .map(|&[r, g, b]| [r * 0.9, g * 0.95, b * 1.05])
281 .collect();
282
283 let source = Rgb::new(
284 source_data.clone(),
285 nz_width,
286 nz_height,
287 TransferCharacteristic::SRGB,
288 ColorPrimaries::BT709,
289 )
290 .unwrap();
291
292 let distorted = Rgb::new(
293 distorted_data,
294 nz_width,
295 nz_height,
296 TransferCharacteristic::SRGB,
297 ColorPrimaries::BT709,
298 )
299 .unwrap();
300
301 let source_clone = Rgb::new(
303 source_data,
304 nz_width,
305 nz_height,
306 TransferCharacteristic::SRGB,
307 ColorPrimaries::BT709,
308 )
309 .unwrap();
310 let full_score = compute_ssimulacra2(source_clone, distorted.clone()).unwrap();
311
312 let precomputed = Ssimulacra2Reference::new(source).unwrap();
314 let precomputed_score = precomputed.compare(distorted).unwrap();
315
316 assert!(
318 (full_score - precomputed_score).abs() < 1e-6,
319 "Scores don't match: full={}, precomputed={}",
320 full_score,
321 precomputed_score
322 );
323 }
324
325 #[test]
326 fn test_precompute_dimension_mismatch() {
327 let source_data: Vec<[f32; 3]> = vec![[0.5, 0.5, 0.5]; 64 * 64];
328 let distorted_data: Vec<[f32; 3]> = vec![[0.4, 0.4, 0.4]; 32 * 32]; let source = Rgb::new(
331 source_data,
332 NonZeroUsize::new(64).unwrap(),
333 NonZeroUsize::new(64).unwrap(),
334 TransferCharacteristic::SRGB,
335 ColorPrimaries::BT709,
336 )
337 .unwrap();
338
339 let distorted = Rgb::new(
340 distorted_data,
341 NonZeroUsize::new(32).unwrap(),
342 NonZeroUsize::new(32).unwrap(),
343 TransferCharacteristic::SRGB,
344 ColorPrimaries::BT709,
345 )
346 .unwrap();
347
348 let precomputed = Ssimulacra2Reference::new(source).unwrap();
349 let result = precomputed.compare(distorted);
350
351 assert!(matches!(
352 result,
353 Err(Ssimulacra2Error::NonMatchingImageDimensions)
354 ));
355 }
356
357 #[test]
358 fn test_precompute_metadata() {
359 let data: Vec<[f32; 3]> = vec![[0.5, 0.5, 0.5]; 128 * 96];
360 let source = Rgb::new(
361 data,
362 NonZeroUsize::new(128).unwrap(),
363 NonZeroUsize::new(96).unwrap(),
364 TransferCharacteristic::SRGB,
365 ColorPrimaries::BT709,
366 )
367 .unwrap();
368
369 let precomputed = Ssimulacra2Reference::new(source).unwrap();
370
371 assert_eq!(precomputed.width(), 128);
372 assert_eq!(precomputed.height(), 96);
373 assert!(precomputed.num_scales() > 0);
374 assert!(precomputed.num_scales() <= NUM_SCALES);
375 }
376}