agent-image-diff 0.2.4

Structured image diff with JSON output for agent workflows
Documentation
pub mod antialias;
pub mod classify;
pub mod cli;
pub mod cluster;
pub mod color;
pub mod compare;
pub mod denoise;
pub mod dilate;
pub mod merge;
pub mod output;
pub mod region;

/// High-level convenience function for library consumers.
pub fn diff_images(
    baseline: &image::RgbaImage,
    candidate: &image::RgbaImage,
    options: DiffOptions,
) -> region::DiffResult {
    let mut compare_result = compare::compare(
        baseline,
        candidate,
        &compare::CompareOptions {
            threshold: options.threshold,
            detect_antialias: options.detect_antialias,
        },
    );

    denoise::denoise(
        &mut compare_result.diff_mask,
        compare_result.width,
        compare_result.height,
        options.denoise,
    );

    let dilated_mask = dilate::dilate(
        &compare_result.diff_mask,
        compare_result.width,
        compare_result.height,
        options.dilate,
    );

    let labels = cluster::label_components(
        &dilated_mask,
        compare_result.width,
        compare_result.height,
        options.connectivity,
    );

    let mut regions = region::extract_regions(
        &labels,
        &compare_result.delta_map,
        compare_result.width,
        compare_result.height,
        options.min_region_size,
    );

    merge::merge_regions(&mut regions, options.merge_distance);

    classify::classify_regions(
        &mut regions,
        baseline,
        candidate,
        &compare_result.diff_mask,
        compare_result.width,
        compare_result.height,
    );

    let total_pixels = compare_result.width as u64 * compare_result.height as u64;
    let changed_pixels = compare_result.diff_mask.iter().filter(|&&v| v).count() as u64;

    let dimension_mismatch =
        if baseline.width() != candidate.width() || baseline.height() != candidate.height() {
            Some(region::DimensionMismatch {
                baseline: region::Dimensions {
                    width: baseline.width(),
                    height: baseline.height(),
                },
                candidate: region::Dimensions {
                    width: candidate.width(),
                    height: candidate.height(),
                },
            })
        } else {
            None
        };

    region::DiffResult {
        dimensions: region::Dimensions {
            width: compare_result.width,
            height: compare_result.height,
        },
        stats: region::DiffStats {
            changed_pixels,
            total_pixels,
            diff_percentage: if total_pixels > 0 {
                (changed_pixels as f64 / total_pixels as f64) * 100.0
            } else {
                0.0
            },
            region_count: regions.len(),
            antialiased_pixels: compare_result.aa_pixel_count,
        },
        is_match: regions.is_empty(),
        regions,
        dimension_mismatch,
    }
}

pub struct DiffOptions {
    pub threshold: f64,
    pub detect_antialias: bool,
    pub connectivity: u8,
    pub min_region_size: u32,
    pub denoise: u32,
    pub dilate: u32,
    pub merge_distance: u32,
}

impl Default for DiffOptions {
    fn default() -> Self {
        Self {
            threshold: 0.1,
            detect_antialias: true,
            connectivity: 8,
            min_region_size: 25,
            denoise: 25,
            dilate: 0,
            merge_distance: 50,
        }
    }
}