use crate::analyze::peak::{Extremum, interpolate_peak};
use crate::features::Corner;
use crate::image::RasterImage;
use crate::pixel::SingleChannel;
use crate::{Coordinate, CoordinateF64, Error};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(transparent)]
pub struct NmsRadius(usize);
impl NmsRadius {
#[must_use]
#[inline]
pub const fn new(radius: usize) -> Option<Self> {
if radius == 0 {
return None;
}
Some(Self(radius))
}
pub fn try_new(radius: usize) -> Result<Self, Error> {
if radius == 0 {
return Err(Error::InvalidParameter(
"window radius must be at least 1".to_string(),
));
}
Ok(Self(radius))
}
#[must_use]
#[inline]
pub const fn get(self) -> usize {
self.0
}
}
#[must_use]
pub fn corner_peaks<I, P>(response: &I, threshold: f32, radius: usize) -> Vec<Corner>
where
I: RasterImage<Pixel = P>,
P: SingleChannel,
P::Channel: PartialOrd + From<f32>,
f64: From<P::Channel>,
{
scan_peaks(response, threshold, radius)
.into_iter()
.map(|(at, response)| Corner::new(at, response))
.collect()
}
pub fn interpolate_corners<I, P>(corners: &mut [Corner], response: &I) -> usize
where
I: RasterImage<Pixel = P>,
P: SingleChannel,
f64: From<P::Channel>,
{
let mut fitted = 0;
for corner in corners {
let Some(at) = pixel_site(corner.at) else {
continue;
};
if let Some(vertex) = interpolate_peak(response, at, Extremum::Maximum) {
corner.at = vertex;
fitted += 1;
}
}
fitted
}
#[inline]
pub(super) fn pixel_site(at: CoordinateF64) -> Option<Coordinate> {
let (x, y) = (at.x.round(), at.y.round());
if x >= 0.0 && y >= 0.0 && x.is_finite() && y.is_finite() {
Some(Coordinate::new(x as usize, y as usize))
} else {
None
}
}
pub(super) fn scan_peaks<I, P>(
response: &I,
threshold: f32,
radius: usize,
) -> Vec<(CoordinateF64, f32)>
where
I: RasterImage<Pixel = P>,
P: SingleChannel,
P::Channel: PartialOrd + From<f32>,
f64: From<P::Channel>,
{
let threshold = <P::Channel as From<f32>>::from(threshold);
let (w, h) = (response.width(), response.height());
let mut peaks = Vec::new();
for y in 0..h {
for x in 0..w {
let value = response.row(y)[x].channel(0);
if value >= threshold && is_local_max(response, x, y, radius, value) {
let at = CoordinateF64::new(x as f64, y as f64);
let response_f32 = f64::from(value) as f32;
peaks.push((at, response_f32));
}
}
}
peaks
}
pub(super) fn lift_peaks(
level: &impl crate::image::Decimated,
peaks: Vec<(CoordinateF64, f32)>,
) -> Vec<Corner> {
peaks
.into_iter()
.map(|(local, response)| Corner::from_level(level, local, response))
.collect()
}
#[inline]
fn is_local_max<I, P>(response: &I, x: usize, y: usize, radius: usize, value: P::Channel) -> bool
where
I: RasterImage<Pixel = P>,
P: SingleChannel,
P::Channel: PartialOrd,
{
let (w, h) = (response.width(), response.height());
let y_lo = y.saturating_sub(radius);
let y_hi = (y + radius + 1).min(h);
let x_lo = x.saturating_sub(radius);
let x_hi = (x + radius + 1).min(w);
for ny in y_lo..y_hi {
let row = response.row(ny);
for (offset, pixel) in row[x_lo..x_hi].iter().enumerate() {
let nx = x_lo + offset;
if nx == x && ny == y {
continue;
}
let neighbour = pixel.channel(0);
let earlier = ny < y || (ny == y && nx < x);
let survives = if earlier {
value > neighbour
} else {
value >= neighbour
};
if !survives {
return false;
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::features::{HasPosition, HasResponse};
use crate::image::Image;
use crate::pixel::MonoF32;
fn positions(corners: &[Corner]) -> Vec<(f64, f64)> {
corners
.iter()
.map(|c| (c.position().x, c.position().y))
.collect()
}
#[test]
fn peaks_are_returned_in_raster_order() {
let response: Image<MonoF32> = Image::generate(9, 9, |x, y| {
MonoF32::new(match (x, y) {
(6, 2) => 0.4, (2, 6) => 0.9,
_ => 0.0,
})
});
let corners = corner_peaks(&response, 0.1, 2);
assert_eq!(positions(&corners), [(6.0, 2.0), (2.0, 6.0)]);
assert_eq!(corners[0].response(), 0.4);
}
#[test]
fn peaks_below_the_threshold_are_dropped() {
let response: Image<MonoF32> = Image::generate(9, 9, |x, y| {
MonoF32::new(if (x, y) == (4, 4) { 0.05 } else { 0.0 })
});
assert!(corner_peaks(&response, 0.1, 2).is_empty());
assert_eq!(corner_peaks(&response, 0.05, 2).len(), 1);
}
#[test]
fn a_plateau_yields_exactly_one_peak() {
let response: Image<MonoF32> = Image::generate(9, 9, |x, y| {
MonoF32::new(if (3..5).contains(&x) && (3..5).contains(&y) {
1.0
} else {
0.0
})
});
let corners = corner_peaks(&response, 0.5, 2);
assert_eq!(corners.len(), 1);
assert_eq!(corners[0].position(), CoordinateF64::new(3.0, 3.0));
}
#[test]
fn a_uniform_response_map_yields_a_single_peak() {
let response: Image<MonoF32> = Image::fill(8, 8, MonoF32::new(1.0));
let corners = corner_peaks(&response, 0.5, 2);
assert_eq!(corners.len(), 1);
assert_eq!(corners[0].position(), CoordinateF64::new(0.0, 0.0));
}
#[test]
fn tied_groups_further_apart_than_the_radius_are_separate_peaks() {
let response: Image<MonoF32> = Image::generate(12, 3, |x, y| {
let tied = (y == 1) && ((2..4).contains(&x) || (8..10).contains(&x));
MonoF32::new(if tied { 1.0 } else { 0.0 })
});
let corners = corner_peaks(&response, 0.5, 2);
assert_eq!(positions(&corners), [(2.0, 1.0), (8.0, 1.0)]);
}
#[test]
fn the_suppression_radius_sets_the_minimum_separation() {
let response: Image<MonoF32> = Image::generate(12, 3, |x, y| {
MonoF32::new(match (x, y) {
(3, 1) => 1.0,
(7, 1) => 0.8,
_ => 0.0,
})
});
assert_eq!(corner_peaks(&response, 0.1, 3).len(), 2);
let merged = corner_peaks(&response, 0.1, 4);
assert_eq!(merged.len(), 1);
assert_eq!(merged[0].position(), CoordinateF64::new(3.0, 1.0));
}
#[test]
fn a_peak_against_the_border_is_reported() {
let response: Image<MonoF32> = Image::generate(6, 6, |x, y| {
MonoF32::new(if (x, y) == (0, 0) { 1.0 } else { 0.0 })
});
let corners = corner_peaks(&response, 0.5, 2);
assert_eq!(corners.len(), 1);
assert_eq!(corners[0].position(), CoordinateF64::new(0.0, 0.0));
}
#[test]
fn a_nan_response_neither_wins_nor_survives() {
let response: Image<MonoF32> = Image::generate(7, 7, |x, y| {
MonoF32::new(match (x, y) {
(3, 3) => f32::NAN,
(5, 5) => 1.0,
_ => 0.0,
})
});
assert_eq!(positions(&corner_peaks(&response, 0.5, 1)), [(5.0, 5.0)]);
}
#[test]
fn a_nan_neighbour_suppresses_a_peak() {
let response: Image<MonoF32> = Image::generate(7, 7, |x, y| {
MonoF32::new(match (x, y) {
(3, 3) => 1.0,
(4, 3) => f32::NAN,
_ => 0.0,
})
});
assert!(corner_peaks(&response, 0.5, 1).is_empty());
}
#[test]
fn an_empty_response_map_yields_no_peaks() {
let response: Image<MonoF32> = Image::fill(5, 5, MonoF32::new(0.0));
assert!(corner_peaks(&response, 0.5, 2).is_empty());
}
fn crest_at(w: usize, h: usize, cx: f64, cy: f64) -> Image<MonoF32> {
Image::generate(w, h, |x, y| {
let (dx, dy) = (x as f64 - cx, y as f64 - cy);
MonoF32::new((1.0 - 0.05 * (dx * dx + dy * dy)) as f32)
})
}
#[test]
fn interpolation_moves_a_corner_onto_the_response_crest() {
let response = crest_at(11, 11, 4.25, 5.6);
let mut corners = corner_peaks(&response, 0.5, 2);
assert_eq!(positions(&corners), [(4.0, 6.0)], "the discrete winner");
assert_eq!(interpolate_corners(&mut corners, &response), 1);
let at = corners[0].position();
assert!((at.x - 4.25).abs() < 1e-3, "{at:?}");
assert!((at.y - 5.60).abs() < 1e-3, "{at:?}");
}
#[test]
fn interpolation_leaves_the_response_alone() {
let response = crest_at(11, 11, 4.25, 5.6);
let mut corners = corner_peaks(&response, 0.5, 2);
let before = corners[0].response();
interpolate_corners(&mut corners, &response);
assert_eq!(
corners[0].response(),
before,
"the value at the vertex is not one the detector measured",
);
}
#[test]
fn a_corner_against_the_border_stays_put() {
let response: Image<MonoF32> = Image::generate(6, 6, |x, y| {
MonoF32::new(if (x, y) == (0, 0) { 1.0 } else { 0.0 })
});
let mut corners = corner_peaks(&response, 0.5, 2);
assert_eq!(positions(&corners), [(0.0, 0.0)]);
assert_eq!(interpolate_corners(&mut corners, &response), 0);
assert_eq!(positions(&corners), [(0.0, 0.0)]);
}
#[test]
fn a_saturated_plateau_is_pulled_toward_its_interior() {
let response: Image<MonoF32> = Image::generate(9, 9, |x, y| {
MonoF32::new(if (3..6).contains(&x) && (3..6).contains(&y) {
1.0
} else {
0.0
})
});
let mut corners = corner_peaks(&response, 0.5, 3);
assert_eq!(positions(&corners), [(3.0, 3.0)]);
assert_eq!(interpolate_corners(&mut corners, &response), 1);
let at = corners[0].position();
assert!(at.x > 3.0 && at.y > 3.0, "moved inward: {at:?}");
assert!(at.x < 4.0 && at.y < 4.0, "bounded by the window: {at:?}");
}
#[test]
fn a_corner_inside_a_flat_region_is_refused() {
let response: Image<MonoF32> = Image::fill(7, 7, MonoF32::new(1.0));
let mut corners = vec![Corner::new(CoordinateF64::new(3.0, 3.0), 1.0)];
assert_eq!(interpolate_corners(&mut corners, &response), 0);
assert_eq!(corners[0].position(), CoordinateF64::new(3.0, 3.0));
}
#[test]
fn the_count_reports_only_the_fits_that_succeeded() {
let response: Image<MonoF32> = Image::generate(13, 7, |x, y| {
let (dx, dy) = (x as f64 - 8.4, y as f64 - 3.0);
let crest = 1.0 - 0.08 * (dx * dx + dy * dy);
MonoF32::new(if (x, y) == (0, 0) {
0.9
} else {
crest.max(0.0) as f32
})
});
let mut corners = corner_peaks(&response, 0.5, 2);
assert_eq!(corners.len(), 2, "{corners:?}");
assert_eq!(interpolate_corners(&mut corners, &response), 1);
assert_eq!(corners[0].position(), CoordinateF64::new(0.0, 0.0));
assert!((corners[1].position().x - 8.4).abs() < 1e-3, "{corners:?}");
}
#[test]
fn interpolating_no_corners_is_no_work() {
let response = crest_at(9, 9, 4.0, 4.0);
assert_eq!(interpolate_corners(&mut [], &response), 0);
}
#[test]
fn a_position_off_the_map_is_left_alone() {
let response = crest_at(9, 9, 4.25, 4.0);
let mut corners = vec![
Corner::new(CoordinateF64::new(40.0, 40.0), 0.9),
Corner::new(CoordinateF64::new(-3.0, 2.0), 0.9),
];
assert_eq!(interpolate_corners(&mut corners, &response), 0);
assert_eq!(corners[0].position(), CoordinateF64::new(40.0, 40.0));
assert_eq!(corners[1].position(), CoordinateF64::new(-3.0, 2.0));
}
}