mirror of
https://github.com/Devolutions/IronRDP.git
synced 2025-08-04 15:18:17 +00:00
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 <marcandre.lureau@redhat.com>
This commit is contained in:
parent
cc78b1e3dc
commit
20581bb6f1
4 changed files with 336 additions and 0 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2579,6 +2579,7 @@ dependencies = [
|
|||
"bitflags 2.9.0",
|
||||
"bitvec",
|
||||
"bmp",
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"expect-test",
|
||||
"ironrdp-core",
|
||||
|
|
|
@ -30,6 +30,7 @@ yuvutils-rs = { version = "0.8", features = ["rdp"] }
|
|||
|
||||
[dev-dependencies]
|
||||
bmp = "0.5"
|
||||
bytemuck = "1.21"
|
||||
expect-test.workspace = true
|
||||
|
||||
[lints]
|
||||
|
|
333
crates/ironrdp-graphics/src/diff.rs
Normal file
333
crates/ironrdp-graphics/src/diff.rs
Normal file
|
@ -0,0 +1,333 @@
|
|||
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||
pub struct Rect {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
|
||||
Self { x, y, width, height }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn add_xy(mut self, x: usize, y: usize) -> Self {
|
||||
self.x += x;
|
||||
self.y += y;
|
||||
self
|
||||
}
|
||||
|
||||
fn intersect(&self, other: &Rect) -> Option<Rect> {
|
||||
let x = self.x.max(other.x);
|
||||
let y = self.y.max(other.y);
|
||||
let width = (self.x + self.width).min(other.x + other.width);
|
||||
if width <= x {
|
||||
return None;
|
||||
}
|
||||
let width = width - x;
|
||||
let height = (self.y + self.height).min(other.y + other.height);
|
||||
if height <= y {
|
||||
return None;
|
||||
}
|
||||
let height = height - y;
|
||||
|
||||
Some(Rect::new(x, y, width, height))
|
||||
}
|
||||
}
|
||||
|
||||
const TILE_SIZE: usize = 64;
|
||||
|
||||
fn find_different_tiles<const BPP: usize>(
|
||||
image1: &[u8],
|
||||
stride1: usize,
|
||||
image2: &[u8],
|
||||
stride2: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
) -> Vec<bool> {
|
||||
assert!(stride1 >= width * BPP);
|
||||
assert!(stride2 >= width * BPP);
|
||||
assert!(image1.len() >= (height - 1) * stride1 + width * BPP);
|
||||
assert!(image2.len() >= (height - 1) * stride2 + width * BPP);
|
||||
|
||||
let tiles_x = width.div_ceil(TILE_SIZE);
|
||||
let tiles_y = height.div_ceil(TILE_SIZE);
|
||||
let mut tile_differences = vec![false; tiles_x * tiles_y];
|
||||
|
||||
tile_differences.iter_mut().enumerate().for_each(|(idx, diff)| {
|
||||
let tile_start_x = (idx % tiles_x) * TILE_SIZE;
|
||||
let tile_end_x = (tile_start_x + TILE_SIZE).min(width);
|
||||
let tile_start_y = (idx / tiles_x) * TILE_SIZE;
|
||||
let tile_end_y = (tile_start_y + TILE_SIZE).min(height);
|
||||
|
||||
// Check for any difference in tile using slice comparisons
|
||||
let has_diff = (tile_start_y..tile_end_y).any(|y| {
|
||||
let row_start1 = y * stride1;
|
||||
let row_start2 = y * stride2;
|
||||
let tile_row_start1 = row_start1 + tile_start_x * BPP;
|
||||
let tile_row_end1 = row_start1 + tile_end_x * BPP;
|
||||
let tile_row_start2 = row_start2 + tile_start_x * BPP;
|
||||
let tile_row_end2 = row_start2 + tile_end_x * BPP;
|
||||
|
||||
image1[tile_row_start1..tile_row_end1] != image2[tile_row_start2..tile_row_end2]
|
||||
});
|
||||
|
||||
*diff = has_diff;
|
||||
});
|
||||
|
||||
tile_differences
|
||||
}
|
||||
|
||||
fn find_different_rects<const BPP: usize>(
|
||||
image1: &[u8],
|
||||
stride1: usize,
|
||||
image2: &[u8],
|
||||
stride2: usize,
|
||||
width: usize,
|
||||
height: usize,
|
||||
) -> Vec<Rect> {
|
||||
let mut tile_differences = find_different_tiles::<BPP>(image1, stride1, image2, stride2, width, height);
|
||||
|
||||
let mod_width = width % TILE_SIZE;
|
||||
let mod_height = height % TILE_SIZE;
|
||||
let tiles_x = width.div_ceil(TILE_SIZE);
|
||||
let tiles_y = height.div_ceil(TILE_SIZE);
|
||||
|
||||
let mut rectangles = Vec::new();
|
||||
let mut current_idx = 0;
|
||||
let total_tiles = tiles_x * tiles_y;
|
||||
|
||||
// Process tiles in linear fashion to find rectangular regions
|
||||
while current_idx < total_tiles {
|
||||
if !tile_differences[current_idx] {
|
||||
current_idx += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let start_y = current_idx / tiles_x;
|
||||
let start_x = current_idx % tiles_x;
|
||||
|
||||
// Expand horizontally as much as possible
|
||||
let mut max_width = 1;
|
||||
while start_x + max_width < tiles_x && tile_differences[current_idx + max_width] {
|
||||
max_width += 1;
|
||||
}
|
||||
|
||||
// Expand vertically as much as possible
|
||||
let mut max_height = 1;
|
||||
'vertical: while start_y + max_height < tiles_y {
|
||||
for x in 0..max_width {
|
||||
let check_idx = (start_y + max_height) * tiles_x + start_x + x;
|
||||
if !tile_differences[check_idx] {
|
||||
break 'vertical;
|
||||
}
|
||||
}
|
||||
max_height += 1;
|
||||
}
|
||||
|
||||
// Calculate pixel coordinates
|
||||
let pixel_x = start_x * TILE_SIZE;
|
||||
let pixel_y = start_y * TILE_SIZE;
|
||||
|
||||
let pixel_width = if start_x + max_width == tiles_x && mod_width > 0 {
|
||||
(max_width - 1) * TILE_SIZE + mod_width
|
||||
} else {
|
||||
max_width * TILE_SIZE
|
||||
};
|
||||
|
||||
let pixel_height = if start_y + max_height == tiles_y && mod_height > 0 {
|
||||
(max_height - 1) * TILE_SIZE + mod_height
|
||||
} else {
|
||||
max_height * TILE_SIZE
|
||||
};
|
||||
|
||||
rectangles.push(Rect {
|
||||
x: pixel_x,
|
||||
y: pixel_y,
|
||||
width: pixel_width,
|
||||
height: pixel_height,
|
||||
});
|
||||
|
||||
// Mark tiles as processed
|
||||
for y in 0..max_height {
|
||||
for x in 0..max_width {
|
||||
let idx = (start_y + y) * tiles_x + start_x + x;
|
||||
tile_differences[idx] = false;
|
||||
}
|
||||
}
|
||||
|
||||
current_idx += max_width;
|
||||
}
|
||||
|
||||
rectangles
|
||||
}
|
||||
|
||||
/// Helper function to find different regions in two images.
|
||||
///
|
||||
/// This function takes two images as input and returns a list of rectangles
|
||||
/// representing the different regions between the two images, in image2 coordinates.
|
||||
///
|
||||
/// ```text
|
||||
/// ┌───────────────────────────────────────────┐
|
||||
/// │ image1 │
|
||||
/// │ │
|
||||
/// │ (x,y) │
|
||||
/// │ ┌───────────────┐ │
|
||||
/// │ │ image2 │ │
|
||||
/// │ │ │ │
|
||||
/// │ │ │ │
|
||||
/// │ │ │ │
|
||||
/// │ │ │ │
|
||||
/// │ │ │ │
|
||||
/// │ └───────────────┘ │
|
||||
/// │ │
|
||||
/// └───────────────────────────────────────────┘
|
||||
/// ```
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn find_different_rects_sub<const BPP: usize>(
|
||||
image1: &[u8],
|
||||
stride1: usize,
|
||||
width1: usize,
|
||||
height1: usize,
|
||||
image2: &[u8],
|
||||
stride2: usize,
|
||||
width2: usize,
|
||||
height2: usize,
|
||||
x: usize,
|
||||
y: usize,
|
||||
) -> Vec<Rect> {
|
||||
let rect1 = Rect::new(0, 0, width1, height1);
|
||||
let rect2 = Rect::new(x, y, width2, height2);
|
||||
let Some(inter) = rect1.intersect(&rect2) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let image1 = &image1[y * stride1 + x * BPP..];
|
||||
find_different_rects::<BPP>(image1, stride1, image2, stride2, inter.width, inter.height)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytemuck::cast_slice;
|
||||
|
||||
#[test]
|
||||
fn test_intersect() {
|
||||
let r1 = Rect::new(0, 0, 640, 480);
|
||||
let r2 = Rect::new(10, 10, 10, 10);
|
||||
let r3 = Rect::new(630, 470, 20, 20);
|
||||
|
||||
assert_eq!(r1.intersect(&r1).as_ref(), Some(&r1));
|
||||
assert_eq!(r1.intersect(&r2).as_ref(), Some(&r2));
|
||||
assert_eq!(r1.intersect(&r3), Some(Rect::new(630, 470, 10, 10)));
|
||||
assert_eq!(r2.intersect(&r3), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_tile() {
|
||||
const SIZE: usize = 128;
|
||||
let image1 = vec![0u32; SIZE * SIZE];
|
||||
let mut image2 = vec![0u32; SIZE * SIZE];
|
||||
image2[65 * 128 + 65] = 1;
|
||||
let result =
|
||||
find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE);
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![Rect {
|
||||
x: 64,
|
||||
y: 64,
|
||||
width: 64,
|
||||
height: 64
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adjacent_tiles() {
|
||||
const SIZE: usize = 256;
|
||||
let image1 = vec![0u32; SIZE * SIZE];
|
||||
let mut image2 = vec![0u32; SIZE * SIZE];
|
||||
// Modify two adjacent tiles
|
||||
image2[65 * SIZE + 65] = 1;
|
||||
image2[65 * SIZE + 129] = 1;
|
||||
let result =
|
||||
find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE);
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![Rect {
|
||||
x: 64,
|
||||
y: 64,
|
||||
width: 128,
|
||||
height: 64
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_tiles() {
|
||||
const SIZE: usize = 100;
|
||||
let image1 = vec![0u32; SIZE * SIZE];
|
||||
let mut image2 = vec![0u32; SIZE * SIZE];
|
||||
image2[65 * SIZE + 65] = 1;
|
||||
let result =
|
||||
find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE);
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![Rect {
|
||||
x: 64,
|
||||
y: 64,
|
||||
width: 36,
|
||||
height: 36
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large() {
|
||||
const SIZE: usize = 4096;
|
||||
let image1 = vec![0u32; SIZE * SIZE];
|
||||
let mut image2 = vec![0u32; SIZE * SIZE];
|
||||
image2[95 * 100 + 95] = 1;
|
||||
let _result =
|
||||
find_different_rects::<4>(cast_slice(&image1), SIZE * 4, cast_slice(&image2), SIZE * 4, SIZE, SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sub_diff() {
|
||||
let image1 = vec![0u32; 2048 * 2048];
|
||||
let mut image2 = vec![0u32; 1024 * 1024];
|
||||
image2[0] = 1;
|
||||
image2[1024 * 65 + 512 - 1] = 1;
|
||||
|
||||
let res = find_different_rects_sub::<4>(
|
||||
cast_slice(&image1),
|
||||
2048 * 4,
|
||||
2048,
|
||||
2048,
|
||||
cast_slice(&image2),
|
||||
1024 * 4,
|
||||
512,
|
||||
512,
|
||||
1024,
|
||||
1024,
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 64,
|
||||
height: 64
|
||||
},
|
||||
Rect {
|
||||
x: 448,
|
||||
y: 64,
|
||||
width: 64,
|
||||
height: 64
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
#![allow(clippy::cast_sign_loss)] // FIXME: remove
|
||||
|
||||
pub mod color_conversion;
|
||||
pub mod diff;
|
||||
pub mod dwt;
|
||||
pub mod image_processing;
|
||||
pub mod pointer;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue