Skip to main content

fast_ssim2/
precompute.rs

1//! Precomputed reference data for fast repeated SSIMULACRA2 comparisons.
2//!
3//! When comparing multiple distorted images against the same reference image,
4//! you can precompute the reference data once and reuse it for ~2x speedup.
5//!
6//! # Example
7//!
8//! ```
9//! use fast_ssim2::Ssimulacra2Reference;
10//! use yuvxyb::{Rgb, TransferCharacteristic, ColorPrimaries};
11//!
12//! // Load reference image
13//! use std::num::NonZeroUsize;
14//! let reference_rgb = vec![[1.0f32, 1.0, 1.0]; 512 * 512];
15//! let reference = Rgb::new(
16//!     reference_rgb,
17//!     NonZeroUsize::new(512).unwrap(),
18//!     NonZeroUsize::new(512).unwrap(),
19//!     TransferCharacteristic::SRGB,
20//!     ColorPrimaries::BT709,
21//! ).unwrap();
22//!
23//! // Precompute reference data once
24//! let precomputed = Ssimulacra2Reference::new(reference).unwrap();
25//!
26//! // Compare against a distorted image
27//! let distorted_rgb = vec![[0.9f32, 0.95, 1.05]; 512 * 512];
28//! let distorted = Rgb::new(
29//!     distorted_rgb,
30//!     NonZeroUsize::new(512).unwrap(),
31//!     NonZeroUsize::new(512).unwrap(),
32//!     TransferCharacteristic::SRGB,
33//!     ColorPrimaries::BT709,
34//! ).unwrap();
35//! let score = precomputed.compare(distorted).unwrap();
36//! println!("SSIMULACRA2 score: {}", score);
37//! ```
38
39use 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/// Precomputed reference data for a single scale.
48#[derive(Clone, Debug)]
49struct ScaleData {
50    /// Planar XYB representation of reference image
51    img1_planar: [Vec<f32>; 3],
52    /// blur(img1) - mean of reference
53    mu1: [Vec<f32>; 3],
54    /// blur(img1 * img1) - variance component of reference
55    sigma1_sq: [Vec<f32>; 3],
56}
57
58/// Precomputed SSIMULACRA2 reference data for fast repeated comparisons.
59///
60/// This struct stores precomputed data for the reference image at all scales,
61/// allowing you to quickly compare multiple distorted images against the same
62/// reference without recomputing the reference-side data each time.
63///
64/// For simulated annealing or other optimization where you compare many variations
65/// against the same source, this provides approximately 2x speedup.
66#[derive(Clone, Debug)]
67pub struct Ssimulacra2Reference {
68    scales: Vec<ScaleData>,
69    original_width: usize,
70    original_height: usize,
71}
72
73impl Ssimulacra2Reference {
74    /// Precompute reference data for the given source image.
75    ///
76    /// Supports:
77    /// - `imgref` types (with the `imgref` feature): `ImgRef<[u8; 3]>`, `ImgRef<[f32; 3]>`, etc.
78    /// - `yuvxyb` types: `Rgb`, `LinearRgb`
79    /// - Custom types implementing [`ToLinearRgb`]
80    ///
81    /// # Errors
82    /// - If the image is smaller than 8x8 pixels
83    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            // Precompute mu1 = blur(img1)
124            let mu1 = blur.blur(&img1_planar);
125
126            // Precompute sigma1_sq = blur(img1 * img1)
127            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    /// Compare a distorted image against the precomputed reference.
145    ///
146    /// This is approximately 2x faster than calling `compute_ssimulacra2`
147    /// because it only needs to process the distorted image and compute cross-terms.
148    ///
149    /// # Errors
150    /// - If the distorted image dimensions don't match the reference
151    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            // Compute mu2 = blur(img2)
191            let mu2 = blur.blur(&img2_planar);
192
193            // Compute sigma2_sq = blur(img2 * img2)
194            image_multiply(&img2_planar, &img2_planar, &mut mul, SimdImpl::default());
195            let sigma2_sq = blur.blur(&mul);
196
197            // Compute sigma12 = blur(img1 * img2) - cross-term
198            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            // Use precomputed mu1 and sigma1_sq from reference
207            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    /// Get the width of the original reference image.
238    #[must_use]
239    pub fn width(&self) -> usize {
240        self.original_width
241    }
242
243    /// Get the height of the original reference image.
244    #[must_use]
245    pub fn height(&self) -> usize {
246        self.original_height
247    }
248
249    /// Get the number of scales that were precomputed.
250    #[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        // Create a simple test image
266        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        // Compute using full method
302        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        // Compute using precomputed reference
313        let precomputed = Ssimulacra2Reference::new(source).unwrap();
314        let precomputed_score = precomputed.compare(distorted).unwrap();
315
316        // Scores should match exactly (both use same SIMD XYB path)
317        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]; // Wrong size
329
330        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}