use crate::circular_feature_descriptor::CircularFeatureDescriptor;
use crate::circular_feature_grid::CircularFeatureGrid;
use crate::kdtree::{KdTree, Point};
use std::time::Duration;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
pub const DEFAULT_MATCH_STRIDE: usize = 4;
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(not(feature = "bench"), allow(dead_code))]
pub(crate) enum MatcherBackend {
BruteForce,
KdTree { stride: usize, parallel: bool },
}
#[cfg(feature = "bench")]
#[derive(Clone, Copy)]
struct Stopwatch(std::time::Instant);
#[cfg(feature = "bench")]
impl Stopwatch {
fn start() -> Self {
Stopwatch(std::time::Instant::now())
}
fn elapsed(&self) -> Duration {
self.0.elapsed()
}
}
#[cfg(not(feature = "bench"))]
#[derive(Clone, Copy)]
struct Stopwatch;
#[cfg(not(feature = "bench"))]
impl Stopwatch {
fn start() -> Self {
Stopwatch
}
fn elapsed(&self) -> Duration {
Duration::ZERO
}
}
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(not(feature = "bench"), allow(dead_code))]
pub(crate) struct MatchTiming {
pub build: Duration,
pub query: Duration,
}
pub const fn parallelism_available() -> bool {
cfg!(feature = "parallel")
}
pub struct CircularFeatureDescriptorMatcher;
impl CircularFeatureDescriptorMatcher {
pub fn new() -> Self {
CircularFeatureDescriptorMatcher {}
}
pub fn match_areas(
&self,
img1: &CircularFeatureGrid,
img2: &CircularFeatureGrid,
) -> Vec<FeatureMatch> {
self.match_areas_with_stride(img1, img2, DEFAULT_MATCH_STRIDE)
}
#[cfg(feature = "bench")]
fn stride_override() -> Option<usize> {
std::env::var("PIXELMAP_MATCH_STRIDE")
.ok()
.and_then(|v| v.parse().ok())
}
pub fn match_areas_with_stride(
&self,
img1: &CircularFeatureGrid,
img2: &CircularFeatureGrid,
stride: usize,
) -> Vec<FeatureMatch> {
#[cfg(feature = "bench")]
let stride = Self::stride_override().unwrap_or(stride);
let backend = MatcherBackend::KdTree {
stride: stride.max(1),
parallel: true,
};
self.match_areas_timed(img1, img2, backend).0
}
pub(crate) fn match_areas_timed(
&self,
img1: &CircularFeatureGrid,
img2: &CircularFeatureGrid,
backend: MatcherBackend,
) -> (Vec<FeatureMatch>, MatchTiming) {
match backend {
MatcherBackend::BruteForce => Self::brute_force(img1, img2),
MatcherBackend::KdTree { stride, parallel } => {
Self::kdtree_search(img1, img2, stride.max(1), parallel)
}
}
}
fn brute_force(
img1: &CircularFeatureGrid,
img2: &CircularFeatureGrid,
) -> (Vec<FeatureMatch>, MatchTiming) {
let (infos1, infos2) = (img1.get_infos(), img2.get_infos());
let query_at = |i: usize| -> Option<FeatureMatch> {
let cai2 = &infos2[i];
let mut best: Option<(u64, usize)> = None;
for (j, cai1) in infos1.iter().enumerate() {
let mut sum = 0i64;
for k in 0..6 {
let d = cai1.feature_vector[k] as i64 - cai2.feature_vector[k] as i64;
sum += d * d;
}
let d2 = sum as u64;
if best.map_or(true, |(bd, _)| d2 < bd) {
best = Some((d2, j));
}
}
best.map(|(_, j)| FeatureMatch::new(&infos1[j], cai2))
};
let t = Stopwatch::start();
let ans = par_query(infos2.len(), 1, &query_at);
(
ans,
MatchTiming {
build: Duration::ZERO,
query: t.elapsed(),
},
)
}
fn kdtree_search(
img1: &CircularFeatureGrid,
img2: &CircularFeatureGrid,
stride: usize,
parallel: bool,
) -> (Vec<FeatureMatch>, MatchTiming) {
let (infos1, infos2) = (img1.get_infos(), img2.get_infos());
let parallel = parallel && parallelism_available();
let t0 = Stopwatch::start();
let points = infos1
.iter()
.enumerate()
.map(|(i, d)| Point {
v: d.feature_vector,
id: i as u32,
})
.collect();
let tree = if parallel {
KdTree::build(points)
} else {
KdTree::build_serial(points)
};
let build = t0.elapsed();
let query_at = |i: usize| -> Option<FeatureMatch> {
let cai2 = &infos2[i];
tree.nearest(&cai2.feature_vector)
.map(|found| FeatureMatch::new(&infos1[found.id as usize], cai2))
};
let t1 = Stopwatch::start();
let ans = if parallel {
par_query(infos2.len(), stride, &query_at)
} else {
(0..infos2.len())
.step_by(stride)
.filter_map(query_at)
.collect()
};
(
ans,
MatchTiming {
build,
query: t1.elapsed(),
},
)
}
}
#[cfg(feature = "parallel")]
fn par_query<F>(len: usize, stride: usize, query_at: &F) -> Vec<FeatureMatch>
where
F: Fn(usize) -> Option<FeatureMatch> + Sync,
{
(0..len)
.into_par_iter()
.step_by(stride)
.filter_map(query_at)
.collect()
}
#[cfg(not(feature = "parallel"))]
fn par_query<F>(len: usize, stride: usize, query_at: &F) -> Vec<FeatureMatch>
where
F: Fn(usize) -> Option<FeatureMatch>,
{
(0..len).step_by(stride).filter_map(query_at).collect()
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FeatureMatch {
pub x1: u16,
pub y1: u16,
pub x2: u16,
pub y2: u16,
pub angle_delta: f32,
}
impl FeatureMatch {
#[inline]
fn new(cai1: &CircularFeatureDescriptor, cai2: &CircularFeatureDescriptor) -> Self {
FeatureMatch {
x1: cai1.center_x,
y1: cai1.center_y,
x2: cai2.center_x,
y2: cai2.center_y,
angle_delta: cai1.total_angle - cai2.total_angle,
}
}
}