use crate::{charuco, chessboard, core, marker, puzzleboard};
use chess_corners::{Detector as ChessDetector, Threshold};
use nalgebra::Point2;
#[cfg(feature = "tracing")]
use tracing::instrument;
pub use core::{DetectorConfig, OrientationMethod};
#[non_exhaustive]
#[derive(thiserror::Error, Debug)]
pub enum DetectError {
#[error("invalid grayscale image buffer length (expected {expected} bytes, got {got})")]
InvalidGrayBuffer {
expected: usize,
got: usize,
},
#[error("invalid grayscale image dimensions (width={width}, height={height})")]
InvalidGrayDimensions {
width: u32,
height: u32,
},
#[error(transparent)]
CharucoBoard(#[from] charuco::CharucoBoardError),
#[error(transparent)]
CharucoDetect(#[from] charuco::CharucoDetectError),
#[error(transparent)]
PuzzleBoardSpec(#[from] puzzleboard::PuzzleBoardSpecError),
#[error(transparent)]
PuzzleBoardDetect(#[from] puzzleboard::PuzzleBoardDetectError),
}
pub fn default_chess_config() -> DetectorConfig {
DetectorConfig::chess().with_threshold(Threshold::Absolute(15.0))
}
pub fn gray_view(img: &::image::GrayImage) -> core::GrayImageView<'_> {
core::GrayImageView {
width: img.width() as usize,
height: img.height() as usize,
data: img.as_raw(),
}
}
pub fn preprocess(img: &::image::GrayImage, blur_sigma_px: f32) -> ::image::GrayImage {
if blur_sigma_px.is_finite() && blur_sigma_px > 0.0 {
::image::imageops::blur(img, blur_sigma_px)
} else {
img.clone()
}
}
#[cfg_attr(
feature = "tracing",
instrument(level = "info", skip(img, cfg), fields(width = img.width(), height = img.height()))
)]
pub fn detect_corners(
img: &::image::GrayImage,
cfg: &DetectorConfig,
) -> Vec<chessboard::ChessCorner> {
let Ok(mut detector) = ChessDetector::new(*cfg) else {
return Vec::new();
};
detector
.detect(img)
.unwrap_or_default()
.iter()
.map(adapt_chess_corner)
.collect()
}
pub fn detect_corners_default(img: &::image::GrayImage) -> Vec<chessboard::ChessCorner> {
detect_corners(img, &default_chess_config())
}
#[cfg_attr(
feature = "tracing",
instrument(
level = "info",
skip(img, chess_cfg, params),
fields(width = img.width(), height = img.height())
)
)]
pub fn detect_chessboard(
img: &::image::GrayImage,
chess_cfg: &DetectorConfig,
params: &chessboard::DetectorParams,
) -> Option<chessboard::ChessboardDetection> {
let corners = detect_corners(img, chess_cfg);
let detector = chessboard::Detector::new(params.clone()).ok()?;
detector.detect(&corners)
}
#[cfg_attr(
feature = "tracing",
instrument(
level = "info",
skip(img, chess_cfg, params),
fields(width = img.width(), height = img.height())
)
)]
pub fn detect_chessboard_all(
img: &::image::GrayImage,
chess_cfg: &DetectorConfig,
params: &chessboard::DetectorParams,
) -> Vec<chessboard::ChessboardDetection> {
let corners = detect_corners(img, chess_cfg);
let Ok(detector) = chessboard::Detector::new(params.clone()) else {
return Vec::new();
};
detector.detect_all(&corners)
}
#[cfg_attr(
feature = "tracing",
instrument(
level = "info",
skip(img, params),
fields(
width = img.width(),
height = img.height(),
board_rows = params.board.rows,
board_cols = params.board.cols
)
)
)]
pub fn detect_charuco(
img: &::image::GrayImage,
params: &charuco::CharucoParams,
) -> Result<charuco::CharucoDetectionResult, DetectError> {
let corners = detect_corners_default(img);
let detector = charuco::CharucoDetector::new(params.clone())?;
Ok(detector.detect(&gray_view(img), &corners)?)
}
#[cfg_attr(
feature = "tracing",
instrument(
level = "info",
skip(img, params),
fields(
width = img.width(),
height = img.height(),
board_rows = params.board.rows,
board_cols = params.board.cols
)
)
)]
pub fn detect_puzzleboard(
img: &::image::GrayImage,
params: &puzzleboard::PuzzleBoardParams,
) -> Result<puzzleboard::PuzzleBoardDetectionResult, DetectError> {
let corners = detect_corners_default(img);
let detector = puzzleboard::PuzzleBoardDetector::new(params.clone())?;
Ok(detector.detect(&gray_view(img), &corners)?)
}
pub fn default_puzzleboard_params(
rows: u32,
cols: u32,
) -> Result<puzzleboard::PuzzleBoardParams, DetectError> {
let spec = puzzleboard::PuzzleBoardSpec::new(rows, cols, 1.0)?;
Ok(puzzleboard::PuzzleBoardParams::for_board(&spec))
}
#[cfg_attr(
feature = "tracing",
instrument(
level = "info",
skip(img, params),
fields(width = img.width(), height = img.height())
)
)]
pub fn detect_marker_board(
img: &::image::GrayImage,
params: &marker::MarkerBoardParams,
) -> Option<marker::MarkerBoardDetectionResult> {
let corners = detect_corners_default(img);
let detector = marker::MarkerBoardDetector::new(params.clone()).ok()?;
detector.detect_from_image_and_corners(&gray_view(img), &corners)
}
pub fn detect_chessboard_best(
img: &::image::GrayImage,
chess_cfg: &DetectorConfig,
param_configs: &[chessboard::DetectorParams],
) -> Option<chessboard::ChessboardDetection> {
let corners = detect_corners(img, chess_cfg);
param_configs
.iter()
.filter_map(|params| {
chessboard::Detector::new(params.clone())
.ok()?
.detect(&corners)
})
.max_by_key(|d| d.corners.len())
}
pub fn detect_charuco_best(
img: &::image::GrayImage,
configs: &[charuco::CharucoParams],
) -> Result<charuco::CharucoDetectionResult, DetectError> {
let mut best: Option<charuco::CharucoDetectionResult> = None;
let mut last_err = None;
let corners = detect_corners_default(img);
for params in configs {
let detector = match charuco::CharucoDetector::new(params.clone()) {
Ok(d) => d,
Err(e) => {
last_err = Some(DetectError::from(e));
continue;
}
};
match detector.detect(&gray_view(img), &corners) {
Ok(result) => {
let dominated = best
.as_ref()
.is_some_and(|b| charuco_score(b) >= charuco_score(&result));
if !dominated {
best = Some(result);
}
}
Err(e) => {
last_err = Some(DetectError::from(e));
}
}
}
best.ok_or_else(|| {
last_err.unwrap_or(DetectError::CharucoDetect(
charuco::CharucoDetectError::NoMarkers,
))
})
}
pub fn detect_puzzleboard_best(
img: &::image::GrayImage,
configs: &[puzzleboard::PuzzleBoardParams],
) -> Result<puzzleboard::PuzzleBoardDetectionResult, DetectError> {
let mut best: Option<puzzleboard::PuzzleBoardDetectionResult> = None;
let mut last_err: Option<DetectError> = None;
for params in configs {
match detect_puzzleboard(img, params) {
Ok(r) => {
let better = match &best {
None => true,
Some(b) => {
let key_new = (r.corners.len(), r.decode.mean_confidence);
let key_old = (b.corners.len(), b.decode.mean_confidence);
key_new.0 > key_old.0 || (key_new.0 == key_old.0 && key_new.1 > key_old.1)
}
};
if better {
best = Some(r);
}
}
Err(e) => last_err = Some(e),
}
}
best.ok_or_else(|| {
last_err.unwrap_or(DetectError::PuzzleBoardDetect(
puzzleboard::PuzzleBoardDetectError::DecodeFailed,
))
})
}
pub fn detect_marker_board_best(
img: &::image::GrayImage,
configs: &[marker::MarkerBoardParams],
) -> Option<marker::MarkerBoardDetectionResult> {
let corners = detect_corners_default(img);
configs
.iter()
.filter_map(|params| {
let detector = marker::MarkerBoardDetector::new(params.clone()).ok()?;
detector.detect_from_image_and_corners(&gray_view(img), &corners)
})
.max_by_key(|r| r.corners.len())
}
fn charuco_score(r: &charuco::CharucoDetectionResult) -> (usize, usize) {
(r.markers.len(), r.corners.len())
}
pub fn gray_image_from_slice(
width: u32,
height: u32,
pixels: &[u8],
) -> Result<::image::GrayImage, DetectError> {
let w = usize::try_from(width).ok();
let h = usize::try_from(height).ok();
let Some((w, h)) = w.zip(h) else {
return Err(DetectError::InvalidGrayDimensions { width, height });
};
let Some(expected) = w.checked_mul(h) else {
return Err(DetectError::InvalidGrayDimensions { width, height });
};
if pixels.len() != expected {
return Err(DetectError::InvalidGrayBuffer {
expected,
got: pixels.len(),
});
}
::image::GrayImage::from_raw(width, height, pixels.to_vec())
.ok_or(DetectError::InvalidGrayDimensions { width, height })
}
pub fn detect_chessboard_from_gray_u8(
width: u32,
height: u32,
pixels: &[u8],
chess_cfg: &DetectorConfig,
params: &chessboard::DetectorParams,
) -> Result<Option<chessboard::ChessboardDetection>, DetectError> {
let img = gray_image_from_slice(width, height, pixels)?;
Ok(detect_chessboard(&img, chess_cfg, params))
}
pub fn detect_charuco_from_gray_u8(
width: u32,
height: u32,
pixels: &[u8],
params: &charuco::CharucoParams,
) -> Result<charuco::CharucoDetectionResult, DetectError> {
let img = gray_image_from_slice(width, height, pixels)?;
detect_charuco(&img, params)
}
pub fn detect_puzzleboard_from_gray_u8(
width: u32,
height: u32,
pixels: &[u8],
params: &puzzleboard::PuzzleBoardParams,
) -> Result<puzzleboard::PuzzleBoardDetectionResult, DetectError> {
let img = gray_image_from_slice(width, height, pixels)?;
detect_puzzleboard(&img, params)
}
pub fn detect_marker_board_from_gray_u8(
width: u32,
height: u32,
pixels: &[u8],
params: &marker::MarkerBoardParams,
) -> Result<Option<marker::MarkerBoardDetectionResult>, DetectError> {
let img = gray_image_from_slice(width, height, pixels)?;
Ok(detect_marker_board(&img, params))
}
fn adapt_chess_corner(c: &chess_corners::CornerDescriptor) -> chessboard::ChessCorner {
chessboard::ChessCorner {
position: Point2::new(c.x, c.y),
axes: [
core::AxisEstimate {
angle: c.axes[0].angle,
sigma: c.axes[0].sigma,
},
core::AxisEstimate {
angle: c.axes[1].angle,
sigma: c.axes[1].sigma,
},
],
contrast: c.contrast,
fit_rms: c.fit_rms,
strength: c.response,
}
}
#[cfg(test)]
mod tests {
use super::*;
use chess_corners::DetectionStrategy;
#[test]
fn default_chess_config_overrides_threshold() {
let cfg = default_chess_config();
assert_eq!(cfg.threshold, Threshold::Absolute(15.0));
assert!(matches!(cfg.strategy, DetectionStrategy::Chess(_)));
let baseline = DetectorConfig::chess();
assert_eq!(cfg.multiscale, baseline.multiscale);
assert_eq!(cfg.upscale, baseline.upscale);
assert_eq!(cfg.merge_radius, baseline.merge_radius);
assert_eq!(cfg.orientation_method, baseline.orientation_method);
let DetectionStrategy::Chess(chess) = cfg.strategy else {
unreachable!("matched above");
};
let DetectionStrategy::Chess(chess_baseline) = baseline.strategy else {
unreachable!("baseline preset is ChESS");
};
assert_eq!(chess.ring, chess_baseline.ring);
assert_eq!(chess.descriptor_ring, chess_baseline.descriptor_ring);
assert_eq!(chess.nms_radius, chess_baseline.nms_radius);
assert_eq!(chess.min_cluster_size, chess_baseline.min_cluster_size);
assert_eq!(chess.refiner, chess_baseline.refiner);
}
}