rustial_engine/
image_compare.rs1pub fn compute_rmse(a: &[u8], b: &[u8]) -> f64 {
23 assert_eq!(a.len(), b.len(), "image buffers must have the same length");
24 assert!(!a.is_empty(), "image buffers must not be empty");
25
26 let sum_sq: f64 = a
27 .iter()
28 .zip(b.iter())
29 .map(|(&va, &vb)| {
30 let diff = va as f64 - vb as f64;
31 diff * diff
32 })
33 .sum();
34
35 (sum_sq / a.len() as f64).sqrt()
36}
37
38pub fn count_differing_pixels(a: &[u8], b: &[u8], threshold: u8) -> usize {
47 assert_eq!(a.len(), b.len(), "image buffers must have the same length");
48 assert_eq!(
49 a.len() % 4,
50 0,
51 "image buffer length must be a multiple of 4"
52 );
53
54 a.chunks_exact(4)
55 .zip(b.chunks_exact(4))
56 .filter(|(pa, pb)| {
57 pa.iter()
58 .zip(pb.iter())
59 .any(|(&ca, &cb)| (ca as i16 - cb as i16).unsigned_abs() > threshold as u16)
60 })
61 .count()
62}
63
64pub fn differing_pixel_fraction(a: &[u8], b: &[u8], threshold: u8) -> f64 {
68 let total_pixels = a.len() / 4;
69 if total_pixels == 0 {
70 return 0.0;
71 }
72 count_differing_pixels(a, b, threshold) as f64 / total_pixels as f64
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn identical_images_have_zero_rmse() {
81 let img = vec![128u8; 64 * 4];
82 assert!((compute_rmse(&img, &img) - 0.0).abs() < f64::EPSILON);
83 }
84
85 #[test]
86 fn different_images_have_positive_rmse() {
87 let a = vec![0u8; 64 * 4];
88 let b = vec![255u8; 64 * 4];
89 let rmse = compute_rmse(&a, &b);
90 assert!(rmse > 200.0, "rmse was {rmse}");
91 }
92
93 #[test]
94 fn count_differing_pixels_exact_threshold() {
95 let a = vec![100u8; 4 * 4];
96 let mut b = a.clone();
97 b[0] = 110;
99 assert_eq!(count_differing_pixels(&a, &b, 9), 1);
100 assert_eq!(count_differing_pixels(&a, &b, 10), 0);
101 }
102
103 #[test]
104 fn differing_fraction_returns_correct_ratio() {
105 let a = vec![0u8; 8 * 4]; let mut b = a.clone();
107 b[0] = 255; b[4] = 255; let frac = differing_pixel_fraction(&a, &b, 0);
110 assert!((frac - 0.25).abs() < 1e-9, "frac was {frac}");
111 }
112}