calib_targets/
detect.rs

1use crate::{charuco, chessboard, core, marker};
2use chess_corners::{find_chess_corners_image, ChessConfig, CornerDescriptor};
3use nalgebra::Point2;
4
5#[cfg(feature = "tracing")]
6use tracing::instrument;
7
8/// Errors produced by the high-level facade helpers.
9#[derive(thiserror::Error, Debug)]
10pub enum DetectError {
11    #[error("invalid grayscale image buffer length (expected {expected} bytes, got {got})")]
12    InvalidGrayBuffer { expected: usize, got: usize },
13
14    #[error("invalid grayscale image dimensions (width={width}, height={height})")]
15    InvalidGrayDimensions { width: u32, height: u32 },
16
17    #[error(transparent)]
18    CharucoBoard(#[from] charuco::CharucoBoardError),
19
20    #[error(transparent)]
21    CharucoDetect(#[from] charuco::CharucoDetectError),
22}
23
24/// Reasonable default settings for the `chess-corners` ChESS detector.
25///
26/// This is tuned for the repo examples and is expected to be overridden by callers
27/// for difficult real-world images.
28pub fn default_chess_config() -> ChessConfig {
29    let mut cfg = ChessConfig::single_scale();
30    cfg.params.threshold_rel = 0.2;
31    cfg.params.nms_radius = 2;
32    cfg
33}
34
35/// Convert an `image::GrayImage` into the lightweight `calib-targets-core` view type.
36pub fn gray_view(img: &::image::GrayImage) -> core::GrayImageView<'_> {
37    core::GrayImageView {
38        width: img.width() as usize,
39        height: img.height() as usize,
40        data: img.as_raw(),
41    }
42}
43
44/// Detect raw ChESS corners using `chess-corners`.
45#[cfg_attr(
46    feature = "tracing",
47    instrument(level = "info", skip(img, cfg), fields(width = img.width(), height = img.height()))
48)]
49pub fn detect_chess_corners_raw(
50    img: &::image::GrayImage,
51    cfg: &ChessConfig,
52) -> Vec<CornerDescriptor> {
53    find_chess_corners_image(img, cfg)
54}
55
56/// Detect ChESS corners and adapt them into `calib-targets-core::Corner`.
57pub fn detect_corners(img: &::image::GrayImage, cfg: &ChessConfig) -> Vec<core::Corner> {
58    detect_chess_corners_raw(img, cfg)
59        .iter()
60        .map(adapt_chess_corner)
61        .collect()
62}
63
64/// Convenience overload using `default_chess_config()`.
65pub fn detect_corners_default(img: &::image::GrayImage) -> Vec<core::Corner> {
66    let cfg = default_chess_config();
67    detect_corners(img, &cfg)
68}
69
70/// Run the chessboard detector end-to-end: ChESS corners -> chessboard grid.
71#[cfg_attr(
72    feature = "tracing",
73    instrument(
74        level = "info",
75        skip(img, chess_cfg, params),
76        fields(width = img.width(), height = img.height())
77    )
78)]
79pub fn detect_chessboard(
80    img: &::image::GrayImage,
81    chess_cfg: &ChessConfig,
82    params: chessboard::ChessboardParams,
83) -> Option<chessboard::ChessboardDetectionResult> {
84    let corners = detect_corners(img, chess_cfg);
85    let detector = chessboard::ChessboardDetector::new(params);
86    detector.detect_from_corners(&corners)
87}
88
89/// Run the ChArUco detector end-to-end: ChESS corners -> grid -> markers -> alignment -> IDs.
90#[cfg_attr(
91    feature = "tracing",
92    instrument(
93        level = "info",
94        skip(img, chess_cfg, params),
95        fields(
96            width = img.width(),
97            height = img.height(),
98            board_rows = params.charuco.rows,
99            board_cols = params.charuco.cols
100        )
101    )
102)]
103pub fn detect_charuco(
104    img: &::image::GrayImage,
105    chess_cfg: &ChessConfig,
106    params: charuco::CharucoDetectorParams,
107) -> Result<charuco::CharucoDetectionResult, DetectError> {
108    let corners = detect_corners(img, chess_cfg);
109    let detector = charuco::CharucoDetector::new(params)?;
110    Ok(detector.detect(&gray_view(img), &corners)?)
111}
112
113/// Convenience overload using `default_chess_config()`.
114pub fn detect_charuco_default(
115    img: &::image::GrayImage,
116    params: charuco::CharucoDetectorParams,
117) -> Result<charuco::CharucoDetectionResult, DetectError> {
118    let chess_cfg = default_chess_config();
119    detect_charuco(img, &chess_cfg, params)
120}
121
122/// Run the checkerboard+circles marker board detector end-to-end.
123#[cfg_attr(
124    feature = "tracing",
125    instrument(
126        level = "info",
127        skip(img, chess_cfg, params),
128        fields(width = img.width(), height = img.height())
129    )
130)]
131pub fn detect_marker_board(
132    img: &::image::GrayImage,
133    chess_cfg: &ChessConfig,
134    params: marker::MarkerBoardParams,
135) -> Option<marker::MarkerBoardDetectionResult> {
136    let corners = detect_corners(img, chess_cfg);
137    let detector = marker::MarkerBoardDetector::new(params);
138    detector.detect_from_image_and_corners(&gray_view(img), &corners)
139}
140
141/// Convenience overload using `default_chess_config()`.
142pub fn detect_marker_board_default(
143    img: &::image::GrayImage,
144    params: marker::MarkerBoardParams,
145) -> Option<marker::MarkerBoardDetectionResult> {
146    let chess_cfg = default_chess_config();
147    detect_marker_board(img, &chess_cfg, params)
148}
149
150/// Build an `image::GrayImage` from a raw grayscale buffer.
151pub fn gray_image_from_slice(
152    width: u32,
153    height: u32,
154    pixels: &[u8],
155) -> Result<::image::GrayImage, DetectError> {
156    let w = usize::try_from(width).ok();
157    let h = usize::try_from(height).ok();
158    let Some((w, h)) = w.zip(h) else {
159        return Err(DetectError::InvalidGrayDimensions { width, height });
160    };
161    let Some(expected) = w.checked_mul(h) else {
162        return Err(DetectError::InvalidGrayDimensions { width, height });
163    };
164    if pixels.len() != expected {
165        return Err(DetectError::InvalidGrayBuffer {
166            expected,
167            got: pixels.len(),
168        });
169    }
170    ::image::GrayImage::from_raw(width, height, pixels.to_vec())
171        .ok_or(DetectError::InvalidGrayDimensions { width, height })
172}
173
174pub fn detect_chessboard_from_gray_u8(
175    width: u32,
176    height: u32,
177    pixels: &[u8],
178    chess_cfg: &ChessConfig,
179    params: chessboard::ChessboardParams,
180) -> Result<Option<chessboard::ChessboardDetectionResult>, DetectError> {
181    let img = gray_image_from_slice(width, height, pixels)?;
182    Ok(detect_chessboard(&img, chess_cfg, params))
183}
184
185pub fn detect_charuco_from_gray_u8(
186    width: u32,
187    height: u32,
188    pixels: &[u8],
189    chess_cfg: &ChessConfig,
190    params: charuco::CharucoDetectorParams,
191) -> Result<charuco::CharucoDetectionResult, DetectError> {
192    let img = gray_image_from_slice(width, height, pixels)?;
193    detect_charuco(&img, chess_cfg, params)
194}
195
196pub fn detect_marker_board_from_gray_u8(
197    width: u32,
198    height: u32,
199    pixels: &[u8],
200    chess_cfg: &ChessConfig,
201    params: marker::MarkerBoardParams,
202) -> Result<Option<marker::MarkerBoardDetectionResult>, DetectError> {
203    let img = gray_image_from_slice(width, height, pixels)?;
204    Ok(detect_marker_board(&img, chess_cfg, params))
205}
206
207fn adapt_chess_corner(c: &CornerDescriptor) -> core::Corner {
208    core::Corner {
209        position: Point2::new(c.x, c.y),
210        orientation: c.orientation,
211        orientation_cluster: None,
212        strength: c.response,
213    }
214}