use crate::{AlignError, AlignResult, Point2D};
use rayon::prelude::*;
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct Keypoint {
pub point: Point2D,
pub scale: f32,
pub orientation: f32,
pub response: f32,
}
impl Keypoint {
#[must_use]
pub fn new(x: f64, y: f64, scale: f32, orientation: f32, response: f32) -> Self {
Self {
point: Point2D::new(x, y),
scale,
orientation,
response,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BinaryDescriptor {
pub data: [u8; 32],
}
impl BinaryDescriptor {
#[must_use]
pub fn new(data: [u8; 32]) -> Self {
Self { data }
}
#[must_use]
pub fn hamming_distance(&self, other: &Self) -> u32 {
self.data
.iter()
.zip(&other.data)
.map(|(a, b)| (a ^ b).count_ones())
.sum()
}
#[must_use]
pub fn zero() -> Self {
Self { data: [0; 32] }
}
}
#[must_use]
pub fn hamming_distance_simd(a: &[u8], b: &[u8]) -> u32 {
assert_eq!(a.len(), b.len(), "descriptor slices must have equal length");
let mut total = 0u32;
let chunks = a.len() / 8;
for i in 0..chunks {
let offset = i * 8;
let au = u64::from_le_bytes([
a[offset],
a[offset + 1],
a[offset + 2],
a[offset + 3],
a[offset + 4],
a[offset + 5],
a[offset + 6],
a[offset + 7],
]);
let bu = u64::from_le_bytes([
b[offset],
b[offset + 1],
b[offset + 2],
b[offset + 3],
b[offset + 4],
b[offset + 5],
b[offset + 6],
b[offset + 7],
]);
total += (au ^ bu).count_ones();
}
let tail_start = chunks * 8;
for (&av, &bv) in a[tail_start..].iter().zip(&b[tail_start..]) {
total += (av ^ bv).count_ones();
}
total
}
#[derive(Debug, Clone)]
pub struct MatchPair {
pub idx1: usize,
pub idx2: usize,
pub distance: u32,
pub point1: Point2D,
pub point2: Point2D,
}
impl MatchPair {
#[must_use]
pub fn new(idx1: usize, idx2: usize, distance: u32, point1: Point2D, point2: Point2D) -> Self {
Self {
idx1,
idx2,
distance,
point1,
point2,
}
}
}
pub struct FastDetector {
pub threshold: u8,
pub nms_window: usize,
}
impl Default for FastDetector {
fn default() -> Self {
Self {
threshold: 20,
nms_window: 3,
}
}
}
impl FastDetector {
#[must_use]
pub fn new(threshold: u8) -> Self {
Self {
threshold,
nms_window: 3,
}
}
pub fn detect(&self, image: &[u8], width: usize, height: usize) -> AlignResult<Vec<Keypoint>> {
if image.len() != width * height {
return Err(AlignError::InvalidConfig("Image size mismatch".to_string()));
}
let mut corners = Vec::new();
let radius = 3;
let circle = self.get_circle_offsets(width);
for y in radius..height - radius {
for x in radius..width - radius {
let idx = y * width + x;
let center = image[idx];
if self.is_corner(image, idx, center, &circle) {
let response = self.compute_response(image, idx, center, &circle);
corners.push(Keypoint::new(x as f64, y as f64, 1.0, 0.0, response));
}
}
}
let corners = self.non_maximum_suppression(&corners, width, height);
Ok(corners)
}
fn get_circle_offsets(&self, width: usize) -> Vec<isize> {
let w = width as isize;
vec![
-w * 3, -w * 3 + 1, -w * 2 + 2, -w + 3, 3, w + 3, w * 2 + 2, w * 3 + 1, w * 3, w * 3 - 1, w * 2 - 2, w - 3, -3, -w - 3, -w * 2 - 2, -w * 3 - 1, ]
}
fn is_corner(&self, image: &[u8], center_idx: usize, center_val: u8, circle: &[isize]) -> bool {
let threshold = i16::from(self.threshold);
let center = i16::from(center_val);
let mut brighter = 0;
let mut darker = 0;
let mut max_consecutive_brighter = 0;
let mut max_consecutive_darker = 0;
for &offset in circle {
let idx = (center_idx as isize + offset) as usize;
let val = i16::from(image[idx]);
let diff = val - center;
if diff > threshold {
brighter += 1;
darker = 0;
max_consecutive_brighter = max_consecutive_brighter.max(brighter);
} else if diff < -threshold {
darker += 1;
brighter = 0;
max_consecutive_darker = max_consecutive_darker.max(darker);
} else {
brighter = 0;
darker = 0;
}
}
max_consecutive_brighter >= 9 || max_consecutive_darker >= 9
}
fn compute_response(
&self,
image: &[u8],
center_idx: usize,
center_val: u8,
circle: &[isize],
) -> f32 {
let center = i16::from(center_val);
let mut sum_abs_diff = 0i16;
for &offset in circle {
let idx = (center_idx as isize + offset) as usize;
let val = i16::from(image[idx]);
sum_abs_diff += (val - center).abs();
}
f32::from(sum_abs_diff)
}
fn non_maximum_suppression(
&self,
corners: &[Keypoint],
_width: usize,
_height: usize,
) -> Vec<Keypoint> {
let mut suppressed = vec![false; corners.len()];
let window = self.nms_window;
for i in 0..corners.len() {
if suppressed[i] {
continue;
}
let ki = &corners[i];
for (j, kj) in corners.iter().enumerate().skip(i + 1) {
if suppressed[j] {
continue;
}
let dx = (ki.point.x - kj.point.x).abs();
let dy = (ki.point.y - kj.point.y).abs();
if dx < window as f64 && dy < window as f64 {
if ki.response > kj.response {
suppressed[j] = true;
} else {
suppressed[i] = true;
break;
}
}
}
}
corners
.iter()
.enumerate()
.filter(|(i, _)| !suppressed[*i])
.map(|(_, k)| k.clone())
.collect()
}
}
pub struct SubPixelRefiner {
pub half_window: usize,
pub max_iterations: usize,
pub epsilon: f64,
}
impl Default for SubPixelRefiner {
fn default() -> Self {
Self {
half_window: 3,
max_iterations: 10,
epsilon: 0.01,
}
}
}
impl SubPixelRefiner {
#[must_use]
pub fn new(half_window: usize, max_iterations: usize, epsilon: f64) -> Self {
Self {
half_window,
max_iterations,
epsilon,
}
}
pub fn refine(
&self,
image: &[u8],
width: usize,
height: usize,
keypoints: &[Keypoint],
) -> AlignResult<Vec<Keypoint>> {
if image.len() != width * height {
return Err(AlignError::InvalidConfig("Image size mismatch".to_string()));
}
let mut refined = Vec::with_capacity(keypoints.len());
let hw = self.half_window as isize;
for kp in keypoints {
let ix = kp.point.x.round() as isize;
let iy = kp.point.y.round() as isize;
if ix < hw + 1
|| iy < hw + 1
|| ix >= (width as isize - hw - 1)
|| iy >= (height as isize - hw - 1)
{
refined.push(kp.clone());
continue;
}
let mut cx = kp.point.x;
let mut cy = kp.point.y;
for _iter in 0..self.max_iterations {
let rx = cx.round() as isize;
let ry = cy.round() as isize;
let mut _s_x2_val = 0.0_f64; let mut _s_y2_val = 0.0_f64; let mut _s_xy_val = 0.0_f64; let mut _s_x_val = 0.0_f64; let mut _s_y_val = 0.0_f64;
let mut _s_x2 = 0.0_f64;
let mut _s_y2 = 0.0_f64;
let mut _s_x4 = 0.0_f64;
let mut _s_y4 = 0.0_f64;
let mut _s_x2y2 = 0.0_f64;
let mut count = 0.0_f64;
for dy in -hw..=hw {
for dx in -hw..=hw {
let px = (rx + dx) as usize;
let py = (ry + dy) as usize;
let val = f64::from(image[py * width + px]);
let dxf = dx as f64;
let dyf = dy as f64;
_s_x2_val += dxf * dxf * val;
_s_y2_val += dyf * dyf * val;
_s_xy_val += dxf * dyf * val;
_s_x_val += dxf * val;
_s_y_val += dyf * val;
_s_x2 += dxf * dxf;
_s_y2 += dyf * dyf;
_s_x4 += dxf * dxf * dxf * dxf;
_s_y4 += dyf * dyf * dyf * dyf;
_s_x2y2 += dxf * dxf * dyf * dyf;
count += 1.0;
}
}
if count < 6.0 {
break;
}
let idx_c = ry as usize * width + rx as usize;
let gx = (f64::from(image[idx_c + 1]) - f64::from(image[idx_c - 1])) / 2.0;
let gy = (f64::from(image[(ry as usize + 1) * width + rx as usize])
- f64::from(image[(ry as usize - 1) * width + rx as usize]))
/ 2.0;
let hxx = f64::from(image[idx_c + 1]) + f64::from(image[idx_c - 1])
- 2.0 * f64::from(image[idx_c]);
let hyy = f64::from(image[(ry as usize + 1) * width + rx as usize])
+ f64::from(image[(ry as usize - 1) * width + rx as usize])
- 2.0 * f64::from(image[idx_c]);
let hxy = (f64::from(image[(ry as usize + 1) * width + rx as usize + 1])
- f64::from(image[(ry as usize + 1) * width + rx as usize - 1])
- f64::from(image[(ry as usize - 1) * width + rx as usize + 1])
+ f64::from(image[(ry as usize - 1) * width + rx as usize - 1]))
/ 4.0;
let det = hxx * hyy - hxy * hxy;
if det.abs() < 1e-10 {
break;
}
let shift_x = -(hyy * gx - hxy * gy) / det;
let shift_y = -(-hxy * gx + hxx * gy) / det;
if shift_x.abs() > 1.0 || shift_y.abs() > 1.0 {
break;
}
cx = rx as f64 + shift_x;
cy = ry as f64 + shift_y;
if shift_x * shift_x + shift_y * shift_y < self.epsilon * self.epsilon {
break;
}
}
cx = cx.clamp(0.0, (width - 1) as f64);
cy = cy.clamp(0.0, (height - 1) as f64);
refined.push(Keypoint::new(cx, cy, kp.scale, kp.orientation, kp.response));
}
Ok(refined)
}
}
#[allow(dead_code)]
fn compute_sobel_gradients(image: &[u8], width: usize, height: usize) -> (Vec<f64>, Vec<f64>) {
let n = width * height;
let mut gx = vec![0.0_f64; n];
let mut gy = vec![0.0_f64; n];
for y in 1..height.saturating_sub(1) {
for x in 1..width.saturating_sub(1) {
let idx = y * width + x;
let i_tl = f64::from(image[(y - 1) * width + (x - 1)]);
let i_t = f64::from(image[(y - 1) * width + x]);
let i_tr = f64::from(image[(y - 1) * width + (x + 1)]);
let i_l = f64::from(image[y * width + (x - 1)]);
let i_r = f64::from(image[y * width + (x + 1)]);
let i_bl = f64::from(image[(y + 1) * width + (x - 1)]);
let i_b = f64::from(image[(y + 1) * width + x]);
let i_br = f64::from(image[(y + 1) * width + (x + 1)]);
gx[idx] = (-i_tl + i_tr - 2.0 * i_l + 2.0 * i_r - i_bl + i_br) / 8.0;
gy[idx] = (-i_tl - 2.0 * i_t - i_tr + i_bl + 2.0 * i_b + i_br) / 8.0;
}
}
(gx, gy)
}
pub struct BriefDescriptor {
pub patch_size: usize,
pattern: Vec<(isize, isize, isize, isize)>,
}
impl Default for BriefDescriptor {
fn default() -> Self {
Self::new(31)
}
}
impl BriefDescriptor {
#[must_use]
pub fn new(patch_size: usize) -> Self {
let pattern = Self::generate_pattern(patch_size);
Self {
patch_size,
pattern,
}
}
fn generate_pattern(patch_size: usize) -> Vec<(isize, isize, isize, isize)> {
let mut pattern = Vec::with_capacity(256);
let half = (patch_size / 2) as isize;
let mut seed = 42u32;
for _ in 0..256 {
let x1 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
let y1 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
let x2 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
let y2 = (Self::next_random(&mut seed) % patch_size as u32) as isize - half;
pattern.push((x1, y1, x2, y2));
}
pattern
}
fn next_random(seed: &mut u32) -> u32 {
*seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
(*seed / 65536) % 32768
}
pub fn extract(
&self,
image: &[u8],
width: usize,
height: usize,
keypoint: &Keypoint,
) -> AlignResult<BinaryDescriptor> {
let x = keypoint.point.x as isize;
let y = keypoint.point.y as isize;
let half = (self.patch_size / 2) as isize;
if x < half || y < half || x >= (width as isize - half) || y >= (height as isize - half) {
return Err(AlignError::FeatureError(
"Keypoint too close to border".to_string(),
));
}
let mut descriptor = [0u8; 32];
for (bit_idx, &(x1, y1, x2, y2)) in self.pattern.iter().enumerate() {
let px1 = (y + y1) as usize * width + (x + x1) as usize;
let px2 = (y + y2) as usize * width + (x + x2) as usize;
if image[px1] < image[px2] {
let byte_idx = bit_idx / 8;
let bit_pos = bit_idx % 8;
descriptor[byte_idx] |= 1 << bit_pos;
}
}
Ok(BinaryDescriptor::new(descriptor))
}
pub fn extract_batch(
&self,
image: &[u8],
width: usize,
height: usize,
keypoints: &[Keypoint],
) -> AlignResult<Vec<BinaryDescriptor>> {
keypoints
.par_iter()
.map(|kp| self.extract(image, width, height, kp))
.collect()
}
}
pub struct OrbDetector {
fast: FastDetector,
brief: BriefDescriptor,
pub max_features: usize,
}
impl Default for OrbDetector {
fn default() -> Self {
Self::new(500)
}
}
impl OrbDetector {
#[must_use]
pub fn new(max_features: usize) -> Self {
Self {
fast: FastDetector::default(),
brief: BriefDescriptor::default(),
max_features,
}
}
pub fn detect_and_compute(
&self,
image: &[u8],
width: usize,
height: usize,
) -> AlignResult<(Vec<Keypoint>, Vec<BinaryDescriptor>)> {
let mut keypoints = self.fast.detect(image, width, height)?;
for kp in &mut keypoints {
kp.orientation = self.compute_orientation(image, width, height, kp);
}
keypoints.sort_by(|a, b| {
b.response
.partial_cmp(&a.response)
.unwrap_or(std::cmp::Ordering::Equal)
});
keypoints.truncate(self.max_features);
let descriptors = self.brief.extract_batch(image, width, height, &keypoints)?;
Ok((keypoints, descriptors))
}
fn compute_orientation(
&self,
image: &[u8],
width: usize,
height: usize,
keypoint: &Keypoint,
) -> f32 {
let x = keypoint.point.x as isize;
let y = keypoint.point.y as isize;
let radius = 15isize;
let mut m01 = 0i64;
let mut m10 = 0i64;
for dy in -radius..=radius {
for dx in -radius..=radius {
if dx * dx + dy * dy > radius * radius {
continue;
}
let px = x + dx;
let py = y + dy;
if px >= 0 && py >= 0 && px < width as isize && py < height as isize {
let idx = py as usize * width + px as usize;
let intensity = i64::from(image[idx]);
m01 += dy as i64 * intensity;
m10 += dx as i64 * intensity;
}
}
}
(m01 as f64).atan2(m10 as f64) as f32
}
}
pub struct FeatureMatcher {
pub max_distance: u32,
pub ratio_threshold: f32,
}
impl Default for FeatureMatcher {
fn default() -> Self {
Self {
max_distance: 50,
ratio_threshold: 0.8,
}
}
}
impl FeatureMatcher {
#[must_use]
pub fn new(max_distance: u32, ratio_threshold: f32) -> Self {
Self {
max_distance,
ratio_threshold,
}
}
#[must_use]
pub fn match_features(
&self,
keypoints1: &[Keypoint],
descriptors1: &[BinaryDescriptor],
keypoints2: &[Keypoint],
descriptors2: &[BinaryDescriptor],
) -> Vec<MatchPair> {
descriptors1
.par_iter()
.enumerate()
.filter_map(|(i, desc1)| {
let mut best_dist = u32::MAX;
let mut second_best_dist = u32::MAX;
let mut best_idx = 0;
for (j, desc2) in descriptors2.iter().enumerate() {
let dist = desc1.hamming_distance(desc2);
if dist < best_dist {
second_best_dist = best_dist;
best_dist = dist;
best_idx = j;
} else if dist < second_best_dist {
second_best_dist = dist;
}
}
if best_dist <= self.max_distance {
let ratio = best_dist as f32 / second_best_dist as f32;
if ratio < self.ratio_threshold {
return Some(MatchPair::new(
i,
best_idx,
best_dist,
keypoints1[i].point,
keypoints2[best_idx].point,
));
}
}
None
})
.collect()
}
#[must_use]
pub fn filter_matches_geometric(
&self,
matches: &[MatchPair],
threshold: f64,
) -> Vec<MatchPair> {
if matches.len() < 4 {
return matches.to_vec();
}
let median_dx = Self::median(
&matches
.iter()
.map(|m| m.point2.x - m.point1.x)
.collect::<Vec<_>>(),
);
let median_dy = Self::median(
&matches
.iter()
.map(|m| m.point2.y - m.point1.y)
.collect::<Vec<_>>(),
);
matches
.iter()
.filter(|m| {
let dx = m.point2.x - m.point1.x;
let dy = m.point2.y - m.point1.y;
(dx - median_dx).abs() < threshold && (dy - median_dy).abs() < threshold
})
.cloned()
.collect()
}
fn median(values: &[f64]) -> f64 {
if values.is_empty() {
return 0.0;
}
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mid = sorted.len() / 2;
if sorted.len() % 2 == 0 {
(sorted[mid - 1] + sorted[mid]) / 2.0
} else {
sorted[mid]
}
}
}
pub struct HarrisDetector {
pub threshold: f32,
pub window_size: usize,
}
impl Default for HarrisDetector {
fn default() -> Self {
Self {
threshold: 0.01,
window_size: 3,
}
}
}
impl HarrisDetector {
#[must_use]
pub fn new(threshold: f32) -> Self {
Self {
threshold,
window_size: 3,
}
}
pub fn detect(&self, image: &[u8], width: usize, height: usize) -> AlignResult<Vec<Keypoint>> {
if image.len() != width * height {
return Err(AlignError::InvalidConfig("Image size mismatch".to_string()));
}
let (grad_x, grad_y) = self.compute_gradients(image, width, height);
let mut corners = Vec::new();
let k = 0.04;
for y in self.window_size..height - self.window_size {
for x in self.window_size..width - self.window_size {
let mut ixx = 0.0;
let mut iyy = 0.0;
let mut ixy = 0.0;
for dy in 0..self.window_size {
for dx in 0..self.window_size {
let idx = (y + dy - self.window_size / 2) * width
+ (x + dx - self.window_size / 2);
let gx = grad_x[idx];
let gy = grad_y[idx];
ixx += gx * gx;
iyy += gy * gy;
ixy += gx * gy;
}
}
let det = ixx * iyy - ixy * ixy;
let trace = ixx + iyy;
let response = det - k * trace * trace;
if response > self.threshold {
corners.push(Keypoint::new(x as f64, y as f64, 1.0, 0.0, response));
}
}
}
Ok(corners)
}
fn compute_gradients(&self, image: &[u8], width: usize, height: usize) -> (Vec<f32>, Vec<f32>) {
let mut grad_x = vec![0.0; width * height];
let mut grad_y = vec![0.0; width * height];
for y in 1..height - 1 {
for x in 1..width - 1 {
let idx = y * width + x;
let gx = -f32::from(image[idx - width - 1])
- 2.0 * f32::from(image[idx - 1])
- f32::from(image[idx + width - 1])
+ f32::from(image[idx - width + 1])
+ 2.0 * f32::from(image[idx + 1])
+ f32::from(image[idx + width + 1]);
let gy = -f32::from(image[idx - width - 1])
- 2.0 * f32::from(image[idx - width])
- f32::from(image[idx - width + 1])
+ f32::from(image[idx + width - 1])
+ 2.0 * f32::from(image[idx + width])
+ f32::from(image[idx + width + 1]);
grad_x[idx] = gx / 8.0;
grad_y[idx] = gy / 8.0;
}
}
(grad_x, grad_y)
}
}
pub struct FeaturePyramid {
pub num_levels: usize,
pub scale_factor: f32,
}
impl Default for FeaturePyramid {
fn default() -> Self {
Self {
num_levels: 4,
scale_factor: 0.5,
}
}
}
impl FeaturePyramid {
#[must_use]
pub fn new(num_levels: usize, scale_factor: f32) -> Self {
Self {
num_levels,
scale_factor,
}
}
#[must_use]
pub fn build_pyramid(
&self,
image: &[u8],
width: usize,
height: usize,
) -> Vec<(Vec<u8>, usize, usize)> {
let mut pyramid = Vec::new();
pyramid.push((image.to_vec(), width, height));
let mut current_image = image.to_vec();
let mut current_width = width;
let mut current_height = height;
for _ in 1..self.num_levels {
let new_width = (current_width as f32 * self.scale_factor) as usize;
let new_height = (current_height as f32 * self.scale_factor) as usize;
if new_width < 16 || new_height < 16 {
break;
}
let downsampled = self.downsample(
¤t_image,
current_width,
current_height,
new_width,
new_height,
);
pyramid.push((downsampled.clone(), new_width, new_height));
current_image = downsampled;
current_width = new_width;
current_height = new_height;
}
pyramid
}
fn downsample(
&self,
image: &[u8],
src_width: usize,
src_height: usize,
dst_width: usize,
dst_height: usize,
) -> Vec<u8> {
let mut output = vec![0u8; dst_width * dst_height];
let scale_x = src_width as f32 / dst_width as f32;
let scale_y = src_height as f32 / dst_height as f32;
for y in 0..dst_height {
for x in 0..dst_width {
let src_x = (x as f32 * scale_x) as usize;
let src_y = (y as f32 * scale_y) as usize;
if src_x < src_width && src_y < src_height {
output[y * dst_width + x] = image[src_y * src_width + src_x];
}
}
}
output
}
pub fn detect_multiscale(
&self,
image: &[u8],
width: usize,
height: usize,
detector: &FastDetector,
) -> AlignResult<Vec<Keypoint>> {
let pyramid = self.build_pyramid(image, width, height);
let mut all_keypoints = Vec::new();
for (level, (img, w, h)) in pyramid.iter().enumerate() {
let keypoints = detector.detect(img, *w, *h)?;
let scale = self.scale_factor.powi(level as i32);
for mut kp in keypoints {
kp.point.x /= f64::from(scale);
kp.point.y /= f64::from(scale);
kp.scale *= scale;
all_keypoints.push(kp);
}
}
Ok(all_keypoints)
}
}
pub struct AdaptiveNMS {
pub radius: f32,
pub num_features: usize,
}
impl AdaptiveNMS {
#[must_use]
pub fn new(radius: f32, num_features: usize) -> Self {
Self {
radius,
num_features,
}
}
#[must_use]
pub fn apply(&self, keypoints: &[Keypoint]) -> Vec<Keypoint> {
if keypoints.len() <= self.num_features {
return keypoints.to_vec();
}
let mut result: Vec<Keypoint> = Vec::new();
let mut sorted = keypoints.to_vec();
sorted.sort_by(|a, b| {
b.response
.partial_cmp(&a.response)
.unwrap_or(std::cmp::Ordering::Equal)
});
for candidate in &sorted {
if result.len() >= self.num_features {
break;
}
let mut too_close = false;
for kept in &result {
let dist = candidate.point.distance(&kept.point);
if dist < f64::from(self.radius) {
too_close = true;
break;
}
}
if !too_close {
result.push(candidate.clone());
}
}
result
}
}
pub struct OutlierFilter {
pub threshold_multiplier: f32,
}
impl Default for OutlierFilter {
fn default() -> Self {
Self {
threshold_multiplier: 2.0,
}
}
}
impl OutlierFilter {
#[must_use]
pub fn new(threshold_multiplier: f32) -> Self {
Self {
threshold_multiplier,
}
}
#[must_use]
pub fn filter(&self, matches: &[MatchPair]) -> Vec<MatchPair> {
if matches.len() < 3 {
return matches.to_vec();
}
let distances: Vec<f64> = matches
.iter()
.map(|m| m.point1.distance(&m.point2))
.collect();
let mean = distances.iter().sum::<f64>() / distances.len() as f64;
let variance: f64 = distances
.iter()
.map(|&d| (d - mean) * (d - mean))
.sum::<f64>()
/ distances.len() as f64;
let std_dev = variance.sqrt();
let threshold = mean + std_dev * f64::from(self.threshold_multiplier);
matches
.iter()
.filter(|m| {
let dist = m.point1.distance(&m.point2);
dist <= threshold
})
.cloned()
.collect()
}
}
pub struct CrossCheckMatcher {
base_matcher: FeatureMatcher,
}
impl Default for CrossCheckMatcher {
fn default() -> Self {
Self::new()
}
}
impl CrossCheckMatcher {
#[must_use]
pub fn new() -> Self {
Self {
base_matcher: FeatureMatcher::default(),
}
}
#[must_use]
pub fn match_with_cross_check(
&self,
keypoints1: &[Keypoint],
descriptors1: &[BinaryDescriptor],
keypoints2: &[Keypoint],
descriptors2: &[BinaryDescriptor],
) -> Vec<MatchPair> {
let forward =
self.base_matcher
.match_features(keypoints1, descriptors1, keypoints2, descriptors2);
let backward =
self.base_matcher
.match_features(keypoints2, descriptors2, keypoints1, descriptors1);
let mut cross_checked = Vec::new();
for fwd in &forward {
for bwd in &backward {
if fwd.idx1 == bwd.idx2 && fwd.idx2 == bwd.idx1 {
cross_checked.push(fwd.clone());
break;
}
}
}
cross_checked
}
}
pub struct FreakDescriptor {
pub num_pairs: usize,
pub pattern_scale: f32,
}
impl Default for FreakDescriptor {
fn default() -> Self {
Self {
num_pairs: 256,
pattern_scale: 1.0,
}
}
}
impl FreakDescriptor {
#[must_use]
pub fn new(num_pairs: usize, pattern_scale: f32) -> Self {
Self {
num_pairs,
pattern_scale,
}
}
pub fn extract(
&self,
image: &[u8],
width: usize,
height: usize,
keypoint: &Keypoint,
) -> AlignResult<BinaryDescriptor> {
let x = keypoint.point.x as isize;
let y = keypoint.point.y as isize;
let radius = (20.0 * self.pattern_scale) as isize;
if x < radius
|| y < radius
|| x >= (width as isize - radius)
|| y >= (height as isize - radius)
{
return Err(AlignError::FeatureError(
"Keypoint too close to border".to_string(),
));
}
let mut descriptor = [0u8; 32];
let mut seed = 123u32;
for bit_idx in 0..256 {
let r1 = (Self::lcg(&mut seed) % (radius as u32)) as isize;
let theta1 = (Self::lcg(&mut seed) as f32 / u32::MAX as f32) * 2.0 * PI as f32;
let x1 = x + (r1 as f32 * theta1.cos()) as isize;
let y1 = y + (r1 as f32 * theta1.sin()) as isize;
let r2 = (Self::lcg(&mut seed) % (radius as u32)) as isize;
let theta2 = (Self::lcg(&mut seed) as f32 / u32::MAX as f32) * 2.0 * PI as f32;
let x2 = x + (r2 as f32 * theta2.cos()) as isize;
let y2 = y + (r2 as f32 * theta2.sin()) as isize;
if x1 >= 0
&& x1 < width as isize
&& y1 >= 0
&& y1 < height as isize
&& x2 >= 0
&& x2 < width as isize
&& y2 >= 0
&& y2 < height as isize
{
let idx1 = y1 as usize * width + x1 as usize;
let idx2 = y2 as usize * width + x2 as usize;
if idx1 < image.len() && idx2 < image.len() && image[idx1] < image[idx2] {
let byte_idx = bit_idx / 8;
let bit_pos = bit_idx % 8;
descriptor[byte_idx] |= 1 << bit_pos;
}
}
}
Ok(BinaryDescriptor::new(descriptor))
}
fn lcg(seed: &mut u32) -> u32 {
*seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
(*seed / 65536) % 32768
}
}
pub struct DescriptorVarianceFilter {
pub min_variance: f32,
}
impl Default for DescriptorVarianceFilter {
fn default() -> Self {
Self { min_variance: 0.1 }
}
}
impl DescriptorVarianceFilter {
#[must_use]
pub fn new(min_variance: f32) -> Self {
Self { min_variance }
}
#[must_use]
pub fn filter(
&self,
keypoints: &[Keypoint],
descriptors: &[BinaryDescriptor],
) -> (Vec<Keypoint>, Vec<BinaryDescriptor>) {
let mut filtered_kp = Vec::new();
let mut filtered_desc = Vec::new();
for (kp, desc) in keypoints.iter().zip(descriptors.iter()) {
let variance = self.compute_variance(desc);
if variance >= self.min_variance {
filtered_kp.push(kp.clone());
filtered_desc.push(desc.clone());
}
}
(filtered_kp, filtered_desc)
}
fn compute_variance(&self, descriptor: &BinaryDescriptor) -> f32 {
let num_set_bits: u32 = descriptor.data.iter().map(|b| b.count_ones()).sum();
let total_bits = descriptor.data.len() * 8;
let ratio = num_set_bits as f32 / total_bits as f32;
1.0 - (ratio - 0.5).abs() * 2.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_binary_descriptor_hamming() {
let desc1 = BinaryDescriptor::new([0xFF; 32]);
let desc2 = BinaryDescriptor::new([0x00; 32]);
assert_eq!(desc1.hamming_distance(&desc2), 256);
let desc3 = BinaryDescriptor::new([0xFF; 32]);
assert_eq!(desc1.hamming_distance(&desc3), 0);
}
#[test]
fn test_keypoint_creation() {
let kp = Keypoint::new(10.0, 20.0, 1.5, 0.5, 100.0);
assert_eq!(kp.point.x, 10.0);
assert_eq!(kp.point.y, 20.0);
assert_eq!(kp.scale, 1.5);
}
#[test]
fn test_fast_detector() {
let detector = FastDetector::new(20);
let image = vec![128u8; 100 * 100];
let result = detector.detect(&image, 100, 100);
assert!(result.is_ok());
}
#[test]
fn test_brief_pattern_generation() {
let brief = BriefDescriptor::new(31);
assert_eq!(brief.pattern.len(), 256);
}
#[test]
fn test_feature_matcher() {
let matcher = FeatureMatcher::default();
assert_eq!(matcher.max_distance, 50);
assert!((matcher.ratio_threshold - 0.8).abs() < f32::EPSILON);
}
#[test]
fn test_median_computation() {
let values = vec![1.0, 3.0, 2.0, 5.0, 4.0];
let median = FeatureMatcher::median(&values);
assert_eq!(median, 3.0);
let values2 = vec![1.0, 2.0, 3.0, 4.0];
let median2 = FeatureMatcher::median(&values2);
assert_eq!(median2, 2.5);
}
#[test]
fn test_feature_pyramid() {
let pyramid = FeaturePyramid::new(4, 0.5);
assert_eq!(pyramid.num_levels, 4);
assert_eq!(pyramid.scale_factor, 0.5);
}
#[test]
fn test_pyramid_building() {
let pyramid = FeaturePyramid::default();
let image = vec![128u8; 100 * 100];
let levels = pyramid.build_pyramid(&image, 100, 100);
assert!(!levels.is_empty());
assert_eq!(levels[0].1, 100); assert_eq!(levels[0].2, 100); }
#[test]
fn test_adaptive_nms() {
let nms = AdaptiveNMS::new(10.0, 5);
let keypoints = vec![
Keypoint::new(0.0, 0.0, 1.0, 0.0, 100.0),
Keypoint::new(5.0, 5.0, 1.0, 0.0, 90.0),
Keypoint::new(50.0, 50.0, 1.0, 0.0, 80.0),
];
let filtered = nms.apply(&keypoints);
assert!(!filtered.is_empty());
assert!(filtered.len() <= 5);
}
#[test]
fn test_outlier_filter() {
let filter = OutlierFilter::default();
assert_eq!(filter.threshold_multiplier, 2.0);
}
#[test]
fn test_cross_check_matcher() {
let matcher = CrossCheckMatcher::new();
let kp1 = vec![Keypoint::new(0.0, 0.0, 1.0, 0.0, 1.0)];
let kp2 = vec![Keypoint::new(0.0, 0.0, 1.0, 0.0, 1.0)];
let desc1 = vec![BinaryDescriptor::zero()];
let desc2 = vec![BinaryDescriptor::zero()];
let matches = matcher.match_with_cross_check(&kp1, &desc1, &kp2, &desc2);
assert_eq!(matches.len(), 1);
}
#[test]
fn test_freak_descriptor() {
let freak = FreakDescriptor::default();
assert_eq!(freak.num_pairs, 256);
assert_eq!(freak.pattern_scale, 1.0);
}
#[test]
fn test_descriptor_variance_filter() {
let filter = DescriptorVarianceFilter::new(0.1);
assert_eq!(filter.min_variance, 0.1);
}
#[test]
fn test_descriptor_variance() {
let filter = DescriptorVarianceFilter::default();
let desc = BinaryDescriptor::new([0xAA; 32]); let variance = filter.compute_variance(&desc);
assert!((variance - 1.0).abs() < 0.01);
}
#[test]
fn test_subpixel_refiner_default() {
let r = SubPixelRefiner::default();
assert_eq!(r.half_window, 3);
assert_eq!(r.max_iterations, 10);
}
#[test]
fn test_subpixel_refiner_empty_keypoints() {
let image = vec![128u8; 64 * 64];
let refiner = SubPixelRefiner::default();
let result = refiner.refine(&image, 64, 64, &[]).expect("should succeed");
assert!(result.is_empty());
}
#[test]
fn test_subpixel_refiner_preserves_count() {
let image = vec![128u8; 64 * 64];
let refiner = SubPixelRefiner::default();
let kps = vec![
Keypoint::new(20.0, 20.0, 1.0, 0.0, 50.0),
Keypoint::new(40.0, 40.0, 1.0, 0.0, 80.0),
];
let result = refiner
.refine(&image, 64, 64, &kps)
.expect("should succeed");
assert_eq!(result.len(), 2);
}
#[test]
fn test_subpixel_refiner_on_gaussian_peak() {
let w = 128usize;
let h = 128usize;
let cx = 64.3_f64;
let cy = 64.7_f64;
let sigma = 8.0_f64;
let mut image = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
let dx = x as f64 - cx;
let dy = y as f64 - cy;
let val = 255.0 * (-0.5 * (dx * dx + dy * dy) / (sigma * sigma)).exp();
image[y * w + x] = val.round().min(255.0) as u8;
}
}
let refiner = SubPixelRefiner::new(5, 30, 0.001);
let kps = vec![Keypoint::new(62.0, 63.0, 1.0, 0.0, 100.0)];
let refined = refiner.refine(&image, w, h, &kps).expect("should succeed");
assert_eq!(refined.len(), 1);
let rp = &refined[0];
let dist_before = ((62.0 - cx).powi(2) + (63.0 - cy).powi(2)).sqrt();
let dist_after = ((rp.point.x - cx).powi(2) + (rp.point.y - cy).powi(2)).sqrt();
assert!(
dist_after <= dist_before + 0.5,
"refinement should improve or maintain: before={dist_before:.3}, after={dist_after:.3}"
);
}
#[test]
fn test_subpixel_refiner_border_keypoint() {
let image = vec![128u8; 32 * 32];
let refiner = SubPixelRefiner::default();
let kps = vec![Keypoint::new(1.0, 1.0, 1.0, 0.0, 50.0)];
let result = refiner
.refine(&image, 32, 32, &kps)
.expect("should succeed");
assert_eq!(result.len(), 1);
assert!((result[0].point.x - 1.0).abs() < 1e-10);
assert!((result[0].point.y - 1.0).abs() < 1e-10);
}
#[test]
fn test_subpixel_refiner_image_size_mismatch() {
let refiner = SubPixelRefiner::default();
let result = refiner.refine(&[0u8; 100], 20, 20, &[]);
assert!(result.is_err());
}
#[test]
fn test_sobel_gradients_constant() {
let image = vec![100u8; 32 * 32];
let (gx, gy) = compute_sobel_gradients(&image, 32, 32);
for y in 2..30 {
for x in 2..30 {
assert!(gx[y * 32 + x].abs() < 1e-10);
assert!(gy[y * 32 + x].abs() < 1e-10);
}
}
}
#[test]
fn test_sobel_gradients_horizontal_ramp() {
let w = 32usize;
let h = 32usize;
let mut image = vec![0u8; w * h];
for y in 0..h {
for x in 0..w {
image[y * w + x] = (x * 8).min(255) as u8;
}
}
let (gx, _gy) = compute_sobel_gradients(&image, w, h);
let mid = 16 * w + 16;
assert!(gx[mid] > 0.0, "horizontal ramp should produce positive gx");
}
#[test]
fn test_hamming_simd_identical() {
let a = [0xAA_u8; 32];
assert_eq!(hamming_distance_simd(&a, &a), 0);
}
#[test]
fn test_hamming_simd_all_differ() {
let a = [0xFF_u8; 32];
let b = [0x00_u8; 32];
assert_eq!(hamming_distance_simd(&a, &b), 256);
}
#[test]
fn test_hamming_simd_known_value() {
let mut a = [0u8; 32];
let mut b = [0u8; 32];
a[0] = 0b1111_0000; b[0] = 0b0000_1111; assert_eq!(hamming_distance_simd(&a, &b), 8);
}
#[test]
fn test_hamming_simd_single_bit() {
let a = [0u8; 16];
let mut b = [0u8; 16];
b[15] = 1;
assert_eq!(hamming_distance_simd(&a, &b), 1);
}
#[test]
fn test_hamming_simd_matches_byte_method() {
let desc_a = BinaryDescriptor::new([0x5A; 32]);
let desc_b = BinaryDescriptor::new([0xA5; 32]);
let byte_result = desc_a.hamming_distance(&desc_b);
let simd_result = hamming_distance_simd(&desc_a.data, &desc_b.data);
assert_eq!(byte_result, simd_result);
}
#[test]
fn test_hamming_simd_non_multiple_of_8_length() {
let a = vec![0xFF_u8; 11];
let b = vec![0x00_u8; 11];
assert_eq!(hamming_distance_simd(&a, &b), 88); }
#[test]
fn test_hamming_simd_symmetry() {
let a: Vec<u8> = (0..32).map(|i| i as u8).collect();
let b: Vec<u8> = (0..32).map(|i| (i * 7 + 3) as u8).collect();
assert_eq!(hamming_distance_simd(&a, &b), hamming_distance_simd(&b, &a));
}
}