use crate::error::{Result, VisionError};
use crate::feature::{extract_feature_coordinates, harris_corners};
use crate::registration::{
identity_transform, ransac_estimate_transform, Point2D, PointMatch, RegistrationParams,
RegistrationResult, TransformType,
};
use image::{DynamicImage, GrayImage};
use scirs2_core::ndarray::Array1;
#[derive(Debug, Clone)]
pub struct FeatureRegistrationConfig {
pub detector_params: FeatureDetectorParams,
pub matcher_params: MatcherParams,
pub registration_params: RegistrationParams,
pub transform_type: TransformType,
}
impl Default for FeatureRegistrationConfig {
fn default() -> Self {
Self {
detector_params: FeatureDetectorParams::default(),
matcher_params: MatcherParams::default(),
registration_params: RegistrationParams::default(),
transform_type: TransformType::Affine,
}
}
}
#[derive(Debug, Clone)]
pub struct OrbParams {
pub n_levels: usize,
pub scale_factor: f32,
pub fast_threshold: u8,
pub patch_size: usize,
pub edge_threshold: usize,
pub first_level: usize,
pub wta_k: usize,
}
impl Default for OrbParams {
fn default() -> Self {
Self {
n_levels: 8,
scale_factor: 1.2,
fast_threshold: 20,
patch_size: 31,
edge_threshold: 31,
first_level: 0,
wta_k: 2,
}
}
}
#[derive(Debug, Clone)]
pub struct FeatureDetectorParams {
pub max_features: usize,
pub threshold: f32,
pub use_orb: bool,
pub orb_params: OrbParams,
pub harris_block_size: usize,
pub harris_k: f32,
}
impl Default for FeatureDetectorParams {
fn default() -> Self {
Self {
max_features: 500,
threshold: 0.01,
use_orb: true,
orb_params: OrbParams::default(),
harris_block_size: 7,
harris_k: 0.04,
}
}
}
#[derive(Debug, Clone)]
pub struct MatcherParams {
pub max_distance: f64,
pub cross_check: bool,
pub use_ratio_test: bool,
pub ratio_threshold: f64,
}
impl Default for MatcherParams {
fn default() -> Self {
Self {
max_distance: 0.8,
cross_check: true,
use_ratio_test: true,
ratio_threshold: 0.75,
}
}
}
#[derive(Debug, Clone)]
pub struct FeatureRegistrationResult {
pub registration: RegistrationResult,
pub ref_features: usize,
pub target_features: usize,
pub initial_matches: usize,
pub final_matches: usize,
}
#[allow(dead_code)]
pub fn register_images(
reference: &DynamicImage,
target: &DynamicImage,
config: &FeatureRegistrationConfig,
) -> Result<FeatureRegistrationResult> {
let (ref_keypoints, ref_descriptors) = detect_and_describe(reference, &config.detector_params)?;
let (target_keypoints, target_descriptors) =
detect_and_describe(target, &config.detector_params)?;
if ref_keypoints.is_empty() || target_keypoints.is_empty() {
return Err(VisionError::OperationError(
"Insufficient features detected".to_string(),
));
}
let matches = match_features(
&ref_descriptors,
&target_descriptors,
&config.matcher_params,
)?;
if matches.len() < 4 {
return Err(VisionError::OperationError(
"Insufficient matches for registration".to_string(),
));
}
let point_matches: Vec<PointMatch> = matches
.iter()
.map(|m| PointMatch {
source: Point2D::new(
ref_keypoints[m.query_idx].x as f64,
ref_keypoints[m.query_idx].y as f64,
),
target: Point2D::new(
target_keypoints[m.train_idx].x as f64,
target_keypoints[m.train_idx].y as f64,
),
confidence: m.confidence as f64,
})
.collect();
let registration_result = ransac_estimate_transform(
&point_matches,
config.transform_type,
&config.registration_params,
)?;
let final_matches = registration_result.inliers.len();
Ok(FeatureRegistrationResult {
registration: registration_result,
ref_features: ref_keypoints.len(),
target_features: target_keypoints.len(),
initial_matches: matches.len(),
final_matches,
})
}
#[allow(dead_code)]
fn detect_and_describe(
image: &DynamicImage,
params: &FeatureDetectorParams,
) -> Result<(Vec<Keypoint>, Vec<Vec<u8>>)> {
if params.use_orb {
detect_orb_features(image, params)
} else {
let corners = harris_corners(
image,
params.harris_block_size,
params.harris_k,
params.threshold,
)?;
let coordinates = extract_feature_coordinates(&corners);
let keypoints: Vec<Keypoint> = coordinates
.iter()
.take(params.max_features)
.map(|&(x, y)| Keypoint {
x: x as f32,
y: y as f32,
scale: 1.0,
angle: 0.0,
response: 1.0,
})
.collect();
let gray = image.to_luma8();
let descriptors = generate_simple_descriptors(&gray, &keypoints)?;
Ok((keypoints, descriptors))
}
}
#[allow(dead_code)]
fn detect_orb_features(
image: &DynamicImage,
params: &FeatureDetectorParams,
) -> Result<(Vec<Keypoint>, Vec<Vec<u8>>)> {
let gray = image.to_luma8();
let _width_height = gray.dimensions();
let pyramid = build_orb_pyramid(&gray, ¶ms.orb_params)?;
let mut all_keypoints = Vec::new();
for (level, level_image) in pyramid.iter().enumerate() {
let scale = params.orb_params.scale_factor.powi(level as i32);
let features_per_level = params.max_features / params.orb_params.n_levels;
let level_keypoints = detect_fast_keypoints(
level_image,
params.orb_params.fast_threshold,
features_per_level,
scale,
level,
)?;
all_keypoints.extend(level_keypoints);
}
all_keypoints.sort_by(|a, b| {
b.response
.partial_cmp(&a.response)
.expect("Operation failed")
});
all_keypoints.truncate(params.max_features);
let keypoints_with_orientation = compute_orb_orientations(&gray, &all_keypoints)?;
let descriptors =
compute_orb_descriptors(&gray, &keypoints_with_orientation, ¶ms.orb_params)?;
Ok((keypoints_with_orientation, descriptors))
}
#[allow(dead_code)]
fn build_orb_pyramid(
image: &image::GrayImage,
params: &OrbParams,
) -> Result<Vec<image::GrayImage>> {
let mut pyramid = Vec::new();
let current_image = image.clone();
for level in 0..params.n_levels {
if level == 0 {
pyramid.push(current_image.clone());
} else {
let scale = params.scale_factor.powi(level as i32);
let new_width = ((image.width() as f32 / scale) as u32).max(50);
let new_height = ((image.height() as f32 / scale) as u32).max(50);
if new_width < 50 || new_height < 50 {
break;
}
let resized = image::imageops::resize(
image,
new_width,
new_height,
image::imageops::FilterType::Lanczos3,
);
pyramid.push(resized);
}
}
Ok(pyramid)
}
#[allow(dead_code)]
fn detect_fast_keypoints(
image: &image::GrayImage,
threshold: u8,
max_features: usize,
scale: f32,
level: usize,
) -> Result<Vec<Keypoint>> {
let (width, height) = image.dimensions();
let mut keypoints = Vec::new();
let circle_offsets = [
(0, -3),
(1, -3),
(2, -2),
(3, -1),
(3, 0),
(3, 1),
(2, 2),
(1, 3),
(0, 3),
(-1, 3),
(-2, 2),
(-3, 1),
(-3, 0),
(-3, -1),
(-2, -2),
(-1, -3),
];
for y in 3..(height - 3) {
for x in 3..(width - 3) {
let center_intensity = image.get_pixel(x, y)[0];
if is_fast_corner(image, x, y, center_intensity, threshold, &circle_offsets) {
let response = compute_fast_response(image, x, y, &circle_offsets);
keypoints.push(Keypoint {
x: (x as f32) * scale,
y: (y as f32) * scale,
scale,
angle: 0.0, response,
});
}
}
}
let suppressed = non_maximum_suppression(&keypoints, 7.0)?;
let mut sorted_keypoints = suppressed;
sorted_keypoints.sort_by(|a, b| {
b.response
.partial_cmp(&a.response)
.expect("Operation failed")
});
sorted_keypoints.truncate(max_features);
Ok(sorted_keypoints)
}
#[allow(dead_code)]
fn is_fast_corner(
image: &image::GrayImage,
x: u32,
y: u32,
center_intensity: u8,
threshold: u8,
circle_offsets: &[(i32, i32)],
) -> bool {
let mut _brighter_count = 0;
let mut _darker_count = 0;
let mut consecutive_brighter = 0;
let mut consecutive_darker = 0;
let mut max_consecutive_brighter = 0;
let mut max_consecutive_darker = 0;
for &(dx, dy) in circle_offsets {
let px = (x as i32 + dx) as u32;
let py = (y as i32 + dy) as u32;
let pixel_intensity = image.get_pixel(px, py)[0];
let diff = pixel_intensity as i32 - center_intensity as i32;
if diff > threshold as i32 {
_brighter_count += 1;
consecutive_brighter += 1;
consecutive_darker = 0;
max_consecutive_brighter = max_consecutive_brighter.max(consecutive_brighter);
} else if diff < -(threshold as i32) {
_darker_count += 1;
consecutive_darker += 1;
consecutive_brighter = 0;
max_consecutive_darker = max_consecutive_darker.max(consecutive_darker);
} else {
consecutive_brighter = 0;
consecutive_darker = 0;
}
}
max_consecutive_brighter >= 9 || max_consecutive_darker >= 9
}
#[allow(dead_code)]
fn compute_fast_response(
image: &image::GrayImage,
x: u32,
y: u32,
circle_offsets: &[(i32, i32)],
) -> f32 {
let center_intensity = image.get_pixel(x, y)[0] as f32;
let mut total_diff = 0.0;
for &(dx, dy) in circle_offsets {
let px = (x as i32 + dx) as u32;
let py = (y as i32 + dy) as u32;
let pixel_intensity = image.get_pixel(px, py)[0] as f32;
total_diff += (pixel_intensity - center_intensity).abs();
}
total_diff / circle_offsets.len() as f32
}
#[allow(dead_code)]
fn non_maximum_suppression(keypoints: &[Keypoint], radius: f32) -> Result<Vec<Keypoint>> {
let mut suppressed: Vec<Keypoint> = Vec::new();
let radius_sq = radius * radius;
let mut sorted_keypoints = keypoints.to_vec();
sorted_keypoints.sort_by(|a, b| {
b.response
.partial_cmp(&a.response)
.expect("Operation failed")
});
for keypoint in sorted_keypoints {
let mut is_local_maximum = true;
for existing in &suppressed {
let dx = keypoint.x - existing.x;
let dy = keypoint.y - existing.y;
let dist_sq = dx * dx + dy * dy;
if dist_sq < radius_sq && existing.response >= keypoint.response {
is_local_maximum = false;
break;
}
}
if is_local_maximum {
suppressed.push(keypoint);
}
}
Ok(suppressed)
}
#[allow(dead_code)]
fn compute_orb_orientations(
image: &image::GrayImage,
keypoints: &[Keypoint],
) -> Result<Vec<Keypoint>> {
let mut oriented_keypoints = Vec::new();
let (width, height) = image.dimensions();
for keypoint in keypoints {
let patch_radius = 15; let x = keypoint.x as u32;
let y = keypoint.y as u32;
if x < patch_radius
|| y < patch_radius
|| x >= width - patch_radius
|| y >= height - patch_radius
{
continue;
}
let mut m01 = 0.0;
let mut m10 = 0.0;
for dy in -(patch_radius as i32)..=(patch_radius as i32) {
for dx in -(patch_radius as i32)..=(patch_radius as i32) {
let px = (x as i32 + dx) as u32;
let py = (y as i32 + dy) as u32;
let intensity = image.get_pixel(px, py)[0] as f32;
m01 += dy as f32 * intensity;
m10 += dx as f32 * intensity;
}
}
let angle = m01.atan2(m10);
let mut oriented_keypoint = keypoint.clone();
oriented_keypoint.angle = angle;
oriented_keypoints.push(oriented_keypoint);
}
Ok(oriented_keypoints)
}
#[allow(dead_code)]
fn compute_orb_descriptors(
image: &image::GrayImage,
keypoints: &[Keypoint],
params: &OrbParams,
) -> Result<Vec<Vec<u8>>> {
let mut descriptors = Vec::new();
let _descriptor_length = 32;
let sampling_pattern = generate_brief_sampling_pattern(params.patch_size);
for keypoint in keypoints {
let descriptor = compute_brief_descriptor(image, keypoint, &sampling_pattern)?;
descriptors.push(descriptor);
}
Ok(descriptors)
}
#[allow(dead_code)]
fn generate_brief_sampling_pattern(_patchsize: usize) -> Vec<((i32, i32), (i32, i32))> {
let mut pattern = Vec::new();
let half_patch = (_patchsize / 2) as i32;
let descriptor_bits = 256;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
42u64.hash(&mut hasher); let mut rng_state = hasher.finish();
for _ in 0..descriptor_bits {
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let x1 = ((rng_state >> 16) % (2 * half_patch as u64)) as i32 - half_patch;
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let y1 = ((rng_state >> 16) % (2 * half_patch as u64)) as i32 - half_patch;
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let x2 = ((rng_state >> 16) % (2 * half_patch as u64)) as i32 - half_patch;
rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
let y2 = ((rng_state >> 16) % (2 * half_patch as u64)) as i32 - half_patch;
pattern.push(((x1, y1), (x2, y2)));
}
pattern
}
type SamplingPattern = [((i32, i32), (i32, i32))];
#[allow(dead_code)]
fn compute_brief_descriptor(
image: &image::GrayImage,
keypoint: &Keypoint,
sampling_pattern: &SamplingPattern,
) -> Result<Vec<u8>> {
let (width, height) = image.dimensions();
let x = keypoint.x as u32;
let y = keypoint.y as u32;
let angle = keypoint.angle;
let cos_angle = angle.cos();
let sin_angle = angle.sin();
let mut descriptor = vec![0u8; 32];
for (bit_idx, &((x1, y1), (x2, y2))) in sampling_pattern.iter().enumerate() {
let rx1 = (x1 as f32 * cos_angle - y1 as f32 * sin_angle) as i32;
let ry1 = (x1 as f32 * sin_angle + y1 as f32 * cos_angle) as i32;
let rx2 = (x2 as f32 * cos_angle - y2 as f32 * sin_angle) as i32;
let ry2 = (x2 as f32 * sin_angle + y2 as f32 * cos_angle) as i32;
let px1 = (x as i32 + rx1) as u32;
let py1 = (y as i32 + ry1) as u32;
let px2 = (x as i32 + rx2) as u32;
let py2 = (y as i32 + ry2) as u32;
if px1 < width && py1 < height && px2 < width && py2 < height {
let intensity1 = image.get_pixel(px1, py1)[0];
let intensity2 = image.get_pixel(px2, py2)[0];
if intensity1 < intensity2 {
let byte_idx = bit_idx / 8;
let bit_idx = bit_idx % 8;
descriptor[byte_idx] |= 1 << bit_idx;
}
}
}
Ok(descriptor)
}
#[derive(Debug, Clone)]
pub struct Keypoint {
pub x: f32,
pub y: f32,
pub scale: f32,
pub angle: f32,
pub response: f32,
}
#[derive(Debug, Clone)]
pub struct FeatureMatch {
pub query_idx: usize,
pub train_idx: usize,
pub distance: f32,
pub confidence: f32,
}
#[allow(dead_code)]
fn match_features(
descriptors1: &[Vec<u8>],
descriptors2: &[Vec<u8>],
params: &MatcherParams,
) -> Result<Vec<FeatureMatch>> {
match_features_simd(descriptors1, descriptors2, params)
}
#[allow(dead_code)]
fn match_features_simd(
descriptors1: &[Vec<u8>],
descriptors2: &[Vec<u8>],
params: &MatcherParams,
) -> Result<Vec<FeatureMatch>> {
if descriptors1.is_empty() || descriptors2.is_empty() {
return Ok(Vec::new());
}
let mut matches = Vec::new();
const CHUNK_SIZE: usize = 32;
for (chunk_start, chunk) in descriptors1.chunks(CHUNK_SIZE).enumerate() {
let chunk_matches =
process_descriptor_chunk_simd(chunk, chunk_start * CHUNK_SIZE, descriptors2, params)?;
matches.extend(chunk_matches);
}
if params.cross_check {
matches = apply_cross_check_simd(matches, descriptors1, descriptors2);
}
Ok(matches)
}
#[allow(dead_code)]
fn process_descriptor_chunk_simd(
chunk: &[Vec<u8>],
chunk_offset: usize,
descriptors2: &[Vec<u8>],
params: &MatcherParams,
) -> Result<Vec<FeatureMatch>> {
let mut chunk_matches = Vec::new();
for (local_i, desc1) in chunk.iter().enumerate() {
let global_i = chunk_offset + local_i;
let distances = compute_hamming_distances_simd(desc1, descriptors2);
let (best_idx, best_dist, second_best_dist) = find_best_matches_simd(&distances);
if let Some(idx) = best_idx {
if best_dist <= params.max_distance as f32 {
let pass_ratio_test = if params.use_ratio_test {
second_best_dist > 0.0
&& best_dist / second_best_dist < params.ratio_threshold as f32
} else {
true
};
if pass_ratio_test {
chunk_matches.push(FeatureMatch {
query_idx: global_i,
train_idx: idx,
distance: best_dist,
confidence: (1.0 - best_dist / 256.0).max(0.0),
});
}
}
}
}
Ok(chunk_matches)
}
#[allow(dead_code)]
fn compute_hamming_distances_simd(desc1: &[u8], descriptors2: &[Vec<u8>]) -> Vec<f32> {
let mut distances = Vec::with_capacity(descriptors2.len());
let desc_len = desc1.len();
const SIMD_CHUNK_SIZE: usize = 8;
for chunk in descriptors2.chunks(SIMD_CHUNK_SIZE) {
let valid_chunk: Vec<&Vec<u8>> = chunk
.iter()
.filter(|desc2| desc2.len() == desc_len)
.collect();
if valid_chunk.is_empty() {
distances.extend(vec![f32::INFINITY; chunk.len()]);
continue;
}
if desc_len >= 32 && valid_chunk.len() >= 4 {
let simd_distances = compute_hamming_simd_optimized(desc1, &valid_chunk);
distances.extend(simd_distances);
let remaining = chunk.len() - valid_chunk.len();
distances.extend(vec![f32::INFINITY; remaining]);
} else {
for desc2 in chunk {
distances.push(hamming_distance(desc1, desc2));
}
}
}
distances
}
#[allow(dead_code)]
fn compute_hamming_simd_optimized(desc1: &[u8], descriptors: &[&Vec<u8>]) -> Vec<f32> {
let mut distances = Vec::with_capacity(descriptors.len());
let desc_len = desc1.len();
let desc1_u32: Vec<u32> = desc1
.chunks_exact(4)
.map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
for desc2 in descriptors {
if desc2.len() != desc_len {
distances.push(f32::INFINITY);
continue;
}
let desc2_u32: Vec<u32> = desc2
.chunks_exact(4)
.map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
.collect();
let distance = if desc1_u32.len() == desc2_u32.len() && desc1_u32.len() >= 4 {
let _desc1_arr = Array1::from_vec(desc1_u32.iter().map(|&x| x as f32).collect());
let _desc2_arr = Array1::from_vec(desc2_u32.iter().map(|&x| x as f32).collect());
let mut hamming_dist = 0u32;
for (v1, v2) in desc1_u32.iter().zip(desc2_u32.iter()) {
hamming_dist += (v1 ^ v2).count_ones();
}
hamming_dist as f32
} else {
hamming_distance(desc1, desc2)
};
distances.push(distance);
}
distances
}
#[allow(dead_code)]
fn find_best_matches_simd(distances: &[f32]) -> (Option<usize>, f32, f32) {
use scirs2_core::simd_ops::SimdUnifiedOps;
if distances.is_empty() {
return (None, f32::INFINITY, f32::INFINITY);
}
const CHUNK_SIZE: usize = 8;
let mut best_dist = f32::INFINITY;
let mut second_best_dist = f32::INFINITY;
let mut best_idx = None;
for (chunk_start, chunk) in distances.chunks(CHUNK_SIZE).enumerate() {
if chunk.len() >= 4 {
let chunk_array = Array1::from_vec(chunk.to_vec());
let chunk_min = f32::simd_min_element(&chunk_array.view());
let chunk_min_idx = chunk
.iter()
.position(|&x| (x - chunk_min).abs() < f32::EPSILON)
.unwrap_or(0);
let global_idx = chunk_start * CHUNK_SIZE + chunk_min_idx;
if chunk_min < best_dist {
second_best_dist = best_dist;
best_dist = chunk_min;
best_idx = Some(global_idx);
} else if chunk_min < second_best_dist {
second_best_dist = chunk_min;
}
for &dist in chunk.iter() {
if dist != chunk_min && dist < second_best_dist {
second_best_dist = dist;
}
}
} else {
for (local_idx, &dist) in chunk.iter().enumerate() {
let global_idx = chunk_start * CHUNK_SIZE + local_idx;
if dist < best_dist {
second_best_dist = best_dist;
best_dist = dist;
best_idx = Some(global_idx);
} else if dist < second_best_dist {
second_best_dist = dist;
}
}
}
}
(best_idx, best_dist, second_best_dist)
}
#[allow(dead_code)]
fn apply_cross_check_simd(
matches: Vec<FeatureMatch>,
descriptors1: &[Vec<u8>],
descriptors2: &[Vec<u8>],
) -> Vec<FeatureMatch> {
let mut validated_matches = Vec::new();
const CHUNK_SIZE: usize = 16;
for chunk in matches.chunks(CHUNK_SIZE) {
let mut chunk_validated = Vec::new();
for match_item in chunk {
let desc2 = &descriptors2[match_item.train_idx];
let reverse_distances = compute_hamming_distances_simd(desc2, descriptors1);
let (best_idx, best_dist, second_best_dist) =
find_best_matches_simd(&reverse_distances);
if best_idx == Some(match_item.query_idx) {
chunk_validated.push(match_item.clone());
}
}
validated_matches.extend(chunk_validated);
}
validated_matches
}
#[allow(dead_code)]
fn hamming_distance(desc1: &[u8], desc2: &[u8]) -> f32 {
let min_len = desc1.len().min(desc2.len());
let mut distance = 0;
for i in 0..min_len {
distance += (desc1[i] ^ desc2[i]).count_ones();
}
distance as f32
}
#[allow(dead_code)]
fn apply_cross_check(
matches: Vec<FeatureMatch>,
descriptors1: &[Vec<u8>],
descriptors2: &[Vec<u8>],
) -> Vec<FeatureMatch> {
let mut validated_matches = Vec::new();
for m in matches {
let desc2 = &descriptors2[m.train_idx];
let mut best_dist = f32::INFINITY;
let mut best_idx = None;
for (i, desc1) in descriptors1.iter().enumerate() {
let dist = hamming_distance(desc2, desc1);
if dist < best_dist {
best_dist = dist;
best_idx = Some(i);
}
}
if best_idx == Some(m.query_idx) {
validated_matches.push(m);
}
}
validated_matches
}
#[allow(dead_code)]
fn generate_simple_descriptors(image: &GrayImage, keypoints: &[Keypoint]) -> Result<Vec<Vec<u8>>> {
let (width, height) = image.dimensions();
let patch_size = 8;
let half_patch = patch_size / 2;
let mut descriptors = Vec::new();
for kp in keypoints {
let x = kp.x as i32;
let y = kp.y as i32;
if x < half_patch as i32
|| y < half_patch as i32
|| x >= (width - half_patch) as i32
|| y >= (height - half_patch) as i32
{
continue;
}
let mut descriptor = Vec::new();
for dy in -(half_patch as i32)..(half_patch as i32) {
for dx in -(half_patch as i32)..(half_patch as i32) {
let px = (x + dx) as u32;
let py = (y + dy) as u32;
if px < width && py < height {
descriptor.push(image.get_pixel(px, py)[0]);
}
}
}
descriptors.push(descriptor);
}
Ok(descriptors)
}
#[allow(dead_code)]
pub fn multi_scale_register(
reference: &DynamicImage,
target: &DynamicImage,
config: &FeatureRegistrationConfig,
num_levels: usize,
) -> Result<FeatureRegistrationResult> {
let mut current_transform = identity_transform();
let mut best_result = None;
let mut best_inliers = 0;
let ref_pyramid = build_pyramid(reference, num_levels);
let target_pyramid = build_pyramid(target, num_levels);
for level in (0..num_levels).rev() {
let scale = 2.0_f64.powi(level as i32);
let mut scaled_transform = current_transform.clone();
scaled_transform[[0, 2]] /= scale;
scaled_transform[[1, 2]] /= scale;
let level_config = config.clone();
let result = register_images(&ref_pyramid[level], &target_pyramid[level], &level_config)?;
current_transform = result.registration.transform.clone();
if level > 0 {
current_transform[[0, 2]] *= 2.0;
current_transform[[1, 2]] *= 2.0;
}
if result.final_matches > best_inliers {
best_inliers = result.final_matches;
best_result = Some(result);
}
}
best_result
.ok_or_else(|| VisionError::OperationError("Multi-scale registration failed".to_string()))
}
#[allow(dead_code)]
fn build_pyramid(image: &DynamicImage, levels: usize) -> Vec<DynamicImage> {
let mut pyramid = vec![image.clone()];
for _ in 1..levels {
let prev = &pyramid[pyramid.len() - 1];
let (width, height) = (prev.width(), prev.height());
if width < 16 || height < 16 {
break;
}
let new_width = width / 2;
let new_height = height / 2;
let downsampled =
prev.resize_exact(new_width, new_height, image::imageops::FilterType::Lanczos3);
pyramid.push(downsampled);
}
pyramid
}
#[allow(dead_code)]
pub fn template_register(
reference: &DynamicImage,
template: &DynamicImage,
search_region: Option<(u32, u32, u32, u32)>, ) -> Result<RegistrationResult> {
let ref_gray = reference.to_luma8();
let template_gray = template.to_luma8();
let (ref_width, ref_height) = ref_gray.dimensions();
let (template_width, template_height) = template_gray.dimensions();
let (search_x, search_y, search_width, search_height) = search_region.unwrap_or((
0,
0,
ref_width - template_width,
ref_height - template_height,
));
let mut best_match = (0, 0);
let mut best_score = f64::NEG_INFINITY;
for y in search_y..(search_y + search_height) {
for x in search_x..(search_x + search_width) {
if x + template_width <= ref_width && y + template_height <= ref_height {
let score = compute_ncc(&ref_gray, &template_gray, x, y);
if score > best_score {
best_score = score;
best_match = (x, y);
}
}
}
}
let mut transform = identity_transform();
transform[[0, 2]] = best_match.0 as f64;
transform[[1, 2]] = best_match.1 as f64;
Ok(RegistrationResult {
transform,
final_cost: -best_score, iterations: 1,
converged: true,
inliers: vec![0], })
}
#[allow(dead_code)]
fn compute_ncc(reference: &GrayImage, template: &GrayImage, offset_x: u32, offsety: u32) -> f64 {
let (template_width, template_height) = template.dimensions();
let mut ref_sum = 0.0;
let mut template_sum = 0.0;
let mut ref_sq_sum = 0.0;
let mut template_sq_sum = 0.0;
let mut cross_sum = 0.0;
let n = (template_width * template_height) as f64;
for y in 0..template_height {
for x in 0..template_width {
let ref_val = reference.get_pixel(offset_x + x, offset_x + y)[0] as f64;
let template_val = template.get_pixel(x, y)[0] as f64;
ref_sum += ref_val;
template_sum += template_val;
ref_sq_sum += ref_val * ref_val;
template_sq_sum += template_val * template_val;
cross_sum += ref_val * template_val;
}
}
let ref_mean = ref_sum / n;
let template_mean = template_sum / n;
let numerator = cross_sum - n * ref_mean * template_mean;
let ref_var = ref_sq_sum - n * ref_mean * ref_mean;
let template_var = template_sq_sum - n * template_mean * template_mean;
let denominator = (ref_var * template_var).sqrt();
if denominator > 1e-10 {
numerator / denominator
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use image::{ImageBuffer, Luma};
fn create_test_image(width: u32, height: u32, pattern: u8) -> DynamicImage {
let img = ImageBuffer::from_fn(width, height, |x, y| {
Luma([((x + y + pattern as u32) % 256) as u8])
});
DynamicImage::ImageLuma8(img)
}
#[test]
fn test_feature_registration_config() {
let config = FeatureRegistrationConfig::default();
assert_eq!(config.transform_type, TransformType::Affine);
assert!(config.detector_params.use_orb);
}
#[test]
fn test_keypoint_creation() {
let kp = Keypoint {
x: 10.0,
y: 20.0,
scale: 1.5,
angle: 0.5,
response: 0.8,
};
assert_eq!(kp.x, 10.0);
assert_eq!(kp.y, 20.0);
}
#[test]
fn test_simple_descriptor_generation() {
let image = create_test_image(100, 100, 0).to_luma8();
let keypoints = vec![Keypoint {
x: 50.0,
y: 50.0,
scale: 1.0,
angle: 0.0,
response: 1.0,
}];
let result = generate_simple_descriptors(&image, &keypoints);
assert!(result.is_ok());
let descriptors = result.expect("Operation failed");
assert_eq!(descriptors.len(), 1);
assert!(!descriptors[0].is_empty());
}
#[test]
fn test_template_registration() {
let reference = create_test_image(100, 100, 0);
let template = create_test_image(20, 20, 0);
let result = template_register(&reference, &template, None);
assert!(result.is_ok());
let reg_result = result.expect("Operation failed");
assert!(reg_result.converged);
}
#[test]
fn test_ncc_computation() {
let ref_img = create_test_image(50, 50, 0).to_luma8();
let template = create_test_image(10, 10, 0).to_luma8();
let ncc = compute_ncc(&ref_img, &template, 0, 0);
assert!((-1.0..=1.0).contains(&ncc));
}
#[test]
fn test_pyramid_building() {
let image = create_test_image(64, 64, 0);
let pyramid = build_pyramid(&image, 3);
assert_eq!(pyramid.len(), 3);
assert_eq!(pyramid[0].width(), 64);
assert_eq!(pyramid[1].width(), 32);
assert_eq!(pyramid[2].width(), 16);
}
}