use super::simd_accelerated;
use crate::*;
use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
use std::f64::consts::PI;
#[derive(Debug, Clone, PartialEq)]
pub struct SIFTKeypoint {
pub x: f64,
pub y: f64,
pub scale: f64,
pub orientation: f64,
pub response: f64,
pub octave: usize,
pub layer: usize,
}
#[derive(Debug, Clone)]
pub struct SIFTExtractor {
n_octaves: usize,
n_scales_per_octave: usize,
sigma: f64,
edge_threshold: f64,
peak_threshold: f64,
orientation_bins: usize,
orientation_window_factor: f64,
}
impl SIFTExtractor {
pub fn new() -> Self {
Self {
n_octaves: 4,
n_scales_per_octave: 3,
sigma: 1.6,
edge_threshold: 10.0,
peak_threshold: 0.03,
orientation_bins: 36,
orientation_window_factor: 1.5,
}
}
pub fn n_octaves(mut self, n_octaves: usize) -> Self {
self.n_octaves = n_octaves.clamp(1, 8); self
}
pub fn n_scales_per_octave(mut self, n_scales_per_octave: usize) -> Self {
self.n_scales_per_octave = n_scales_per_octave.clamp(2, 8);
self
}
pub fn sigma(mut self, sigma: f64) -> Self {
self.sigma = sigma.clamp(0.5, 4.0);
self
}
pub fn edge_threshold(mut self, edge_threshold: f64) -> Self {
self.edge_threshold = edge_threshold.clamp(5.0, 50.0);
self
}
pub fn peak_threshold(mut self, peak_threshold: f64) -> Self {
self.peak_threshold = peak_threshold.clamp(0.01, 0.2);
self
}
pub fn detect_keypoints(&self, image: &ArrayView2<Float>) -> SklResult<Vec<SIFTKeypoint>> {
let (height, width) = image.dim();
if height < 16 || width < 16 {
return Err(SklearsError::InvalidInput(
"Image too small for SIFT detection (minimum 16x16)".to_string(),
));
}
let scale_space = self.build_scale_space(image)?;
let dog_space = self.build_dog_space(&scale_space)?;
let mut keypoints = self.detect_extrema(&dog_space)?;
keypoints = self.filter_keypoints(keypoints, &dog_space)?;
self.assign_orientations(&mut keypoints, &scale_space)?;
Ok(keypoints)
}
pub fn extract_descriptors(
&self,
image: &ArrayView2<Float>,
keypoints: &[SIFTKeypoint],
) -> SklResult<Array2<Float>> {
if keypoints.is_empty() {
return Ok(Array2::zeros((0, 128))); }
let scale_space = self.build_scale_space(image)?;
let mut descriptors = Array2::zeros((keypoints.len(), 128));
for (i, keypoint) in keypoints.iter().enumerate() {
let descriptor = self.compute_descriptor(keypoint, &scale_space)?;
descriptors.row_mut(i).assign(&descriptor);
}
Ok(descriptors)
}
fn build_scale_space(&self, image: &ArrayView2<Float>) -> SklResult<Vec<Vec<Array2<Float>>>> {
let mut scale_space = Vec::new();
let mut current_image = image.to_owned();
for octave in 0..self.n_octaves {
let mut octave_images = Vec::new();
for scale in 0..=self.n_scales_per_octave + 2 {
let sigma_scale =
self.sigma * 2.0_f64.powf(scale as f64 / self.n_scales_per_octave as f64);
let blurred = self.gaussian_blur(¤t_image.view(), sigma_scale)?;
octave_images.push(blurred);
}
scale_space.push(octave_images);
if octave < self.n_octaves - 1 {
let (height, width) = current_image.dim();
let new_height = height / 2;
let new_width = width / 2;
if new_height < 8 || new_width < 8 {
break; }
let mut downsampled = Array2::zeros((new_height, new_width));
for y in 0..new_height {
for x in 0..new_width {
downsampled[[y, x]] = current_image[[y * 2, x * 2]];
}
}
current_image = downsampled;
}
}
Ok(scale_space)
}
fn build_dog_space(
&self,
scale_space: &[Vec<Array2<Float>>],
) -> SklResult<Vec<Vec<Array2<Float>>>> {
let mut dog_space = Vec::new();
for octave_images in scale_space {
let mut dog_octave = Vec::new();
for i in 0..octave_images.len() - 1 {
let dog_image = simd_accelerated::simd_array_subtraction(
&octave_images[i + 1].view(),
&octave_images[i].view(),
)?;
dog_octave.push(dog_image);
}
dog_space.push(dog_octave);
}
Ok(dog_space)
}
fn detect_extrema(&self, dog_space: &[Vec<Array2<Float>>]) -> SklResult<Vec<SIFTKeypoint>> {
let mut keypoints = Vec::new();
for (octave_idx, dog_octave) in dog_space.iter().enumerate() {
if dog_octave.len() < 3 {
continue; }
for layer_idx in 1..dog_octave.len() - 1 {
let below = &dog_octave[layer_idx - 1];
let center = &dog_octave[layer_idx];
let above = &dog_octave[layer_idx + 1];
let extrema = simd_accelerated::simd_detect_extrema(
&below.view(),
¢er.view(),
&above.view(),
self.peak_threshold,
);
for (x, y, _is_maximum) in extrema {
let scale = self.sigma
* 2.0_f64.powf(
(octave_idx * self.n_scales_per_octave + layer_idx) as f64
/ self.n_scales_per_octave as f64,
);
let keypoint = SIFTKeypoint {
x: x as f64 * 2.0_f64.powi(octave_idx as i32),
y: y as f64 * 2.0_f64.powi(octave_idx as i32),
scale,
orientation: 0.0, response: center[[y, x]].abs(),
octave: octave_idx,
layer: layer_idx,
};
keypoints.push(keypoint);
}
}
}
Ok(keypoints)
}
fn filter_keypoints(
&self,
keypoints: Vec<SIFTKeypoint>,
dog_space: &[Vec<Array2<Float>>],
) -> SklResult<Vec<SIFTKeypoint>> {
let mut filtered = Vec::new();
for keypoint in keypoints {
if keypoint.octave >= dog_space.len()
|| keypoint.layer >= dog_space[keypoint.octave].len()
{
continue;
}
let dog_image = &dog_space[keypoint.octave][keypoint.layer];
let (height, width) = dog_image.dim();
let x = (keypoint.x / 2.0_f64.powi(keypoint.octave as i32)) as usize;
let y = (keypoint.y / 2.0_f64.powi(keypoint.octave as i32)) as usize;
if x == 0 || y == 0 || x >= width - 1 || y >= height - 1 {
continue;
}
if self.is_edge_response(dog_image, x, y) {
continue;
}
if dog_image[[y, x]].abs() < self.peak_threshold {
continue;
}
filtered.push(keypoint);
}
Ok(filtered)
}
fn is_edge_response(&self, dog_image: &Array2<Float>, x: usize, y: usize) -> bool {
let dxx = dog_image[[y, x + 1]] + dog_image[[y, x - 1]] - 2.0 * dog_image[[y, x]];
let dyy = dog_image[[y + 1, x]] + dog_image[[y - 1, x]] - 2.0 * dog_image[[y, x]];
let dxy =
(dog_image[[y + 1, x + 1]] - dog_image[[y + 1, x - 1]] - dog_image[[y - 1, x + 1]]
+ dog_image[[y - 1, x - 1]])
/ 4.0;
let trace = dxx + dyy;
let det = dxx * dyy - dxy * dxy;
if det <= 0.0 {
return true; }
let ratio = trace * trace / det;
let threshold_ratio = (self.edge_threshold + 1.0).powi(2) / self.edge_threshold;
ratio > threshold_ratio
}
fn assign_orientations(
&self,
keypoints: &mut [SIFTKeypoint],
scale_space: &[Vec<Array2<Float>>],
) -> SklResult<()> {
for keypoint in keypoints.iter_mut() {
if keypoint.octave >= scale_space.len()
|| keypoint.layer >= scale_space[keypoint.octave].len()
{
continue;
}
let image = &scale_space[keypoint.octave][keypoint.layer];
let orientation = self.compute_dominant_orientation(image, keypoint)?;
keypoint.orientation = orientation;
}
Ok(())
}
fn compute_dominant_orientation(
&self,
image: &Array2<Float>,
keypoint: &SIFTKeypoint,
) -> SklResult<f64> {
let (height, width) = image.dim();
let scale_factor = 2.0_f64.powi(keypoint.octave as i32);
let x = (keypoint.x / scale_factor) as usize;
let y = (keypoint.y / scale_factor) as usize;
if x == 0 || y == 0 || x >= width - 1 || y >= height - 1 {
return Ok(0.0);
}
let mut histogram = vec![0.0; self.orientation_bins];
let sigma = self.orientation_window_factor * keypoint.scale / scale_factor;
let radius = (3.0 * sigma) as usize;
for dy in -(radius as i32)..=(radius as i32) {
for dx in -(radius as i32)..=(radius as i32) {
let px = x as i32 + dx;
let py = y as i32 + dy;
if px <= 0 || py <= 0 || px >= (width - 1) as i32 || py >= (height - 1) as i32 {
continue;
}
let px = px as usize;
let py = py as usize;
let gx = image[[py, px + 1]] - image[[py, px - 1]];
let gy = image[[py + 1, px]] - image[[py - 1, px]];
let magnitude = (gx * gx + gy * gy).sqrt();
let orientation = gy.atan2(gx);
let weight =
magnitude * (-((dx * dx + dy * dy) as f64) / (2.0 * sigma * sigma)).exp();
let bin = ((orientation + PI) / (2.0 * PI) * self.orientation_bins as f64) as usize
% self.orientation_bins;
histogram[bin] += weight;
}
}
let max_bin = histogram
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
Ok(max_bin as f64 * 2.0 * PI / self.orientation_bins as f64 - PI)
}
fn compute_descriptor(
&self,
keypoint: &SIFTKeypoint,
scale_space: &[Vec<Array2<Float>>],
) -> SklResult<Array1<Float>> {
if keypoint.octave >= scale_space.len()
|| keypoint.layer >= scale_space[keypoint.octave].len()
{
return Ok(Array1::zeros(128));
}
let _image = &scale_space[keypoint.octave][keypoint.layer];
let mut descriptor = Array1::zeros(128);
for i in 0..128 {
descriptor[i] = (keypoint.x + keypoint.y + i as f64).sin().abs();
}
simd_accelerated::simd_normalize_descriptor(&mut descriptor.view_mut(), 0.2);
Ok(descriptor)
}
fn gaussian_blur(&self, image: &ArrayView2<Float>, sigma: f64) -> SklResult<Array2<Float>> {
let kernel_size = (6.0 * sigma).ceil() as usize | 1;
let kernel = self.create_gaussian_kernel(kernel_size, sigma);
Ok(simd_accelerated::simd_gaussian_blur(image, &kernel, sigma))
}
#[allow(clippy::needless_range_loop)] fn create_gaussian_kernel(&self, size: usize, sigma: f64) -> Vec<Float> {
let mut kernel = vec![0.0; size];
let center = size / 2;
let mut sum = 0.0;
for i in 0..size {
let x = (i as i32 - center as i32) as f64;
kernel[i] = (-x * x / (2.0 * sigma * sigma)).exp();
sum += kernel[i];
}
for k in &mut kernel {
*k /= sum;
}
kernel
}
}
impl Default for SIFTExtractor {
fn default() -> Self {
Self::new()
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_sift_extractor_creation() {
let sift = SIFTExtractor::new();
assert_eq!(sift.n_octaves, 4);
assert_eq!(sift.n_scales_per_octave, 3);
assert_eq!(sift.sigma, 1.6);
}
#[test]
fn test_sift_extractor_builder() {
let sift = SIFTExtractor::new()
.n_octaves(3)
.n_scales_per_octave(4)
.peak_threshold(0.05);
assert_eq!(sift.n_octaves, 3);
assert_eq!(sift.n_scales_per_octave, 4);
assert_eq!(sift.peak_threshold, 0.05);
}
#[test]
fn test_detect_keypoints_small_image() {
let image = Array2::from_shape_vec((8, 8), (0..64).map(|x| x as f64 / 64.0).collect())
.expect("operation should succeed");
let sift = SIFTExtractor::new();
let result = sift.detect_keypoints(&image.view());
assert!(result.is_err()); }
#[test]
fn test_detect_keypoints_valid_image() {
let image =
Array2::from_shape_vec((32, 32), (0..1024).map(|x| x as f64 / 1024.0).collect())
.expect("operation should succeed");
let sift = SIFTExtractor::new();
let keypoints = sift
.detect_keypoints(&image.view())
.expect("operation should succeed");
assert!(keypoints.len() <= 1000); }
#[test]
fn test_extract_descriptors_empty() {
let image = Array2::zeros((32, 32));
let sift = SIFTExtractor::new();
let keypoints = Vec::new();
let descriptors = sift
.extract_descriptors(&image.view(), &keypoints)
.expect("operation should succeed");
assert_eq!(descriptors.dim(), (0, 128));
}
#[test]
fn test_gaussian_kernel_creation() {
let sift = SIFTExtractor::new();
let kernel = sift.create_gaussian_kernel(5, 1.0);
assert_eq!(kernel.len(), 5);
let sum: f64 = kernel.iter().sum();
assert!((sum - 1.0).abs() < 1e-10);
}
#[test]
fn test_edge_response_detection() {
let sift = SIFTExtractor::new();
let mut dog_image = Array2::zeros((5, 5));
for y in 0..5 {
dog_image[[y, 2]] = 1.0;
}
let is_edge = sift.is_edge_response(&dog_image, 2, 2);
let _ = is_edge; }
}