Skip to content

Commit 230270e

Browse files
committed
feat(graphics): add helper to find diff between images
FIXME: remotefx renders some line artefacts, likely due to bad compression behaviour on the tile edges. Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
1 parent 68b7237 commit 230270e

File tree

4 files changed

+329
-0
lines changed

4 files changed

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

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)