use crate::core::image_view::OwnedImage;
use crate::core::nms::{non_maximum_suppression, NmsConfig, Peak};
use crate::core::polarity::Polarity;
use crate::core::scalar::Scalar;
use super::seed::{Proposal, ProposalSource, SeedPoint};
#[derive(Debug)]
pub struct ResponseMap {
data: OwnedImage<Scalar>,
source: ProposalSource,
}
impl ResponseMap {
pub fn new(data: OwnedImage<Scalar>, source: ProposalSource) -> Self {
Self { data, source }
}
pub fn view(&self) -> crate::core::image_view::ImageView<'_, Scalar> {
self.data.view()
}
pub fn response(&self) -> &OwnedImage<Scalar> {
&self.data
}
pub fn source(&self) -> ProposalSource {
self.source
}
pub fn into_response(self) -> OwnedImage<Scalar> {
self.data
}
}
pub fn extract_proposals(
response: &ResponseMap,
nms_config: &NmsConfig,
polarity: Polarity,
) -> Vec<Proposal> {
let peaks: Vec<Peak> = non_maximum_suppression(&response.view(), nms_config);
peaks
.into_iter()
.map(|peak| Proposal {
seed: SeedPoint {
position: peak.position,
score: peak.score,
},
scale_hint: None,
polarity,
source: response.source(),
})
.collect()
}
pub fn suppress_proposals_by_distance(
proposals: &[Proposal],
min_distance: Scalar,
max_detections: usize,
) -> Vec<Proposal> {
if proposals.is_empty() || max_detections == 0 {
return Vec::new();
}
let min_distance_sq = min_distance.max(0.0).powi(2);
let mut kept: Vec<Proposal> = Vec::with_capacity(proposals.len().min(max_detections));
'proposal: for proposal in proposals {
let position = proposal.seed.position;
for other in &kept {
let dx = position.x - other.seed.position.x;
let dy = position.y - other.seed.position.y;
if dx * dx + dy * dy < min_distance_sq {
continue 'proposal;
}
}
kept.push(proposal.clone());
if kept.len() == max_detections {
break;
}
}
kept
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::image_view::OwnedImage;
#[test]
fn extract_from_response_map() {
let mut response_data = OwnedImage::<Scalar>::zeros(30, 30).unwrap();
*response_data.get_mut(10, 10).unwrap() = 5.0;
*response_data.get_mut(20, 20).unwrap() = 3.0;
let response = ResponseMap::new(response_data, ProposalSource::Frst);
let nms = NmsConfig {
radius: 3,
threshold: 1.0,
max_detections: 10,
};
let proposals = extract_proposals(&response, &nms, Polarity::Both);
assert_eq!(proposals.len(), 2);
assert_eq!(proposals[0].seed.score, 5.0);
assert_eq!(proposals[1].seed.score, 3.0);
assert_eq!(proposals[0].source, ProposalSource::Frst);
assert_eq!(proposals[0].polarity, Polarity::Both);
}
#[test]
fn extract_respects_budget() {
let mut response_data = OwnedImage::<Scalar>::zeros(40, 40).unwrap();
*response_data.get_mut(5, 5).unwrap() = 1.0;
*response_data.get_mut(15, 15).unwrap() = 2.0;
*response_data.get_mut(30, 30).unwrap() = 3.0;
let response = ResponseMap::new(response_data, ProposalSource::Frst);
let nms = NmsConfig {
radius: 2,
threshold: 0.5,
max_detections: 2,
};
let proposals = extract_proposals(&response, &nms, Polarity::Bright);
assert_eq!(proposals.len(), 2);
assert_eq!(proposals[0].seed.score, 3.0);
assert_eq!(proposals[1].seed.score, 2.0);
}
#[test]
fn suppress_proposals_by_distance_keeps_strongest_per_cluster() {
let proposals = vec![
Proposal {
seed: SeedPoint {
position: crate::core::coords::PixelCoord::new(10.0, 10.0),
score: 5.0,
},
scale_hint: Some(12.0),
polarity: Polarity::Dark,
source: ProposalSource::Frst,
},
Proposal {
seed: SeedPoint {
position: crate::core::coords::PixelCoord::new(14.0, 12.0),
score: 4.0,
},
scale_hint: Some(12.0),
polarity: Polarity::Dark,
source: ProposalSource::Frst,
},
Proposal {
seed: SeedPoint {
position: crate::core::coords::PixelCoord::new(40.0, 40.0),
score: 3.0,
},
scale_hint: Some(12.0),
polarity: Polarity::Dark,
source: ProposalSource::Frst,
},
];
let kept = suppress_proposals_by_distance(&proposals, 6.0, 8);
assert_eq!(kept.len(), 2);
assert_eq!(kept[0].seed.position.x, 10.0);
assert_eq!(kept[0].seed.position.y, 10.0);
assert_eq!(kept[1].seed.position.x, 40.0);
assert_eq!(kept[1].seed.position.y, 40.0);
}
}