1use crate::error::{Result, VisionError};
14use image::DynamicImage;
15
16#[derive(Debug, Clone, Copy)]
18pub struct CornerPoint {
19 pub x: f32,
21 pub y: f32,
23 pub score: f32,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum CornerMethod {
30 Harris,
32 ShiTomasi,
34 Fast,
36}
37
38#[derive(Debug, Clone)]
40pub struct CornerDetectParams {
41 pub method: CornerMethod,
43 pub threshold: f32,
48 pub max_corners: usize,
50 pub min_distance: usize,
52 pub block_size: usize,
54 pub harris_k: f32,
56 pub fast_n: usize,
58 pub non_max_suppression: bool,
60}
61
62impl Default for CornerDetectParams {
63 fn default() -> Self {
64 Self {
65 method: CornerMethod::Harris,
66 threshold: 0.01,
67 max_corners: 500,
68 min_distance: 10,
69 block_size: 3,
70 harris_k: 0.04,
71 fast_n: 9,
72 non_max_suppression: true,
73 }
74 }
75}
76
77pub fn detect_corners(img: &DynamicImage, params: &CornerDetectParams) -> Result<Vec<CornerPoint>> {
118 match params.method {
119 CornerMethod::Harris => detect_harris(img, params),
120 CornerMethod::ShiTomasi => detect_shi_tomasi(img, params),
121 CornerMethod::Fast => detect_fast(img, params),
122 }
123}
124
125fn detect_harris(img: &DynamicImage, params: &CornerDetectParams) -> Result<Vec<CornerPoint>> {
127 let array = crate::feature::image_to_array(img)?;
128 let (height, width) = array.dim();
129
130 let block_size = ensure_odd_block_size(params.block_size);
132 let radius = block_size / 2;
133 let k = params.harris_k;
134
135 let mut ix2 = scirs2_core::ndarray::Array2::<f32>::zeros((height, width));
137 let mut iy2 = scirs2_core::ndarray::Array2::<f32>::zeros((height, width));
138 let mut ixy = scirs2_core::ndarray::Array2::<f32>::zeros((height, width));
139
140 for y in 1..height - 1 {
141 for x in 1..width - 1 {
142 let dx = (array[[y, x + 1]] - array[[y, x - 1]]) / 2.0;
143 let dy = (array[[y + 1, x]] - array[[y - 1, x]]) / 2.0;
144 ix2[[y, x]] = dx * dx;
145 iy2[[y, x]] = dy * dy;
146 ixy[[y, x]] = dx * dy;
147 }
148 }
149
150 let mut response = scirs2_core::ndarray::Array2::<f32>::zeros((height, width));
152
153 for y in radius..height.saturating_sub(radius) {
154 for x in radius..width.saturating_sub(radius) {
155 let mut sum_ix2 = 0.0f32;
156 let mut sum_iy2 = 0.0f32;
157 let mut sum_ixy = 0.0f32;
158
159 for dy in y.saturating_sub(radius)..=(y + radius).min(height - 1) {
160 for dx in x.saturating_sub(radius)..=(x + radius).min(width - 1) {
161 sum_ix2 += ix2[[dy, dx]];
162 sum_iy2 += iy2[[dy, dx]];
163 sum_ixy += ixy[[dy, dx]];
164 }
165 }
166
167 let det = sum_ix2 * sum_iy2 - sum_ixy * sum_ixy;
168 let trace = sum_ix2 + sum_iy2;
169 response[[y, x]] = det - k * trace * trace;
170 }
171 }
172
173 extract_corners_from_response(&response, params)
175}
176
177fn detect_shi_tomasi(img: &DynamicImage, params: &CornerDetectParams) -> Result<Vec<CornerPoint>> {
179 let block_size = ensure_odd_block_size(params.block_size);
180
181 let features = crate::feature::shi_tomasi::good_features_to_track(
182 img,
183 block_size,
184 params.threshold,
185 params.max_corners,
186 params.min_distance,
187 )?;
188
189 let corners: Vec<CornerPoint> = features
190 .iter()
191 .map(|&(x, y, score)| CornerPoint { x, y, score })
192 .collect();
193
194 Ok(corners)
195}
196
197fn detect_fast(img: &DynamicImage, params: &CornerDetectParams) -> Result<Vec<CornerPoint>> {
199 let corner_img = crate::feature::fast::fast_corners(
201 img,
202 params.threshold,
203 params.fast_n,
204 params.non_max_suppression,
205 )?;
206
207 let (width, height) = corner_img.dimensions();
208 let mut corners = Vec::new();
209
210 for y in 0..height {
211 for x in 0..width {
212 let val = corner_img.get_pixel(x, y)[0];
213 if val > 0 {
214 corners.push(CornerPoint {
215 x: x as f32,
216 y: y as f32,
217 score: val as f32,
218 });
219 }
220 }
221 }
222
223 corners.sort_by(|a, b| {
225 b.score
226 .partial_cmp(&a.score)
227 .unwrap_or(std::cmp::Ordering::Equal)
228 });
229
230 if params.max_corners > 0 && corners.len() > params.max_corners {
232 corners.truncate(params.max_corners);
233 }
234
235 if params.min_distance > 0 {
237 corners = filter_by_distance(&corners, params.min_distance as f32);
238 }
239
240 Ok(corners)
241}
242
243fn extract_corners_from_response(
245 response: &scirs2_core::ndarray::Array2<f32>,
246 params: &CornerDetectParams,
247) -> Result<Vec<CornerPoint>> {
248 let (height, width) = response.dim();
249 let nms_radius = (params.block_size / 2).max(1);
250 let mut corners = Vec::new();
251
252 for y in nms_radius..height.saturating_sub(nms_radius) {
253 for x in nms_radius..width.saturating_sub(nms_radius) {
254 let r = response[[y, x]];
255 if r <= params.threshold {
256 continue;
257 }
258
259 if params.non_max_suppression {
261 let mut is_max = true;
262
263 'nms: for dy in y.saturating_sub(nms_radius)..=(y + nms_radius).min(height - 1) {
264 for dx in x.saturating_sub(nms_radius)..=(x + nms_radius).min(width - 1) {
265 if dy == y && dx == x {
266 continue;
267 }
268 if response[[dy, dx]] >= r {
269 is_max = false;
270 break 'nms;
271 }
272 }
273 }
274
275 if !is_max {
276 continue;
277 }
278 }
279
280 corners.push(CornerPoint {
281 x: x as f32,
282 y: y as f32,
283 score: r,
284 });
285 }
286 }
287
288 corners.sort_by(|a, b| {
290 b.score
291 .partial_cmp(&a.score)
292 .unwrap_or(std::cmp::Ordering::Equal)
293 });
294
295 if params.max_corners > 0 && corners.len() > params.max_corners {
297 corners.truncate(params.max_corners);
298 }
299
300 if params.min_distance > 0 {
302 corners = filter_by_distance(&corners, params.min_distance as f32);
303 }
304
305 Ok(corners)
306}
307
308fn filter_by_distance(corners: &[CornerPoint], min_dist: f32) -> Vec<CornerPoint> {
310 let min_dist_sq = min_dist * min_dist;
311 let mut filtered = Vec::new();
312
313 for corner in corners {
314 let too_close = filtered.iter().any(|existing: &CornerPoint| {
315 let dx = corner.x - existing.x;
316 let dy = corner.y - existing.y;
317 dx * dx + dy * dy < min_dist_sq
318 });
319
320 if !too_close {
321 filtered.push(*corner);
322 }
323 }
324
325 filtered
326}
327
328fn ensure_odd_block_size(block_size: usize) -> usize {
330 let bs = block_size.max(3);
331 if bs.is_multiple_of(2) {
332 bs + 1
333 } else {
334 bs
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use image::{GrayImage, Luma};
342
343 fn create_corner_image() -> DynamicImage {
344 let mut buf = GrayImage::new(40, 40);
346 for y in 0..40u32 {
347 for x in 0..40u32 {
348 let val = if (10..30).contains(&x) && (10..30).contains(&y) {
349 200u8
350 } else {
351 30u8
352 };
353 buf.put_pixel(x, y, Luma([val]));
354 }
355 }
356 DynamicImage::ImageLuma8(buf)
357 }
358
359 #[test]
360 fn test_detect_corners_harris() {
361 let img = create_corner_image();
362 let params = CornerDetectParams {
363 method: CornerMethod::Harris,
364 threshold: 0.00001,
365 max_corners: 100,
366 min_distance: 3,
367 block_size: 3,
368 harris_k: 0.04,
369 non_max_suppression: true,
370 ..CornerDetectParams::default()
371 };
372
373 let corners = detect_corners(&img, ¶ms).expect("Harris detection failed");
374 assert!(
375 !corners.is_empty(),
376 "Harris should detect corners of the square"
377 );
378 }
379
380 #[test]
381 fn test_detect_corners_shi_tomasi() {
382 let img = create_corner_image();
383 let params = CornerDetectParams {
384 method: CornerMethod::ShiTomasi,
385 threshold: 0.001,
386 max_corners: 50,
387 min_distance: 5,
388 block_size: 3,
389 ..CornerDetectParams::default()
390 };
391
392 let corners = detect_corners(&img, ¶ms).expect("Shi-Tomasi detection failed");
393 assert!(
394 !corners.is_empty(),
395 "Shi-Tomasi should detect corners of the square"
396 );
397 }
398
399 #[test]
400 fn test_detect_corners_fast() {
401 let img = create_corner_image();
402 let params = CornerDetectParams {
403 method: CornerMethod::Fast,
404 threshold: 20.0,
405 max_corners: 50,
406 min_distance: 5,
407 fast_n: 9,
408 non_max_suppression: true,
409 ..CornerDetectParams::default()
410 };
411
412 let corners = detect_corners(&img, ¶ms).expect("FAST detection failed");
413 assert!(corners.len() <= 50, "Should respect max_corners limit");
415 }
416
417 #[test]
418 fn test_detect_corners_no_corners_uniform() {
419 let img = DynamicImage::new_luma8(30, 30);
421 let params = CornerDetectParams {
422 method: CornerMethod::Harris,
423 threshold: 0.01,
424 ..CornerDetectParams::default()
425 };
426
427 let corners = detect_corners(&img, ¶ms).expect("Harris on uniform failed");
428 assert!(corners.is_empty(), "Uniform image should have no corners");
429 }
430
431 #[test]
432 fn test_corner_point_scores_sorted() {
433 let img = create_corner_image();
434 let params = CornerDetectParams {
435 method: CornerMethod::Harris,
436 threshold: 0.000001,
437 max_corners: 0,
438 min_distance: 0,
439 non_max_suppression: false,
440 ..CornerDetectParams::default()
441 };
442
443 let corners = detect_corners(&img, ¶ms).expect("Harris detection failed");
444 for window in corners.windows(2) {
446 assert!(
447 window[0].score >= window[1].score,
448 "Corners should be sorted by descending score"
449 );
450 }
451 }
452
453 #[test]
454 fn test_min_distance_filtering() {
455 let corners = vec![
456 CornerPoint {
457 x: 0.0,
458 y: 0.0,
459 score: 1.0,
460 },
461 CornerPoint {
462 x: 1.0,
463 y: 0.0,
464 score: 0.9,
465 },
466 CornerPoint {
467 x: 10.0,
468 y: 10.0,
469 score: 0.8,
470 },
471 CornerPoint {
472 x: 11.0,
473 y: 10.0,
474 score: 0.7,
475 },
476 ];
477
478 let filtered = filter_by_distance(&corners, 5.0);
479 assert_eq!(filtered.len(), 2, "Should keep 2 clusters");
480 assert!((filtered[0].x - 0.0).abs() < 0.01);
481 assert!((filtered[1].x - 10.0).abs() < 0.01);
482 }
483
484 #[test]
485 fn test_corner_detect_params_default() {
486 let params = CornerDetectParams::default();
487 assert_eq!(params.method, CornerMethod::Harris);
488 assert!(params.threshold > 0.0);
489 assert!(params.max_corners > 0);
490 assert!(params.block_size >= 3);
491 }
492
493 #[test]
494 fn test_ensure_odd_block_size() {
495 assert_eq!(ensure_odd_block_size(3), 3);
496 assert_eq!(ensure_odd_block_size(4), 5);
497 assert_eq!(ensure_odd_block_size(5), 5);
498 assert_eq!(ensure_odd_block_size(1), 3);
499 assert_eq!(ensure_odd_block_size(2), 3);
500 }
501}