use crate::{charuco, chessboard, core, marker, puzzleboard};
use chess_corners::find_chess_corners_image;
use nalgebra::Point2;
#[cfg(feature = "tracing")]
use tracing::instrument;
pub use core::{
CenterOfMassConfig, ChessConfig, DescriptorMode, DetectorMode, ForstnerConfig,
RadonDetectorParams, RefinementMethod, RefinerConfig, SaddlePointConfig, ThresholdMode,
UpscaleConfig, UpscaleMode,
};
#[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() -> ChessConfig {
ChessConfig {
threshold_mode: ThresholdMode::Relative,
threshold_value: 0.2,
nms_radius: 2,
..ChessConfig::single_scale()
}
}
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(),
}
}
#[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: &ChessConfig) -> Vec<core::Corner> {
let cfg = to_chess_corners_config(cfg);
find_chess_corners_image(img, &cfg)
.unwrap_or_default()
.iter()
.map(adapt_chess_corner)
.collect()
}
pub fn detect_corners_default(img: &::image::GrayImage) -> Vec<core::Corner> {
let cfg = default_chess_config();
detect_corners(img, &cfg)
}
#[cfg_attr(
feature = "tracing",
instrument(
level = "info",
skip(img, params),
fields(width = img.width(), height = img.height())
)
)]
pub fn detect_chessboard(
img: &::image::GrayImage,
params: &chessboard::DetectorParams,
) -> Option<chessboard::Detection> {
let corners = detect_corners(img, &default_chess_config());
let detector = chessboard::Detector::new(params.clone());
detector.detect(&corners)
}
#[cfg_attr(
feature = "tracing",
instrument(
level = "info",
skip(img, params),
fields(width = img.width(), height = img.height())
)
)]
pub fn detect_chessboard_all(
img: &::image::GrayImage,
params: &chessboard::DetectorParams,
) -> Vec<chessboard::Detection> {
let corners = detect_corners(img, &default_chess_config());
let detector = chessboard::Detector::new(params.clone());
detector.detect_all(&corners)
}
#[cfg_attr(
feature = "tracing",
instrument(
level = "info",
skip(img, params),
fields(width = img.width(), height = img.height())
)
)]
pub fn detect_chessboard_debug(
img: &::image::GrayImage,
params: &chessboard::DetectorParams,
) -> chessboard::DebugFrame {
let corners = detect_corners(img, &default_chess_config());
let detector = chessboard::Detector::new(params.clone());
detector.detect_debug(&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(img, &default_chess_config());
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(img, &default_chess_config());
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(img, &default_chess_config());
let detector = marker::MarkerBoardDetector::new(params.clone());
detector.detect_from_image_and_corners(&gray_view(img), &corners)
}
pub fn detect_chessboard_best(
img: &::image::GrayImage,
configs: &[chessboard::DetectorParams],
) -> Option<chessboard::Detection> {
let corners = detect_corners(img, &default_chess_config());
configs
.iter()
.filter_map(|params| chessboard::Detector::new(params.clone()).detect(&corners))
.max_by_key(|d| d.target.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(img, &default_chess_config());
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.detection.corners.len(), r.decode.mean_confidence);
let key_old = (b.detection.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(img, &default_chess_config());
configs
.iter()
.filter_map(|params| {
let detector = marker::MarkerBoardDetector::new(params.clone());
detector.detect_from_image_and_corners(&gray_view(img), &corners)
})
.max_by_key(|r| r.detection.corners.len())
}
fn charuco_score(r: &charuco::CharucoDetectionResult) -> (usize, usize) {
(r.markers.len(), r.detection.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],
params: &chessboard::DetectorParams,
) -> Result<Option<chessboard::Detection>, DetectError> {
let img = gray_image_from_slice(width, height, pixels)?;
Ok(detect_chessboard(&img, 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) -> core::Corner {
core::Corner {
position: Point2::new(c.x, c.y),
orientation_cluster: None,
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,
}
}
fn to_chess_corners_config(cfg: &ChessConfig) -> chess_corners::ChessConfig {
cfg.to_chess_corners_config()
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_refiner_eq(
actual: &chess_corners::RefinerConfig,
expected: &chess_corners::RefinerConfig,
) {
assert_eq!(actual.kind, expected.kind);
assert_eq!(actual.center_of_mass, expected.center_of_mass);
assert_eq!(actual.forstner, expected.forstner);
assert_eq!(actual.saddle_point, expected.saddle_point);
}
fn assert_chess_config_eq(
actual: &chess_corners::ChessConfig,
expected: &chess_corners::ChessConfig,
) {
assert_eq!(actual.detector_mode, expected.detector_mode);
assert_eq!(actual.descriptor_mode, expected.descriptor_mode);
assert_eq!(actual.threshold_mode, expected.threshold_mode);
assert_eq!(actual.threshold_value, expected.threshold_value);
assert_eq!(actual.nms_radius, expected.nms_radius);
assert_eq!(actual.min_cluster_size, expected.min_cluster_size);
assert_refiner_eq(&actual.refiner, &expected.refiner);
assert_eq!(actual.pyramid_levels, expected.pyramid_levels);
assert_eq!(actual.pyramid_min_size, expected.pyramid_min_size);
assert_eq!(actual.refinement_radius, expected.refinement_radius);
assert_eq!(actual.merge_radius, expected.merge_radius);
assert_eq!(actual.upscale, expected.upscale);
}
fn assert_chess_config_non_threshold_eq(
actual: &chess_corners::ChessConfig,
expected: &chess_corners::ChessConfig,
) {
assert_eq!(actual.detector_mode, expected.detector_mode);
assert_eq!(actual.descriptor_mode, expected.descriptor_mode);
assert_eq!(actual.nms_radius, expected.nms_radius);
assert_eq!(actual.min_cluster_size, expected.min_cluster_size);
assert_refiner_eq(&actual.refiner, &expected.refiner);
assert_eq!(actual.pyramid_levels, expected.pyramid_levels);
assert_eq!(actual.pyramid_min_size, expected.pyramid_min_size);
assert_eq!(actual.refinement_radius, expected.refinement_radius);
assert_eq!(actual.merge_radius, expected.merge_radius);
assert_eq!(actual.upscale, expected.upscale);
}
#[test]
fn owned_default_matches_upstream_default() {
let actual = to_chess_corners_config(&ChessConfig::default());
let expected = chess_corners::ChessConfig::default();
assert_chess_config_non_threshold_eq(&actual, &expected);
assert_eq!(
actual.threshold_mode,
chess_corners::ThresholdMode::Relative
);
assert_eq!(actual.threshold_value, 0.2);
}
#[test]
fn owned_multiscale_matches_upstream_multiscale() {
let actual = to_chess_corners_config(&ChessConfig::multiscale());
let expected = chess_corners::ChessConfig::multiscale();
assert_chess_config_non_threshold_eq(&actual, &expected);
assert_eq!(
actual.threshold_mode,
chess_corners::ThresholdMode::Relative
);
assert_eq!(actual.threshold_value, 0.2);
}
#[test]
fn non_default_conversion_preserves_all_fields() {
let forstner = ForstnerConfig {
radius: 3,
min_trace: 9.0,
min_det: 2.0,
max_condition_number: 123.0,
max_offset: 2.5,
};
let mut refiner = RefinerConfig::default();
refiner.kind = RefinementMethod::Forstner;
refiner.forstner = forstner;
let cfg = ChessConfig {
detector_mode: DetectorMode::Broad,
descriptor_mode: DescriptorMode::Canonical,
threshold_mode: ThresholdMode::Absolute,
threshold_value: 12.5,
nms_radius: 5,
min_cluster_size: 7,
refiner,
pyramid_levels: 4,
pyramid_min_size: 96,
refinement_radius: 6,
merge_radius: 4.5,
upscale: UpscaleConfig::fixed(3),
radon_detector: RadonDetectorParams::default(),
};
let converted = to_chess_corners_config(&cfg);
let mut expected = chess_corners::ChessConfig::default();
expected.detector_mode = chess_corners::DetectorMode::Broad;
expected.descriptor_mode = chess_corners::DescriptorMode::Canonical;
expected.threshold_mode = chess_corners::ThresholdMode::Absolute;
expected.threshold_value = 12.5;
expected.nms_radius = 5;
expected.min_cluster_size = 7;
expected.refiner = chess_corners::RefinerConfig::build(
chess_corners::RefinementMethod::Forstner,
chess_corners::CenterOfMassConfig::default(),
forstner,
chess_corners::SaddlePointConfig::default(),
chess_corners::RadonPeakConfig::default(),
);
expected.pyramid_levels = 4;
expected.pyramid_min_size = 96;
expected.refinement_radius = 6;
expected.merge_radius = 4.5;
expected.upscale = chess_corners::UpscaleConfig::fixed(3);
assert_chess_config_eq(&converted, &expected);
}
#[test]
fn all_refiner_variants_convert() {
let mut com = RefinerConfig::default();
com.kind = RefinementMethod::CenterOfMass;
com.center_of_mass = CenterOfMassConfig { radius: 4 };
let mut forstner = RefinerConfig::default();
forstner.kind = RefinementMethod::Forstner;
forstner.forstner = ForstnerConfig {
radius: 3,
min_trace: 11.0,
min_det: 0.75,
max_condition_number: 512.0,
max_offset: 1.75,
};
let mut saddle = RefinerConfig::default();
saddle.kind = RefinementMethod::SaddlePoint;
saddle.saddle_point = SaddlePointConfig {
radius: 5,
det_margin: 0.25,
max_offset: 1.25,
min_abs_det: 0.125,
};
let refiners = [com, forstner, saddle];
for refiner in refiners.iter() {
let cfg = ChessConfig {
refiner: refiner.clone(),
..ChessConfig::default()
};
let converted = to_chess_corners_config(&cfg);
assert_refiner_eq(&converted.refiner, refiner);
}
}
}