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:
Marc-André Lureau 2025-02-25 16:36:05 +04:00 committed by Benoît Cortier
parent cc78b1e3dc
commit 20581bb6f1
4 changed files with 336 additions and 0 deletions

View file

@ -30,6 +30,7 @@ yuvutils-rs = { version = "0.8", features = ["rdp"] }
[dev-dependencies]
bmp = "0.5"
bytemuck = "1.21"
expect-test.workspace = true
[lints]

View 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
}
]
)
}
}

View file

@ -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;