Skip to content

Commit 20581bb

Browse files
elmarcoCBenoit
authored andcommitted
feat(graphics): add helper to find diff between images
Add some helper to find "damaged" regions, as 64x64 tiles. Signed-off-by: Marc-André Lureau <[email protected]>
1 parent cc78b1e commit 20581bb

File tree

4 files changed

+336
-0
lines changed

4 files changed

+336
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ironrdp-graphics/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ yuvutils-rs = { version = "0.8", features = ["rdp"] }
3030

3131
[dev-dependencies]
3232
bmp = "0.5"
33+
bytemuck = "1.21"
3334
expect-test.workspace = true
3435

3536
[lints]

crates/ironrdp-graphics/src/diff.rs

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
2+
pub struct Rect {
3+
pub x: usize,
4+
pub y: usize,
5+
pub width: usize,
6+
pub height: usize,
7+
}
8+
9+
impl Rect {
10+
pub fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
11+
Self { x, y, width, height }
12+
}
13+
14+
#[must_use]
15+
pub fn add_xy(mut self, x: usize, y: usize) -> Self {
16+
self.x += x;
17+
self.y += y;
18+
self
19+
}
20+
21+
fn intersect(&self, other: &Rect) -> Option<Rect> {
22+
let x = self.x.max(other.x);
23+
let y = self.y.max(other.y);
24+
let width = (self.x + self.width).min(other.x + other.width);
25+
if width <= x {
26+
return None;
27+
}
28+
let width = width - x;
29+
let height = (self.y + self.height).min(other.y + other.height);
30+
if height <= y {
31+
return None;
32+
}
33+
let height = height - y;
34+
35+
Some(Rect::new(x, y, width, height))
36+
}
37+
}
38+
39+
const TILE_SIZE: usize = 64;
40+
41+
fn find_different_tiles<const BPP: usize>(
42+
image1: &[u8],
43+
stride1: usize,
44+
image2: &[u8],
45+
stride2: usize,
46+
width: usize,
47+
height: usize,
48+
) -> Vec<bool> {
49+
assert!(stride1 >= width * BPP);
50+
assert!(stride2 >= width * BPP);
51+
assert!(image1.len() >= (height - 1) * stride1 + width * BPP);
52+
assert!(image2.len() >= (height - 1) * stride2 + width * BPP);
53+
54+
let tiles_x = width.div_ceil(TILE_SIZE);
55+
let tiles_y = height.div_ceil(TILE_SIZE);
56+
let mut tile_differences = vec![false; tiles_x * tiles_y];
57+
58+
tile_differences.iter_mut().enumerate().for_each(|(idx, diff)| {
59+
let tile_start_x = (idx % tiles_x) * TILE_SIZE;
60+
let tile_end_x = (tile_start_x + TILE_SIZE).min(width);
61+
let tile_start_y = (idx / tiles_x) * TILE_SIZE;
62+
let tile_end_y = (tile_start_y + TILE_SIZE).min(height);
63+
64+
// Check for any difference in tile using slice comparisons
65+
let has_diff = (tile_start_y..tile_end_y).any(|y| {
66+
let row_start1 = y * stride1;
67+
let row_start2 = y * stride2;
68+
let tile_row_start1 = row_start1 + tile_start_x * BPP;
69+
let tile_row_end1 = row_start1 + tile_end_x * BPP;
70+
let tile_row_start2 = row_start2 + tile_start_x * BPP;
71+
let tile_row_end2 = row_start2 + tile_end_x * BPP;
72+
73+
image1[tile_row_start1..tile_row_end1] != image2[tile_row_start2..tile_row_end2]
74+
});
75+
76+
*diff = has_diff;
77+
});
78+
79+
tile_differences
80+
}
81+
82+
fn find_different_rects<const BPP: usize>(
83+
image1: &[u8],
84+
stride1: usize,
85+
image2: &[u8],
86+
stride2: usize,
87+
width: usize,
88+
height: usize,
89+
) -> Vec<Rect> {
90+
let mut tile_differences = find_different_tiles::<BPP>(image1, stride1, image2, stride2, width, height);
91+
92+
let mod_width = width % TILE_SIZE;
93+
let mod_height = height % TILE_SIZE;
94+
let tiles_x = width.div_ceil(TILE_SIZE);
95+
let tiles_y = height.div_ceil(TILE_SIZE);
96+
97+
let mut rectangles = Vec::new();
98+
let mut current_idx = 0;
99+
let total_tiles = tiles_x * tiles_y;
100+
101+
// Process tiles in linear fashion to find rectangular regions
102+
while current_idx < total_tiles {
103+
if !tile_differences[current_idx] {
104+
current_idx += 1;
105+
continue;
106+
}
107+
108+
let start_y = current_idx / tiles_x;
109+
let start_x = current_idx % tiles_x;
110+
111+
// Expand horizontally as much as possible
112+
let mut max_width = 1;
113+
while start_x + max_width < tiles_x && tile_differences[current_idx + max_width] {
114+
max_width += 1;
115+
}
116+
117+
// Expand vertically as much as possible
118+
let mut max_height = 1;
119+
'vertical: while start_y + max_height < tiles_y {
120+
for x in 0..max_width {
121+
let check_idx = (start_y + max_height) * tiles_x + start_x + x;
122+
if !tile_differences[check_idx] {
123+
break 'vertical;
124+
}
125+
}
126+
max_height += 1;
127+
}
128+
129+
// Calculate pixel coordinates
130+
let pixel_x = start_x * TILE_SIZE;
131+
let pixel_y = start_y * TILE_SIZE;
132+
133+
let pixel_width = if start_x + max_width == tiles_x && mod_width > 0 {
134+
(max_width - 1) * TILE_SIZE + mod_width
135+
} else {
136+
max_width * TILE_SIZE
137+
};
138+
139+
let pixel_height = if start_y + max_height == tiles_y && mod_height > 0 {
140+
(max_height - 1) * TILE_SIZE + mod_height
141+
} else {
142+
max_height * TILE_SIZE
143+
};
144+
145+
rectangles.push(Rect {
146+
x: pixel_x,
147+
y: pixel_y,
148+
width: pixel_width,
149+
height: pixel_height,
150+
});
151+
152+
// Mark tiles as processed
153+
for y in 0..max_height {
154+
for x in 0..max_width {
155+
let idx = (start_y + y) * tiles_x + start_x + x;
156+
tile_differences[idx] = false;
157+
}
158+
}
159+
160+
current_idx += max_width;
161+
}
162+
163+
rectangles
164+
}
165+
166+
/// Helper function to find different regions in two images.
167+
///
168+
/// This function takes two images as input and returns a list of rectangles
169+
/// representing the different regions between the two images, in image2 coordinates.
170+
///
171+
/// ```text
172+
/// ┌───────────────────────────────────────────┐
173+
/// │ image1 │
174+
/// │ │
175+
/// │ (x,y) │
176+
/// │ ┌───────────────┐ │
177+
/// │ │ image2 │ │
178+
/// │ │ │ │
179+
/// │ │ │ │
180+
/// │ │ │ │
181+
/// │ │ │ │
182+
/// │ │ │ │
183+
/// │ └───────────────┘ │
184+
/// │ │
185+
/// └───────────────────────────────────────────┘
186+
/// ```
187+
#[allow(clippy::too_many_arguments)]
188+
pub fn find_different_rects_sub<const BPP: usize>(
189+
image1: &[u8],
190+
stride1: usize,
191+
width1: usize,
192+
height1: usize,
193+
image2: &[u8],
194+
stride2: usize,
195+
width2: usize,
196+
height2: usize,
197+
x: usize,
198+
y: usize,
199+
) -> Vec<Rect> {
200+
let rect1 = Rect::new(0, 0, width1, height1);
201+
let rect2 = Rect::new(x, y, width2, height2);
202+
let Some(inter) = rect1.intersect(&rect2) else {
203+
return vec![];
204+
};
205+
206+
let image1 = &image1[y * stride1 + x * BPP..];
207+
find_different_rects::<BPP>(image1, stride1, image2, stride2, inter.width, inter.height)
208+
}
209+
210+
#[cfg(test)]
211+
mod tests {
212+
use super::*;
213+
use bytemuck::cast_slice;
214+
215+
#[test]
216+
fn test_intersect() {
217+
let r1 = Rect::new(0, 0, 640, 480);
218+
let r2 = Rect::new(10, 10, 10, 10);
219+
let r3 = Rect::new(630, 470, 20, 20);
220+
221+
assert_eq!(r1.intersect(&r1).as_ref(), Some(&r1));
222+
assert_eq!(r1.intersect(&r2).as_ref(), Some(&r2));
223+
assert_eq!(r1.intersect(&r3), Some(Rect::new(630, 470, 10, 10)));
224+
assert_eq!(r2.intersect(&r3), None);
225+
}
226+
227+
#[test]
228+
fn test_single_tile() {
229+
const SIZE: usize = 128;
230+
let image1 = vec![0u32; SIZE * SIZE];
231+
let mut image2 = vec![0u32; SIZE * SIZE];
232+
image2[65 * 128 + 65] = 1;
233+
let result =
234+
find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE);
235+
assert_eq!(
236+
result,
237+
vec![Rect {
238+
x: 64,
239+
y: 64,
240+
width: 64,
241+
height: 64
242+
}]
243+
);
244+
}
245+
246+
#[test]
247+
fn test_adjacent_tiles() {
248+
const SIZE: usize = 256;
249+
let image1 = vec![0u32; SIZE * SIZE];
250+
let mut image2 = vec![0u32; SIZE * SIZE];
251+
// Modify two adjacent tiles
252+
image2[65 * SIZE + 65] = 1;
253+
image2[65 * SIZE + 129] = 1;
254+
let result =
255+
find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE);
256+
assert_eq!(
257+
result,
258+
vec![Rect {
259+
x: 64,
260+
y: 64,
261+
width: 128,
262+
height: 64
263+
}]
264+
);
265+
}
266+
267+
#[test]
268+
fn test_edge_tiles() {
269+
const SIZE: usize = 100;
270+
let image1 = vec![0u32; SIZE * SIZE];
271+
let mut image2 = vec![0u32; SIZE * SIZE];
272+
image2[65 * SIZE + 65] = 1;
273+
let result =
274+
find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE);
275+
assert_eq!(
276+
result,
277+
vec![Rect {
278+
x: 64,
279+
y: 64,
280+
width: 36,
281+
height: 36
282+
}]
283+
);
284+
}
285+
286+
#[test]
287+
fn test_large() {
288+
const SIZE: usize = 4096;
289+
let image1 = vec![0u32; SIZE * SIZE];
290+
let mut image2 = vec![0u32; SIZE * SIZE];
291+
image2[95 * 100 + 95] = 1;
292+
let _result =
293+
find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE);
294+
}
295+
296+
#[test]
297+
fn test_sub_diff() {
298+
let image1 = vec![0u32; 2048 * 2048];
299+
let mut image2 = vec![0u32; 1024 * 1024];
300+
image2[0] = 1;
301+
image2[1024 * 65 + 512 - 1] = 1;
302+
303+
let res = find_different_rects_sub::<4>(
304+
cast_slice(&image1),
305+
2048 * 4,
306+
2048,
307+
2048,
308+
cast_slice(&image2),
309+
1024 * 4,
310+
512,
311+
512,
312+
1024,
313+
1024,
314+
);
315+
assert_eq!(
316+
res,
317+
vec![
318+
Rect {
319+
x: 0,
320+
y: 0,
321+
width: 64,
322+
height: 64
323+
},
324+
Rect {
325+
x: 448,
326+
y: 64,
327+
width: 64,
328+
height: 64
329+
}
330+
]
331+
)
332+
}
333+
}

crates/ironrdp-graphics/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#![allow(clippy::cast_sign_loss)] // FIXME: remove
88

99
pub mod color_conversion;
10+
pub mod diff;
1011
pub mod dwt;
1112
pub mod image_processing;
1213
pub mod pointer;

0 commit comments

Comments
 (0)