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] }
}
}
#[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 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());
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());
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());
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);
}
}