use image::RgbaImage;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ScrollAxis {
Vertical,
Horizontal,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct ViewportPixelMotion {
pub estimated_shift: i32,
pub normalized_diff: f64,
pub no_motion: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct ViewportPixelPolicy {
pub max_shift: u32,
pub no_motion_threshold: f64,
pub sample_step: u32,
}
impl Default for ViewportPixelPolicy {
fn default() -> Self {
Self {
max_shift: 24,
no_motion_threshold: 0.01,
sample_step: 4,
}
}
}
pub fn compare_viewport_pixels(before: &RgbaImage, after: &RgbaImage, axis: ScrollAxis, policy: ViewportPixelPolicy) -> ViewportPixelMotion {
if before.width() == 0 || before.height() == 0 || before.dimensions() != after.dimensions() {
return ViewportPixelMotion {
estimated_shift: 0,
normalized_diff: 1.0,
no_motion: false,
};
}
let extent = match axis {
ScrollAxis::Vertical => before.height(),
ScrollAxis::Horizontal => before.width(),
};
let max_shift = policy.max_shift.min(extent.saturating_sub(1)) as i32;
let step = policy.sample_step.max(1);
let (mut best_shift, mut best_diff) = (0, f64::INFINITY);
for shift in -max_shift..=max_shift {
let diff = shifted_diff(before, after, axis, shift, step);
if diff < best_diff || (diff == best_diff && shift == 0) {
best_shift = shift;
best_diff = diff;
}
}
ViewportPixelMotion {
estimated_shift: best_shift,
normalized_diff: best_diff,
no_motion: best_shift == 0 && best_diff <= policy.no_motion_threshold,
}
}
fn shifted_diff(before: &RgbaImage, after: &RgbaImage, axis: ScrollAxis, shift: i32, step: u32) -> f64 {
let (width, height) = (before.width() as i32, before.height() as i32);
let (dx, dy) = match axis {
ScrollAxis::Vertical => (0, shift),
ScrollAxis::Horizontal => (shift, 0),
};
let (overlap_width, overlap_height) = (width - dx.abs(), height - dy.abs());
if overlap_width <= 0 || overlap_height <= 0 {
return f64::INFINITY;
}
let (before_x, after_x) = if dx >= 0 { (0, dx) } else { (-dx, 0) };
let (before_y, after_y) = if dy >= 0 { (0, dy) } else { (-dy, 0) };
let mut total = 0.0;
let mut samples = 0usize;
for row in (0..overlap_height).step_by(step as usize) {
for column in (0..overlap_width).step_by(step as usize) {
let a = before.get_pixel((before_x + column) as u32, (before_y + row) as u32).0;
let b = after.get_pixel((after_x + column) as u32, (after_y + row) as u32).0;
for channel in 0..3 {
total += (f64::from(a[channel]) - f64::from(b[channel])).abs() / 255.0;
samples += 1;
}
}
}
total / samples as f64
}
pub(crate) fn crop_ratio(image: &RgbaImage, region: Option<auv_driver::RatioRect>) -> RgbaImage {
let Some(region) = region else {
return image.clone();
};
let (width, height) = (f64::from(image.width()), f64::from(image.height()));
let x = (region.x * width).round().clamp(0.0, width) as u32;
let y = (region.y * height).round().clamp(0.0, height) as u32;
let right = ((region.x + region.width) * width).round().clamp(0.0, width) as u32;
let bottom = ((region.y + region.height) * height).round().clamp(0.0, height) as u32;
image::imageops::crop_imm(image, x, y, right.saturating_sub(x), bottom.saturating_sub(y)).to_image()
}
#[cfg(test)]
#[path = "viewport_pixels_test.rs"]
mod tests;