use core::cmp::Ordering;
use crate::image::{Decimated, ScaleLevel};
use crate::{CoordinateF64, Orientation, Sigma};
pub trait HasPosition {
fn position(&self) -> CoordinateF64;
}
pub trait HasResponse {
fn response(&self) -> f32;
}
pub trait HasScale {
fn scale(&self) -> Sigma;
}
pub trait HasOrientation {
fn orientation(&self) -> Orientation;
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Corner {
pub at: CoordinateF64,
pub response: f32,
}
impl Corner {
pub fn new(at: CoordinateF64, response: f32) -> Self {
Self { at, response }
}
pub fn from_level(level: &impl Decimated, local: CoordinateF64, response: f32) -> Self {
Self {
at: level.to_base(local),
response,
}
}
}
impl HasPosition for Corner {
#[inline]
fn position(&self) -> CoordinateF64 {
self.at
}
}
impl HasResponse for Corner {
#[inline]
fn response(&self) -> f32 {
self.response
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScaleKeypoint {
pub at: CoordinateF64,
pub response: f32,
pub scale: Sigma,
}
impl ScaleKeypoint {
pub fn new(at: CoordinateF64, response: f32, scale: Sigma) -> Self {
Self {
at,
response,
scale,
}
}
pub fn from_level<L>(level: &L, local: CoordinateF64, response: f32) -> Self
where
L: Decimated + ScaleLevel,
{
Self {
at: level.to_base(local),
response,
scale: level.sigma(),
}
}
}
impl HasPosition for ScaleKeypoint {
#[inline]
fn position(&self) -> CoordinateF64 {
self.at
}
}
impl HasResponse for ScaleKeypoint {
#[inline]
fn response(&self) -> f32 {
self.response
}
}
impl HasScale for ScaleKeypoint {
#[inline]
fn scale(&self) -> Sigma {
self.scale
}
}
pub fn by_response_then_position<K>(a: &K, b: &K) -> Ordering
where
K: HasPosition + HasResponse,
{
b.response().total_cmp(&a.response()).then_with(|| {
let (pa, pb) = (a.position(), b.position());
pa.y.total_cmp(&pb.y).then_with(|| pa.x.total_cmp(&pb.x))
})
}
pub fn sort_by_response<K>(keypoints: &mut [K])
where
K: HasPosition + HasResponse,
{
keypoints.sort_by(by_response_then_position);
}
pub fn retain_top_n<K>(keypoints: &mut Vec<K>, n: usize)
where
K: HasPosition + HasResponse,
{
sort_by_response(keypoints);
keypoints.truncate(n);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::PixelDistance;
use crate::image::{Image, OriginOffset, ScaledImage};
use crate::pixel::MonoF32;
use crate::sigma;
fn level(distance: f64, offset: (f64, f64), sigma: f32) -> ScaledImage<MonoF32> {
ScaledImage::new(
Image::<MonoF32>::zero(4, 4),
PixelDistance::new(distance).unwrap(),
OriginOffset::try_new(offset.0, offset.1).unwrap(),
Sigma::new(sigma).unwrap(),
)
}
#[test]
fn corner_new_reports_its_fields() {
let corner = Corner::new(CoordinateF64::new(1.5, 2.5), 0.75);
assert_eq!(corner.position(), CoordinateF64::new(1.5, 2.5));
assert_eq!(corner.response(), 0.75);
assert_eq!(corner.at, CoordinateF64::new(1.5, 2.5));
assert_eq!(corner.response, 0.75);
}
#[test]
fn corner_from_level_lifts_position() {
let corner = Corner::from_level(
&level(2.0, (0.0, 0.0), 1.0),
CoordinateF64::new(3.5, 2.0),
0.7,
);
assert_eq!(corner.position(), CoordinateF64::new(7.0, 4.0));
assert_eq!(corner.response(), 0.7);
}
#[test]
fn corner_from_level_applies_the_origin_offset() {
let corner = Corner::from_level(
&level(2.0, (0.5, 0.5), 1.0),
CoordinateF64::new(2.0, 1.0),
0.1,
);
assert_eq!(corner.position(), CoordinateF64::new(4.5, 2.5));
}
#[test]
fn corner_from_level_on_the_base_level_is_the_identity() {
let corner = Corner::from_level(
&level(1.0, (0.0, 0.0), 0.5),
CoordinateF64::new(6.25, 7.75),
0.3,
);
assert_eq!(corner.position(), CoordinateF64::new(6.25, 7.75));
}
#[test]
fn corner_from_level_handles_an_upsampled_level() {
let corner = Corner::from_level(
&level(0.5, (0.0, 0.0), 0.8),
CoordinateF64::new(6.0, 10.0),
0.2,
);
assert_eq!(corner.position(), CoordinateF64::new(3.0, 5.0));
}
#[test]
fn corner_is_copy_and_comparable() {
let a = Corner::new(CoordinateF64::new(1.0, 1.0), 0.5);
let b = a; assert_eq!(a, b);
assert_ne!(a, Corner::new(CoordinateF64::new(1.0, 1.0), 0.6));
assert!(format!("{a:?}").contains("Corner"));
}
#[test]
fn scale_keypoint_new_reports_its_fields() {
let kp = ScaleKeypoint::new(CoordinateF64::new(2.0, 3.0), 0.4, sigma!(1.6));
assert_eq!(kp.position(), CoordinateF64::new(2.0, 3.0));
assert_eq!(kp.response(), 0.4);
assert_eq!(kp.scale(), sigma!(1.6));
}
#[test]
fn scale_keypoint_from_level_takes_position_and_sigma_from_the_level() {
let kp = ScaleKeypoint::from_level(
&level(2.0, (0.0, 0.0), 1.6),
CoordinateF64::new(1.0, 2.5),
0.5,
);
assert_eq!(kp.position(), CoordinateF64::new(2.0, 5.0));
assert_eq!(kp.scale(), sigma!(1.6));
assert_eq!(kp.response(), 0.5);
}
#[test]
fn scale_keypoint_from_level_sigma_is_absolute_not_rescaled() {
let coarse = ScaleKeypoint::from_level(
&level(4.0, (0.0, 0.0), 3.2),
CoordinateF64::new(0.0, 0.0),
1.0,
);
assert_eq!(coarse.scale(), sigma!(3.2));
}
#[test]
fn scale_keypoint_is_copy_and_comparable() {
let a = ScaleKeypoint::new(CoordinateF64::new(1.0, 1.0), 0.5, sigma!(1.0));
let b = a;
assert_eq!(a, b);
assert_ne!(
a,
ScaleKeypoint::new(CoordinateF64::new(1.0, 1.0), 0.5, sigma!(2.0))
);
}
#[test]
fn ordering_puts_the_strongest_response_first() {
let strong = Corner::new(CoordinateF64::new(0.0, 9.0), 0.9);
let weak = Corner::new(CoordinateF64::new(0.0, 0.0), 0.1);
assert_eq!(by_response_then_position(&strong, &weak), Ordering::Less);
assert_eq!(by_response_then_position(&weak, &strong), Ordering::Greater);
}
#[test]
fn ordering_breaks_response_ties_on_y_then_x() {
let r = 0.5;
let upper = Corner::new(CoordinateF64::new(9.0, 1.0), r);
let lower = Corner::new(CoordinateF64::new(0.0, 2.0), r);
assert_eq!(by_response_then_position(&upper, &lower), Ordering::Less);
let left = Corner::new(CoordinateF64::new(1.0, 5.0), r);
let right = Corner::new(CoordinateF64::new(2.0, 5.0), r);
assert_eq!(by_response_then_position(&left, &right), Ordering::Less);
}
#[test]
fn ordering_is_equal_for_identical_keypoints() {
let a = Corner::new(CoordinateF64::new(3.0, 4.0), 0.5);
assert_eq!(by_response_then_position(&a, &a), Ordering::Equal);
}
#[test]
fn ordering_of_negative_responses_is_still_descending() {
let better = Corner::new(CoordinateF64::new(0.0, 0.0), -0.1);
let worse = Corner::new(CoordinateF64::new(0.0, 0.0), -0.9);
assert_eq!(by_response_then_position(&better, &worse), Ordering::Less);
}
#[test]
fn sort_by_response_orders_descending() {
let mut corners = vec![
Corner::new(CoordinateF64::new(0.0, 0.0), 0.2),
Corner::new(CoordinateF64::new(1.0, 1.0), 0.9),
Corner::new(CoordinateF64::new(2.0, 2.0), 0.5),
];
sort_by_response(&mut corners);
let responses: Vec<f32> = corners.iter().map(HasResponse::response).collect();
assert_eq!(responses, [0.9, 0.5, 0.2]);
}
#[test]
fn sort_is_independent_of_input_order() {
let a = Corner::new(CoordinateF64::new(4.0, 1.0), 0.5);
let b = Corner::new(CoordinateF64::new(2.0, 1.0), 0.5);
let c = Corner::new(CoordinateF64::new(0.0, 7.0), 0.5);
let mut forward = vec![a, b, c];
let mut reverse = vec![c, b, a];
sort_by_response(&mut forward);
sort_by_response(&mut reverse);
assert_eq!(forward, reverse);
assert_eq!(forward, vec![b, a, c]);
}
#[test]
fn sort_handles_a_nan_response_without_losing_keypoints() {
let mut corners = vec![
Corner::new(CoordinateF64::new(0.0, 0.0), 0.5),
Corner::new(CoordinateF64::new(1.0, 1.0), f32::NAN),
Corner::new(CoordinateF64::new(2.0, 2.0), 0.1),
];
sort_by_response(&mut corners);
assert_eq!(corners.len(), 3);
assert!(corners[0].response().is_nan());
assert_eq!(corners[1].response(), 0.5);
assert_eq!(corners[2].response(), 0.1);
}
#[test]
fn sort_of_empty_and_single_slices() {
let mut empty: Vec<Corner> = vec![];
sort_by_response(&mut empty);
assert!(empty.is_empty());
let mut one = vec![Corner::new(CoordinateF64::new(1.0, 2.0), 0.3)];
sort_by_response(&mut one);
assert_eq!(one.len(), 1);
}
#[test]
fn sort_works_for_scale_keypoints_too() {
let mut kps = vec![
ScaleKeypoint::new(CoordinateF64::new(0.0, 0.0), 0.1, sigma!(1.0)),
ScaleKeypoint::new(CoordinateF64::new(0.0, 0.0), 0.8, sigma!(2.0)),
];
sort_by_response(&mut kps);
assert_eq!(kps[0].scale(), sigma!(2.0));
}
#[test]
fn sort_is_stable_for_fully_tied_keypoints() {
let coarse = ScaleKeypoint::new(CoordinateF64::new(1.0, 1.0), 0.5, sigma!(3.2));
let fine = ScaleKeypoint::new(CoordinateF64::new(1.0, 1.0), 0.5, sigma!(1.6));
let mut kps = vec![coarse, fine];
sort_by_response(&mut kps);
assert_eq!(kps, vec![coarse, fine]);
}
#[test]
fn retain_top_n_keeps_the_strongest() {
let mut corners = vec![
Corner::new(CoordinateF64::new(0.0, 0.0), 0.2),
Corner::new(CoordinateF64::new(1.0, 1.0), 0.9),
Corner::new(CoordinateF64::new(2.0, 2.0), 0.5),
];
retain_top_n(&mut corners, 2);
let responses: Vec<f32> = corners.iter().map(HasResponse::response).collect();
assert_eq!(responses, [0.9, 0.5]);
}
#[test]
fn retain_top_n_resolves_a_tie_at_the_cut_deterministically() {
let mut corners = vec![
Corner::new(CoordinateF64::new(9.0, 9.0), 0.1),
Corner::new(CoordinateF64::new(0.0, 5.0), 0.6),
Corner::new(CoordinateF64::new(0.0, 3.0), 0.6),
];
retain_top_n(&mut corners, 2);
let ys: Vec<f64> = corners.iter().map(|c| c.position().y).collect();
assert_eq!(ys, [3.0, 5.0]);
}
#[test]
fn retain_top_n_with_n_over_length_keeps_everything_sorted() {
let mut corners = vec![
Corner::new(CoordinateF64::new(0.0, 0.0), 0.2),
Corner::new(CoordinateF64::new(1.0, 1.0), 0.9),
];
retain_top_n(&mut corners, 10);
assert_eq!(corners.len(), 2);
assert_eq!(corners[0].response(), 0.9);
}
#[test]
fn retain_top_n_with_zero_empties() {
let mut corners = vec![Corner::new(CoordinateF64::new(0.0, 0.0), 0.2)];
retain_top_n(&mut corners, 0);
assert!(corners.is_empty());
}
#[test]
fn retain_top_n_on_empty_input() {
let mut corners: Vec<Corner> = vec![];
retain_top_n(&mut corners, 5);
assert!(corners.is_empty());
}
#[test]
fn a_function_can_bind_the_minimum_capability_it_needs() {
fn patch_center(kp: &impl HasPosition) -> (f64, f64) {
let p = kp.position();
(p.x, p.y)
}
fn patch_radius(kp: &(impl HasPosition + HasScale)) -> f64 {
3.0 * f64::from(kp.scale().get())
}
let corner = Corner::new(CoordinateF64::new(2.0, 4.0), 0.5);
let kp = ScaleKeypoint::new(CoordinateF64::new(2.0, 4.0), 0.5, sigma!(2.0));
assert_eq!(patch_center(&corner), (2.0, 4.0));
assert_eq!(patch_center(&kp), (2.0, 4.0));
assert_eq!(patch_radius(&kp), 6.0);
}
}