agent-image-diff 0.2.4

Structured image diff with JSON output for agent workflows
Documentation
use crate::cluster;

/// Remove small noise clusters from a diff mask before dilation.
///
/// Runs a quick connected-component pass on the raw diff mask and zeros out
/// any component with fewer than `min_size` pixels. This prevents tiny
/// rendering-noise specks from being dilated and bridging real change regions.
pub fn denoise(mask: &mut Vec<bool>, width: u32, height: u32, min_size: u32) {
    if min_size <= 1 {
        return;
    }

    // Label raw connected components (8-connectivity to match default)
    let labels = cluster::label_components(mask, width, height, 8);

    // Count pixels per component
    let max_label = labels.iter().copied().max().unwrap_or(0) as usize;
    let mut counts = vec![0u32; max_label + 1];
    for &label in &labels {
        if label > 0 {
            counts[label as usize] += 1;
        }
    }

    // Zero out mask entries for small components
    for (i, &label) in labels.iter().enumerate() {
        if label > 0 && counts[label as usize] < min_size {
            mask[i] = false;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn removes_small_clusters() {
        // 5x5 grid: one 4-pixel cluster and one 1-pixel speck
        #[rustfmt::skip]
        let mut mask = vec![
            true,  true,  false, false, false,
            true,  true,  false, false, false,
            false, false, false, false, false,
            false, false, false, false, true,
            false, false, false, false, false,
        ];
        denoise(&mut mask, 5, 5, 3);
        // The 4-pixel cluster survives, the 1-pixel speck is removed
        assert!(mask[0]); // part of big cluster
        assert!(!mask[19]); // the speck is gone
    }

    #[test]
    fn noop_when_disabled() {
        let mut mask = vec![true, false, true, false];
        let original = mask.clone();
        denoise(&mut mask, 2, 2, 1);
        assert_eq!(mask, original);
    }
}