1use super::response::chess_response_u8;
3use crate::detect::{Corner, CornerDescriptor};
4use crate::imageview::ImageView;
5use crate::orientation::{describe_corners, OrientationMethod};
6use crate::refine::{CornerRefiner, RefineContext, RefineStatus, Refiner};
7use crate::{ChessParams, ResponseMap};
8
9#[cfg(feature = "tracing")]
10use tracing::instrument;
11
12pub fn find_corners_u8(
19 img: &[u8],
20 w: usize,
21 h: usize,
22 params: &ChessParams,
23) -> Vec<CornerDescriptor> {
24 let mut refiner = Refiner::from_kind(params.refiner.clone());
25 find_corners_u8_with_refiner(img, w, h, params, &mut refiner)
26}
27
28pub fn find_corners_u8_with_refiner(
30 img: &[u8],
31 w: usize,
32 h: usize,
33 params: &ChessParams,
34 refiner: &mut dyn CornerRefiner,
35) -> Vec<CornerDescriptor> {
36 let resp = chess_response_u8(img, w, h, params);
37 let image =
38 ImageView::from_u8_slice(w, h, img).expect("image dimensions must match buffer length");
39 let corners = detect_corners_from_response_with_refiner(&resp, params, Some(image), refiner);
40 let desc_radius = params.descriptor_ring_radius();
41 describe_corners(img, w, h, desc_radius, corners, OrientationMethod::RingFit)
42}
43
44pub fn detect_corners_from_response(resp: &ResponseMap, params: &ChessParams) -> Vec<Corner> {
50 let mut refiner = Refiner::from_kind(params.refiner.clone());
51 detect_corners_from_response_with_refiner(resp, params, None, &mut refiner)
52}
53
54pub fn detect_corners_from_response_with_refiner(
62 resp: &ResponseMap,
63 params: &ChessParams,
64 image: Option<ImageView<'_>>,
65 refiner: &mut dyn CornerRefiner,
66) -> Vec<Corner> {
67 let peaks = detect_peaks_from_response_with_refine_radius(resp, params, refiner.radius());
68 refine_corners_on_image(peaks, image, Some(resp), refiner)
69}
70
71#[cfg_attr(
85 feature = "tracing",
86 instrument(level = "debug", skip(resp, params), fields(w = resp.w, h = resp.h))
87)]
88pub fn detect_peaks_from_response(resp: &ResponseMap, params: &ChessParams) -> Vec<Corner> {
89 detect_peaks_from_response_with_refine_radius(resp, params, 0)
90}
91
92#[cfg_attr(
103 feature = "tracing",
104 instrument(level = "debug", skip(resp, params), fields(w = resp.w, h = resp.h))
105)]
106pub fn detect_peaks_from_response_with_refine_radius(
107 resp: &ResponseMap,
108 params: &ChessParams,
109 refine_radius: i32,
110) -> Vec<Corner> {
111 let w = resp.w;
112 let h = resp.h;
113
114 if w == 0 || h == 0 {
115 return Vec::new();
116 }
117
118 let mut max_r = f32::NEG_INFINITY;
120 for &v in &resp.data {
121 if v > max_r {
122 max_r = v;
123 }
124 }
125 if !max_r.is_finite() {
126 return Vec::new();
127 }
128
129 let mut thr = params.threshold_abs.unwrap_or(params.threshold_rel * max_r);
130
131 if thr < 0.0 {
132 thr = 0.0;
134 }
135
136 let nms_r = params.nms_radius as i32;
142 let ring_r = params.ring_radius() as i32;
143
144 let border = (ring_r + nms_r + refine_radius).max(0) as usize;
150
151 if w <= 2 * border || h <= 2 * border {
152 return Vec::new();
153 }
154
155 let mut corners = Vec::new();
156
157 for y in border..(h - border) {
158 for x in border..(w - border) {
159 let v = resp.at(x, y);
160 if v <= thr {
161 continue;
162 }
163
164 if !is_local_max(resp.data(), resp.w, resp.h, x, y, nms_r, v) {
166 continue;
167 }
168
169 let cluster_size = count_positive_neighbors(resp.data(), resp.w, resp.h, x, y, nms_r);
172 if cluster_size < params.min_cluster_size {
173 continue;
174 }
175
176 corners.push(Corner {
177 x: x as f32,
178 y: y as f32,
179 strength: v,
180 });
181 }
182 }
183
184 corners
185}
186
187#[cfg_attr(
201 feature = "tracing",
202 instrument(level = "debug", skip(corners, image, response, refiner))
203)]
204pub fn refine_corners_on_image(
205 corners: Vec<Corner>,
206 image: Option<ImageView<'_>>,
207 response: Option<&ResponseMap>,
208 refiner: &mut dyn CornerRefiner,
209) -> Vec<Corner> {
210 if corners.is_empty() {
211 return Vec::new();
212 }
213
214 let ctx = RefineContext { image, response };
215 let mut out = Vec::with_capacity(corners.len());
216 for c in corners {
217 let seed_xy = [c.x, c.y];
218 let res = refiner.refine(seed_xy, ctx);
219 if matches!(res.status, RefineStatus::Accepted) {
220 out.push(Corner {
221 x: res.x,
222 y: res.y,
223 strength: c.strength,
224 });
225 }
226 }
227 out
228}
229
230pub(crate) fn is_local_max(
235 data: &[f32],
236 w: usize,
237 h: usize,
238 x: usize,
239 y: usize,
240 r: i32,
241 v: f32,
242) -> bool {
243 let wi = w as i32;
244 let hi = h as i32;
245 let cx = x as i32;
246 let cy = y as i32;
247
248 for dy in -r..=r {
249 for dx in -r..=r {
250 if dx == 0 && dy == 0 {
251 continue;
252 }
253 let xx = cx + dx;
254 let yy = cy + dy;
255 if xx < 0 || yy < 0 || xx >= wi || yy >= hi {
256 continue;
257 }
258 let vv = data[(yy as usize) * w + (xx as usize)];
259 if vv > v {
260 return false;
261 }
262 }
263 }
264 true
265}
266
267pub(crate) fn count_positive_neighbors(
270 data: &[f32],
271 w: usize,
272 h: usize,
273 x: usize,
274 y: usize,
275 r: i32,
276) -> u32 {
277 let wi = w as i32;
278 let hi = h as i32;
279 let cx = x as i32;
280 let cy = y as i32;
281 let mut count = 0;
282
283 for dy in -r..=r {
284 for dx in -r..=r {
285 if dx == 0 && dy == 0 {
286 continue;
287 }
288 let xx = cx + dx;
289 let yy = cy + dy;
290 if xx < 0 || yy < 0 || xx >= wi || yy >= hi {
291 continue;
292 }
293 let vv = data[(yy as usize) * w + (xx as usize)];
294 if vv > 0.0 {
295 count += 1;
296 }
297 }
298 }
299
300 count
301}
302
303#[cfg_attr(feature = "tracing", instrument(level = "info", skip(corners)))]
318pub fn merge_corners_simple(corners: &mut Vec<Corner>, radius: f32) -> Vec<Corner> {
319 let n = corners.len();
320 if n == 0 {
321 return Vec::new();
322 }
323 if !radius.is_finite() || radius <= 0.0 {
324 return std::mem::take(corners);
325 }
326
327 let r2 = radius * radius;
328
329 let (mut min_x, mut min_y) = (f32::INFINITY, f32::INFINITY);
331 let (mut max_x, mut max_y) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
332 for c in corners.iter() {
333 if c.x < min_x {
334 min_x = c.x;
335 }
336 if c.y < min_y {
337 min_y = c.y;
338 }
339 if c.x > max_x {
340 max_x = c.x;
341 }
342 if c.y > max_y {
343 max_y = c.y;
344 }
345 }
346
347 if !(min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite()) {
350 return merge_corners_naive(corners, r2);
351 }
352
353 let cell = radius;
354 let inv_cell = 1.0 / cell;
355 let grid_w = ((max_x - min_x) * inv_cell).floor() as usize + 1;
356 let grid_h = ((max_y - min_y) * inv_cell).floor() as usize + 1;
357
358 let mut grid: Vec<Vec<u32>> = vec![Vec::new(); grid_w * grid_h];
362 let mut out: Vec<Corner> = Vec::with_capacity(n);
363
364 let cell_of = |c: &Corner| -> (usize, usize) {
365 let gx = (((c.x - min_x) * inv_cell) as usize).min(grid_w - 1);
366 let gy = (((c.y - min_y) * inv_cell) as usize).min(grid_h - 1);
367 (gx, gy)
368 };
369
370 for c in corners.drain(..) {
371 let (gx, gy) = cell_of(&c);
372
373 let y0 = gy.saturating_sub(1);
379 let y1 = (gy + 1).min(grid_h - 1);
380 let x0 = gx.saturating_sub(1);
381 let x1 = (gx + 1).min(grid_w - 1);
382
383 let mut hit: Option<usize> = None;
384 let mut best_idx = u32::MAX;
385 for ny in y0..=y1 {
386 for nx in x0..=x1 {
387 for &idx in &grid[ny * grid_w + nx] {
388 if idx >= best_idx {
389 continue;
390 }
391 let i = idx as usize;
392 let dx = c.x - out[i].x;
393 let dy = c.y - out[i].y;
394 if dx * dx + dy * dy <= r2 {
395 best_idx = idx;
396 hit = Some(i);
397 }
398 }
399 }
400 }
401
402 if let Some(i) = hit {
403 if c.strength > out[i].strength {
404 let (old_gx, old_gy) = cell_of(&out[i]);
405 out[i] = c;
406 let (new_gx, new_gy) = cell_of(&out[i]);
407 if old_gx != new_gx || old_gy != new_gy {
408 let id = i as u32;
409 let old_cell = old_gy * grid_w + old_gx;
410 let new_cell = new_gy * grid_w + new_gx;
411 grid[old_cell].retain(|&j| j != id);
412 grid[new_cell].push(id);
413 }
414 }
415 } else {
416 let new_idx = out.len() as u32;
417 let (gx, gy) = cell_of(&c);
418 out.push(c);
419 grid[gy * grid_w + gx].push(new_idx);
420 }
421 }
422
423 out
424}
425
426fn merge_corners_naive(corners: &mut Vec<Corner>, r2: f32) -> Vec<Corner> {
429 let mut out: Vec<Corner> = Vec::new();
430 'outer: for c in corners.drain(..) {
431 for o in &mut out {
432 let dx = c.x - o.x;
433 let dy = c.y - o.y;
434 if dx * dx + dy * dy <= r2 {
435 if c.strength > o.strength {
436 *o = c;
437 }
438 continue 'outer;
439 }
440 }
441 out.push(c);
442 }
443 out
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use crate::refine::{
450 CenterOfMassConfig, CenterOfMassRefiner, RefineContext, RefineStatus, RefinerKind,
451 };
452 use image::{GrayImage, Luma};
453
454 fn make_quadrant_corner(size: u32, dark: u8, bright: u8) -> GrayImage {
455 let mut img = GrayImage::from_pixel(size, size, Luma([dark]));
456 let mid = size / 2;
457 for y in 0..size {
458 for x in 0..size {
459 let in_top = y < mid;
460 let in_left = x < mid;
461 if in_top ^ in_left {
462 img.put_pixel(x, y, Luma([bright]));
463 }
464 }
465 }
466 img
467 }
468
469 #[test]
470 fn descriptors_report_two_axes_stable() {
471 use core::f32::consts::{FRAC_PI_2, PI};
472
473 let size = 32u32;
474 let params = ChessParams {
475 threshold_rel: 0.01,
476 ..Default::default()
477 };
478
479 let img = make_quadrant_corner(size, 20, 220);
480 let corners = find_corners_u8(img.as_raw(), size as usize, size as usize, ¶ms);
481 assert!(!corners.is_empty(), "expected at least one descriptor");
482
483 let best = corners
484 .iter()
485 .max_by(|a, b| a.response.partial_cmp(&b.response).unwrap())
486 .expect("non-empty");
487
488 assert!(best.axes[0].angle >= 0.0 && best.axes[0].angle < PI);
490 assert!(
491 best.axes[1].angle > best.axes[0].angle && best.axes[1].angle < best.axes[0].angle + PI
492 );
493
494 let near_line = |x: f32, target: f32| -> f32 {
498 let xr = x.rem_euclid(PI);
499 let tr = target.rem_euclid(PI);
500 let d = (xr - tr).abs();
501 d.min(PI - d)
502 };
503 let horiz = near_line(best.axes[0].angle, 0.0).min(near_line(best.axes[1].angle, 0.0));
505 let vert =
506 near_line(best.axes[0].angle, FRAC_PI_2).min(near_line(best.axes[1].angle, FRAC_PI_2));
507 assert!(
508 horiz < 0.35,
509 "horiz line miss: {horiz}, axes {:?}",
510 best.axes
511 );
512 assert!(vert < 0.35, "vert line miss: {vert}, axes {:?}", best.axes);
513 assert!(best.contrast > 0.0);
514
515 let mut brighter = img.clone();
518 for p in brighter.pixels_mut() {
519 p[0] = p[0].saturating_add(5);
520 }
521
522 let brighter_corners =
523 find_corners_u8(brighter.as_raw(), size as usize, size as usize, ¶ms);
524 assert!(!brighter_corners.is_empty());
525 let best_brighter = brighter_corners
526 .iter()
527 .max_by(|a, b| a.response.partial_cmp(&b.response).unwrap())
528 .expect("non-empty brighter");
529
530 assert!((best.x - best_brighter.x).abs() < 0.5 && (best.y - best_brighter.y).abs() < 0.5);
531
532 let da0 = near_line(best.axes[0].angle, best_brighter.axes[0].angle);
533 let da1 = near_line(best.axes[1].angle, best_brighter.axes[1].angle);
534 assert!(da0 < 0.35, "axis0 delta after brightness shift: {da0}");
535 assert!(da1 < 0.35, "axis1 delta after brightness shift: {da1}");
536 }
537
538 #[test]
539 fn default_refiner_matches_center_of_mass() {
540 let mut resp = ResponseMap {
541 w: 32,
542 h: 32,
543 data: vec![0.0; 32 * 32],
544 };
545
546 let cx = 16usize;
547 let cy = 16usize;
548 let w = resp.w;
549
550 resp.data[cy * w + cx] = 10.0;
551 resp.data[cy * w + (cx + 1)] = 6.0;
552 resp.data[(cy + 1) * w + cx] = 5.0;
553 resp.data[(cy + 1) * w + (cx + 1)] = 4.0;
554
555 let params = ChessParams {
556 threshold_rel: 0.01,
557 ..Default::default()
558 };
559
560 let mut refiner = CenterOfMassRefiner::new(CenterOfMassConfig::default());
561 let ctx = RefineContext {
562 image: None,
563 response: Some(&resp),
564 };
565 let expected = refiner.refine([cx as f32, cy as f32], ctx);
566 assert_eq!(expected.status, RefineStatus::Accepted);
567
568 let corners = detect_corners_from_response(&resp, ¶ms);
569 assert_eq!(corners.len(), 1);
570 let c = &corners[0];
571 assert!((c.x - expected.x).abs() < 1e-6);
572 assert!((c.y - expected.y).abs() < 1e-6);
573 }
574
575 #[test]
576 fn params_refiner_controls_margin() {
577 let mut resp = ResponseMap {
578 w: 30,
579 h: 30,
580 data: vec![0.0; 30 * 30],
581 };
582
583 let cx = 10usize;
584 let cy = 10usize;
585 let w = resp.w;
586
587 resp.data[cy * w + cx] = 10.0;
588 resp.data[cy * w + (cx + 1)] = 1.0;
589 resp.data[(cy + 1) * w + cx] = 1.0;
590
591 let mut params = ChessParams {
592 threshold_abs: Some(0.5),
593 threshold_rel: 0.0,
594 ..Default::default()
595 };
596
597 let baseline = detect_corners_from_response(&resp, ¶ms);
598 assert_eq!(baseline.len(), 1, "expected baseline detection");
599
600 params.refiner = RefinerKind::CenterOfMass(CenterOfMassConfig { radius: 4 });
601 let shrunk = detect_corners_from_response(&resp, ¶ms);
602 assert!(
603 shrunk.is_empty(),
604 "larger refiner radius should increase border and skip the corner"
605 );
606 }
607
608 #[test]
609 fn merge_corners_prefers_stronger_entries() {
610 let mut corners = vec![
611 Corner {
612 x: 10.0,
613 y: 10.0,
614 strength: 1.0,
615 },
616 Corner {
617 x: 11.0,
618 y: 11.0,
619 strength: 5.0,
620 },
621 Corner {
622 x: 20.0,
623 y: 20.0,
624 strength: 3.0,
625 },
626 ];
627 let merged = merge_corners_simple(&mut corners, 2.5);
628 assert_eq!(merged.len(), 2);
629 assert!(merged.iter().any(|c| (c.x - 11.0).abs() < 1e-6
630 && (c.y - 11.0).abs() < 1e-6
631 && (c.strength - 5.0).abs() < 1e-6));
632 assert!(merged
633 .iter()
634 .any(|c| (c.x - 20.0).abs() < 1e-6 && (c.y - 20.0).abs() < 1e-6));
635 }
636
637 fn lcg(state: &mut u64) -> u32 {
640 *state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
641 (*state >> 33) as u32
642 }
643
644 fn rand_corners(seed: u64, n: usize, span: f32) -> Vec<Corner> {
645 let mut state = seed.wrapping_add(0x9E3779B97F4A7C15);
646 let mut out = Vec::with_capacity(n);
647 for _ in 0..n {
648 let rx = lcg(&mut state) as f32 / u32::MAX as f32;
649 let ry = lcg(&mut state) as f32 / u32::MAX as f32;
650 let rs = lcg(&mut state) as f32 / u32::MAX as f32;
651 out.push(Corner {
652 x: rx * span,
653 y: ry * span,
654 strength: rs * 100.0,
655 });
656 }
657 out
658 }
659
660 fn corners_eq_unordered(a: &[Corner], b: &[Corner]) -> bool {
661 if a.len() != b.len() {
662 return false;
663 }
664 let mut used = vec![false; b.len()];
665 'outer: for ca in a.iter() {
666 for (i, cb) in b.iter().enumerate() {
667 if used[i] {
668 continue;
669 }
670 if (ca.x - cb.x).abs() < 1e-5
671 && (ca.y - cb.y).abs() < 1e-5
672 && (ca.strength - cb.strength).abs() < 1e-3
673 {
674 used[i] = true;
675 continue 'outer;
676 }
677 }
678 return false;
679 }
680 true
681 }
682
683 #[test]
684 fn merge_corners_grid_matches_naive_scan() {
685 for seed in [1u64, 7, 42, 123, 1729] {
691 for &n in &[16usize, 64, 256, 1024] {
692 for &radius in &[1.0f32, 2.5, 6.0] {
693 let corners = rand_corners(seed, n, 50.0);
694 let mut a = corners.clone();
695 let mut b = corners.clone();
696 let r2 = radius * radius;
697 let merged_grid = merge_corners_simple(&mut a, radius);
698 let merged_naive = merge_corners_naive(&mut b, r2);
699 assert!(
700 corners_eq_unordered(&merged_grid, &merged_naive),
701 "mismatch at seed={seed}, n={n}, radius={radius}: grid={} naive={}",
702 merged_grid.len(),
703 merged_naive.len(),
704 );
705 }
706 }
707 }
708 }
709
710 #[test]
711 fn merge_corners_handles_empty_and_zero_radius() {
712 let mut empty: Vec<Corner> = Vec::new();
713 let merged = merge_corners_simple(&mut empty, 1.0);
714 assert!(merged.is_empty());
715
716 let mut few = vec![
717 Corner {
718 x: 1.0,
719 y: 1.0,
720 strength: 1.0,
721 },
722 Corner {
723 x: 1.5,
724 y: 1.5,
725 strength: 2.0,
726 },
727 ];
728 let merged = merge_corners_simple(&mut few, 0.0);
730 assert_eq!(merged.len(), 2);
731 }
732}