corrmatch 0.1.0

CPU-first template matching with ZNCC/SSD and coarse-to-fine pyramid search
Documentation
use corrmatch::{
    CompileConfig, CompileConfigNoRot, CompiledTemplate, ImageView, MatchConfig, Matcher,
    RotationMode, Template,
};

fn make_template(width: usize, height: usize) -> Vec<u8> {
    let mut data = Vec::with_capacity(width * height);
    for y in 0..height {
        for x in 0..width {
            let value = ((x * 7) ^ (y * 11) ^ (x * y)) & 0xFF;
            data.push(value as u8);
        }
    }
    data
}

fn make_mirror_template(width: usize, height: usize) -> Vec<u8> {
    let mut data = Vec::with_capacity(width * height);
    let mid = (width - 1) / 2;
    for y in 0..height {
        for x in 0..width {
            let sym_x = x.min(width - 1 - x).min(mid);
            let value = ((sym_x * 13 + y * 5 + sym_x * y) & 0xFF) as u8;
            data.push(value);
        }
    }
    data
}

fn angle_diff_deg(a: f32, b: f32) -> f32 {
    let mut diff = (a - b) % 360.0;
    if diff < -180.0 {
        diff += 360.0;
    }
    if diff >= 180.0 {
        diff -= 360.0;
    }
    diff.abs()
}

#[test]
fn refinement_handles_border_candidates() {
    let tpl_width = 12;
    let tpl_height = 10;
    let tpl_data = make_template(tpl_width, tpl_height);
    let template = Template::new(tpl_data.clone(), tpl_width, tpl_height).unwrap();

    let img_width = 40;
    let img_height = 30;
    let mut image = vec![0u8; img_width * img_height];
    for y in 0..tpl_height {
        for x in 0..tpl_width {
            image[y * img_width + x] = tpl_data[y * tpl_width + x];
        }
    }

    let compiled =
        CompiledTemplate::compile_unrotated(&template, CompileConfigNoRot { max_levels: 1 })
            .unwrap();
    let cfg = MatchConfig {
        max_image_levels: 1,
        beam_width: 4,
        per_angle_topk: 2,
        roi_radius: 4,
        nms_radius: 3,
        angle_half_range_steps: 1,
        rotation: RotationMode::Disabled,
        ..MatchConfig::default()
    };
    let matcher = Matcher::new(compiled).with_config(cfg);
    let image_view = ImageView::from_slice(&image, img_width, img_height).unwrap();
    let best = matcher.match_image(image_view).unwrap();

    assert!(best.x.is_finite());
    assert!(best.y.is_finite());
    assert!(best.angle_deg.is_finite());
    assert!(best.score.is_finite());
}

#[test]
fn refinement_keeps_center_angle_on_symmetric_template() {
    let tpl_width = 17;
    let tpl_height = 13;
    let tpl_data = make_mirror_template(tpl_width, tpl_height);
    let template = Template::new(tpl_data.clone(), tpl_width, tpl_height).unwrap();

    let img_width = 60;
    let img_height = 50;
    let x0 = 14;
    let y0 = 12;
    let mut image = vec![0u8; img_width * img_height];
    for y in 0..tpl_height {
        for x in 0..tpl_width {
            image[(y0 + y) * img_width + (x0 + x)] = tpl_data[y * tpl_width + x];
        }
    }

    let compiled = CompiledTemplate::compile_rotated(
        &template,
        CompileConfig {
            max_levels: 1,
            coarse_step_deg: 30.0,
            min_step_deg: 30.0,
            fill_value: 0,
            precompute_coarsest: true,
        },
    )
    .unwrap();
    let step = compiled.angle_grid(0).unwrap().step_deg();
    let cfg = MatchConfig {
        max_image_levels: 1,
        beam_width: 4,
        per_angle_topk: 2,
        roi_radius: 4,
        nms_radius: 3,
        angle_half_range_steps: 1,
        rotation: RotationMode::Enabled,
        ..MatchConfig::default()
    };
    let matcher = Matcher::new(compiled).with_config(cfg);
    let image_view = ImageView::from_slice(&image, img_width, img_height).unwrap();
    let best = matcher.match_image(image_view).unwrap();

    assert!(angle_diff_deg(best.angle_deg, 0.0) <= step * 0.25);
}