use image::RgbaImage;
use crate::region::Region;
pub const LABEL_ADDED: &str = "added";
pub const LABEL_REMOVED: &str = "removed";
pub const LABEL_COLOR_CHANGE: &str = "color-change";
pub const LABEL_CONTENT_CHANGE: &str = "content-change";
pub fn classify_regions(
regions: &mut [Region],
baseline: &RgbaImage,
candidate: &RgbaImage,
diff_mask: &[bool],
width: u32,
height: u32,
) {
for region in regions.iter_mut() {
region.label = classify_one(region, baseline, candidate, diff_mask, width, height);
}
}
fn classify_one(
region: &Region,
baseline: &RgbaImage,
candidate: &RgbaImage,
diff_mask: &[bool],
width: u32,
height: u32,
) -> String {
let bb = ®ion.bounding_box;
let mut baseline_variances = Vec::new();
let mut candidate_variances = Vec::new();
let mut gradient_matches = 0u32;
let mut gradient_total = 0u32;
let x_end = (bb.x + bb.width).min(width);
let y_end = (bb.y + bb.height).min(height);
for y in bb.y..y_end {
for x in bb.x..x_end {
let idx = (y * width + x) as usize;
if idx >= diff_mask.len() || !diff_mask[idx] {
continue;
}
if x < baseline.width() && y < baseline.height() {
baseline_variances.push(pixel_luminance(baseline, x, y));
}
if x < candidate.width() && y < candidate.height() {
candidate_variances.push(pixel_luminance(candidate, x, y));
}
if x > 0 && x + 1 < width && y > 0 && y + 1 < height
&& x < baseline.width().saturating_sub(1)
&& y < baseline.height().saturating_sub(1)
&& x < candidate.width().saturating_sub(1)
&& y < candidate.height().saturating_sub(1)
{
gradient_total += 1;
if gradients_match(baseline, candidate, x, y) {
gradient_matches += 1;
}
}
}
}
let baseline_var = variance(&baseline_variances);
let candidate_var = variance(&candidate_variances);
let low_var_threshold = 100.0;
if baseline_var < low_var_threshold && candidate_var >= low_var_threshold {
return LABEL_ADDED.to_string();
}
if candidate_var < low_var_threshold && baseline_var >= low_var_threshold {
return LABEL_REMOVED.to_string();
}
if gradient_total > 0 {
let match_ratio = gradient_matches as f64 / gradient_total as f64;
if match_ratio > 0.7 {
return LABEL_COLOR_CHANGE.to_string();
}
}
LABEL_CONTENT_CHANGE.to_string()
}
fn pixel_luminance(img: &RgbaImage, x: u32, y: u32) -> f64 {
let px = img.get_pixel(x, y).0;
0.299 * px[0] as f64 + 0.587 * px[1] as f64 + 0.114 * px[2] as f64
}
fn gradients_match(baseline: &RgbaImage, candidate: &RgbaImage, x: u32, y: u32) -> bool {
let b_gx = pixel_luminance(baseline, x + 1, y) - pixel_luminance(baseline, x - 1, y);
let b_gy = pixel_luminance(baseline, x, y + 1) - pixel_luminance(baseline, x, y - 1);
let c_gx = pixel_luminance(candidate, x + 1, y) - pixel_luminance(candidate, x - 1, y);
let c_gy = pixel_luminance(candidate, x, y + 1) - pixel_luminance(candidate, x, y - 1);
let threshold = 5.0; let gx_match = (b_gx.abs() < threshold && c_gx.abs() < threshold)
|| (b_gx.signum() == c_gx.signum());
let gy_match = (b_gy.abs() < threshold && c_gy.abs() < threshold)
|| (b_gy.signum() == c_gy.signum());
gx_match && gy_match
}
fn variance(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
let mean = values.iter().sum::<f64>() / values.len() as f64;
values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn variance_of_uniform_is_zero() {
let vals = vec![100.0, 100.0, 100.0];
assert_eq!(variance(&vals), 0.0);
}
#[test]
fn variance_of_varied_is_high() {
let vals = vec![0.0, 255.0, 0.0, 255.0];
assert!(variance(&vals) > 1000.0);
}
#[test]
fn variance_empty_is_zero() {
assert_eq!(variance(&[]), 0.0);
}
}