1#[cfg(feature = "ml-refiner")]
23use crate::ml_refiner;
24use crate::{ChessConfig, ChessParams};
25use box_image_pyramid::{build_pyramid, PyramidBuffers, PyramidParams};
26use chess_corners_core::descriptor::{corners_to_descriptors, Corner};
27use chess_corners_core::detect::{detect_corners_from_response_with_refiner, merge_corners_simple};
28use chess_corners_core::response::{chess_response_u8, chess_response_u8_patch, Roi};
29use chess_corners_core::{CornerDescriptor, CornerRefiner};
30use chess_corners_core::{ImageView, Refiner, RefinerKind, ResponseMap};
31
32fn to_pyramid_view(v: ImageView<'_>) -> box_image_pyramid::ImageView<'_> {
34 box_image_pyramid::ImageView::new(v.width, v.height, v.data).unwrap()
35}
36#[cfg(feature = "rayon")]
37use rayon::prelude::*;
38#[cfg(feature = "tracing")]
39use tracing::{info_span, instrument};
40
41#[derive(Clone, Debug)]
46#[non_exhaustive]
47pub struct CoarseToFineParams {
48 pub pyramid: PyramidParams,
50 pub refinement_radius: u32,
54 pub merge_radius: f32,
57}
58
59impl Default for CoarseToFineParams {
60 fn default() -> Self {
61 Self {
62 pyramid: PyramidParams::default(),
63 refinement_radius: 3,
67 merge_radius: 3.0,
69 }
70 }
71}
72
73impl CoarseToFineParams {
74 pub fn new() -> Self {
75 Self::default()
76 }
77}
78
79#[cfg(feature = "ml-refiner")]
85fn detect_with_ml_refiner(
86 resp: &ResponseMap,
87 params: &ChessParams,
88 image: Option<ImageView<'_>>,
89 ml_state: &mut ml_refiner::MlRefinerState,
90) -> Vec<Corner> {
91 ml_refiner::detect_corners_with_ml(resp, params, image, ml_state)
92}
93
94fn detect_with_refiner_kind(
95 resp: &ResponseMap,
96 params: &ChessParams,
97 image: Option<ImageView<'_>>,
98 refiner_kind: &RefinerKind,
99) -> Vec<Corner> {
100 let mut refiner = Refiner::from_kind(refiner_kind.clone());
101 detect_corners_from_response_with_refiner(resp, params, image, &mut refiner)
102}
103
104fn refiner_radius(refiner_kind: &RefinerKind) -> i32 {
105 Refiner::from_kind(refiner_kind.clone()).radius()
106}
107
108struct RoiContext {
114 inv_scale: f32,
115 border: i32,
116 safe_margin: i32,
117 roi_r: i32,
118 base_w_i: i32,
119 base_h_i: i32,
120}
121
122impl RoiContext {
123 fn compute_roi(&self, c: &Corner) -> Option<(i32, i32, i32, i32)> {
127 let cx = (c.x * self.inv_scale).round() as i32;
128 let cy = (c.y * self.inv_scale).round() as i32;
129
130 if cx < self.safe_margin
131 || cy < self.safe_margin
132 || cx >= self.base_w_i - self.safe_margin
133 || cy >= self.base_h_i - self.safe_margin
134 {
135 return None;
136 }
137
138 let mut x0 = cx - self.roi_r;
139 let mut y0 = cy - self.roi_r;
140 let mut x1 = cx + self.roi_r + 1;
141 let mut y1 = cy + self.roi_r + 1;
142
143 let min_xy = self.border;
144 let max_x = self.base_w_i - self.border;
145 let max_y = self.base_h_i - self.border;
146
147 if x0 < min_xy {
148 x0 = min_xy;
149 }
150 if y0 < min_xy {
151 y0 = min_xy;
152 }
153 if x1 > max_x {
154 x1 = max_x;
155 }
156 if y1 > max_y {
157 y1 = max_y;
158 }
159
160 if x1 - x0 <= 2 * self.border || y1 - y0 <= 2 * self.border {
161 return None;
162 }
163
164 Some((x0, y0, x1, y1))
165 }
166}
167
168fn refine_seed_in_roi(
175 base: ImageView<'_>,
176 params: &ChessParams,
177 roi_bounds: (i32, i32, i32, i32),
178 mut detect: impl FnMut(&ResponseMap, &ChessParams, Option<ImageView<'_>>) -> Vec<Corner>,
179) -> Option<Vec<Corner>> {
180 let (x0, y0, x1, y1) = roi_bounds;
181 let base_w = base.width;
182 let base_h = base.height;
183
184 let roi = Roi::new(x0 as usize, y0 as usize, x1 as usize, y1 as usize)?;
185 let patch_resp = chess_response_u8_patch(base.data, base_w, base_h, params, roi);
186
187 if patch_resp.width() == 0 || patch_resp.height() == 0 {
188 return None;
189 }
190
191 let refine_view = ImageView::with_origin(base_w, base_h, base.data, [x0, y0])
192 .expect("base image dimensions must match buffer length");
193 let mut patch_corners = detect(&patch_resp, params, Some(refine_view));
194
195 for pc in &mut patch_corners {
196 pc.x += x0 as f32;
197 pc.y += y0 as f32;
198 }
199
200 if patch_corners.is_empty() {
201 None
202 } else {
203 Some(patch_corners)
204 }
205}
206
207fn single_scale_detect(
209 lvl_data: &[u8],
210 lvl_w: usize,
211 lvl_h: usize,
212 params: &ChessParams,
213 merge_radius: f32,
214 mut detect: impl FnMut(&ResponseMap, &ChessParams, Option<ImageView<'_>>) -> Vec<Corner>,
215) -> Vec<CornerDescriptor> {
216 #[cfg(feature = "tracing")]
217 let single_span = info_span!("single_scale", w = lvl_w, h = lvl_h).entered();
218
219 let resp = chess_response_u8(lvl_data, lvl_w, lvl_h, params);
220 let refine_view = ImageView::from_u8_slice(lvl_w, lvl_h, lvl_data)
221 .expect("image dimensions must match buffer length");
222 let mut raw = detect(&resp, params, Some(refine_view));
223 let merged = merge_corners_simple(&mut raw, merge_radius);
224 let desc = corners_to_descriptors(
225 lvl_data,
226 lvl_w,
227 lvl_h,
228 params.descriptor_ring_radius(),
229 merged,
230 );
231
232 #[cfg(feature = "tracing")]
233 drop(single_span);
234 desc
235}
236
237fn merge_and_describe(
239 base: ImageView<'_>,
240 params: &ChessParams,
241 merge_radius: f32,
242 refined: &mut Vec<Corner>,
243) -> Vec<CornerDescriptor> {
244 #[cfg(feature = "tracing")]
245 let merge_span = info_span!(
246 "merge",
247 merge_radius = merge_radius,
248 candidates = refined.len()
249 )
250 .entered();
251 let merged = merge_corners_simple(refined, merge_radius);
252 #[cfg(feature = "tracing")]
253 drop(merge_span);
254
255 corners_to_descriptors(
256 base.data,
257 base.width,
258 base.height,
259 params.descriptor_ring_radius(),
260 merged,
261 )
262}
263
264pub fn find_chess_corners_buff(
278 base: ImageView<'_>,
279 cfg: &ChessConfig,
280 buffers: &mut PyramidBuffers,
281) -> Vec<CornerDescriptor> {
282 find_chess_corners_buff_with_refiner(base, cfg, buffers, &cfg.params.refiner)
283}
284
285pub fn find_chess_corners_buff_with_refiner(
287 base: ImageView<'_>,
288 cfg: &ChessConfig,
289 buffers: &mut PyramidBuffers,
290 refiner: &RefinerKind,
291) -> Vec<CornerDescriptor> {
292 let params = &cfg.params;
293 let cf = &cfg.multiscale;
294
295 let pyramid = build_pyramid(to_pyramid_view(base), &cf.pyramid, buffers);
296 if pyramid.levels.is_empty() {
297 return Vec::new();
298 }
299
300 if pyramid.levels.len() == 1 {
302 let lvl = &pyramid.levels[0];
303 return single_scale_detect(
304 lvl.img.data,
305 lvl.img.width,
306 lvl.img.height,
307 params,
308 cf.merge_radius,
309 |resp, params, image| detect_with_refiner_kind(resp, params, image, refiner),
310 );
311 }
312
313 let coarse_lvl = pyramid.levels.last().unwrap();
316 let coarse_w = coarse_lvl.img.width;
317 let coarse_h = coarse_lvl.img.height;
318
319 #[cfg(feature = "tracing")]
320 let coarse_span = info_span!("coarse_detect", w = coarse_w, h = coarse_h).entered();
321 let coarse_resp = chess_response_u8(coarse_lvl.img.data, coarse_w, coarse_h, params);
322 let coarse_view = ImageView::from_u8_slice(coarse_w, coarse_h, coarse_lvl.img.data).unwrap();
323 let coarse_corners = detect_with_refiner_kind(&coarse_resp, params, Some(coarse_view), refiner);
324 #[cfg(feature = "tracing")]
325 drop(coarse_span);
326
327 if coarse_corners.is_empty() {
328 return Vec::new();
329 }
330
331 let roi_ctx = make_roi_context(base, coarse_lvl.scale, params, refiner_radius(refiner), cf);
332
333 let refine_one = |c: Corner| -> Option<Vec<Corner>> {
334 let roi_bounds = roi_ctx.compute_roi(&c)?;
335 refine_seed_in_roi(base, params, roi_bounds, |resp, params, image| {
336 detect_with_refiner_kind(resp, params, image, refiner)
337 })
338 };
339
340 #[cfg(feature = "tracing")]
341 let refine_span = info_span!(
342 "refine",
343 seeds = coarse_corners.len(),
344 roi_r = roi_ctx.roi_r
345 )
346 .entered();
347
348 #[cfg(feature = "rayon")]
349 let mut refined: Vec<Corner> = coarse_corners
350 .into_par_iter()
351 .filter_map(refine_one)
352 .flatten()
353 .collect();
354
355 #[cfg(not(feature = "rayon"))]
356 let mut refined: Vec<Corner> = coarse_corners
357 .into_iter()
358 .filter_map(refine_one)
359 .flatten()
360 .collect();
361
362 #[cfg(feature = "tracing")]
363 drop(refine_span);
364
365 merge_and_describe(base, params, cf.merge_radius, &mut refined)
366}
367
368#[cfg(feature = "ml-refiner")]
374pub fn find_chess_corners_buff_with_ml(
375 base: ImageView<'_>,
376 cfg: &ChessConfig,
377 buffers: &mut PyramidBuffers,
378) -> Vec<CornerDescriptor> {
379 let ml_params = ml_refiner::MlRefinerParams::default();
380 let mut ml_state = ml_refiner::MlRefinerState::new(&ml_params, &cfg.params.refiner);
381 find_chess_corners_buff_with_ml_state(base, cfg, buffers, &ml_params, &mut ml_state)
382}
383
384#[cfg(feature = "ml-refiner")]
385fn find_chess_corners_buff_with_ml_state(
386 base: ImageView<'_>,
387 cfg: &ChessConfig,
388 buffers: &mut PyramidBuffers,
389 ml: &ml_refiner::MlRefinerParams,
390 ml_state: &mut ml_refiner::MlRefinerState,
391) -> Vec<CornerDescriptor> {
392 let params = &cfg.params;
393 let cf = &cfg.multiscale;
394
395 let pyramid = build_pyramid(to_pyramid_view(base), &cf.pyramid, buffers);
396 if pyramid.levels.is_empty() {
397 return Vec::new();
398 }
399
400 if pyramid.levels.len() == 1 {
402 let lvl = &pyramid.levels[0];
403 return single_scale_detect(
404 lvl.img.data,
405 lvl.img.width,
406 lvl.img.height,
407 params,
408 cf.merge_radius,
409 |resp, params, image| detect_with_ml_refiner(resp, params, image, ml_state),
410 );
411 }
412
413 let coarse_lvl = pyramid.levels.last().unwrap();
417 let coarse_w = coarse_lvl.img.width;
418 let coarse_h = coarse_lvl.img.height;
419
420 #[cfg(feature = "tracing")]
421 let coarse_span = info_span!("coarse_detect", w = coarse_w, h = coarse_h).entered();
422 let coarse_resp = chess_response_u8(coarse_lvl.img.data, coarse_w, coarse_h, params);
423 let coarse_view = ImageView::from_u8_slice(coarse_w, coarse_h, coarse_lvl.img.data).unwrap();
424 let coarse_corners =
425 detect_with_refiner_kind(&coarse_resp, params, Some(coarse_view), ¶ms.refiner);
426 #[cfg(feature = "tracing")]
427 drop(coarse_span);
428
429 if coarse_corners.is_empty() {
430 return Vec::new();
431 }
432
433 let roi_ctx = make_roi_context(
434 base,
435 coarse_lvl.scale,
436 params,
437 ml_refiner::patch_radius(ml),
438 cf,
439 );
440
441 #[cfg(feature = "tracing")]
442 let refine_span = info_span!(
443 "refine",
444 seeds = coarse_corners.len(),
445 roi_r = roi_ctx.roi_r
446 )
447 .entered();
448
449 let mut refined: Vec<Corner> = coarse_corners
451 .into_iter()
452 .filter_map(|c| {
453 let roi_bounds = roi_ctx.compute_roi(&c)?;
454 refine_seed_in_roi(base, params, roi_bounds, |resp, params, image| {
455 detect_with_ml_refiner(resp, params, image, ml_state)
456 })
457 })
458 .flatten()
459 .collect();
460
461 #[cfg(feature = "tracing")]
462 drop(refine_span);
463
464 merge_and_describe(base, params, cf.merge_radius, &mut refined)
465}
466
467fn make_roi_context(
472 base: ImageView<'_>,
473 coarse_scale: f32,
474 params: &ChessParams,
475 refine_border: i32,
476 cf: &CoarseToFineParams,
477) -> RoiContext {
478 let ring_r = params.ring_radius() as i32;
479 let nms_r = params.nms_radius as i32;
480 let border = (ring_r + nms_r + refine_border).max(0);
481 let safe_margin = border + 1;
482 let roi_r_base = (cf.refinement_radius as f32 / coarse_scale).ceil() as i32;
483 let min_roi_r = border + 2;
484
485 RoiContext {
486 inv_scale: 1.0 / coarse_scale,
487 border,
488 safe_margin,
489 roi_r: roi_r_base.max(min_roi_r),
490 base_w_i: base.width as i32,
491 base_h_i: base.height as i32,
492 }
493}
494
495#[must_use]
508#[cfg_attr(
509 feature = "tracing",
510 instrument(
511 level = "info",
512 skip(base, cfg),
513 fields(levels = cfg.multiscale.pyramid.num_levels, min_size = cfg.multiscale.pyramid.min_size)
514 )
515)]
516pub fn find_chess_corners(base: ImageView<'_>, cfg: &ChessConfig) -> Vec<CornerDescriptor> {
517 find_chess_corners_with_refiner(base, cfg, &cfg.params.refiner)
518}
519
520#[must_use]
522pub fn find_chess_corners_with_refiner(
523 base: ImageView<'_>,
524 cfg: &ChessConfig,
525 refiner: &RefinerKind,
526) -> Vec<CornerDescriptor> {
527 let mut buffers = PyramidBuffers::with_capacity(cfg.multiscale.pyramid.num_levels);
528 find_chess_corners_buff_with_refiner(base, cfg, &mut buffers, refiner)
529}
530
531#[cfg(feature = "ml-refiner")]
533#[must_use]
534pub fn find_chess_corners_with_ml(base: ImageView<'_>, cfg: &ChessConfig) -> Vec<CornerDescriptor> {
535 let mut buffers = PyramidBuffers::with_capacity(cfg.multiscale.pyramid.num_levels);
536 find_chess_corners_buff_with_ml(base, cfg, &mut buffers)
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542 use box_image_pyramid::ImageBuffer;
543
544 #[test]
545 fn default_coarse_to_fine_config_is_single_scale() {
546 let cfg = CoarseToFineParams::default();
547 assert_eq!(cfg.pyramid.num_levels, 1);
548 assert_eq!(cfg.pyramid.min_size, 128);
549 assert_eq!(cfg.refinement_radius, 3);
550 assert_eq!(cfg.merge_radius, 3.0);
551 }
552
553 #[test]
554 fn chess_config_multiscale_preset_has_expected_pyramid() {
555 let cfg = ChessConfig::multiscale();
556 assert_eq!(cfg.multiscale.pyramid.num_levels, 3);
557 assert_eq!(cfg.multiscale.pyramid.min_size, 128);
558 assert_eq!(cfg.multiscale.refinement_radius, 3);
559 assert_eq!(cfg.multiscale.merge_radius, 3.0);
560 }
561
562 #[test]
563 fn coarse_to_fine_trace_reports_timings() {
564 let buf = ImageBuffer::new(32, 32);
565 let view = ImageView::from_u8_slice(buf.width, buf.height, &buf.data)
566 .expect("dimensions must match");
567 let cfg = ChessConfig::default();
568 let corners = find_chess_corners(view, &cfg);
569 assert!(corners.is_empty());
570 }
571}