cranpose_render_common/
image_compare.rs1use cranpose_ui_graphics::Rect;
2
3#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct PixelDifference {
6 pub x: u32,
8 pub y: u32,
10 pub lhs: [u8; 4],
12 pub rhs: [u8; 4],
14 pub difference: u32,
16}
17
18#[derive(Clone, Debug, Default, PartialEq, Eq)]
20pub struct ImageDifferenceStats {
21 pub differing_pixels: u32,
23 pub max_difference: u32,
25 pub first_difference: Option<PixelDifference>,
27}
28
29pub fn sample_pixel(pixels: &[u8], width: u32, x: u32, y: u32) -> [u8; 4] {
32 let idx = ((y * width + x) * 4) as usize;
33 [
34 pixels[idx],
35 pixels[idx + 1],
36 pixels[idx + 2],
37 pixels[idx + 3],
38 ]
39}
40
41pub fn pixel_difference(lhs: [u8; 4], rhs: [u8; 4]) -> u32 {
43 lhs.into_iter()
44 .zip(rhs)
45 .map(|(left, right)| left.abs_diff(right) as u32)
46 .sum()
47}
48
49pub fn normalize_rgba_region(
53 pixels: &[u8],
54 width: u32,
55 height: u32,
56 rect: Rect,
57 output_width: u32,
58 output_height: u32,
59) -> Vec<u8> {
60 assert!(output_width > 0, "normalized output_width must be positive");
61 assert!(
62 output_height > 0,
63 "normalized output_height must be positive"
64 );
65
66 let mut out = Vec::with_capacity((output_width * output_height * 4) as usize);
67 for y in 0..output_height {
68 for x in 0..output_width {
69 let sample_x = rect.x + ((x as f32 + 0.5) * rect.width / output_width as f32);
70 let sample_y = rect.y + ((y as f32 + 0.5) * rect.height / output_height as f32);
71 out.extend_from_slice(&sample_pixel_bilinear(
72 pixels, width, height, sample_x, sample_y,
73 ));
74 }
75 }
76 out
77}
78
79pub fn image_difference_stats(
82 lhs: &[u8],
83 rhs: &[u8],
84 width: u32,
85 height: u32,
86 tolerance: u32,
87) -> ImageDifferenceStats {
88 assert_eq!(
89 lhs.len(),
90 rhs.len(),
91 "image_difference_stats requires equal buffer lengths"
92 );
93
94 let mut differing_pixels = 0;
95 let mut max_difference = 0;
96 let mut first_difference = None;
97 for y in 0..height {
98 for x in 0..width {
99 let index = ((y * width + x) * 4) as usize;
100 let lhs_pixel = [lhs[index], lhs[index + 1], lhs[index + 2], lhs[index + 3]];
101 let rhs_pixel = [rhs[index], rhs[index + 1], rhs[index + 2], rhs[index + 3]];
102 let difference = pixel_difference(lhs_pixel, rhs_pixel);
103 if difference > tolerance {
104 differing_pixels += 1;
105 max_difference = max_difference.max(difference);
106 if first_difference.is_none() {
107 first_difference = Some(PixelDifference {
108 x,
109 y,
110 lhs: lhs_pixel,
111 rhs: rhs_pixel,
112 difference,
113 });
114 }
115 }
116 }
117 }
118
119 ImageDifferenceStats {
120 differing_pixels,
121 max_difference,
122 first_difference,
123 }
124}
125
126fn sample_pixel_bilinear(pixels: &[u8], width: u32, height: u32, x: f32, y: f32) -> [u8; 4] {
127 let max_x = width.saturating_sub(1) as f32;
128 let max_y = height.saturating_sub(1) as f32;
129 let source_x = (x - 0.5).clamp(0.0, max_x);
130 let source_y = (y - 0.5).clamp(0.0, max_y);
131 let x0 = source_x.floor() as u32;
132 let y0 = source_y.floor() as u32;
133 let x1 = (x0 + 1).min(width.saturating_sub(1));
134 let y1 = (y0 + 1).min(height.saturating_sub(1));
135 let tx = source_x - x0 as f32;
136 let ty = source_y - y0 as f32;
137 let top_left = sample_pixel(pixels, width, x0, y0);
138 let top_right = sample_pixel(pixels, width, x1, y0);
139 let bottom_left = sample_pixel(pixels, width, x0, y1);
140 let bottom_right = sample_pixel(pixels, width, x1, y1);
141
142 let lerp_channel = |index: usize| {
143 let top = top_left[index] as f32 * (1.0 - tx) + top_right[index] as f32 * tx;
144 let bottom = bottom_left[index] as f32 * (1.0 - tx) + bottom_right[index] as f32 * tx;
145 (top * (1.0 - ty) + bottom * ty).round() as u8
146 };
147
148 [
149 lerp_channel(0),
150 lerp_channel(1),
151 lerp_channel(2),
152 lerp_channel(3),
153 ]
154}
155
156#[cfg(test)]
157#[path = "tests/image_compare_tests.rs"]
158mod tests;