use crate::error::{Result, VisionError};
use crate::feature::image_to_array;
use image::{DynamicImage, GrayImage};
use scirs2_core::ndarray::Array2;
#[allow(dead_code)]
pub fn shi_tomasi_corners(
img: &DynamicImage,
block_size: usize,
threshold: f32,
max_corners: usize,
min_distance: usize,
) -> Result<GrayImage> {
let array = image_to_array(img)?;
let (height, width) = array.dim();
if block_size.is_multiple_of(2) || block_size < 3 {
return Err(VisionError::InvalidParameter(
"block_size must be odd and at least 3".to_string(),
));
}
let mut ix2 = Array2::zeros((height, width));
let mut iy2 = Array2::zeros((height, width));
let mut ixy = Array2::zeros((height, width));
for y in 1..(height - 1) {
for x in 1..(width - 1) {
let gx = -array[[y - 1, x - 1]]
+ 1.0 * array[[y - 1, x + 1]]
+ -2.0 * array[[y, x - 1]]
+ 2.0 * array[[y, x + 1]]
+ -array[[y + 1, x - 1]]
+ 1.0 * array[[y + 1, x + 1]];
let gy = -array[[y - 1, x - 1]]
+ -2.0 * array[[y - 1, x]]
+ -array[[y - 1, x + 1]]
+ 1.0 * array[[y + 1, x - 1]]
+ 2.0 * array[[y + 1, x]]
+ 1.0 * array[[y + 1, x + 1]];
ix2[[y, x]] = gx * gx;
iy2[[y, x]] = gy * gy;
ixy[[y, x]] = gx * gy;
}
}
let radius = block_size / 2;
let mut smoothed_ix2 = Array2::zeros((height, width));
let mut smoothed_iy2 = Array2::zeros((height, width));
let mut smoothed_ixy = Array2::zeros((height, width));
for y in radius..(height - radius) {
for x in radius..(width - radius) {
let mut sum_ix2 = 0.0;
let mut sum_iy2 = 0.0;
let mut sum_ixy = 0.0;
let mut count = 0;
for dy in (y - radius)..=(y + radius) {
for dx in (x - radius)..=(x + radius) {
sum_ix2 += ix2[[dy, dx]];
sum_iy2 += iy2[[dy, dx]];
sum_ixy += ixy[[dy, dx]];
count += 1;
}
}
smoothed_ix2[[y, x]] = sum_ix2 / count as f32;
smoothed_iy2[[y, x]] = sum_iy2 / count as f32;
smoothed_ixy[[y, x]] = sum_ixy / count as f32;
}
}
let mut response = Array2::zeros((height, width));
for y in radius..(height - radius) {
for x in radius..(width - radius) {
let a = smoothed_ix2[[y, x]];
let b = smoothed_ixy[[y, x]];
let c = smoothed_iy2[[y, x]];
let trace = a + c;
let _det = a * c - b * b;
let discriminant = ((a - c) * (a - c) + 4.0 * b * b).sqrt();
let min_eigenvalue = (trace - discriminant) / 2.0;
response[[y, x]] = min_eigenvalue;
}
}
let mut _corners = Vec::new();
for y in radius..(height - radius) {
for x in radius..(width - radius) {
if response[[y, x]] > threshold {
_corners.push((x, y, response[[y, x]]));
}
}
}
_corners.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
let mut selected_corners = Vec::new();
for (x, y, score) in _corners {
let mut too_close = false;
for &(sx, sy_, _) in &selected_corners {
let dist_sq = ((x as i32 - sx as i32).pow(2) + (y as i32 - sy_ as i32).pow(2)) as usize;
if dist_sq < min_distance * min_distance {
too_close = true;
break;
}
}
if !too_close {
selected_corners.push((x, y, score));
if max_corners > 0 && selected_corners.len() >= max_corners {
break;
}
}
}
let mut output = Array2::zeros((height, width));
for (x, y_, _) in selected_corners {
output[[y_, x]] = 1.0;
}
crate::feature::array_to_image(&output)
}
#[allow(dead_code)]
pub fn shi_tomasi_corners_simple(_img: &DynamicImage, maxcorners: usize) -> Result<GrayImage> {
shi_tomasi_corners(_img, 3, 0.01, maxcorners, 10)
}
#[allow(dead_code)]
pub fn good_features_to_track(
img: &DynamicImage,
block_size: usize,
threshold: f32,
max_corners: usize,
min_distance: usize,
) -> Result<Vec<(f32, f32, f32)>> {
let array = image_to_array(img)?;
let (height, width) = array.dim();
if block_size.is_multiple_of(2) || block_size < 3 {
return Err(VisionError::InvalidParameter(
"block_size must be odd and at least 3".to_string(),
));
}
let mut ix2 = Array2::zeros((height, width));
let mut iy2 = Array2::zeros((height, width));
let mut ixy = Array2::zeros((height, width));
for y in 1..(height - 1) {
for x in 1..(width - 1) {
let gx = -array[[y - 1, x - 1]]
+ 1.0 * array[[y - 1, x + 1]]
+ -2.0 * array[[y, x - 1]]
+ 2.0 * array[[y, x + 1]]
+ -array[[y + 1, x - 1]]
+ 1.0 * array[[y + 1, x + 1]];
let gy = -array[[y - 1, x - 1]]
+ -2.0 * array[[y - 1, x]]
+ -array[[y - 1, x + 1]]
+ 1.0 * array[[y + 1, x - 1]]
+ 2.0 * array[[y + 1, x]]
+ 1.0 * array[[y + 1, x + 1]];
ix2[[y, x]] = gx * gx;
iy2[[y, x]] = gy * gy;
ixy[[y, x]] = gx * gy;
}
}
let radius = block_size / 2;
let mut response = Array2::zeros((height, width));
for y in radius..(height - radius) {
for x in radius..(width - radius) {
let mut sum_ix2 = 0.0;
let mut sum_iy2 = 0.0;
let mut sum_ixy = 0.0;
let mut count = 0;
for dy in (y - radius)..=(y + radius) {
for dx in (x - radius)..=(x + radius) {
sum_ix2 += ix2[[dy, dx]];
sum_iy2 += iy2[[dy, dx]];
sum_ixy += ixy[[dy, dx]];
count += 1;
}
}
let a = sum_ix2 / count as f32;
let b = sum_ixy / count as f32;
let c = sum_iy2 / count as f32;
let trace = a + c;
let discriminant = ((a - c) * (a - c) + 4.0 * b * b).sqrt();
response[[y, x]] = (trace - discriminant) / 2.0;
}
}
let mut _corners = Vec::new();
for y in (radius + 1)..(height - radius - 1) {
for x in (radius + 1)..(width - radius - 1) {
let r = response[[y, x]];
if r > threshold {
let mut is_max = true;
for dy in -1..=1 {
for dx in -1..=1 {
if dy == 0 && dx == 0 {
continue;
}
if response[[(y as i32 + dy) as usize, (x as i32 + dx) as usize]] >= r {
is_max = false;
break;
}
}
if !is_max {
break;
}
}
if is_max {
let dx = (response[[y, x + 1]] - response[[y, x - 1]]) / 2.0;
let dy = (response[[y + 1, x]] - response[[y - 1, x]]) / 2.0;
let dxx = response[[y, x + 1]] - 2.0 * r + response[[y, x - 1]];
let dyy = response[[y + 1, x]] - 2.0 * r + response[[y - 1, x]];
let mut sub_x = x as f32;
let mut sub_y = y as f32;
if dxx.abs() > 1e-6 {
sub_x -= dx / dxx;
}
if dyy.abs() > 1e-6 {
sub_y -= dy / dyy;
}
_corners.push((sub_x, sub_y, r));
}
}
}
}
_corners.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
let mut selected_corners = Vec::new();
for (x, y, score) in _corners {
let mut too_close = false;
for &(sx, sy_, _) in &selected_corners {
let x_diff: f32 = x - sx;
let y_diff: f32 = y - sy_;
let dist_sq: f32 = x_diff.powi(2) + y_diff.powi(2);
if dist_sq < (min_distance as f32 * min_distance as f32) {
too_close = true;
break;
}
}
if !too_close {
selected_corners.push((x, y, score));
if max_corners > 0 && selected_corners.len() >= max_corners {
break;
}
}
}
Ok(selected_corners)
}
#[cfg(test)]
mod tests {
use super::*;
use image::Luma;
#[test]
fn test_shi_tomasi_detection() {
let mut img = GrayImage::new(20, 20);
for y in 0..20 {
for x in 0..20 {
img.put_pixel(x, y, Luma([128u8]));
}
}
for y in 5..15 {
for x in 5..15 {
img.put_pixel(x, y, Luma([255u8]));
}
}
let dynamic_img = DynamicImage::ImageLuma8(img);
let result = shi_tomasi_corners_simple(&dynamic_img, 10);
assert!(result.is_ok());
let corners = result.expect("Operation failed");
let mut corner_count = 0;
for y in 0..20 {
for x in 0..20 {
if corners.get_pixel(x, y)[0] > 0 {
corner_count += 1;
}
}
}
assert!(
corner_count >= 4,
"Should detect at least 4 corners, found {corner_count}"
);
}
#[test]
fn test_good_features_to_track() {
let img = GrayImage::new(20, 20);
let dynamic_img = DynamicImage::ImageLuma8(img);
let result = good_features_to_track(&dynamic_img, 3, 0.01, 10, 5);
assert!(result.is_ok());
let features = result.expect("Operation failed");
assert!(features.len() <= 10, "Should not exceed max_corners");
}
#[test]
fn test_invalid_block_size() {
let img = GrayImage::new(20, 20);
let dynamic_img = DynamicImage::ImageLuma8(img);
let result = shi_tomasi_corners(&dynamic_img, 4, 0.01, 10, 5);
assert!(result.is_err());
let result = shi_tomasi_corners(&dynamic_img, 1, 0.01, 10, 5);
assert!(result.is_err());
}
}