1#[cfg(feature = "ml-refiner")]
19use crate::ml_refiner;
20#[cfg(feature = "ml-refiner")]
21use crate::ChessParams;
22use crate::{DetectionStrategy, DetectorConfig};
23use box_image_pyramid::{build_pyramid, PyramidBuffers, PyramidParams};
24#[cfg(feature = "ml-refiner")]
25use chess_corners_core::detect::chess::response::{
26 chess_response_u8, chess_response_u8_patch, Roi,
27};
28#[cfg(feature = "ml-refiner")]
29use chess_corners_core::detect::detect_corners_from_response_with_refiner;
30use chess_corners_core::detect::merge_corners_simple;
31use chess_corners_core::detect::Corner;
32use chess_corners_core::orientation::describe_corners;
33#[cfg(feature = "ml-refiner")]
34use chess_corners_core::ResponseMap;
35use chess_corners_core::{ChessBuffers, ChessDetector, CornerDescriptor, DenseDetector};
36use chess_corners_core::{
37 CornerRefiner, ImageView, OrientationMethod, RadonBuffers, RadonDetector, Refiner, RefinerKind,
38};
39
40fn to_pyramid_view(v: ImageView<'_>) -> box_image_pyramid::ImageView<'_> {
42 box_image_pyramid::ImageView::new(v.width, v.height, v.data).unwrap()
44}
45#[cfg(feature = "tracing")]
46use tracing::info_span;
47
48#[derive(Clone, Debug)]
53#[non_exhaustive]
54pub struct CoarseToFineParams {
55 pub pyramid: PyramidParams,
57 pub refinement_radius: u32,
61 pub merge_radius: f32,
64}
65
66impl Default for CoarseToFineParams {
67 fn default() -> Self {
68 Self {
69 pyramid: PyramidParams::default(),
70 refinement_radius: 3,
74 merge_radius: 3.0,
76 }
77 }
78}
79
80impl CoarseToFineParams {
81 pub fn new() -> Self {
82 Self::default()
83 }
84}
85
86#[cfg(feature = "ml-refiner")]
92fn detect_with_ml_refiner(
93 resp: &ResponseMap,
94 params: &ChessParams,
95 image: Option<ImageView<'_>>,
96 ml_state: &mut ml_refiner::MlRefinerState,
97) -> Vec<Corner> {
98 ml_refiner::detect_corners_with_ml(resp, params, image, ml_state)
99}
100
101#[cfg(feature = "ml-refiner")]
102fn detect_with_refiner_kind(
103 resp: &ResponseMap,
104 params: &ChessParams,
105 image: Option<ImageView<'_>>,
106 refiner_kind: &RefinerKind,
107) -> Vec<Corner> {
108 let mut refiner = Refiner::from_kind(refiner_kind.clone());
109 detect_corners_from_response_with_refiner(resp, params, image, &mut refiner)
110}
111
112fn refiner_radius(refiner_kind: &RefinerKind) -> i32 {
113 Refiner::from_kind(refiner_kind.clone()).radius()
114}
115
116struct RoiContext {
122 inv_scale: f32,
123 border: i32,
124 safe_margin: i32,
125 roi_r: i32,
126 base_w_i: i32,
127 base_h_i: i32,
128}
129
130impl RoiContext {
131 fn compute_roi(&self, c: &Corner) -> Option<(i32, i32, i32, i32)> {
135 let cx = (c.x * self.inv_scale).round() as i32;
136 let cy = (c.y * self.inv_scale).round() as i32;
137
138 if cx < self.safe_margin
139 || cy < self.safe_margin
140 || cx >= self.base_w_i - self.safe_margin
141 || cy >= self.base_h_i - self.safe_margin
142 {
143 return None;
144 }
145
146 let mut x0 = cx - self.roi_r;
147 let mut y0 = cy - self.roi_r;
148 let mut x1 = cx + self.roi_r + 1;
149 let mut y1 = cy + self.roi_r + 1;
150
151 let min_xy = self.border;
152 let max_x = self.base_w_i - self.border;
153 let max_y = self.base_h_i - self.border;
154
155 if x0 < min_xy {
156 x0 = min_xy;
157 }
158 if y0 < min_xy {
159 y0 = min_xy;
160 }
161 if x1 > max_x {
162 x1 = max_x;
163 }
164 if y1 > max_y {
165 y1 = max_y;
166 }
167
168 if x1 - x0 <= 2 * self.border || y1 - y0 <= 2 * self.border {
169 return None;
170 }
171
172 Some((x0, y0, x1, y1))
173 }
174}
175
176fn make_roi_context(
177 base: ImageView<'_>,
178 coarse_scale: f32,
179 detector_border: i32,
180 refine_border: i32,
181 cf: &CoarseToFineParams,
182) -> RoiContext {
183 let border = (detector_border + refine_border).max(0);
184 let safe_margin = border + 1;
185 let roi_r_base = (cf.refinement_radius as f32 / coarse_scale).ceil() as i32;
186 let min_roi_r = border + 2;
187
188 RoiContext {
189 inv_scale: 1.0 / coarse_scale,
190 border,
191 safe_margin,
192 roi_r: roi_r_base.max(min_roi_r),
193 base_w_i: base.width as i32,
194 base_h_i: base.height as i32,
195 }
196}
197
198struct DetectorShape<'r> {
206 refiner_kind: &'r RefinerKind,
207 descriptor_ring_radius: u32,
208 orientation_method: OrientationMethod,
209 merge_radius: f32,
210}
211
212fn detect_multiscale<D: DenseDetector>(
234 base: ImageView<'_>,
235 detector: &D,
236 params: &D::Params,
237 detector_buffers: &mut D::Buffers,
238 pyramid_buffers: &mut PyramidBuffers,
239 multiscale: Option<&CoarseToFineParams>,
240 shape: &DetectorShape<'_>,
241) -> Vec<CornerDescriptor> {
242 let base_view = ImageView::from_u8_slice(base.width, base.height, base.data)
243 .expect("base image dimensions must match buffer length");
244
245 let refine_border = if detector.refines_on_image() {
253 refiner_radius(shape.refiner_kind)
254 } else {
255 0
256 };
257
258 let Some(cf) = multiscale else {
262 let resp = detector.compute_response(base, params, detector_buffers);
263 let peaks = detector.detect_corners(&resp, params, refine_border);
264 let mut refiner = Refiner::from_kind(shape.refiner_kind.clone());
265 let mut corners = detector.refine_peaks_on_image(peaks, base_view, &resp, &mut refiner);
266 let merged = merge_corners_simple(&mut corners, shape.merge_radius);
267 return describe_corners(
268 base.data,
269 base.width,
270 base.height,
271 shape.descriptor_ring_radius,
272 merged,
273 shape.orientation_method,
274 );
275 };
276
277 let pyramid = build_pyramid(to_pyramid_view(base), &cf.pyramid, pyramid_buffers);
278 if pyramid.levels.is_empty() {
279 return Vec::new();
280 }
281
282 if pyramid.levels.len() == 1 {
286 let lvl = &pyramid.levels[0];
287 let lvl_view = ImageView::from_u8_slice(lvl.img.width, lvl.img.height, lvl.img.data)
288 .expect("pyramid level dimensions must match buffer length");
289 let resp = detector.compute_response(lvl_view, params, detector_buffers);
290 let peaks = detector.detect_corners(&resp, params, refine_border);
291 let mut refiner = Refiner::from_kind(shape.refiner_kind.clone());
292 let mut corners = detector.refine_peaks_on_image(peaks, lvl_view, &resp, &mut refiner);
293 let merged = merge_corners_simple(&mut corners, cf.merge_radius);
294 return describe_corners(
295 lvl.img.data,
296 lvl.img.width,
297 lvl.img.height,
298 shape.descriptor_ring_radius,
299 merged,
300 shape.orientation_method,
301 );
302 }
303
304 let coarse_lvl = pyramid.levels.last().unwrap();
308 let coarse_w = coarse_lvl.img.width;
309 let coarse_h = coarse_lvl.img.height;
310
311 #[cfg(feature = "tracing")]
312 let coarse_span = info_span!("coarse_detect", w = coarse_w, h = coarse_h).entered();
313 let coarse_view = ImageView::from_u8_slice(coarse_w, coarse_h, coarse_lvl.img.data).unwrap();
315 let coarse_resp = detector.compute_response(coarse_view, params, detector_buffers);
316 let coarse_peaks = detector.detect_corners(&coarse_resp, params, refine_border);
317 let mut refiner = Refiner::from_kind(shape.refiner_kind.clone());
318 let coarse_corners =
319 detector.refine_peaks_on_image(coarse_peaks, coarse_view, &coarse_resp, &mut refiner);
320 drop(coarse_resp);
323 #[cfg(feature = "tracing")]
324 drop(coarse_span);
325
326 if coarse_corners.is_empty() {
327 return Vec::new();
328 }
329
330 let detector_border = detector.roi_border(params);
331 let roi_ctx = make_roi_context(base, coarse_lvl.scale, detector_border, refine_border, cf);
332
333 #[cfg(feature = "tracing")]
334 let refine_span = info_span!(
335 "refine",
336 seeds = coarse_corners.len(),
337 roi_r = roi_ctx.roi_r
338 )
339 .entered();
340
341 let mut refined: Vec<Corner> = Vec::new();
349 for c in coarse_corners {
350 let Some(roi_bounds) = roi_ctx.compute_roi(&c) else {
351 continue;
352 };
353 let (x0, y0, _x1, _y1) = roi_bounds;
354 let patch_resp =
355 detector.compute_response_patch(base, roi_bounds, params, detector_buffers);
356 let patch_peaks = detector.detect_corners(&patch_resp, params, refine_border);
363 if patch_peaks.is_empty() {
364 continue;
365 }
366
367 let patch_image = ImageView::with_origin(base.width, base.height, base.data, [x0, y0])
373 .expect("base image dimensions must match buffer length");
374 let mut patch_refined =
375 detector.refine_peaks_on_image(patch_peaks, patch_image, &patch_resp, &mut refiner);
376
377 drop(patch_resp);
380
381 for pc in &mut patch_refined {
383 pc.x += x0 as f32;
384 pc.y += y0 as f32;
385 }
386 refined.extend(patch_refined);
387 }
388
389 #[cfg(feature = "tracing")]
390 drop(refine_span);
391
392 #[cfg(feature = "tracing")]
393 let merge_span = info_span!(
394 "merge",
395 merge_radius = cf.merge_radius,
396 candidates = refined.len()
397 )
398 .entered();
399 let merged = merge_corners_simple(&mut refined, cf.merge_radius);
400 #[cfg(feature = "tracing")]
401 drop(merge_span);
402
403 describe_corners(
404 base.data,
405 base.width,
406 base.height,
407 shape.descriptor_ring_radius,
408 merged,
409 shape.orientation_method,
410 )
411}
412
413pub(crate) fn detect_with_buffers(
421 base: ImageView<'_>,
422 cfg: &DetectorConfig,
423 pyramid_buffers: &mut PyramidBuffers,
424 chess_buffers: &mut ChessBuffers,
425 radon_buffers: &mut RadonBuffers,
426) -> Vec<CornerDescriptor> {
427 let multiscale = cfg.to_coarse_to_fine_params();
428
429 match &cfg.strategy {
430 DetectionStrategy::Chess(_) => {
431 let chess_params = cfg.to_chess_params();
432 let refiner_kind = chess_params.refiner.clone();
433 let shape = DetectorShape {
434 refiner_kind: &refiner_kind,
435 descriptor_ring_radius: chess_params.descriptor_ring_radius(),
436 orientation_method: chess_params.orientation_method,
437 merge_radius: cfg.merge_radius,
438 };
439 detect_multiscale(
440 base,
441 &ChessDetector,
442 &chess_params,
443 chess_buffers,
444 pyramid_buffers,
445 multiscale.as_ref(),
446 &shape,
447 )
448 }
449 DetectionStrategy::Radon(_) => {
450 let radon_params = cfg.to_radon_detector_params();
451 let refiner_kind = radon_params.refiner.clone();
452 let shape = DetectorShape {
456 refiner_kind: &refiner_kind,
457 descriptor_ring_radius: chess_corners_core::ChessParams::default()
458 .descriptor_ring_radius(),
459 orientation_method: cfg.orientation_method,
460 merge_radius: cfg.merge_radius,
461 };
462 detect_multiscale(
463 base,
464 &RadonDetector,
465 &radon_params,
466 radon_buffers,
467 pyramid_buffers,
468 multiscale.as_ref(),
469 &shape,
470 )
471 }
472 }
473}
474
475#[cfg(feature = "ml-refiner")]
488pub(crate) fn detect_with_ml(
489 base: ImageView<'_>,
490 cfg: &DetectorConfig,
491 pyramid_buffers: &mut PyramidBuffers,
492 chess_buffers: &mut ChessBuffers,
493 radon_buffers: &mut RadonBuffers,
494 ml: &ml_refiner::MlRefinerParams,
495 ml_state: &mut ml_refiner::MlRefinerState,
496) -> Vec<CornerDescriptor> {
497 if matches!(&cfg.strategy, DetectionStrategy::Radon(_)) {
500 return detect_with_buffers(base, cfg, pyramid_buffers, chess_buffers, radon_buffers);
501 }
502
503 let _ = (radon_buffers,); let params = cfg.to_chess_params();
506 let ml_border = ml_refiner::patch_radius(ml);
507 coarse_to_fine_with_ml(
508 base,
509 cfg,
510 pyramid_buffers,
511 chess_buffers,
512 ¶ms,
513 ml_border,
514 &mut |resp, p, image| detect_with_ml_refiner(resp, p, image, ml_state),
515 )
516}
517
518#[cfg(feature = "ml-refiner")]
523fn coarse_to_fine_with_ml<R>(
524 base: ImageView<'_>,
525 cfg: &DetectorConfig,
526 pyramid_buffers: &mut PyramidBuffers,
527 chess_buffers: &mut ChessBuffers,
528 params: &ChessParams,
529 refine_border: i32,
530 detect_fn: &mut R,
531) -> Vec<CornerDescriptor>
532where
533 R: FnMut(&ResponseMap, &ChessParams, Option<ImageView<'_>>) -> Vec<Corner>,
534{
535 let Some(cf) = cfg.to_coarse_to_fine_params() else {
537 let detector = ChessDetector;
538 let resp = detector.compute_response(base, params, chess_buffers);
539 let view = ImageView::from_u8_slice(base.width, base.height, base.data)
540 .expect("image dimensions must match buffer length");
541 let mut raw = detect_fn(resp, params, Some(view));
542 let merged = merge_corners_simple(&mut raw, cfg.merge_radius);
543 return describe_corners(
544 base.data,
545 base.width,
546 base.height,
547 params.descriptor_ring_radius(),
548 merged,
549 params.orientation_method,
550 );
551 };
552
553 let pyramid = build_pyramid(to_pyramid_view(base), &cf.pyramid, pyramid_buffers);
554 if pyramid.levels.is_empty() {
555 return Vec::new();
556 }
557
558 if pyramid.levels.len() == 1 {
560 let lvl = &pyramid.levels[0];
561 let resp = chess_response_u8(lvl.img.data, lvl.img.width, lvl.img.height, params);
562 let view = ImageView::from_u8_slice(lvl.img.width, lvl.img.height, lvl.img.data)
563 .expect("image dimensions must match buffer length");
564 let mut raw = detect_fn(&resp, params, Some(view));
565 let merged = merge_corners_simple(&mut raw, cf.merge_radius);
566 return describe_corners(
567 lvl.img.data,
568 lvl.img.width,
569 lvl.img.height,
570 params.descriptor_ring_radius(),
571 merged,
572 params.orientation_method,
573 );
574 }
575
576 let coarse_lvl = pyramid.levels.last().unwrap();
579 let coarse_w = coarse_lvl.img.width;
580 let coarse_h = coarse_lvl.img.height;
581
582 #[cfg(feature = "tracing")]
583 let coarse_span = info_span!("coarse_detect", w = coarse_w, h = coarse_h).entered();
584 let coarse_resp = chess_response_u8(coarse_lvl.img.data, coarse_w, coarse_h, params);
585 let coarse_view = ImageView::from_u8_slice(coarse_w, coarse_h, coarse_lvl.img.data).unwrap();
586 let coarse_corners =
587 detect_with_refiner_kind(&coarse_resp, params, Some(coarse_view), ¶ms.refiner);
588 #[cfg(feature = "tracing")]
589 drop(coarse_span);
590
591 if coarse_corners.is_empty() {
592 return Vec::new();
593 }
594
595 let detector_border = ChessDetector.roi_border(params);
596 let roi_ctx = make_roi_context(base, coarse_lvl.scale, detector_border, refine_border, &cf);
597
598 #[cfg(feature = "tracing")]
599 let refine_span = info_span!(
600 "refine",
601 seeds = coarse_corners.len(),
602 roi_r = roi_ctx.roi_r
603 )
604 .entered();
605
606 let mut refined: Vec<Corner> = Vec::new();
607 for c in coarse_corners {
608 let Some((x0, y0, x1, y1)) = roi_ctx.compute_roi(&c) else {
609 continue;
610 };
611 let roi = match Roi::new(x0 as usize, y0 as usize, x1 as usize, y1 as usize) {
612 Some(r) => r,
613 None => continue,
614 };
615 let patch_resp = chess_response_u8_patch(base.data, base.width, base.height, params, roi);
616 if patch_resp.width() == 0 || patch_resp.height() == 0 {
617 continue;
618 }
619 let refine_view = ImageView::with_origin(base.width, base.height, base.data, [x0, y0])
620 .expect("base image dimensions must match buffer length");
621 let mut patch_corners = detect_fn(&patch_resp, params, Some(refine_view));
622 for pc in &mut patch_corners {
623 pc.x += x0 as f32;
624 pc.y += y0 as f32;
625 }
626 refined.extend(patch_corners);
627 }
628
629 #[cfg(feature = "tracing")]
630 drop(refine_span);
631
632 #[cfg(feature = "tracing")]
633 let merge_span = info_span!(
634 "merge",
635 merge_radius = cf.merge_radius,
636 candidates = refined.len()
637 )
638 .entered();
639 let merged = merge_corners_simple(&mut refined, cf.merge_radius);
640 #[cfg(feature = "tracing")]
641 drop(merge_span);
642
643 describe_corners(
644 base.data,
645 base.width,
646 base.height,
647 params.descriptor_ring_radius(),
648 merged,
649 params.orientation_method,
650 )
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656 use box_image_pyramid::ImageBuffer;
657
658 #[test]
659 fn default_coarse_to_fine_config_is_single_scale() {
660 let cfg = CoarseToFineParams::default();
661 assert_eq!(cfg.pyramid.num_levels, 1);
662 assert_eq!(cfg.pyramid.min_size, 128);
663 assert_eq!(cfg.refinement_radius, 3);
664 assert_eq!(cfg.merge_radius, 3.0);
665 }
666
667 #[test]
668 fn chess_config_multiscale_preset_has_expected_pyramid() {
669 let cfg = DetectorConfig::chess_multiscale();
670 let cf = cfg
671 .to_coarse_to_fine_params()
672 .expect("chess_multiscale preset must produce CoarseToFineParams");
673 assert_eq!(cf.pyramid.num_levels, 3);
674 assert_eq!(cf.pyramid.min_size, 128);
675 assert_eq!(cf.refinement_radius, 3);
676 assert_eq!(cf.merge_radius, 3.0);
677 }
678
679 #[test]
680 fn coarse_to_fine_trace_reports_timings() {
681 let buf = ImageBuffer::new(32, 32);
682 let view = ImageView::from_u8_slice(buf.width, buf.height, &buf.data)
683 .expect("dimensions must match");
684 let cfg = DetectorConfig::default();
685 let mut pyramid = PyramidBuffers::default();
686 let mut chess_buffers = ChessBuffers::default();
687 let mut radon_buffers = RadonBuffers::default();
688 let corners = detect_with_buffers(
689 view,
690 &cfg,
691 &mut pyramid,
692 &mut chess_buffers,
693 &mut radon_buffers,
694 );
695 assert!(corners.is_empty());
696 }
697}