calib_targets_charuco/detector/
pipeline.rs

1use super::alignment_select::select_alignment;
2use super::corner_mapping::map_charuco_corners;
3use super::marker_sampling::{build_corner_map, build_marker_cells};
4use super::{CharucoDetectError, CharucoDetectionResult, CharucoDetectorParams};
5use crate::alignment::CharucoAlignment;
6use crate::board::{CharucoBoard, CharucoBoardError};
7use calib_targets_aruco::{scan_decode_markers_in_cells, MarkerDetection, Matcher};
8use calib_targets_chessboard::ChessboardDetector;
9use calib_targets_core::{Corner, GrayImageView};
10
11#[cfg(feature = "tracing")]
12use tracing::instrument;
13
14/// Grid-first ChArUco detector.
15#[derive(Debug)]
16pub struct CharucoDetector {
17    board: CharucoBoard,
18    params: CharucoDetectorParams,
19    matcher: Matcher,
20}
21
22impl CharucoDetector {
23    /// Create a detector from parameters (board spec lives in `params.charuco`).
24    pub fn new(mut params: CharucoDetectorParams) -> Result<Self, CharucoBoardError> {
25        let board_cfg = params.charuco;
26        if params.chessboard.expected_rows.is_none() {
27            // `board_cfg.rows/cols` are square counts; chessboard detector expects inner corners.
28            params.chessboard.expected_rows = board_cfg.rows.checked_sub(1);
29        }
30        if params.chessboard.expected_cols.is_none() {
31            params.chessboard.expected_cols = board_cfg.cols.checked_sub(1);
32        }
33        if !params.scan.marker_size_rel.is_finite() || params.scan.marker_size_rel <= 0.0 {
34            params.scan.marker_size_rel = board_cfg.marker_size_rel;
35        }
36
37        let max_hamming = params
38            .max_hamming
39            .min(board_cfg.dictionary.max_correction_bits);
40        params.max_hamming = max_hamming;
41
42        let matcher = Matcher::new(board_cfg.dictionary, max_hamming);
43        let board = CharucoBoard::new(board_cfg)?;
44
45        Ok(Self {
46            board,
47            params,
48            matcher,
49        })
50    }
51
52    /// Board definition used by the detector.
53    #[inline]
54    pub fn board(&self) -> &CharucoBoard {
55        &self.board
56    }
57
58    /// Detector parameters.
59    #[inline]
60    pub fn params(&self) -> &CharucoDetectorParams {
61        &self.params
62    }
63
64    /// Detect a ChArUco board from an image and a set of corners.
65    ///
66    /// This uses per-cell marker sampling by default. Set
67    /// `build_rectified_image` if you need a rectified output image.
68    #[cfg_attr(feature = "tracing", instrument(level = "info", skip(self, image, corners), fields(num_corners=corners.len())))]
69    pub fn detect(
70        &self,
71        image: &GrayImageView<'_>,
72        corners: &[Corner],
73    ) -> Result<CharucoDetectionResult, CharucoDetectError> {
74        let detector = ChessboardDetector::new(self.params.chessboard.clone())
75            .with_grid_search(self.params.graph.clone());
76        let chessboard = detector
77            .detect_from_corners(corners)
78            .ok_or(CharucoDetectError::ChessboardNotDetected)?;
79
80        let corner_map = build_corner_map(&chessboard.detection.corners, &chessboard.inliers);
81        let cells = build_marker_cells(&corner_map);
82
83        let scan_cfg = self.params.scan.clone();
84
85        let markers = scan_decode_markers_in_cells(
86            image,
87            &cells,
88            self.params.px_per_square,
89            &scan_cfg,
90            &self.matcher,
91        );
92
93        if markers.is_empty() {
94            return Err(CharucoDetectError::NoMarkers);
95        }
96
97        let (markers, alignment) = self
98            .select_and_refine_markers(markers)
99            .ok_or(CharucoDetectError::AlignmentFailed { inliers: 0usize })?;
100
101        if alignment.marker_inliers.len() < self.params.min_marker_inliers {
102            return Err(CharucoDetectError::AlignmentFailed {
103                inliers: alignment.marker_inliers.len(),
104            });
105        }
106
107        let detection = map_charuco_corners(&self.board, &chessboard.detection, &alignment);
108
109        Ok(CharucoDetectionResult {
110            detection,
111            markers,
112            alignment: alignment.alignment,
113        })
114    }
115
116    #[cfg_attr(feature = "tracing", instrument(level = "info", skip(self, markers),
117      fields(markers=markers.len())))]
118    fn select_and_refine_markers(
119        &self,
120        markers: Vec<MarkerDetection>,
121    ) -> Option<(Vec<MarkerDetection>, CharucoAlignment)> {
122        // TODO: just run solve_aligment on the full set of markers
123        let (markers, alignment) = select_alignment(&self.board, markers)?;
124
125        Some((markers, alignment))
126    }
127}