Skip to main content

dssim_core/
dssim.rs

1#![allow(non_upper_case_globals)]
2#![allow(non_snake_case)]
3/*
4 * © 2011-2017 Kornel Lesiński. All rights reserved.
5 *
6 * This file is part of DSSIM.
7 *
8 * DSSIM is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU Affero General Public License
10 * as published by the Free Software Foundation, either version 3
11 * of the License, or (at your option) any later version.
12 *
13 * DSSIM is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU Affero General Public License for more details.
17 *
18 * You should have received a copy of the license along with DSSIM.
19 * If not, see <http://www.gnu.org/licenses/agpl.txt>.
20 */
21
22use crate::blur;
23use crate::image::*;
24use crate::linear::ToRGBAPLU;
25pub use crate::tolab::ToLABBitmap;
26pub use crate::val::Dssim as Val;
27use imgref::*;
28#[cfg(not(feature = "threads"))]
29use crate::lieon as rayon;
30use rayon::prelude::*;
31use rgb::{RGB, RGBA};
32use std::borrow::Borrow;
33use std::mem::MaybeUninit;
34use std::ops;
35use std::ops::Deref;
36use std::sync::Arc;
37
38trait Channable<T, I> {
39    fn img1_img2_blur(&self, modified: &Self, tmp: &mut [MaybeUninit<I>]) -> Vec<T>;
40}
41
42#[derive(Clone)]
43struct DssimChan<T> {
44    pub width: usize,
45    pub height: usize,
46    pub img: Option<ImgVec<T>>,
47    pub mu: Vec<T>,
48    pub img_sq_blur: Vec<T>,
49    pub is_chroma: bool,
50}
51
52/// Configuration for the comparison
53#[derive(Clone, Debug)]
54pub struct Dssim {
55    scale_weights: Vec<f64>,
56    save_maps_scales: u8,
57}
58
59#[derive(Clone)]
60struct DssimChanScale<T> {
61    chan: Vec<DssimChan<T>>,
62}
63
64/// Abstract wrapper for images. See [`Dssim::create_image()`]
65#[derive(Clone)]
66pub struct DssimImage<T> {
67    scale: Vec<DssimChanScale<T>>,
68}
69
70impl<T> DssimImage<T> {
71    #[inline]
72    #[must_use]
73    pub fn width(&self) -> usize {
74        self.scale[0].chan[0].width
75    }
76
77    #[inline]
78    #[must_use]
79    pub fn height(&self) -> usize {
80        self.scale[0].chan[0].height
81    }
82}
83
84// Weighed scales are inspired by the IW-SSIM, but details of the algorithm and weights are different
85const DEFAULT_WEIGHTS: [f64; 5] = [0.028, 0.197, 0.322, 0.298, 0.155];
86
87/// Detailed comparison result
88#[derive(Clone)]
89pub struct SsimMap {
90    /// SSIM scores
91    pub map: ImgVec<f32>,
92    /// Average SSIM (not DSSIM)
93    pub ssim: f64,
94}
95
96/// Create new context for a comparison
97#[must_use]
98pub fn new() -> Dssim {
99    Dssim::new()
100}
101
102impl DssimChan<f32> {
103    pub fn new(bitmap: ImgVec<f32>, is_chroma: bool) -> Self {
104        debug_assert!(bitmap.pixels().all(|i| i.is_finite() && i >= 0.0 && i <= 1.0));
105
106        Self {
107            width: bitmap.width(),
108            height: bitmap.height(),
109            mu: Vec::new(),
110            img: Some(bitmap),
111            img_sq_blur: Vec::new(),
112            is_chroma,
113        }
114    }
115}
116
117impl DssimChan<f32> {
118    fn preprocess(&mut self, tmp: &mut [MaybeUninit<f32>]) {
119        let width = self.width;
120        let height = self.height;
121        assert!(width > 0);
122        assert!(height > 0);
123
124        let img = self.img.as_mut().unwrap();
125        debug_assert_eq!(width * height, img.pixels().count());
126        debug_assert!(img.pixels().all(f32::is_finite));
127
128        if self.is_chroma {
129            blur::blur_in_place(img.as_mut(), tmp);
130        }
131        let (mu, ..) = blur::blur(img.as_ref(), tmp).into_contiguous_buf();
132        self.mu = mu;
133
134        // Fused squared-image blur: blur_mul(img, img) does a single H5*V5 pass
135        // over img*img, avoiding both the materialized i*i vector and the
136        // separate in-place blur over it.
137        self.img_sq_blur = blur::blur_mul(img.as_ref(), img.as_ref(), tmp);
138        debug_assert_eq!(self.img_sq_blur.len(), width * height);
139    }
140}
141
142impl Channable<f32, f32> for DssimChan<f32> {
143    fn img1_img2_blur(&self, modified: &Self, tmp32: &mut [MaybeUninit<f32>]) -> Vec<f32> {
144        let src = self.img.as_ref().unwrap();
145        let modified_img = modified.img.as_ref().unwrap();
146        // Fused multiply+blur: avoids materializing the product as a Vec.
147        blur::blur_mul(src.as_ref(), modified_img.as_ref(), tmp32)
148    }
149}
150
151impl Dssim {
152    /// Create new context for comparisons
153    #[must_use]
154    pub fn new() -> Self {
155        Self {
156            scale_weights: DEFAULT_WEIGHTS[..].to_owned(),
157            save_maps_scales: 0,
158        }
159    }
160
161    /// Set how many scales will be used, and weights of each scale
162    pub fn set_scales(&mut self, scales: &[f64]) {
163        self.scale_weights = scales.to_vec();
164    }
165
166    /// Set how many scales will be kept for saving
167    pub fn set_save_ssim_maps(&mut self, num_scales: u8) {
168        self.save_maps_scales = num_scales;
169    }
170
171    /// Create image from an array of RGBA pixels (sRGB, non-premultiplied, alpha last).
172    ///
173    /// If you have a slice of `u8`, then see `rgb` crate's `as_rgba()`.
174    #[must_use]
175    pub fn create_image_rgba(&self, bitmap: &[RGBA<u8>], width: usize, height: usize) -> Option<DssimImage<f32>> {
176        if width * height < bitmap.len() {
177            return None;
178        }
179        let img = ImgVec::new(bitmap.to_rgbaplu(), width, height);
180        self.create_image(&img)
181    }
182
183    /// Create image from an array of packed RGB pixels (sRGB).
184    ///
185    /// If you have a slice of `u8`, then see `rgb` crate's `as_rgb()`.
186    #[must_use]
187    pub fn create_image_rgb(&self, bitmap: &[RGB<u8>], width: usize, height: usize) -> Option<DssimImage<f32>> {
188        if width * height < bitmap.len() {
189            return None;
190        }
191        let img = ImgVec::new(bitmap.to_rgblu(), width, height);
192        self.create_image(&img)
193    }
194
195    /// The input image is defined using the `imgref` crate, and the pixel type can be:
196    ///
197    /// * `ImgVec<RGBAPLU>` — RGBA premultiplied alpha, linear, float scaled to 0..1
198    /// * `ImgVec<RGBLU>` — RGBA linear, float scaled to 0..1
199    /// * `ImgVec<f32>` — linear light grayscale, float scaled to 0..1
200    ///
201    /// And there's [`ToRGBAPLU::to_rgbaplu()`][crate::ToRGBAPLU::to_rgbaplu()] trait to convert the input pixels from
202    /// `[RGBA<u8>]`, `[RGBA<u16>]`, `[RGB<u8>]`, or `RGB<u16>`. See `lib.rs` for example how it's done.
203    ///
204    /// You can implement `ToLABBitmap` and `Downsample` traits on your own image type.
205    pub fn create_image<InBitmap, OutBitmap>(&self, src_img: &InBitmap) -> Option<DssimImage<f32>>
206    where
207        InBitmap: ToLABBitmap + Send + Sync + Downsample<Output = OutBitmap>,
208        OutBitmap: ToLABBitmap + Send + Sync + Downsample<Output = OutBitmap>,
209    {
210        let num_scales = self.scale_weights.len();
211        let mut scale = Vec::with_capacity(num_scales);
212        Self::make_scales_recursive(num_scales, MaybeArc::Borrowed(src_img), &mut scale);
213        scale.reverse(); // depth-first made smallest scales first
214
215        Some(DssimImage { scale })
216    }
217
218    #[inline(never)]
219    fn make_scales_recursive<InBitmap, OutBitmap>(scales_left: usize, image: MaybeArc<'_, InBitmap>, scales: &mut Vec<DssimChanScale<f32>>)
220    where
221        InBitmap: ToLABBitmap + Send + Sync + Downsample<Output = OutBitmap>,
222        OutBitmap: ToLABBitmap + Send + Sync + Downsample<Output = OutBitmap>,
223    {
224        // Run to_lab and next downsampling in parallel
225        let (chan, _) = rayon::join({
226            let image = image.clone();
227            move || {
228                let lab = image.to_lab();
229                drop(image); // Free larger RGB image ASAP
230                DssimChanScale {
231                    chan: lab.into_par_iter().with_max_len(1).enumerate().map(|(n,l)| {
232                        let w = l.width();
233                        let h = l.height();
234                        let mut ch = DssimChan::new(l, n > 0);
235
236                        let pixels = w * h;
237                        let mut tmp = Vec::with_capacity(pixels);
238                        ch.preprocess(&mut tmp.spare_capacity_mut()[..pixels]);
239                        ch
240                    }).collect(),
241                }
242            }
243        }, {
244            let scales = &mut *scales;
245            move || {
246                if scales_left > 0 {
247                    let down = image.downsample();
248                    drop(image);
249                    if let Some(downsampled) = down {
250                        Self::make_scales_recursive(scales_left - 1, MaybeArc::Owned(Arc::new(downsampled)), scales);
251                    }
252                }
253            }
254        });
255        scales.push(chan);
256    }
257
258    /// Compare original with another image. See `create_image`
259    ///
260    /// The `SsimMap`s are returned only if you've enabled them first.
261    ///
262    /// `Val` is a fancy wrapper for `f64`
263    pub fn compare<M: Borrow<DssimImage<f32>>>(&self, original_image: &DssimImage<f32>, modified_image: M) -> (Val, Vec<SsimMap>) {
264        self.compare_inner(original_image, modified_image.borrow())
265    }
266
267    #[inline(never)]
268    fn compare_inner(&self, original_image: &DssimImage<f32>, modified_image: &DssimImage<f32>) -> (Val, Vec<SsimMap>) {
269        let scaled_images_iter = modified_image.scale.iter().zip(original_image.scale.iter());
270        let combined_iter = self.scale_weights.iter().copied().zip(scaled_images_iter).enumerate();
271
272        let res: Vec<_> = combined_iter.par_bridge().map(|(n, (weight, (modified_image_scale, original_image_scale)))| {
273            let scale_width = original_image_scale.chan[0].width;
274            let scale_height = original_image_scale.chan[0].height;
275            let pixels = scale_width * scale_height;
276
277            let ssim_map = match original_image_scale.chan.len() {
278                3 => {
279                    // Compute the per-channel cross-blur (img1·img2 then blur) for L, a, b
280                    // in parallel — three independent blurs over disjoint memory.
281                    // Each channel gets its own tmp buffer.
282                    let img1_img2_blur: Vec<Vec<f32>> = (0..3usize).into_par_iter().map(|c| {
283                        let mut tmp_buf: Vec<f32> = Vec::with_capacity(pixels);
284                        let tmp = &mut tmp_buf.spare_capacity_mut()[..pixels];
285                        original_image_scale.chan[c]
286                            .img1_img2_blur(&modified_image_scale.chan[c], tmp)
287                    }).collect();
288                    Self::compare_scale_3ch(original_image_scale, modified_image_scale, &img1_img2_blur)
289                },
290                1 => {
291                    let mut tmp_buf: Vec<f32> = Vec::with_capacity(pixels);
292                    let tmp = &mut tmp_buf.spare_capacity_mut()[..pixels];
293                    let img1_img2_blur = original_image_scale.chan[0].img1_img2_blur(&modified_image_scale.chan[0], tmp);
294                    Self::compare_scale(&original_image_scale.chan[0], &modified_image_scale.chan[0], &img1_img2_blur)
295                },
296                _ => panic!(),
297            };
298
299            let sum = ssim_map.pixels().fold(0., |sum, i| sum + f64::from(i));
300            let len = (ssim_map.width()*ssim_map.height()) as f64;
301            let avg = (sum / len).max(0.0).powf((0.5_f64).powf(n as f64));
302            let score = 1.0 - (ssim_map.pixels().fold(0., |sum, i| sum + (avg - f64::from(i)).abs()) / len);
303
304            let map = if self.save_maps_scales as usize > n {
305                Some(SsimMap {
306                    map: ssim_map,
307                    ssim: score,
308                })
309            } else {
310                None
311            };
312            (score, weight, map)
313        }).collect();
314
315        let mut ssim_sum = 0.0;
316        let mut weight_sum = 0.0;
317        let mut ssim_maps = Vec::new();
318        for (score, weight, map) in res {
319            ssim_sum = score.mul_add(weight, ssim_sum);
320            weight_sum += weight;
321            if let Some(m) = map {
322                ssim_maps.push(m);
323            }
324        }
325
326        (to_dssim(ssim_sum / weight_sum).into(), ssim_maps)
327    }
328
329    /// 3-channel SSIM combine, scalar but unrolled across L/a/b. Reads the three
330    /// channels directly from the per-channel `mu` and `img_sq_blur` Vecs and the
331    /// three `img1_img2_blur` Vecs computed earlier in parallel — no LAB struct
332    /// interleaving, no zip-iterator overhead, and per-channel slices stay
333    /// cache-friendly. Algebraically identical to `compare_scale::<LAB>`.
334    #[inline(never)]
335    fn compare_scale_3ch(
336        original: &DssimChanScale<f32>,
337        modified: &DssimChanScale<f32>,
338        img1_img2_blur: &[Vec<f32>],
339    ) -> ImgVec<f32> {
340        let width = original.chan[0].width;
341        let height = original.chan[0].height;
342        let pixels = width * height;
343
344        let (o0, o1, o2) = (&original.chan[0], &original.chan[1], &original.chan[2]);
345        let (m0, m1, m2) = (&modified.chan[0], &modified.chan[1], &modified.chan[2]);
346
347        let o0_mu = &o0.mu[..pixels];
348        let o1_mu = &o1.mu[..pixels];
349        let o2_mu = &o2.mu[..pixels];
350        let m0_mu = &m0.mu[..pixels];
351        let m1_mu = &m1.mu[..pixels];
352        let m2_mu = &m2.mu[..pixels];
353        let o0_sq = &o0.img_sq_blur[..pixels];
354        let o1_sq = &o1.img_sq_blur[..pixels];
355        let o2_sq = &o2.img_sq_blur[..pixels];
356        let m0_sq = &m0.img_sq_blur[..pixels];
357        let m1_sq = &m1.img_sq_blur[..pixels];
358        let m2_sq = &m2.img_sq_blur[..pixels];
359        let i12_0 = &img1_img2_blur[0][..pixels];
360        let i12_1 = &img1_img2_blur[1][..pixels];
361        let i12_2 = &img1_img2_blur[2][..pixels];
362
363        let c1: f32 = 0.01 * 0.01;
364        let c2: f32 = 0.03 * 0.03;
365        let inv3: f32 = 1.0 / 3.0;
366
367        let map_out: Vec<f32> = (0..pixels).into_par_iter().with_min_len(1 << 10).map(|i| {
368            let mu1_0 = o0_mu[i]; let mu2_0 = m0_mu[i];
369            let mu1_1 = o1_mu[i]; let mu2_1 = m1_mu[i];
370            let mu1_2 = o2_mu[i]; let mu2_2 = m2_mu[i];
371
372            let mu1mu1_0 = mu1_0 * mu1_0;
373            let mu1mu1_1 = mu1_1 * mu1_1;
374            let mu1mu1_2 = mu1_2 * mu1_2;
375            let mu2mu2_0 = mu2_0 * mu2_0;
376            let mu2mu2_1 = mu2_1 * mu2_1;
377            let mu2mu2_2 = mu2_2 * mu2_2;
378            let mu1mu2_0 = mu1_0 * mu2_0;
379            let mu1mu2_1 = mu1_1 * mu2_1;
380            let mu1mu2_2 = mu1_2 * mu2_2;
381
382            let mu1_sq  = (mu1mu1_0 + mu1mu1_1 + mu1mu1_2) * inv3;
383            let mu2_sq  = (mu2mu2_0 + mu2mu2_1 + mu2mu2_2) * inv3;
384            let mu1_mu2 = (mu1mu2_0 + mu1mu2_1 + mu1mu2_2) * inv3;
385
386            let sigma1_sq = ((o0_sq[i] - mu1mu1_0) + (o1_sq[i] - mu1mu1_1) + (o2_sq[i] - mu1mu1_2)) * inv3;
387            let sigma2_sq = ((m0_sq[i] - mu2mu2_0) + (m1_sq[i] - mu2mu2_1) + (m2_sq[i] - mu2mu2_2)) * inv3;
388            let sigma12  = ((i12_0[i] - mu1mu2_0) + (i12_1[i] - mu1mu2_1) + (i12_2[i] - mu1mu2_2)) * inv3;
389
390            2.0f32.mul_add(mu1_mu2, c1) * 2.0f32.mul_add(sigma12, c2)
391                / ((mu1_sq + mu2_sq + c1) * (sigma1_sq + sigma2_sq + c2))
392        }).collect();
393
394        ImgVec::new(map_out, width, height)
395    }
396
397    #[inline(never)]
398    fn compare_scale<L>(original: &DssimChan<L>, modified: &DssimChan<L>, img1_img2_blur: &[L]) -> ImgVec<f32>
399    where
400        L: Send + Sync + Clone + Copy + ops::Mul<Output = L> + ops::Sub<Output = L> + 'static,
401        f32: From<L>,
402    {
403        assert_eq!(original.width, modified.width);
404        assert_eq!(original.height, modified.height);
405
406        let width = original.width;
407        let height = original.height;
408
409        let c1 = 0.01 * 0.01;
410        let c2 = 0.03 * 0.03;
411
412        debug_assert_eq!(original.mu.len(), modified.mu.len());
413        debug_assert_eq!(original.img_sq_blur.len(), modified.img_sq_blur.len());
414        debug_assert_eq!(img1_img2_blur.len(), original.mu.len());
415        debug_assert_eq!(img1_img2_blur.len(), original.img_sq_blur.len());
416
417        let mu_iter = original.mu.as_slice().par_iter().with_min_len(1<<10).cloned().zip_eq(modified.mu.as_slice().par_iter().with_min_len(1<<10).cloned());
418        let sq_iter = original.img_sq_blur.as_slice().par_iter().with_min_len(1<<10).cloned().zip_eq(modified.img_sq_blur.as_slice().par_iter().with_min_len(1<<10).cloned());
419        let map_out = img1_img2_blur.par_iter().with_min_len(1<<10).cloned().zip_eq(mu_iter).zip_eq(sq_iter)
420        .map(|((img1_img2_blur, (mu1, mu2)), (img1_sq_blur, img2_sq_blur))| {
421            let mu1mu1 = mu1 * mu1;
422            let mu1mu2 = mu1 * mu2;
423            let mu2mu2 = mu2 * mu2;
424            let mu1_sq: f32 = mu1mu1.into();
425            let mu2_sq: f32 = mu2mu2.into();
426            let mu1_mu2: f32 = mu1mu2.into();
427            let sigma1_sq: f32 = (img1_sq_blur - mu1mu1).into();
428            let sigma2_sq: f32 = (img2_sq_blur - mu2mu2).into();
429            let sigma12: f32 = (img1_img2_blur - mu1mu2).into();
430
431            2.0f32.mul_add(mu1_mu2, c1) * 2.0f32.mul_add(sigma12, c2) /
432                       ((mu1_sq + mu2_sq + c1) * (sigma1_sq + sigma2_sq + c2))
433        }).collect();
434
435        ImgVec::new(map_out, width, height)
436    }
437}
438
439fn to_dssim(ssim: f64) -> f64 {
440    1.0 / ssim.max(f64::EPSILON) - 1.0
441}
442
443#[test]
444fn png_compare() {
445    use crate::linear::*;
446    use imgref::*;
447
448    let d = new();
449    let file1 = lodepng::decode32_file("../tests/test1-sm.png").unwrap();
450    let file2 = lodepng::decode32_file("../tests/test2-sm.png").unwrap();
451
452    let buf1 = &file1.buffer.to_rgbaplu()[..];
453    let buf2 = &file2.buffer.to_rgbaplu()[..];
454    let img1 = d.create_image(&Img::new(buf1, file1.width, file1.height)).unwrap();
455    let img2 = d.create_image(&Img::new(buf2, file2.width, file2.height)).unwrap();
456
457    let (res, _) = d.compare(&img1, img2);
458    assert!((0.001 - res).abs() < 0.0005, "res is {res}");
459
460    let img1b = d.create_image(&Img::new(buf1, file1.width, file1.height)).unwrap();
461    let (res, _) = d.compare(&img1, img1b);
462
463    assert!(0.000000000000001 > res);
464    assert!(res < 0.000000000000001);
465    assert_eq!(res, res);
466
467    let sub_img1 = d.create_image(&Img::new(buf1, file1.width, file1.height).sub_image(2,3,44,33)).unwrap();
468    let sub_img2 = d.create_image(&Img::new(buf2, file2.width, file2.height).sub_image(17,9,44,33)).unwrap();
469    // Test passing second image directly
470    let (res, _) = d.compare(&sub_img1, sub_img2);
471    assert!(res > 0.1);
472
473    let sub_img1 = d.create_image(&Img::new(buf1, file1.width, file1.height).sub_image(22,8,61,40)).unwrap();
474    let sub_img2 = d.create_image(&Img::new(buf2, file2.width, file2.height).sub_image(22,8,61,40)).unwrap();
475    // Test passing second image as reference
476    let (res, _) = d.compare(&sub_img1, sub_img2);
477    assert!(res < 0.01);
478}
479
480/// Locked-value regression tests for the bundled `test1-sm.png` /
481/// `test2-sm.png` fixture pair. Each scenario asserts the DSSIM value
482/// produced at this branch's HEAD within `5×10⁻⁶` absolute tolerance.
483///
484/// The new fused 5-tap blur (with H1·H1-derived edge weights) is
485/// bit-equivalent to the upstream double-3×3 form modulo FP reordering,
486/// so the locked values match upstream `kornelski/dssim:main` to within
487/// ~10⁻⁷ on every scenario. The 5×10⁻⁶ bound covers:
488///   - upstream-vs-this-branch FP reordering drift (≤ 1.5×10⁻⁷),
489///   - SIMD-path drift from PR2's `tolab` SIMD layer (≤ 5.6×10⁻⁷),
490///   - SIMD ↔ scalar fallback divergence in PR2 (≤ 5.6×10⁻⁷),
491/// with ≈10× margin. A real correctness bug — matrix typo, dropped scale
492/// weight, sigma sign-flip, edge-handling regression — moves SSIM by
493/// ≥10⁻³, so this bound catches everything that matters while admitting
494/// only legitimate last-bit FP reordering. Also 2× tighter than the
495/// existing `image_gray` test's 1×10⁻⁵.
496///
497/// Identity (image vs itself) is locked to exactly zero — mathematical fact,
498/// not numerical.
499#[test]
500fn ssim_locked_values() {
501    use crate::linear::*;
502    use imgref::*;
503
504    /// 5×10⁻⁶ absolute tolerance. See module-level comment for derivation.
505    const ABS_TOL: f64 = 5e-6;
506
507    fn approx_eq(name: &str, got: f64, expected: f64) {
508        let diff = (got - expected).abs();
509        assert!(
510            diff <= ABS_TOL,
511            "{name}: got {got}, expected {expected} (abs diff={diff:.3e}, allowed={ABS_TOL:.0e})",
512        );
513    }
514
515    let d = new();
516    let file1 = lodepng::decode32_file("../tests/test1-sm.png").unwrap();
517    let file2 = lodepng::decode32_file("../tests/test2-sm.png").unwrap();
518    let buf1 = &file1.buffer.to_rgbaplu()[..];
519    let buf2 = &file2.buffer.to_rgbaplu()[..];
520    let img1 = || Img::new(buf1, file1.width, file1.height);
521    let img2 = || Img::new(buf2, file2.width, file2.height);
522
523    // 1. Full-image test1 vs test2 — headline DSSIM for this fixture pair.
524    //    Upstream produces 0.0009482581 for the same input; this branch's
525    //    1.34×10⁻⁷ drift is FMA / 3-channel-combine reordering only.
526    let a = d.create_image(&img1()).unwrap();
527    let b = d.create_image(&img2()).unwrap();
528    let (got, _) = d.compare(&a, b);
529    approx_eq("full test1 vs test2", f64::from(got), 0.0009483923725199794);
530
531    // 2. Identity: image vs itself must be exactly zero. Mathematical fact —
532    //    any drift here means a real bug, not numerical noise.
533    let a2 = d.create_image(&img1()).unwrap();
534    let b2 = d.create_image(&img1()).unwrap();
535    let (got, _) = d.compare(&a2, b2);
536    assert_eq!(f64::from(got), 0.0, "identity must be exactly 0, got {got}");
537
538    // 3. Sub-image regions of differing offsets — exercises the strided path
539    //    (sub_image returns a non-tightly-packed view).
540    let s1 = d.create_image(&img1().sub_image(2, 3, 44, 33)).unwrap();
541    let s2 = d.create_image(&img2().sub_image(17, 9, 44, 33)).unwrap();
542    let (got, _) = d.compare(&s1, s2);
543    approx_eq("sub [2,3,44x33] vs [17,9,44x33]", f64::from(got), 0.10810340934514495);
544
545    // 4. Sub-image regions with same offset — typical aligned-crop case.
546    let s1 = d.create_image(&img1().sub_image(22, 8, 61, 40)).unwrap();
547    let s2 = d.create_image(&img2().sub_image(22, 8, 61, 40)).unwrap();
548    let (got, _) = d.compare(&s1, s2);
549    approx_eq("sub [22,8,61x40] aligned", f64::from(got), 0.001675780079775091);
550}
551
552enum MaybeArc<'a, T> {
553    Owned(Arc<T>),
554    Borrowed(&'a T),
555}
556
557impl<T> Clone for MaybeArc<'_, T> {
558    fn clone(&self) -> Self {
559        match self {
560            Self::Owned(t) => Self::Owned(t.clone()),
561            Self::Borrowed(t) => Self::Borrowed(t),
562        }
563    }
564}
565
566impl<T> Deref for MaybeArc<'_, T> {
567    type Target = T;
568
569    fn deref(&self) -> &Self::Target {
570        match self {
571            Self::Owned(t) => t,
572            Self::Borrowed(t) => t,
573        }
574    }
575}
576
577#[test]
578fn poison() {
579    let a = RGBAPLU::new(1.,1.,1.,1.);
580    let b = RGBAPLU::new(0.,0.,0.,0.);
581    let n = 1./0.;
582    let n = RGBAPLU::new(n,n,n,n);
583    let buf = vec![
584      b,a,a,b,n,n,
585      a,b,b,a,n,n,
586      b,a,a,b,n,
587    ];
588    let img = ImgVec::new_stride(buf, 4, 3, 6);
589    assert!(img.pixels().all(|p| p.r.is_finite() && p.a.is_finite()));
590    assert!(img.as_ref().pixels().all(|p| p.g.is_finite() && p.b.is_finite()));
591
592    let d = new();
593    let sub_img1 = d.create_image(&img.as_ref()).unwrap();
594    let sub_img2 = d.create_image(&img.as_ref()).unwrap();
595    let (res, _) = d.compare(&sub_img1, sub_img2);
596    assert!(res < 0.000001);
597}