use super::simd_accelerated;
use crate::*;
use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
#[derive(Debug, Clone, PartialEq)]
pub struct SURFKeypoint {
pub x: f64,
pub y: f64,
pub scale: f64,
pub orientation: f64,
pub response: f64,
pub octave: usize,
pub layer: usize,
pub laplacian_sign: bool,
}
#[derive(Debug, Clone)]
pub struct SURFExtractor {
hessian_threshold: f64,
n_octaves: usize,
n_octave_layers: usize,
extended: bool,
upright: bool,
initial_filter_size: usize,
}
impl SURFExtractor {
pub fn new() -> Self {
Self {
hessian_threshold: 400.0,
n_octaves: 4,
n_octave_layers: 4,
extended: false,
upright: false,
initial_filter_size: 9,
}
}
pub fn hessian_threshold(mut self, threshold: f64) -> Self {
self.hessian_threshold = threshold.clamp(50.0, 5000.0);
self
}
pub fn n_octaves(mut self, n_octaves: usize) -> Self {
self.n_octaves = n_octaves.clamp(1, 8);
self
}
pub fn n_octave_layers(mut self, n_layers: usize) -> Self {
self.n_octave_layers = n_layers.clamp(2, 8);
self
}
pub fn extended_descriptors(mut self, extended: bool) -> Self {
self.extended = extended;
self
}
pub fn upright_mode(mut self, upright: bool) -> Self {
self.upright = upright;
self
}
pub fn detect_keypoints(&self, image: &ArrayView2<Float>) -> SklResult<Vec<SURFKeypoint>> {
let (height, width) = image.dim();
if height < 16 || width < 16 {
return Err(SklearsError::InvalidInput(
"Image too small for SURF detection (minimum 16x16)".to_string(),
));
}
let integral_image = self.compute_integral_image(image)?;
let scale_space = self.build_surf_scale_space(&integral_image)?;
let mut keypoints = self.detect_hessian_points(&scale_space)?;
keypoints = self.refine_keypoints(keypoints, &scale_space)?;
if !self.upright {
self.assign_surf_orientations(&mut keypoints, &integral_image)?;
}
Ok(keypoints)
}
pub fn extract_descriptors(
&self,
image: &ArrayView2<Float>,
keypoints: &[SURFKeypoint],
) -> SklResult<Array2<Float>> {
if keypoints.is_empty() {
let desc_size = if self.extended { 128 } else { 64 };
return Ok(Array2::zeros((0, desc_size)));
}
let integral_image = self.compute_integral_image(image)?;
let desc_size = if self.extended { 128 } else { 64 };
let mut descriptors = Array2::zeros((keypoints.len(), desc_size));
for (i, keypoint) in keypoints.iter().enumerate() {
let descriptor = self.compute_surf_descriptor(keypoint, &integral_image)?;
descriptors.row_mut(i).assign(&descriptor);
}
Ok(descriptors)
}
fn compute_integral_image(&self, image: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
Ok(simd_accelerated::simd_compute_integral_image(image))
}
fn build_surf_scale_space(
&self,
integral: &Array2<Float>,
) -> SklResult<Vec<Vec<Array2<Float>>>> {
let mut scale_space = Vec::new();
for octave in 0..self.n_octaves {
let mut octave_layers = Vec::new();
let base_size = self.initial_filter_size + 6 * octave;
for layer in 0..self.n_octave_layers {
let filter_size = base_size + 6 * layer;
let hessian_response = self.compute_hessian_response(integral, filter_size)?;
octave_layers.push(hessian_response);
}
scale_space.push(octave_layers);
}
Ok(scale_space)
}
fn compute_hessian_response(
&self,
integral: &Array2<Float>,
filter_size: usize,
) -> SklResult<Array2<Float>> {
let (height, width) = integral.dim();
if height <= filter_size || width <= filter_size {
return Ok(Array2::zeros((1, 1)));
}
let response_height = height - filter_size;
let response_width = width - filter_size;
let mut response = Array2::zeros((response_height, response_width));
let half_size = filter_size / 2;
let third_size = filter_size / 3;
for y in half_size..height - half_size {
for x in half_size..width - half_size {
if x >= third_size
&& y >= third_size
&& x + third_size < width
&& y + third_size < height
{
let dxx = self.box_filter_sum(
integral,
x - half_size,
y - third_size,
filter_size,
2 * third_size,
) - 3.0
* self.box_filter_sum(
integral,
x - third_size,
y - third_size,
2 * third_size,
2 * third_size,
);
let dyy = self.box_filter_sum(
integral,
x - third_size,
y - half_size,
2 * third_size,
filter_size,
) - 3.0
* self.box_filter_sum(
integral,
x - third_size,
y - third_size,
2 * third_size,
2 * third_size,
);
let dxy1 = self.box_filter_sum(
integral,
x - half_size,
y - half_size,
half_size,
half_size,
);
let dxy2 =
self.box_filter_sum(integral, x, y - half_size, half_size, half_size);
let dxy3 =
self.box_filter_sum(integral, x - half_size, y, half_size, half_size);
let dxy4 = self.box_filter_sum(integral, x, y, half_size, half_size);
let dxy = dxy1 + dxy4 - dxy2 - dxy3;
let det_h = dxx * dyy - 0.81 * dxy * dxy;
let response_y = y - half_size;
let response_x = x - half_size;
if response_y < response_height && response_x < response_width {
response[[response_y, response_x]] = det_h;
}
}
}
}
Ok(response)
}
fn box_filter_sum(
&self,
integral: &Array2<Float>,
x: usize,
y: usize,
w: usize,
h: usize,
) -> f64 {
let (height, width) = integral.dim();
let x1 = x.saturating_sub(1);
let y1 = y.saturating_sub(1);
let x2 = (x + w).min(width - 1);
let y2 = (y + h).min(height - 1);
if x2 >= width || y2 >= height {
return 0.0;
}
integral[[y2, x2]] + integral[[y1, x1]] - integral[[y1, x2]] - integral[[y2, x1]]
}
fn detect_hessian_points(
&self,
scale_space: &[Vec<Array2<Float>>],
) -> SklResult<Vec<SURFKeypoint>> {
let mut keypoints = Vec::new();
for (octave_idx, octave_layers) in scale_space.iter().enumerate() {
if octave_layers.len() < 3 {
continue;
}
for layer_idx in 1..octave_layers.len() - 1 {
let (height, width) = octave_layers[layer_idx].dim();
for y in 1..height - 1 {
for x in 1..width - 1 {
let response = octave_layers[layer_idx][[y, x]];
if response > self.hessian_threshold
&& self.is_local_maximum(octave_layers, layer_idx, y, x)
{
let scale = 1.2_f64.powi(
octave_idx as i32 * self.n_octave_layers as i32 + layer_idx as i32,
);
let keypoint = SURFKeypoint {
x: x as f64,
y: y as f64,
scale,
octave: octave_idx,
layer: layer_idx,
orientation: 0.0, response,
laplacian_sign: response > 0.0,
};
keypoints.push(keypoint);
}
}
}
}
}
Ok(keypoints)
}
fn is_local_maximum(
&self,
octave_layers: &[Array2<Float>],
layer_idx: usize,
y: usize,
x: usize,
) -> bool {
let center_response = octave_layers[layer_idx][[y, x]];
for layer_offset in -1i32..=1 {
let layer = (layer_idx as i32 + layer_offset) as usize;
if layer >= octave_layers.len() {
continue;
}
for dy in -1i32..=1 {
for dx in -1i32..=1 {
let ny = (y as i32 + dy) as usize;
let nx = (x as i32 + dx) as usize;
if ny >= octave_layers[layer].nrows() || nx >= octave_layers[layer].ncols() {
continue;
}
if layer_offset == 0 && dy == 0 && dx == 0 {
continue;
}
if octave_layers[layer][[ny, nx]] >= center_response {
return false;
}
}
}
}
true
}
fn refine_keypoints(
&self,
keypoints: Vec<SURFKeypoint>,
_scale_space: &[Vec<Array2<Float>>],
) -> SklResult<Vec<SURFKeypoint>> {
Ok(keypoints)
}
fn assign_surf_orientations(
&self,
keypoints: &mut [SURFKeypoint],
integral_image: &Array2<Float>,
) -> SklResult<()> {
for keypoint in keypoints.iter_mut() {
let orientation = self.compute_surf_orientation(keypoint, integral_image)?;
keypoint.orientation = orientation;
}
Ok(())
}
fn compute_surf_orientation(
&self,
keypoint: &SURFKeypoint,
integral_image: &Array2<Float>,
) -> SklResult<f64> {
let x = keypoint.x as usize;
let y = keypoint.y as usize;
let scale = keypoint.scale;
let radius = (6.0 * scale) as usize;
let mut resp_x = Vec::new();
let mut resp_y = Vec::new();
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 >= integral_image.ncols() as i32
|| py >= integral_image.nrows() as i32
{
continue;
}
let distance = ((dx * dx + dy * dy) as f64).sqrt();
if distance > radius as f64 {
continue;
}
let haar_x = simd_accelerated::simd_haar_x_response(
&integral_image.view(),
px as usize,
py as usize,
(2.0 * scale) as usize,
);
let haar_y = simd_accelerated::simd_haar_y_response(
&integral_image.view(),
px as usize,
py as usize,
(2.0 * scale) as usize,
);
resp_x.push(haar_x);
resp_y.push(haar_y);
}
}
if resp_x.is_empty() {
return Ok(0.0);
}
let sum_x: f64 = resp_x.iter().sum();
let sum_y: f64 = resp_y.iter().sum();
Ok(sum_y.atan2(sum_x))
}
fn compute_surf_descriptor(
&self,
keypoint: &SURFKeypoint,
integral_image: &Array2<Float>,
) -> SklResult<Array1<Float>> {
let desc_size = if self.extended { 128 } else { 64 };
let mut descriptor = Array1::zeros(desc_size);
let x = keypoint.x as usize;
let y = keypoint.y as usize;
let scale = keypoint.scale;
let cos_ori = keypoint.orientation.cos();
let sin_ori = keypoint.orientation.sin();
let mut desc_idx = 0;
for i in 0..4 {
for j in 0..4 {
let region_x = x as f64 + (i as f64 - 1.5) * 5.0 * scale;
let region_y = y as f64 + (j as f64 - 1.5) * 5.0 * scale;
let mut dx_sum = 0.0;
let mut dy_sum = 0.0;
let mut abs_dx_sum = 0.0;
let mut abs_dy_sum = 0.0;
for sy in 0..5 {
for sx in 0..5 {
let sample_x = region_x + sx as f64 * scale;
let sample_y = region_y + sy as f64 * scale;
let rot_x = sample_x * cos_ori - sample_y * sin_ori;
let rot_y = sample_x * sin_ori + sample_y * cos_ori;
if rot_x >= 0.0
&& rot_y >= 0.0
&& rot_x < integral_image.ncols() as f64
&& rot_y < integral_image.nrows() as f64
{
let haar_x = simd_accelerated::simd_haar_x_response(
&integral_image.view(),
rot_x as usize,
rot_y as usize,
(2.0 * scale) as usize,
);
let haar_y = simd_accelerated::simd_haar_y_response(
&integral_image.view(),
rot_x as usize,
rot_y as usize,
(2.0 * scale) as usize,
);
let dx = haar_x * cos_ori + haar_y * sin_ori;
let dy = -haar_x * sin_ori + haar_y * cos_ori;
dx_sum += dx;
dy_sum += dy;
abs_dx_sum += dx.abs();
abs_dy_sum += dy.abs();
}
}
}
if desc_idx + 3 < desc_size {
descriptor[desc_idx] = dx_sum;
descriptor[desc_idx + 1] = dy_sum;
descriptor[desc_idx + 2] = abs_dx_sum;
descriptor[desc_idx + 3] = abs_dy_sum;
}
desc_idx += if self.extended { 8 } else { 4 };
}
}
simd_accelerated::simd_normalize_descriptor(&mut descriptor.view_mut(), 0.2);
Ok(descriptor)
}
}
impl Default for SURFExtractor {
fn default() -> Self {
Self::new()
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_surf_extractor_creation() {
let surf = SURFExtractor::new();
assert_eq!(surf.hessian_threshold, 400.0);
assert_eq!(surf.n_octaves, 4);
assert_eq!(surf.n_octave_layers, 4);
assert!(!surf.extended);
assert!(!surf.upright);
}
#[test]
fn test_surf_extractor_builder() {
let surf = SURFExtractor::new()
.hessian_threshold(500.0)
.n_octaves(3)
.extended_descriptors(true)
.upright_mode(true);
assert_eq!(surf.hessian_threshold, 500.0);
assert_eq!(surf.n_octaves, 3);
assert!(surf.extended);
assert!(surf.upright);
}
#[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 surf = SURFExtractor::new();
let result = surf.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 surf = SURFExtractor::new();
let keypoints = surf
.detect_keypoints(&image.view())
.expect("operation should succeed");
assert!(keypoints.len() <= 500); }
#[test]
fn test_extract_descriptors_empty() {
let image = Array2::zeros((32, 32));
let surf = SURFExtractor::new();
let keypoints = Vec::new();
let descriptors = surf
.extract_descriptors(&image.view(), &keypoints)
.expect("operation should succeed");
assert_eq!(descriptors.dim(), (0, 64)); }
#[test]
fn test_extract_descriptors_extended() {
let image = Array2::zeros((32, 32));
let surf = SURFExtractor::new().extended_descriptors(true);
let keypoints = Vec::new();
let descriptors = surf
.extract_descriptors(&image.view(), &keypoints)
.expect("operation should succeed");
assert_eq!(descriptors.dim(), (0, 128)); }
#[test]
fn test_integral_image() {
let surf = SURFExtractor::new();
let image =
Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
.expect("operation should succeed");
let integral = surf
.compute_integral_image(&image.view())
.expect("operation should succeed");
assert_eq!(integral.dim(), (3, 3));
assert_eq!(integral[[0, 0]], 1.0);
assert_eq!(integral[[2, 2]], 45.0); }
#[test]
fn test_box_filter_sum() {
let surf = SURFExtractor::new();
let integral = Array2::from_shape_vec((4, 4), (1..=16).map(|x| x as f64).collect())
.expect("operation should succeed");
let sum = surf.box_filter_sum(&integral, 1, 1, 2, 2);
assert!(sum >= 0.0); }
#[test]
fn test_is_local_maximum() {
let surf = SURFExtractor::new();
let mut octave_layers = Vec::new();
for _ in 0..3 {
let mut layer = Array2::zeros((5, 5));
layer[[2, 2]] = 10.0; octave_layers.push(layer);
}
let is_max = surf.is_local_maximum(&octave_layers, 1, 2, 2);
assert!(!is_max); }
#[test]
fn test_hessian_response_computation() {
let surf = SURFExtractor::new();
let integral = Array2::from_shape_vec((20, 20), (0..400).map(|x| x as f64).collect())
.expect("operation should succeed");
let response = surf
.compute_hessian_response(&integral, 9)
.expect("operation should succeed");
assert!(response.nrows() > 0 && response.ncols() > 0);
}
}