Skip to main content

wlr_capture/
diff.rs

1//! Frame-difference metric for change detection.
2//!
3//! A dependency-free helper for the change monitor: how much did two captured
4//! frames differ? Pixels are compared on RGB only (captures force alpha to 255), and
5//! a per-pixel `tolerance` absorbs codec/dither noise so a blinking cursor or a
6//! one-bit jitter doesn't read as a change.
7
8use crate::wl::CapturedImage;
9
10/// A sensible default per-channel tolerance (out of 255): below this, two pixels
11/// are considered equal.
12pub const DEFAULT_TOLERANCE: u8 = 8;
13
14/// Fraction (0.0–1.0) of pixels that differ between `a` and `b` by more than
15/// `tolerance` on any RGB channel. Differing dimensions count as fully changed
16/// (1.0); two empty frames are unchanged (0.0).
17pub fn changed_fraction(a: &CapturedImage, b: &CapturedImage, tolerance: u8) -> f64 {
18    if a.width != b.width || a.height != b.height {
19        return 1.0;
20    }
21    let total = (a.width as usize) * (a.height as usize);
22    if total == 0 {
23        return 0.0;
24    }
25    let changed = a
26        .rgba
27        .chunks_exact(4)
28        .zip(b.rgba.chunks_exact(4))
29        .filter(|(pa, pb)| {
30            pa[0].abs_diff(pb[0]) > tolerance
31                || pa[1].abs_diff(pb[1]) > tolerance
32                || pa[2].abs_diff(pb[2]) > tolerance
33        })
34        .count();
35    changed as f64 / total as f64
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    fn img(w: u32, h: u32, fill: [u8; 4]) -> CapturedImage {
43        CapturedImage {
44            width: w,
45            height: h,
46            rgba: fill.repeat((w * h) as usize),
47        }
48    }
49
50    #[test]
51    fn identical_is_zero() {
52        let a = img(4, 4, [10, 20, 30, 255]);
53        let b = img(4, 4, [10, 20, 30, 255]);
54        assert_eq!(changed_fraction(&a, &b, DEFAULT_TOLERANCE), 0.0);
55    }
56
57    #[test]
58    fn within_tolerance_is_zero() {
59        let a = img(2, 2, [100, 100, 100, 255]);
60        let b = img(2, 2, [105, 100, 100, 255]); // +5 < tolerance 8
61        assert_eq!(changed_fraction(&a, &b, DEFAULT_TOLERANCE), 0.0);
62    }
63
64    #[test]
65    fn counts_changed_pixels() {
66        let mut a = img(2, 1, [0, 0, 0, 255]);
67        let b = img(2, 1, [0, 0, 0, 255]);
68        a.rgba[0] = 200; // one of two pixels differs well past tolerance
69        assert_eq!(changed_fraction(&a, &b, DEFAULT_TOLERANCE), 0.5);
70    }
71
72    #[test]
73    fn alpha_is_ignored() {
74        let a = img(1, 1, [0, 0, 0, 255]);
75        let b = img(1, 1, [0, 0, 0, 0]); // only alpha differs
76        assert_eq!(changed_fraction(&a, &b, DEFAULT_TOLERANCE), 0.0);
77    }
78
79    #[test]
80    fn size_mismatch_is_full() {
81        let a = img(2, 2, [0, 0, 0, 255]);
82        let b = img(3, 2, [0, 0, 0, 255]);
83        assert_eq!(changed_fraction(&a, &b, DEFAULT_TOLERANCE), 1.0);
84    }
85}