fast_ssim2/
lib.rs

1mod blur;
2mod input;
3mod precompute;
4// Reference data for parity testing (hidden from docs but accessible for tests)
5#[doc(hidden)]
6pub mod reference_data;
7mod simd_ops;
8mod xyb_simd;
9
10#[cfg(feature = "unsafe-simd")]
11mod xyb_unsafe_simd;
12
13#[cfg(feature = "unsafe-simd")]
14mod ssim_unsafe_simd;
15
16pub use blur::Blur;
17pub use input::{LinearRgbImage, ToLinearRgb};
18pub use precompute::Ssimulacra2Reference;
19// Re-export commonly used types from yuvxyb for convenience
20pub use yuvxyb::{
21    ColorPrimaries, Frame, LinearRgb, MatrixCoefficients, Pixel, Plane, Rgb, TransferCharacteristic,
22    Yuv, YuvConfig,
23};
24
25// Re-export sRGB conversion functions for users implementing custom input types
26pub use input::{srgb_to_linear, srgb_u16_to_linear, srgb_u8_to_linear};
27
28// Internal imports for XYB color space
29use yuvxyb::Xyb;
30
31#[cfg(all(feature = "unsafe-simd", target_arch = "x86_64"))]
32use safe_unaligned_simd::x86_64 as safe_simd;
33
34// How often to downscale and score the input images.
35// Each scaling step will downscale by a factor of two.
36pub(crate) const NUM_SCALES: usize = 6;
37
38/// SIMD implementation backend for all operations (blur, XYB conversion, SSIM computation).
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum SimdImpl {
41    /// Scalar implementation (baseline, most portable)
42    Scalar,
43    /// Safe SIMD via wide crate (default, good balance of speed and safety)
44    #[default]
45    Simd,
46    /// Raw x86 intrinsics (fastest, requires unsafe-simd feature)
47    #[cfg(feature = "unsafe-simd")]
48    UnsafeSimd,
49}
50
51impl SimdImpl {
52    /// Returns the name of this implementation
53    pub fn name(&self) -> &'static str {
54        match self {
55            SimdImpl::Scalar => "scalar",
56            SimdImpl::Simd => "simd (wide crate)",
57            #[cfg(feature = "unsafe-simd")]
58            SimdImpl::UnsafeSimd => "unsafe-simd (raw intrinsics)",
59        }
60    }
61}
62
63/// Configuration for SSIMULACRA2 computation.
64#[derive(Debug, Clone, Copy, Default)]
65pub struct Ssimulacra2Config {
66    /// Implementation backend for all operations
67    pub impl_type: SimdImpl,
68}
69
70impl Ssimulacra2Config {
71    /// Create configuration with specified implementation
72    pub fn new(impl_type: SimdImpl) -> Self {
73        Self { impl_type }
74    }
75
76    /// Default configuration using safe SIMD for all operations
77    pub fn simd() -> Self {
78        Self::new(SimdImpl::Simd)
79    }
80
81    /// Configuration using unsafe SIMD for all operations (fastest)
82    #[cfg(feature = "unsafe-simd")]
83    pub fn unsafe_simd() -> Self {
84        Self::new(SimdImpl::UnsafeSimd)
85    }
86
87    /// Scalar configuration (baseline, most compatible)
88    pub fn scalar() -> Self {
89        Self::new(SimdImpl::Scalar)
90    }
91}
92
93/// Errors which can occur when attempting to calculate a SSIMULACRA2 score from two input images.
94#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
95pub enum Ssimulacra2Error {
96    /// The conversion from input image to [LinearRgb] (via [TryFrom]) returned an [Err].
97    #[error("Failed to convert input image to linear RGB")]
98    LinearRgbConversionFailed,
99
100    /// The two input images do not have the same width and height.
101    #[error("Source and distorted image width and height must be equal")]
102    NonMatchingImageDimensions,
103
104    /// One of the input images has a width and/or height of less than 8 pixels.
105    #[error("Images must be at least 8x8 pixels")]
106    InvalidImageSize,
107
108    /// Gaussian blur operation failed.
109    #[error("Gaussian blur operation failed")]
110    GaussianBlurError,
111}
112
113/// Computes the SSIMULACRA2 score with default configuration (safe SIMD).
114pub fn compute_frame_ssimulacra2<T, U>(source: T, distorted: U) -> Result<f64, Ssimulacra2Error>
115where
116    LinearRgb: TryFrom<T> + TryFrom<U>,
117{
118    compute_frame_ssimulacra2_impl(source, distorted, Ssimulacra2Config::default())
119}
120
121/// Computes the SSIMULACRA2 score with custom implementation configuration.
122pub fn compute_frame_ssimulacra2_with_config<T, U>(
123    source: T,
124    distorted: U,
125    config: Ssimulacra2Config,
126) -> Result<f64, Ssimulacra2Error>
127where
128    LinearRgb: TryFrom<T> + TryFrom<U>,
129{
130    compute_frame_ssimulacra2_impl(source, distorted, config)
131}
132
133/// Computes the SSIMULACRA2 score from any input type implementing [`ToLinearRgb`].
134///
135/// This is the recommended API for new code. It supports:
136/// - `imgref` types (with the `imgref` feature): `ImgRef<[u8; 3]>`, `ImgRef<[f32; 3]>`, etc.
137/// - `yuvxyb` types: `Rgb`, `LinearRgb`
138/// - Custom types implementing [`ToLinearRgb`]
139///
140/// # Color space conventions
141/// - Integer types (`u8`, `u16`) are assumed to be sRGB (gamma-encoded)
142/// - Float types (`f32`) are assumed to be linear RGB
143/// - Grayscale types are expanded to RGB (R=G=B)
144///
145/// # Example
146/// ```ignore
147/// use imgref::ImgVec;
148/// use fast_ssim2::compute_ssimulacra2;
149///
150/// let source: ImgVec<[u8; 3]> = /* ... */;
151/// let distorted: ImgVec<[u8; 3]> = /* ... */;
152/// let score = compute_ssimulacra2(&source, &distorted)?;
153/// ```
154pub fn compute_ssimulacra2<S, D>(source: S, distorted: D) -> Result<f64, Ssimulacra2Error>
155where
156    S: ToLinearRgb,
157    D: ToLinearRgb,
158{
159    compute_ssimulacra2_with_config(source, distorted, Ssimulacra2Config::default())
160}
161
162/// Computes the SSIMULACRA2 score with custom configuration from [`ToLinearRgb`] inputs.
163pub fn compute_ssimulacra2_with_config<S, D>(
164    source: S,
165    distorted: D,
166    config: Ssimulacra2Config,
167) -> Result<f64, Ssimulacra2Error>
168where
169    S: ToLinearRgb,
170    D: ToLinearRgb,
171{
172    let img1: LinearRgb = source.to_linear_rgb().into();
173    let img2: LinearRgb = distorted.to_linear_rgb().into();
174    compute_frame_ssimulacra2_impl(img1, img2, config)
175}
176
177fn compute_frame_ssimulacra2_impl<T, U>(
178    source: T,
179    distorted: U,
180    config: Ssimulacra2Config,
181) -> Result<f64, Ssimulacra2Error>
182where
183    LinearRgb: TryFrom<T> + TryFrom<U>,
184{
185    let Ok(mut img1) = LinearRgb::try_from(source) else {
186        return Err(Ssimulacra2Error::LinearRgbConversionFailed);
187    };
188
189    let Ok(mut img2) = LinearRgb::try_from(distorted) else {
190        return Err(Ssimulacra2Error::LinearRgbConversionFailed);
191    };
192
193    if img1.width() != img2.width() || img1.height() != img2.height() {
194        return Err(Ssimulacra2Error::NonMatchingImageDimensions);
195    }
196
197    if img1.width() < 8 || img1.height() < 8 {
198        return Err(Ssimulacra2Error::InvalidImageSize);
199    }
200
201    let mut width = img1.width();
202    let mut height = img1.height();
203    let impl_type = config.impl_type;
204
205    // Pre-allocate reusable buffers (sized for initial dimensions, shrunk per scale)
206    let alloc_plane = || vec![0.0f32; width * height];
207    let alloc_3planes = || [alloc_plane(), alloc_plane(), alloc_plane()];
208
209    let mut mul = alloc_3planes();
210    let mut sigma1_sq = alloc_3planes();
211    let mut sigma2_sq = alloc_3planes();
212    let mut sigma12 = alloc_3planes();
213    let mut mu1 = alloc_3planes();
214    let mut mu2 = alloc_3planes();
215    let mut img1_planar = alloc_3planes();
216    let mut img2_planar = alloc_3planes();
217
218    let mut blur = Blur::with_simd_impl(width, height, impl_type);
219    let mut msssim = Msssim::default();
220
221    for scale in 0..NUM_SCALES {
222        if width < 8 || height < 8 {
223            break;
224        }
225
226        if scale > 0 {
227            img1 = downscale_by_2(&img1);
228            img2 = downscale_by_2(&img2);
229            width = img1.width();
230            height = img2.height();
231        }
232
233        // Shrink all buffers to current scale size
234        let size = width * height;
235        for buf in [
236            &mut mul,
237            &mut sigma1_sq,
238            &mut sigma2_sq,
239            &mut sigma12,
240            &mut mu1,
241            &mut mu2,
242            &mut img1_planar,
243            &mut img2_planar,
244        ] {
245            for c in buf.iter_mut() {
246                c.truncate(size);
247            }
248        }
249        blur.shrink_to(width, height);
250
251        let mut img1_xyb = linear_rgb_to_xyb(img1.clone(), impl_type);
252        let mut img2_xyb = linear_rgb_to_xyb(img2.clone(), impl_type);
253
254        make_positive_xyb(&mut img1_xyb);
255        make_positive_xyb(&mut img2_xyb);
256
257        xyb_to_planar_into(&img1_xyb, &mut img1_planar);
258        xyb_to_planar_into(&img2_xyb, &mut img2_planar);
259
260        image_multiply(&img1_planar, &img1_planar, &mut mul, impl_type);
261        blur.blur_into(&mul, &mut sigma1_sq);
262
263        image_multiply(&img2_planar, &img2_planar, &mut mul, impl_type);
264        blur.blur_into(&mul, &mut sigma2_sq);
265
266        image_multiply(&img1_planar, &img2_planar, &mut mul, impl_type);
267        blur.blur_into(&mul, &mut sigma12);
268
269        blur.blur_into(&img1_planar, &mut mu1);
270        blur.blur_into(&img2_planar, &mut mu2);
271
272        let avg_ssim = ssim_map(
273            width, height, &mu1, &mu2, &sigma1_sq, &sigma2_sq, &sigma12, impl_type,
274        );
275        let avg_edgediff = edge_diff_map(
276            width,
277            height,
278            &img1_planar,
279            &mu1,
280            &img2_planar,
281            &mu2,
282            impl_type,
283        );
284        msssim.scales.push(MsssimScale {
285            avg_ssim,
286            avg_edgediff,
287        });
288    }
289
290    Ok(msssim.score())
291}
292
293/// Convert LinearRgb to Xyb using the specified implementation
294fn linear_rgb_to_xyb(linear_rgb: LinearRgb, impl_type: SimdImpl) -> Xyb {
295    match impl_type {
296        SimdImpl::Scalar => Xyb::from(linear_rgb),
297        SimdImpl::Simd => {
298            let width = linear_rgb.width();
299            let height = linear_rgb.height();
300            let mut data = linear_rgb.into_data();
301            xyb_simd::linear_rgb_to_xyb_simd(&mut data);
302            Xyb::new(data, width, height).expect("XYB construction should not fail")
303        }
304        #[cfg(feature = "unsafe-simd")]
305        SimdImpl::UnsafeSimd => {
306            let width = linear_rgb.width();
307            let height = linear_rgb.height();
308            let mut data = linear_rgb.into_data();
309            xyb_unsafe_simd::linear_rgb_to_xyb_unsafe(&mut data);
310            Xyb::new(data, width, height).expect("XYB construction should not fail")
311        }
312    }
313}
314
315// For backwards compatibility
316pub(crate) fn linear_rgb_to_xyb_simd(linear_rgb: LinearRgb) -> Xyb {
317    linear_rgb_to_xyb(linear_rgb, SimdImpl::Simd)
318}
319
320pub(crate) fn make_positive_xyb(xyb: &mut Xyb) {
321    for pix in xyb.data_mut().iter_mut() {
322        pix[2] = (pix[2] - pix[1]) + 0.55;
323        pix[0] = (pix[0]).mul_add(14.0, 0.42);
324        pix[1] += 0.01;
325    }
326}
327
328// Note: make_positive_xyb doesn't benefit much from AVX2 due to complex RGB3 deinterleaving
329// The scalar version is already well-optimized by the compiler
330
331// Note: xyb_to_planar doesn't benefit much from AVX2 due to complex RGB3 deinterleaving
332// The scalar version is already well-optimized by the compiler
333pub(crate) fn xyb_to_planar(xyb: &Xyb) -> [Vec<f32>; 3] {
334    let size = xyb.width() * xyb.height();
335    let mut out = [vec![0.0f32; size], vec![0.0f32; size], vec![0.0f32; size]];
336    xyb_to_planar_into(xyb, &mut out);
337    out
338}
339
340/// Convert XYB to planar format into pre-allocated buffers (zero-allocation)
341pub(crate) fn xyb_to_planar_into(xyb: &Xyb, out: &mut [Vec<f32>; 3]) {
342    let [out0, out1, out2] = out;
343    for (((i, o0), o1), o2) in xyb
344        .data()
345        .iter()
346        .copied()
347        .zip(out0.iter_mut())
348        .zip(out1.iter_mut())
349        .zip(out2.iter_mut())
350    {
351        *o0 = i[0];
352        *o1 = i[1];
353        *o2 = i[2];
354    }
355}
356
357pub(crate) fn image_multiply(
358    img1: &[Vec<f32>; 3],
359    img2: &[Vec<f32>; 3],
360    out: &mut [Vec<f32>; 3],
361    impl_type: SimdImpl,
362) {
363    match impl_type {
364        SimdImpl::Scalar => image_multiply_scalar(img1, img2, out),
365        SimdImpl::Simd => simd_ops::image_multiply_simd(img1, img2, out),
366        #[cfg(feature = "unsafe-simd")]
367        SimdImpl::UnsafeSimd => {
368            #[cfg(target_arch = "x86_64")]
369            {
370                if is_x86_feature_detected!("avx2") {
371                    unsafe { image_multiply_avx2(img1, img2, out) };
372                    return;
373                }
374            }
375            // Fallback to portable SIMD if AVX2 not available
376            simd_ops::image_multiply_simd(img1, img2, out);
377        }
378    }
379}
380
381fn image_multiply_scalar(img1: &[Vec<f32>; 3], img2: &[Vec<f32>; 3], out: &mut [Vec<f32>; 3]) {
382    for ((plane1, plane2), out_plane) in img1.iter().zip(img2.iter()).zip(out.iter_mut()) {
383        for ((&p1, &p2), o) in plane1.iter().zip(plane2.iter()).zip(out_plane.iter_mut()) {
384            *o = p1 * p2;
385        }
386    }
387}
388
389#[cfg(all(feature = "unsafe-simd", target_arch = "x86_64"))]
390#[target_feature(enable = "avx2")]
391unsafe fn image_multiply_avx2(img1: &[Vec<f32>; 3], img2: &[Vec<f32>; 3], out: &mut [Vec<f32>; 3]) {
392    use std::arch::x86_64::*;
393
394    for c in 0..3 {
395        let plane1 = &img1[c];
396        let plane2 = &img2[c];
397        let out_plane = &mut out[c];
398        let len = plane1.len();
399
400        let chunks_8 = len / 8;
401        for chunk in 0..chunks_8 {
402            let base = chunk * 8;
403            // Safe loads via safe_unaligned_simd
404            let v1 = safe_simd::_mm256_loadu_ps(plane1[base..].first_chunk::<8>().unwrap());
405            let v2 = safe_simd::_mm256_loadu_ps(plane2[base..].first_chunk::<8>().unwrap());
406            let result = _mm256_mul_ps(v1, v2);
407            // Safe store
408            safe_simd::_mm256_storeu_ps(out_plane[base..].first_chunk_mut::<8>().unwrap(), result);
409        }
410
411        // Remainder
412        for i in (chunks_8 * 8)..len {
413            out_plane[i] = plane1[i] * plane2[i];
414        }
415    }
416}
417
418pub(crate) fn downscale_by_2(in_data: &LinearRgb) -> LinearRgb {
419    const SCALE: usize = 2;
420    let in_w = in_data.width();
421    let in_h = in_data.height();
422    let out_w = in_w.div_ceil(SCALE);
423    let out_h = in_h.div_ceil(SCALE);
424    let mut out_data = vec![[0.0f32; 3]; out_w * out_h];
425
426    let in_data = &in_data.data();
427    for oy in 0..out_h {
428        for ox in 0..out_w {
429            for c in 0..3 {
430                let mut sum = 0f64;
431                for iy in 0..SCALE {
432                    for ix in 0..SCALE {
433                        let x = (ox * SCALE + ix).min(in_w - 1);
434                        let y = (oy * SCALE + iy).min(in_h - 1);
435                        let in_pix = in_data[y * in_w + x];
436                        sum += f64::from(in_pix[c]);
437                    }
438                }
439                let out_pix = &mut out_data[oy * out_w + ox];
440                out_pix[c] = (sum / (SCALE * SCALE) as f64) as f32;
441            }
442        }
443    }
444
445    LinearRgb::new(out_data, out_w, out_h).expect("Resolution and data size match")
446}
447
448#[allow(clippy::too_many_arguments)]
449pub(crate) fn ssim_map(
450    width: usize,
451    height: usize,
452    m1: &[Vec<f32>; 3],
453    m2: &[Vec<f32>; 3],
454    s11: &[Vec<f32>; 3],
455    s22: &[Vec<f32>; 3],
456    s12: &[Vec<f32>; 3],
457    impl_type: SimdImpl,
458) -> [f64; 3 * 2] {
459    match impl_type {
460        SimdImpl::Scalar => ssim_map_scalar(width, height, m1, m2, s11, s22, s12),
461        SimdImpl::Simd => simd_ops::ssim_map_simd(width, height, m1, m2, s11, s22, s12),
462        #[cfg(feature = "unsafe-simd")]
463        SimdImpl::UnsafeSimd => {
464            ssim_unsafe_simd::ssim_map_unsafe(width, height, m1, m2, s11, s22, s12)
465        }
466    }
467}
468
469fn ssim_map_scalar(
470    width: usize,
471    height: usize,
472    m1: &[Vec<f32>; 3],
473    m2: &[Vec<f32>; 3],
474    s11: &[Vec<f32>; 3],
475    s22: &[Vec<f32>; 3],
476    s12: &[Vec<f32>; 3],
477) -> [f64; 3 * 2] {
478    const C2: f32 = 0.0009f32;
479
480    let one_per_pixels = 1.0f64 / (width * height) as f64;
481    let mut plane_averages = [0f64; 3 * 2];
482
483    for c in 0..3 {
484        let mut sum1 = [0.0f64; 2];
485        for (row_m1, (row_m2, (row_s11, (row_s22, row_s12)))) in m1[c].chunks_exact(width).zip(
486            m2[c].chunks_exact(width).zip(
487                s11[c]
488                    .chunks_exact(width)
489                    .zip(s22[c].chunks_exact(width).zip(s12[c].chunks_exact(width))),
490            ),
491        ) {
492            for x in 0..width {
493                let mu1 = row_m1[x];
494                let mu2 = row_m2[x];
495                let mu11 = mu1 * mu1;
496                let mu22 = mu2 * mu2;
497                let mu12 = mu1 * mu2;
498                let mu_diff = mu1 - mu2;
499
500                let num_m = f64::from(mu_diff).mul_add(-f64::from(mu_diff), 1.0f64);
501                let num_s = 2f64.mul_add(f64::from(row_s12[x] - mu12), f64::from(C2));
502                let denom_s =
503                    f64::from(row_s11[x] - mu11) + f64::from(row_s22[x] - mu22) + f64::from(C2);
504                let mut d = 1.0f64 - (num_m * num_s) / denom_s;
505                d = d.max(0.0);
506                sum1[0] += d;
507                sum1[1] += d.powi(4);
508            }
509        }
510        plane_averages[c * 2] = one_per_pixels * sum1[0];
511        plane_averages[c * 2 + 1] = (one_per_pixels * sum1[1]).sqrt().sqrt();
512    }
513
514    plane_averages
515}
516
517pub(crate) fn edge_diff_map(
518    width: usize,
519    height: usize,
520    img1: &[Vec<f32>; 3],
521    mu1: &[Vec<f32>; 3],
522    img2: &[Vec<f32>; 3],
523    mu2: &[Vec<f32>; 3],
524    impl_type: SimdImpl,
525) -> [f64; 3 * 4] {
526    match impl_type {
527        SimdImpl::Scalar => edge_diff_map_scalar(width, height, img1, mu1, img2, mu2),
528        SimdImpl::Simd => simd_ops::edge_diff_map_simd(width, height, img1, mu1, img2, mu2),
529        #[cfg(feature = "unsafe-simd")]
530        SimdImpl::UnsafeSimd => {
531            ssim_unsafe_simd::edge_diff_map_unsafe(width, height, img1, mu1, img2, mu2)
532        }
533    }
534}
535
536fn edge_diff_map_scalar(
537    width: usize,
538    height: usize,
539    img1: &[Vec<f32>; 3],
540    mu1: &[Vec<f32>; 3],
541    img2: &[Vec<f32>; 3],
542    mu2: &[Vec<f32>; 3],
543) -> [f64; 3 * 4] {
544    let one_per_pixels = 1.0f64 / (width * height) as f64;
545    let mut plane_averages = [0f64; 3 * 4];
546
547    for c in 0..3 {
548        let mut sum1 = [0.0f64; 4];
549        for (row1, (row2, (rowm1, rowm2))) in img1[c].chunks_exact(width).zip(
550            img2[c]
551                .chunks_exact(width)
552                .zip(mu1[c].chunks_exact(width).zip(mu2[c].chunks_exact(width))),
553        ) {
554            for x in 0..width {
555                let d1: f64 = (1.0 + f64::from((row2[x] - rowm2[x]).abs()))
556                    / (1.0 + f64::from((row1[x] - rowm1[x]).abs()))
557                    - 1.0;
558
559                let artifact = d1.max(0.0);
560                sum1[0] += artifact;
561                sum1[1] += artifact.powi(4);
562
563                let detail_lost = (-d1).max(0.0);
564                sum1[2] += detail_lost;
565                sum1[3] += detail_lost.powi(4);
566            }
567        }
568        plane_averages[c * 4] = one_per_pixels * sum1[0];
569        plane_averages[c * 4 + 1] = (one_per_pixels * sum1[1]).sqrt().sqrt();
570        plane_averages[c * 4 + 2] = one_per_pixels * sum1[2];
571        plane_averages[c * 4 + 3] = (one_per_pixels * sum1[3]).sqrt().sqrt();
572    }
573
574    plane_averages
575}
576
577#[derive(Debug, Clone, Default)]
578pub(crate) struct Msssim {
579    pub scales: Vec<MsssimScale>,
580}
581
582#[derive(Debug, Clone, Copy, Default)]
583pub(crate) struct MsssimScale {
584    pub avg_ssim: [f64; 3 * 2],
585    pub avg_edgediff: [f64; 3 * 4],
586}
587
588impl Msssim {
589    #[allow(clippy::too_many_lines)]
590    pub fn score(&self) -> f64 {
591        const WEIGHT: [f64; 108] = [
592            0.0,
593            0.000_737_660_670_740_658_6,
594            0.0,
595            0.0,
596            0.000_779_348_168_286_730_9,
597            0.0,
598            0.0,
599            0.000_437_115_573_010_737_9,
600            0.0,
601            1.104_172_642_665_734_6,
602            0.000_662_848_341_292_71,
603            0.000_152_316_327_837_187_52,
604            0.0,
605            0.001_640_643_745_659_975_4,
606            0.0,
607            1.842_245_552_053_929_8,
608            11.441_172_603_757_666,
609            0.0,
610            0.000_798_910_943_601_516_3,
611            0.000_176_816_438_078_653,
612            0.0,
613            1.878_759_497_954_638_7,
614            10.949_069_906_051_42,
615            0.0,
616            0.000_728_934_699_150_807_2,
617            0.967_793_708_062_683_3,
618            0.0,
619            0.000_140_034_242_854_358_84,
620            0.998_176_697_785_496_7,
621            0.000_319_497_559_344_350_53,
622            0.000_455_099_211_379_206_3,
623            0.0,
624            0.0,
625            0.001_364_876_616_324_339_8,
626            0.0,
627            0.0,
628            0.0,
629            0.0,
630            0.0,
631            7.466_890_328_078_848,
632            0.0,
633            17.445_833_984_131_262,
634            0.000_623_560_163_404_146_6,
635            0.0,
636            0.0,
637            6.683_678_146_179_332,
638            0.000_377_244_079_796_112_96,
639            1.027_889_937_768_264,
640            225.205_153_008_492_74,
641            0.0,
642            0.0,
643            19.213_238_186_143_016,
644            0.001_140_152_458_661_836_1,
645            0.001_237_755_635_509_985,
646            176.393_175_984_506_94,
647            0.0,
648            0.0,
649            24.433_009_998_704_76,
650            0.285_208_026_121_177_57,
651            0.000_448_543_692_383_340_8,
652            0.0,
653            0.0,
654            0.0,
655            34.779_063_444_837_72,
656            44.835_625_328_877_896,
657            0.0,
658            0.0,
659            0.0,
660            0.0,
661            0.0,
662            0.0,
663            0.0,
664            0.0,
665            0.000_868_055_657_329_169_8,
666            0.0,
667            0.0,
668            0.0,
669            0.0,
670            0.0,
671            0.000_531_319_187_435_874_7,
672            0.0,
673            0.000_165_338_141_613_791_12,
674            0.0,
675            0.0,
676            0.0,
677            0.0,
678            0.0,
679            0.000_417_917_180_325_133_6,
680            0.001_729_082_823_472_283_3,
681            0.0,
682            0.002_082_700_584_663_643_7,
683            0.0,
684            0.0,
685            8.826_982_764_996_862,
686            23.192_433_439_989_26,
687            0.0,
688            95.108_049_881_108_6,
689            0.986_397_803_440_068_2,
690            0.983_438_279_246_535_3,
691            0.001_228_640_504_827_849_3,
692            171.266_725_589_730_7,
693            0.980_785_887_243_537_9,
694            0.0,
695            0.0,
696            0.0,
697            0.000_513_006_458_899_067_9,
698            0.0,
699            0.000_108_540_578_584_115_37,
700        ];
701
702        let mut ssim = 0.0f64;
703
704        let mut i = 0usize;
705        for c in 0..3 {
706            for scale in &self.scales {
707                for n in 0..2 {
708                    ssim = WEIGHT[i].mul_add(scale.avg_ssim[c * 2 + n].abs(), ssim);
709                    i += 1;
710                    ssim = WEIGHT[i].mul_add(scale.avg_edgediff[c * 4 + n].abs(), ssim);
711                    i += 1;
712                    ssim = WEIGHT[i].mul_add(scale.avg_edgediff[c * 4 + n + 2].abs(), ssim);
713                    i += 1;
714                }
715            }
716        }
717
718        ssim *= 0.956_238_261_683_484_4_f64;
719        ssim = (6.248_496_625_763_138e-5 * ssim * ssim).mul_add(
720            ssim,
721            2.326_765_642_916_932f64.mul_add(ssim, -0.020_884_521_182_843_837 * ssim * ssim),
722        );
723
724        if ssim > 0.0f64 {
725            ssim = ssim
726                .powf(0.627_633_646_783_138_7)
727                .mul_add(-10.0f64, 100.0f64);
728        } else {
729            ssim = 100.0f64;
730        }
731
732        ssim
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use std::path::PathBuf;
739
740    use super::*;
741    use yuvxyb::{ColorPrimaries, Rgb, TransferCharacteristic};
742
743    #[test]
744    fn test_ssimulacra2() {
745        let source = image::open(
746            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
747                .join("test_data")
748                .join("tank_source.png"),
749        )
750        .unwrap();
751        let distorted = image::open(
752            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
753                .join("test_data")
754                .join("tank_distorted.png"),
755        )
756        .unwrap();
757        let source_data = source
758            .to_rgb32f()
759            .chunks_exact(3)
760            .map(|chunk| [chunk[0], chunk[1], chunk[2]])
761            .collect::<Vec<_>>();
762        let source_data = Xyb::try_from(
763            Rgb::new(
764                source_data,
765                source.width() as usize,
766                source.height() as usize,
767                TransferCharacteristic::SRGB,
768                ColorPrimaries::BT709,
769            )
770            .unwrap(),
771        )
772        .unwrap();
773        let distorted_data = distorted
774            .to_rgb32f()
775            .chunks_exact(3)
776            .map(|chunk| [chunk[0], chunk[1], chunk[2]])
777            .collect::<Vec<_>>();
778        let distorted_data = Xyb::try_from(
779            Rgb::new(
780                distorted_data,
781                distorted.width() as usize,
782                distorted.height() as usize,
783                TransferCharacteristic::SRGB,
784                ColorPrimaries::BT709,
785            )
786            .unwrap(),
787        )
788        .unwrap();
789        let result = compute_frame_ssimulacra2(source_data, distorted_data).unwrap();
790        let expected = 17.398_505_f64;
791        assert!(
792            (result - expected).abs() < 0.25f64,
793            "Result {result:.6} not equal to expected {expected:.6}",
794        );
795    }
796
797    #[test]
798    fn test_xyb_simd_vs_yuvxyb() {
799        use yuvxyb::{ColorPrimaries, TransferCharacteristic};
800
801        let source = image::open(
802            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
803                .join("test_data")
804                .join("tank_source.png"),
805        )
806        .unwrap();
807
808        let source_data: Vec<[f32; 3]> = source
809            .to_rgb32f()
810            .chunks_exact(3)
811            .map(|chunk| [chunk[0], chunk[1], chunk[2]])
812            .collect();
813
814        let width = source.width() as usize;
815        let height = source.height() as usize;
816
817        let rgb_for_yuvxyb = Rgb::new(
818            source_data.clone(),
819            width,
820            height,
821            TransferCharacteristic::SRGB,
822            ColorPrimaries::BT709,
823        )
824        .unwrap();
825        let lrgb_for_yuvxyb = yuvxyb::LinearRgb::try_from(rgb_for_yuvxyb).unwrap();
826        let xyb_yuvxyb = yuvxyb::Xyb::from(lrgb_for_yuvxyb);
827
828        let rgb_for_simd = Rgb::new(
829            source_data,
830            width,
831            height,
832            TransferCharacteristic::SRGB,
833            ColorPrimaries::BT709,
834        )
835        .unwrap();
836        let lrgb_for_simd = LinearRgb::try_from(rgb_for_simd).unwrap();
837        let xyb_simd = linear_rgb_to_xyb_simd(lrgb_for_simd);
838
839        let mut max_diff = [0.0f32; 3];
840        for (yuvxyb_pix, simd_pix) in xyb_yuvxyb.data().iter().zip(xyb_simd.data().iter()) {
841            for c in 0..3 {
842                let diff = (yuvxyb_pix[c] - simd_pix[c]).abs();
843                max_diff[c] = max_diff[c].max(diff);
844            }
845        }
846
847        assert!(
848            max_diff[0] < 1e-5 && max_diff[1] < 1e-5 && max_diff[2] < 1e-5,
849            "SIMD XYB differs from yuvxyb: max_diff={:?}",
850            max_diff
851        );
852    }
853}