use crate::core::{Feature, ColmapError, Point2};
use crate::feature::FeatureDetector;
use image::{GrayImage, Luma};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct FastConfig {
pub threshold: u8,
pub non_max_suppression: bool,
pub max_features: usize,
pub min_arc_length: usize,
}
impl Default for FastConfig {
fn default() -> Self {
Self {
threshold: 20,
non_max_suppression: true,
max_features: 1000,
min_arc_length: 9,
}
}
}
#[derive(Debug, Clone)]
pub struct FastDetector {
config: FastConfig,
circle_offsets: Vec<(i32, i32)>,
}
impl FastDetector {
pub fn new(config: FastConfig) -> Self {
let circle_offsets = vec![
(0, -3), (1, -3), (2, -2), (3, -1),
(3, 0), (3, 1), (2, 2), (1, 3),
(0, 3), (-1, 3), (-2, 2), (-3, 1),
(-3, 0), (-3, -1), (-2, -2), (-1, -3),
];
Self {
config,
circle_offsets,
}
}
fn is_corner(&self, image: &GrayImage, x: u32, y: u32) -> Option<u8> {
let (width, height) = image.dimensions();
let center_intensity = image.get_pixel(x, y).0[0];
if x < 3 || y < 3 || x >= width - 3 || y >= height - 3 {
return None;
}
let mut circle_intensities = Vec::with_capacity(16);
for &(dx, dy) in &self.circle_offsets {
let px = (x as i32 + dx) as u32;
let py = (y as i32 + dy) as u32;
circle_intensities.push(image.get_pixel(px, py).0[0]);
}
let threshold = self.config.threshold;
let min_arc = self.config.min_arc_length;
if self.check_arc(&circle_intensities, center_intensity, threshold, min_arc, true) {
return Some(center_intensity);
}
if self.check_arc(&circle_intensities, center_intensity, threshold, min_arc, false) {
return Some(center_intensity);
}
None
}
fn check_arc(&self, intensities: &[u8], center: u8, threshold: u8, min_arc: usize, brighter: bool) -> bool {
let mut max_arc_length = 0;
let mut current_arc_length = 0;
for i in 0..(intensities.len() * 2) {
let idx = i % intensities.len();
let pixel_intensity = intensities[idx];
let condition = if brighter {
pixel_intensity > center.saturating_add(threshold)
} else {
pixel_intensity < center.saturating_sub(threshold)
};
if condition {
current_arc_length += 1;
max_arc_length = max_arc_length.max(current_arc_length);
} else {
current_arc_length = 0;
}
if max_arc_length >= min_arc {
return true;
}
}
false
}
fn compute_response(&self, image: &GrayImage, x: u32, y: u32) -> f32 {
let center_intensity = image.get_pixel(x, y).0[0] as f32;
let mut response = 0.0;
for &(dx, dy) in &self.circle_offsets {
let px = (x as i32 + dx) as u32;
let py = (y as i32 + dy) as u32;
let pixel_intensity = image.get_pixel(px, py).0[0] as f32;
response += (pixel_intensity - center_intensity).abs();
}
response / self.circle_offsets.len() as f32
}
fn non_max_suppression(&self, corners: &[(u32, u32, f32)]) -> Vec<(u32, u32, f32)> {
if !self.config.non_max_suppression {
return corners.to_vec();
}
let mut suppressed = vec![false; corners.len()];
let mut result = Vec::new();
let mut sorted_indices: Vec<usize> = (0..corners.len()).collect();
sorted_indices.sort_by(|&a, &b| corners[b].2.partial_cmp(&corners[a].2).unwrap());
for &i in &sorted_indices {
if suppressed[i] {
continue;
}
let (x1, y1, response1) = corners[i];
result.push((x1, y1, response1));
for j in 0..corners.len() {
if i == j || suppressed[j] {
continue;
}
let (x2, y2, _) = corners[j];
let dx = (x1 as i32 - x2 as i32).abs();
let dy = (y1 as i32 - y2 as i32).abs();
if dx <= 3 && dy <= 3 {
suppressed[j] = true;
}
}
}
result
}
}
impl FeatureDetector for FastDetector {
fn detect(&self, image: &GrayImage) -> Result<Vec<Feature>, ColmapError> {
let (width, height) = image.dimensions();
let mut corners = Vec::new();
for y in 3..height-3 {
for x in 3..width-3 {
if let Some(_intensity) = self.is_corner(image, x, y) {
let response = self.compute_response(image, x, y);
corners.push((x, y, response));
}
}
}
let suppressed_corners = self.non_max_suppression(&corners);
let mut final_corners = suppressed_corners;
if final_corners.len() > self.config.max_features {
final_corners.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap());
final_corners.truncate(self.config.max_features);
}
let features = final_corners
.into_iter()
.map(|(x, y, response)| Feature {
point: Point2::new(x as f64, y as f64),
descriptor: Vec::new(), scale: 1.0,
angle: 0.0,
response,
octave: 0,
point3d_id: None,
})
.collect();
Ok(features)
}
fn name(&self) -> &str {
"FAST"
}
fn params(&self) -> HashMap<String, f64> {
let mut params = HashMap::new();
params.insert("threshold".to_string(), self.config.threshold as f64);
params.insert("non_max_suppression".to_string(), if self.config.non_max_suppression { 1.0 } else { 0.0 });
params.insert("max_features".to_string(), self.config.max_features as f64);
params.insert("min_arc_length".to_string(), self.config.min_arc_length as f64);
params
}
fn set_params(&mut self, params: HashMap<String, f64>) -> Result<(), ColmapError> {
if let Some(threshold) = params.get("threshold") {
self.config.threshold = *threshold as u8;
}
if let Some(nms) = params.get("non_max_suppression") {
self.config.non_max_suppression = *nms > 0.5;
}
if let Some(max_features) = params.get("max_features") {
self.config.max_features = *max_features as usize;
}
if let Some(min_arc) = params.get("min_arc_length") {
self.config.min_arc_length = *min_arc as usize;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use image::{ImageBuffer, Luma};
fn create_test_image() -> GrayImage {
let mut image = ImageBuffer::from_pixel(50, 50, Luma([50]));
for y in 20..30 {
for x in 23..27 {
image.put_pixel(x, y, Luma([255]));
}
}
for y in 23..27 {
for x in 20..30 {
image.put_pixel(x, y, Luma([255]));
}
}
image
}
#[test]
fn test_fast_detector_creation() {
let config = FastConfig::default();
let detector = FastDetector::new(config);
assert_eq!(detector.name(), "FAST");
assert_eq!(detector.circle_offsets.len(), 16);
}
#[test]
fn test_fast_detection() {
let config = FastConfig::default();
let detector = FastDetector::new(config);
let image = create_test_image();
let features = detector.detect(&image).unwrap();
assert!(!features.is_empty(), "应该检测到特征点");
for feature in &features {
assert!(feature.point.x >= 0.0);
assert!(feature.point.y >= 0.0);
assert!(feature.response >= 0.0);
}
}
#[test]
fn test_corner_detection() {
let config = FastConfig {
threshold: 10, ..Default::default()
};
let detector = FastDetector::new(config);
let image = create_test_image();
let features = detector.detect(&image).unwrap();
if !features.is_empty() {
println!("检测到 {} 个特征点", features.len());
return;
}
let result = detector.is_corner(&image, 10, 10);
assert!(result.is_none(), "在平坦区域不应该检测到角点");
}
#[test]
fn test_response_computation() {
let config = FastConfig::default();
let detector = FastDetector::new(config);
let image = create_test_image();
let response = detector.compute_response(&image, 25, 25);
assert!(response > 0.0, "角点应该有正的响应值");
}
#[test]
fn test_non_max_suppression() {
let config = FastConfig {
non_max_suppression: true,
..Default::default()
};
let detector = FastDetector::new(config);
let corners = vec![
(10, 10, 100.0),
(11, 10, 90.0), (20, 20, 120.0),
(21, 21, 80.0), ];
let suppressed = detector.non_max_suppression(&corners);
assert_eq!(suppressed.len(), 2, "应该保留2个角点");
assert_eq!(suppressed[0].2, 120.0, "应该保留最强的角点");
assert_eq!(suppressed[1].2, 100.0, "应该保留第二强的角点");
}
#[test]
fn test_params_get_set() {
let config = FastConfig::default();
let mut detector = FastDetector::new(config);
let params = detector.params();
assert_eq!(params["threshold"], 20.0);
assert_eq!(params["max_features"], 1000.0);
let mut new_params = HashMap::new();
new_params.insert("threshold".to_string(), 30.0);
new_params.insert("max_features".to_string(), 500.0);
detector.set_params(new_params).unwrap();
assert_eq!(detector.config.threshold, 30);
assert_eq!(detector.config.max_features, 500);
}
#[test]
fn test_arc_checking() {
let config = FastConfig::default();
let detector = FastDetector::new(config);
let intensities = vec![200, 200, 200, 200, 200, 200, 200, 200, 200, 100, 100, 100, 100, 100, 100, 100];
let center = 128;
let has_bright_arc = detector.check_arc(&intensities, center, 20, 9, true);
assert!(has_bright_arc, "应该检测到亮点弧");
let has_dark_arc = detector.check_arc(&intensities, center, 20, 9, false);
assert!(!has_dark_arc, "不应该检测到暗点弧");
}
}