#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct MatchPair {
pub query_idx: usize,
pub train_idx: usize,
pub dist: f32,
}
impl MatchPair {
#[must_use]
pub fn new(query_idx: usize, train_idx: usize, dist: f32) -> Self {
Self {
query_idx,
train_idx,
dist: dist.max(0.0),
}
}
#[must_use]
pub fn distance(&self) -> f32 {
self.dist
}
#[must_use]
pub fn is_good(&self, max_dist: f32) -> bool {
self.dist <= max_dist
}
}
#[derive(Debug, Clone, Default)]
pub struct FeatureMatch {
pub all_matches: Vec<MatchPair>,
}
impl FeatureMatch {
#[must_use]
pub fn new() -> Self {
Self {
all_matches: Vec::new(),
}
}
#[must_use]
pub fn good_matches(&self, max_dist: f32) -> Vec<&MatchPair> {
self.all_matches
.iter()
.filter(|m| m.is_good(max_dist))
.collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.all_matches.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.all_matches.is_empty()
}
#[must_use]
pub fn min_distance(&self) -> f32 {
self.all_matches
.iter()
.map(|m| m.dist)
.fold(f32::MAX, f32::min)
}
}
pub struct FeatureMatcher {
pub cross_check: bool,
pub ratio_threshold: f32,
}
impl FeatureMatcher {
#[must_use]
pub fn new(cross_check: bool, ratio_threshold: f32) -> Self {
Self {
cross_check,
ratio_threshold: ratio_threshold.clamp(0.0, 1.0),
}
}
#[must_use]
fn l2(a: &[f32], b: &[f32]) -> f32 {
let sum: f32 = a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum();
sum.sqrt()
}
#[must_use]
pub fn match_descriptors(&self, query: &[f32], train: &[f32], desc_len: usize) -> FeatureMatch {
if desc_len == 0 || query.len() % desc_len != 0 || train.len() % desc_len != 0 {
return FeatureMatch::new();
}
let n_query = query.len() / desc_len;
let n_train = train.len() / desc_len;
let mut result = FeatureMatch::new();
if n_query == 0 || n_train == 0 {
return result;
}
for qi in 0..n_query {
let qd = &query[qi * desc_len..(qi + 1) * desc_len];
let mut best_dist = f32::MAX;
let mut best_ti = 0usize;
let mut second_dist = f32::MAX;
for ti in 0..n_train {
let td = &train[ti * desc_len..(ti + 1) * desc_len];
let d = Self::l2(qd, td);
if d < best_dist {
second_dist = best_dist;
best_dist = d;
best_ti = ti;
} else if d < second_dist {
second_dist = d;
}
}
result
.all_matches
.push(MatchPair::new(qi, best_ti, best_dist));
let _ = second_dist; }
result
}
#[must_use]
pub fn ratio_test<'a>(
&self,
matches: &'a FeatureMatch,
query: &[f32],
train: &[f32],
desc_len: usize,
) -> Vec<&'a MatchPair> {
if desc_len == 0 || self.ratio_threshold <= 0.0 {
return matches.all_matches.iter().collect();
}
if query.len() % desc_len != 0 || train.len() % desc_len != 0 {
return Vec::new();
}
let n_train = train.len() / desc_len;
matches
.all_matches
.iter()
.filter(|m| {
let qd = &query[m.query_idx * desc_len..(m.query_idx + 1) * desc_len];
let mut second_dist = f32::MAX;
for ti in 0..n_train {
if ti == m.train_idx {
continue;
}
let td = &train[ti * desc_len..(ti + 1) * desc_len];
let d = Self::l2(qd, td);
if d < second_dist {
second_dist = d;
}
}
#[allow(clippy::float_cmp)]
if second_dist == f32::MAX || second_dist == 0.0 {
return true;
}
m.dist / second_dist < self.ratio_threshold
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct SubPixelMatch {
pub query_idx: usize,
pub train_idx: usize,
pub query_pos_int: (f32, f32),
pub train_pos_int: (f32, f32),
pub query_pos_sub: (f64, f64),
pub train_pos_sub: (f64, f64),
pub refinement_error: f64,
pub dist: f32,
}
pub struct SubPixelRefiner {
window_half: usize,
max_displacement: f64,
}
impl SubPixelRefiner {
#[must_use]
pub fn new() -> Self {
Self {
window_half: 3,
max_displacement: 1.5,
}
}
pub fn set_window_half(&mut self, half: usize) {
self.window_half = half.clamp(1, 8);
}
pub fn set_max_displacement(&mut self, max: f64) {
self.max_displacement = max.clamp(0.1, 3.0);
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn refine(
&self,
matches: &FeatureMatch,
kps_query: &[(f32, f32)],
kps_train: &[(f32, f32)],
img_query: &[f32],
query_w: usize,
query_h: usize,
img_train: &[f32],
train_w: usize,
train_h: usize,
) -> Vec<SubPixelMatch> {
matches
.all_matches
.iter()
.filter_map(|m| {
let qi = m.query_idx;
let ti = m.train_idx;
if qi >= kps_query.len() || ti >= kps_train.len() {
return None;
}
let (qx, qy) = kps_query[qi];
let (tx, ty) = kps_train[ti];
let (qxs, qys, q_err) =
self.refine_point(img_query, query_w, query_h, qx as f64, qy as f64);
let (txs, tys, t_err) =
self.refine_point(img_train, train_w, train_h, tx as f64, ty as f64);
let refinement_error = (q_err * q_err + t_err * t_err).sqrt();
Some(SubPixelMatch {
query_idx: qi,
train_idx: ti,
query_pos_int: (qx, qy),
train_pos_int: (tx, ty),
query_pos_sub: (qxs, qys),
train_pos_sub: (txs, tys),
refinement_error,
dist: m.dist,
})
})
.collect()
}
fn refine_point(
&self,
image: &[f32],
width: usize,
height: usize,
cx: f64,
cy: f64,
) -> (f64, f64, f64) {
let ix = cx.round() as usize;
let iy = cy.round() as usize;
if ix < self.window_half
|| iy < self.window_half
|| ix + self.window_half >= width
|| iy + self.window_half >= height
{
return (cx, cy, 0.0);
}
let get = |x: usize, y: usize| image[y * width + x] as f64;
let c = get(ix, iy);
let l = get(ix - 1, iy);
let r = get(ix + 1, iy);
let u = get(ix, iy - 1);
let d = get(ix, iy + 1);
let dxx = r - 2.0 * c + l;
let dyy = d - 2.0 * c + u;
let dx = (r - l) * 0.5;
let dy = (d - u) * 0.5;
let shift_x = if dxx.abs() > 1e-6 {
(-dx / (2.0 * dxx)).clamp(-self.max_displacement, self.max_displacement)
} else {
0.0
};
let shift_y = if dyy.abs() > 1e-6 {
(-dy / (2.0 * dyy)).clamp(-self.max_displacement, self.max_displacement)
} else {
0.0
};
let refined_x = cx + shift_x;
let refined_y = cy + shift_y;
let displacement = (shift_x * shift_x + shift_y * shift_y).sqrt();
(refined_x, refined_y, displacement)
}
}
impl Default for SubPixelRefiner {
fn default() -> Self {
Self::new()
}
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn match_with_subpixel(
query: &[f32],
train: &[f32],
desc_len: usize,
kps_query: &[(f32, f32)],
kps_train: &[(f32, f32)],
img_query: &[f32],
qw: usize,
qh: usize,
img_train: &[f32],
tw: usize,
th: usize,
) -> Vec<SubPixelMatch> {
let matcher = FeatureMatcher::new(false, 0.0);
let coarse = matcher.match_descriptors(query, train, desc_len);
let refiner = SubPixelRefiner::new();
refiner.refine(
&coarse, kps_query, kps_train, img_query, qw, qh, img_train, tw, th,
)
}
#[derive(Debug, Clone)]
pub struct RansacConfig {
pub inlier_threshold: f64,
pub max_iterations: usize,
pub min_inliers: usize,
}
impl Default for RansacConfig {
fn default() -> Self {
Self {
inlier_threshold: 3.0,
max_iterations: 1000,
min_inliers: 4,
}
}
}
#[derive(Debug, Clone)]
pub struct Homography {
pub m: [f64; 9],
}
impl Homography {
#[must_use]
pub fn identity() -> Self {
Self {
m: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
}
}
#[must_use]
pub fn project(&self, x: f64, y: f64) -> Option<(f64, f64)> {
let m = &self.m;
let wx = m[0] * x + m[1] * y + m[2];
let wy = m[3] * x + m[4] * y + m[5];
let w = m[6] * x + m[7] * y + m[8];
if w.abs() < 1e-12 {
return None;
}
Some((wx / w, wy / w))
}
#[must_use]
pub fn reprojection_error(&self, qx: f64, qy: f64, tx: f64, ty: f64) -> f64 {
let fwd = self
.project(qx, qy)
.map(|(px, py)| ((px - tx).powi(2) + (py - ty).powi(2)).sqrt())
.unwrap_or(f64::MAX);
fwd
}
}
pub struct HomographyEstimator {
cfg: RansacConfig,
}
impl HomographyEstimator {
#[must_use]
pub fn new(cfg: RansacConfig) -> Self {
Self { cfg }
}
#[must_use]
pub fn estimate(&self, matches: &[SubPixelMatch]) -> Option<(Homography, usize)> {
if matches.len() < 4 {
return None;
}
let mut best_h = Homography::identity();
let mut best_inliers = 0usize;
let n = matches.len();
let step = (n / self.cfg.max_iterations).max(1);
let mut iter_count = 0usize;
let mut start = 0usize;
while iter_count < self.cfg.max_iterations && start < n {
let i0 = start % n;
let i1 = (start + 1) % n;
let i2 = (start + 2) % n;
let i3 = (start + 3) % n;
let pts = [&matches[i0], &matches[i1], &matches[i2], &matches[i3]];
if let Some(h) = Self::dlt_4point(pts) {
let inliers = matches
.iter()
.filter(|m| {
h.reprojection_error(
m.query_pos_sub.0,
m.query_pos_sub.1,
m.train_pos_sub.0,
m.train_pos_sub.1,
) < self.cfg.inlier_threshold
})
.count();
if inliers > best_inliers {
best_inliers = inliers;
best_h = h;
}
}
start += step;
iter_count += 1;
}
if best_inliers >= self.cfg.min_inliers {
Some((best_h, best_inliers))
} else {
None
}
}
fn dlt_4point(pts: [&SubPixelMatch; 4]) -> Option<Homography> {
let mut a = [[0.0f64; 9]; 8];
for (k, m) in pts.iter().enumerate() {
let (x, y) = m.query_pos_sub;
let (xp, yp) = m.train_pos_sub;
let row0 = k * 2;
let row1 = row0 + 1;
a[row0] = [-x, -y, -1.0, 0.0, 0.0, 0.0, xp * x, xp * y, xp];
a[row1] = [0.0, 0.0, 0.0, -x, -y, -1.0, yp * x, yp * y, yp];
}
let h_vec = gaussian_null_space(&mut a)?;
Some(Homography { m: h_vec })
}
}
impl Default for HomographyEstimator {
fn default() -> Self {
Self::new(RansacConfig::default())
}
}
fn gaussian_null_space(a: &mut [[f64; 9]; 8]) -> Option<[f64; 9]> {
let rows = 8usize;
let cols = 9usize;
for col in 0..cols - 1 {
let pivot = (col..rows).max_by(|&i, &j| {
a[i][col]
.abs()
.partial_cmp(&a[j][col].abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
let pivot = pivot?;
if a[pivot][col].abs() < 1e-12 {
continue;
}
a.swap(col.min(rows - 1), pivot);
let diag = a[col.min(rows - 1)][col];
for c in 0..cols {
a[col.min(rows - 1)][c] /= diag;
}
for r in 0..rows {
if r == col.min(rows - 1) {
continue;
}
let factor = a[r][col];
for c in 0..cols {
let sub = factor * a[col.min(rows - 1)][c];
a[r][c] -= sub;
}
}
}
let mut h = [0.0f64; 9];
for r in 0..8 {
h[r] = -a[r][8];
}
h[8] = 1.0;
Some(h)
}
#[must_use]
pub fn subpixel_match_quality(matches: &[SubPixelMatch], max_error: f64) -> f32 {
if matches.is_empty() {
return 0.0;
}
let good = matches
.iter()
.filter(|m| m.refinement_error < max_error)
.count();
good as f32 / matches.len() as f32
}
#[must_use]
pub fn filter_subpixel_matches(
matches: &[SubPixelMatch],
max_dist: f32,
max_refinement_error: f64,
) -> Vec<&SubPixelMatch> {
matches
.iter()
.filter(|m| m.dist <= max_dist && m.refinement_error <= max_refinement_error)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_match_pair_distance() {
let m = MatchPair::new(0, 1, 3.5);
assert!((m.distance() - 3.5).abs() < f32::EPSILON);
}
#[test]
fn test_match_pair_negative_dist_clamped() {
let m = MatchPair::new(0, 0, -1.0);
assert!((m.distance() - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_match_pair_is_good_true() {
let m = MatchPair::new(0, 1, 2.0);
assert!(m.is_good(3.0));
}
#[test]
fn test_match_pair_is_good_false() {
let m = MatchPair::new(0, 1, 4.0);
assert!(!m.is_good(3.0));
}
#[test]
fn test_feature_match_good_matches_filter() {
let mut fm = FeatureMatch::new();
fm.all_matches.push(MatchPair::new(0, 0, 1.0));
fm.all_matches.push(MatchPair::new(1, 1, 5.0));
let good = fm.good_matches(3.0);
assert_eq!(good.len(), 1);
}
#[test]
fn test_feature_match_min_distance_empty() {
let fm = FeatureMatch::new();
assert_eq!(fm.min_distance(), f32::MAX);
}
#[test]
fn test_feature_match_min_distance_non_empty() {
let mut fm = FeatureMatch::new();
fm.all_matches.push(MatchPair::new(0, 0, 3.0));
fm.all_matches.push(MatchPair::new(1, 1, 1.5));
assert!((fm.min_distance() - 1.5).abs() < f32::EPSILON);
}
#[test]
fn test_feature_matcher_match_descriptors_empty() {
let matcher = FeatureMatcher::new(false, 0.0);
let result = matcher.match_descriptors(&[], &[], 4);
assert!(result.is_empty());
}
#[test]
fn test_feature_matcher_match_descriptors_single() {
let matcher = FeatureMatcher::new(false, 0.0);
let query = vec![1.0_f32, 0.0, 0.0, 0.0];
let train = vec![1.0_f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0];
let result = matcher.match_descriptors(&query, &train, 4);
assert_eq!(result.len(), 1);
assert!((result.all_matches[0].dist - 0.0).abs() < 1e-5);
assert_eq!(result.all_matches[0].train_idx, 0);
}
#[test]
fn test_feature_matcher_match_descriptors_wrong_desc_len() {
let matcher = FeatureMatcher::new(false, 0.0);
let query = vec![1.0_f32; 5];
let train = vec![1.0_f32; 4];
let result = matcher.match_descriptors(&query, &train, 4);
assert!(result.is_empty());
}
#[test]
fn test_feature_matcher_ratio_test_all_pass_single_train() {
let matcher = FeatureMatcher::new(false, 0.75);
let query = vec![1.0_f32, 0.0];
let train = vec![1.0_f32, 0.0]; let fm = matcher.match_descriptors(&query, &train, 2);
let passed = matcher.ratio_test(&fm, &query, &train, 2);
assert_eq!(passed.len(), 1);
}
#[test]
fn test_feature_matcher_ratio_test_rejects_ambiguous() {
let matcher = FeatureMatcher::new(false, 0.75);
let query = vec![0.0_f32, 0.0, 0.0, 0.0];
let train = vec![
1.0_f32, 0.0, 0.0, 0.0, 1.1_f32, 0.0, 0.0, 0.0, ];
let fm = matcher.match_descriptors(&query, &train, 4);
let passed = matcher.ratio_test(&fm, &query, &train, 4);
assert_eq!(passed.len(), 0);
}
#[test]
fn test_subpixel_refiner_default() {
let refiner = SubPixelRefiner::new();
assert_eq!(refiner.window_half, 3);
}
#[test]
fn test_subpixel_refine_empty_matches() {
let refiner = SubPixelRefiner::new();
let fm = FeatureMatch::new();
let img = vec![0.5f32; 10 * 10];
let result = refiner.refine(&fm, &[], &[], &img, 10, 10, &img, 10, 10);
assert!(result.is_empty());
}
#[test]
fn test_subpixel_refine_single_match() {
let refiner = SubPixelRefiner::new();
let mut fm = FeatureMatch::new();
fm.all_matches.push(MatchPair::new(0, 0, 0.0));
let img = vec![0.5f32; 20 * 20];
let kps_q = vec![(10.0f32, 10.0f32)];
let kps_t = vec![(10.0f32, 10.0f32)];
let result = refiner.refine(&fm, &kps_q, &kps_t, &img, 20, 20, &img, 20, 20);
assert_eq!(result.len(), 1);
let m = &result[0];
assert!((m.query_pos_sub.0 - 10.0).abs() < 0.01);
assert!((m.query_pos_sub.1 - 10.0).abs() < 0.01);
}
#[test]
fn test_subpixel_refine_with_gradient() {
let refiner = SubPixelRefiner::new();
let mut fm = FeatureMatch::new();
fm.all_matches.push(MatchPair::new(0, 0, 0.5));
let w = 20usize;
let h = 20usize;
let mut img = vec![0.0f32; w * h];
for y in 0..h {
for x in 0..w {
let dx = x as f32 - 10.0;
let dy = y as f32 - 10.0;
img[y * w + x] = 1.0 - 0.01 * (dx * dx + dy * dy);
}
}
let kps_q = vec![(10.0f32, 10.0f32)];
let kps_t = vec![(10.0f32, 10.0f32)];
let result = refiner.refine(&fm, &kps_q, &kps_t, &img, w, h, &img, w, h);
assert_eq!(result.len(), 1);
assert!(result[0].refinement_error < 0.5);
}
#[test]
fn test_match_with_subpixel_empty() {
let result = match_with_subpixel(&[], &[], 4, &[], &[], &[], 0, 0, &[], 0, 0);
assert!(result.is_empty());
}
#[test]
fn test_homography_identity_projects_correctly() {
let h = Homography::identity();
let p = h.project(10.0, 20.0);
assert!(p.is_some());
let (px, py) = p.unwrap();
assert!((px - 10.0).abs() < 1e-10);
assert!((py - 20.0).abs() < 1e-10);
}
#[test]
fn test_homography_reprojection_error_identity() {
let h = Homography::identity();
let err = h.reprojection_error(5.0, 7.0, 5.0, 7.0);
assert!(
err < 1e-10,
"Identity reprojection error should be 0, got {err}"
);
}
#[test]
fn test_homography_reprojection_error_nonzero() {
let h = Homography::identity();
let err = h.reprojection_error(0.0, 0.0, 3.0, 4.0);
assert!((err - 5.0).abs() < 1e-6, "Expected error=5.0, got {err}");
}
#[test]
fn test_ransac_config_default() {
let cfg = RansacConfig::default();
assert!(cfg.inlier_threshold > 0.0);
assert!(cfg.max_iterations > 0);
assert!(cfg.min_inliers >= 4);
}
#[test]
fn test_homography_estimator_too_few_matches_returns_none() {
let est = HomographyEstimator::default();
let matches: Vec<SubPixelMatch> = (0..3)
.map(|i| SubPixelMatch {
query_idx: i,
train_idx: i,
query_pos_int: (i as f32, i as f32),
train_pos_int: (i as f32, i as f32),
query_pos_sub: (i as f64, i as f64),
train_pos_sub: (i as f64, i as f64),
refinement_error: 0.0,
dist: 0.0,
})
.collect();
assert!(est.estimate(&matches).is_none());
}
#[test]
fn test_homography_estimator_with_pure_translation() {
let tx = 5.0_f64;
let ty = -3.0_f64;
let points: Vec<(f64, f64)> = vec![
(10.0, 20.0),
(50.0, 30.0),
(10.0, 80.0),
(90.0, 20.0),
(60.0, 60.0),
];
let matches: Vec<SubPixelMatch> = points
.iter()
.enumerate()
.map(|(i, &(qx, qy))| SubPixelMatch {
query_idx: i,
train_idx: i,
query_pos_int: (qx as f32, qy as f32),
train_pos_int: ((qx + tx) as f32, (qy + ty) as f32),
query_pos_sub: (qx, qy),
train_pos_sub: (qx + tx, qy + ty),
refinement_error: 0.0,
dist: 0.1,
})
.collect();
let mut cfg = RansacConfig::default();
cfg.max_iterations = 50;
cfg.min_inliers = 4;
let est = HomographyEstimator::new(cfg);
if let Some((_, inliers)) = est.estimate(&matches) {
assert!(inliers >= 4, "Expected >=4 inliers, got {inliers}");
}
}
#[test]
fn test_subpixel_match_quality_empty_returns_zero() {
let q = subpixel_match_quality(&[], 1.0);
assert!((q - 0.0).abs() < f32::EPSILON);
}
#[test]
fn test_subpixel_match_quality_all_below_threshold() {
let matches: Vec<SubPixelMatch> = (0..5)
.map(|i| SubPixelMatch {
query_idx: i,
train_idx: i,
query_pos_int: (0.0, 0.0),
train_pos_int: (0.0, 0.0),
query_pos_sub: (0.0, 0.0),
train_pos_sub: (0.0, 0.0),
refinement_error: 0.1, dist: 0.0,
})
.collect();
let q = subpixel_match_quality(&matches, 0.5);
assert!(
(q - 1.0).abs() < f32::EPSILON,
"All matches below threshold → quality=1.0, got {q}"
);
}
#[test]
fn test_subpixel_match_quality_none_below_threshold() {
let matches: Vec<SubPixelMatch> = (0..5)
.map(|i| SubPixelMatch {
query_idx: i,
train_idx: i,
query_pos_int: (0.0, 0.0),
train_pos_int: (0.0, 0.0),
query_pos_sub: (0.0, 0.0),
train_pos_sub: (0.0, 0.0),
refinement_error: 2.0, dist: 0.0,
})
.collect();
let q = subpixel_match_quality(&matches, 0.5);
assert!(
(q - 0.0).abs() < f32::EPSILON,
"No matches below threshold → quality=0.0, got {q}"
);
}
#[test]
fn test_filter_subpixel_matches_all_pass() {
let matches: Vec<SubPixelMatch> = (0..4)
.map(|i| SubPixelMatch {
query_idx: i,
train_idx: i,
query_pos_int: (0.0, 0.0),
train_pos_int: (0.0, 0.0),
query_pos_sub: (0.0, 0.0),
train_pos_sub: (0.0, 0.0),
refinement_error: 0.2,
dist: 1.0,
})
.collect();
let filtered = filter_subpixel_matches(&matches, 2.0, 1.0);
assert_eq!(filtered.len(), 4);
}
#[test]
fn test_filter_subpixel_matches_by_dist() {
let matches: Vec<SubPixelMatch> = vec![
SubPixelMatch {
query_idx: 0,
train_idx: 0,
query_pos_int: (0.0, 0.0),
train_pos_int: (0.0, 0.0),
query_pos_sub: (0.0, 0.0),
train_pos_sub: (0.0, 0.0),
refinement_error: 0.0,
dist: 0.5, },
SubPixelMatch {
query_idx: 1,
train_idx: 1,
query_pos_int: (0.0, 0.0),
train_pos_int: (0.0, 0.0),
query_pos_sub: (0.0, 0.0),
train_pos_sub: (0.0, 0.0),
refinement_error: 0.0,
dist: 5.0, },
];
let filtered = filter_subpixel_matches(&matches, 2.0, 1.0);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].query_idx, 0);
}
#[test]
fn test_filter_subpixel_matches_by_refinement_error() {
let matches: Vec<SubPixelMatch> = vec![
SubPixelMatch {
query_idx: 0,
train_idx: 0,
query_pos_int: (0.0, 0.0),
train_pos_int: (0.0, 0.0),
query_pos_sub: (0.0, 0.0),
train_pos_sub: (0.0, 0.0),
refinement_error: 0.1,
dist: 1.0, },
SubPixelMatch {
query_idx: 1,
train_idx: 1,
query_pos_int: (0.0, 0.0),
train_pos_int: (0.0, 0.0),
query_pos_sub: (0.0, 0.0),
train_pos_sub: (0.0, 0.0),
refinement_error: 3.0,
dist: 1.0, },
];
let filtered = filter_subpixel_matches(&matches, 2.0, 1.5);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].query_idx, 0);
}
#[test]
fn test_subpixel_refiner_set_window_and_displacement() {
let mut refiner = SubPixelRefiner::new();
refiner.set_window_half(5);
refiner.set_max_displacement(1.0);
assert_eq!(refiner.window_half, 5);
assert!((refiner.max_displacement - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_subpixel_refiner_out_of_bounds_keypoint_returns_original() {
let refiner = SubPixelRefiner::new();
let mut fm = FeatureMatch::new();
fm.all_matches.push(MatchPair::new(0, 0, 0.0));
let img = vec![0.5f32; 10 * 10];
let kps_q = vec![(0.0f32, 0.0f32)]; let kps_t = vec![(5.0f32, 5.0f32)];
let result = refiner.refine(&fm, &kps_q, &kps_t, &img, 10, 10, &img, 10, 10);
assert_eq!(result.len(), 1);
assert!((result[0].query_pos_sub.0 - 0.0).abs() < 0.5);
}
}