use crate::{charuco, chessboard, core, marker, puzzleboard};
use chess_corners::Detector as ChessDetector;
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),
#[error(transparent)]
ChessboardParams(#[from] chessboard::ChessboardParamsError),
#[error("no {} was detected in the image", target_label(.target))]
NoDetection {
target: core::TargetKind,
},
}
fn target_label(target: &core::TargetKind) -> &'static str {
match target {
core::TargetKind::Chessboard => "chessboard",
core::TargetKind::Charuco => "ChArUco board",
core::TargetKind::CheckerboardMarker => "marker board",
core::TargetKind::PuzzleBoard => "PuzzleBoard",
_ => "calibration target",
}
}
fn cached_corners<'cache>(
img: &::image::GrayImage,
chess_cfg: &DetectorConfig,
cache: &'cache mut Vec<(DetectorConfig, Vec<chessboard::ChessCorner>)>,
) -> &'cache [chessboard::ChessCorner] {
let idx = match cache.iter().position(|(cfg, _)| cfg == chess_cfg) {
Some(idx) => idx,
None => {
let corners = detect_corners(img, chess_cfg);
cache.push((*chess_cfg, corners));
cache.len() - 1
}
};
&cache[idx].1
}
pub use calib_targets_core::default_chess_config;
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::ChessboardParams,
) -> Result<chessboard::ChessboardDetection, DetectError> {
let corners = detect_corners(img, chess_cfg);
let detector = chessboard::ChessboardDetector::new(params.clone())?;
detector.detect(&corners).ok_or(DetectError::NoDetection {
target: core::TargetKind::Chessboard,
})
}
#[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::ChessboardParams,
) -> Vec<chessboard::ChessboardDetection> {
let corners = detect_corners(img, chess_cfg);
let Ok(detector) = chessboard::ChessboardDetector::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::CharucoDetection, DetectError> {
let corners = detect_corners(img, ¶ms.chess);
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::PuzzleBoardDetection, DetectError> {
let corners = detect_corners(img, ¶ms.chess);
detect_puzzleboard_with_corners(img, params, &corners)
}
fn detect_puzzleboard_with_corners(
img: &::image::GrayImage,
params: &puzzleboard::PuzzleBoardParams,
corners: &[chessboard::ChessCorner],
) -> Result<puzzleboard::PuzzleBoardDetection, DetectError> {
let detector = puzzleboard::PuzzleBoardDetector::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())
)
)]
pub fn detect_marker_board(
img: &::image::GrayImage,
params: &marker::MarkerBoardParams,
) -> Result<marker::MarkerBoardDetection, DetectError> {
let corners = detect_corners(img, ¶ms.chess);
let detector = marker::MarkerBoardDetector::new(params.clone())?;
detector
.detect(&gray_view(img), &corners)
.ok_or(DetectError::NoDetection {
target: core::TargetKind::CheckerboardMarker,
})
}
pub fn detect_chessboard_best(
img: &::image::GrayImage,
chess_cfg: &DetectorConfig,
param_configs: &[chessboard::ChessboardParams],
) -> Result<chessboard::ChessboardDetection, DetectError> {
let corners = detect_corners(img, chess_cfg);
param_configs
.iter()
.filter_map(|params| {
chessboard::ChessboardDetector::new(params.clone())
.ok()?
.detect(&corners)
})
.max_by_key(|d| d.corners.len())
.ok_or(DetectError::NoDetection {
target: core::TargetKind::Chessboard,
})
}
pub fn detect_charuco_best(
img: &::image::GrayImage,
configs: &[charuco::CharucoParams],
) -> Result<charuco::CharucoDetection, DetectError> {
let mut best: Option<charuco::CharucoDetection> = None;
let mut last_err = None;
let mut corner_cache: Vec<(DetectorConfig, Vec<chessboard::ChessCorner>)> = Vec::new();
for params in configs {
let detector = match charuco::CharucoDetector::new(params.clone()) {
Ok(d) => d,
Err(e) => {
last_err = Some(DetectError::from(e));
continue;
}
};
let corners = cached_corners(img, ¶ms.chess, &mut corner_cache);
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::PuzzleBoardDetection, DetectError> {
let mut best: Option<puzzleboard::PuzzleBoardDetection> = None;
let mut last_err: Option<DetectError> = None;
let mut corner_cache: Vec<(DetectorConfig, Vec<chessboard::ChessCorner>)> = Vec::new();
for params in configs {
let corners = cached_corners(img, ¶ms.chess, &mut corner_cache);
match detect_puzzleboard_with_corners(img, params, corners) {
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],
) -> Result<marker::MarkerBoardDetection, DetectError> {
let mut best: Option<marker::MarkerBoardDetection> = None;
let mut corner_cache: Vec<(DetectorConfig, Vec<chessboard::ChessCorner>)> = Vec::new();
for params in configs {
let Ok(detector) = marker::MarkerBoardDetector::new(params.clone()) else {
continue;
};
let corners = cached_corners(img, ¶ms.chess, &mut corner_cache);
let Some(result) = detector.detect(&gray_view(img), corners) else {
continue;
};
let replace = best
.as_ref()
.is_none_or(|b| result.corners.len() >= b.corners.len());
if replace {
best = Some(result);
}
}
best.ok_or(DetectError::NoDetection {
target: core::TargetKind::CheckerboardMarker,
})
}
fn charuco_score(r: &charuco::CharucoDetection) -> (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::ChessboardParams,
) -> Result<chessboard::ChessboardDetection, DetectError> {
let img = gray_image_from_slice(width, height, pixels)?;
detect_chessboard(&img, chess_cfg, params)
}
pub fn detect_charuco_from_gray_u8(
width: u32,
height: u32,
pixels: &[u8],
params: &charuco::CharucoParams,
) -> Result<charuco::CharucoDetection, 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::PuzzleBoardDetection, 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<marker::MarkerBoardDetection, DetectError> {
let img = gray_image_from_slice(width, height, pixels)?;
detect_marker_board(&img, params)
}
fn adapt_chess_corner(c: &chess_corners::CornerDescriptor) -> chessboard::ChessCorner {
let axes = c.axes.map_or_else(
|| [core::AxisEstimate::default(); 2],
|axes| {
[
core::AxisEstimate {
angle: axes[0].angle,
sigma: axes[0].sigma,
},
core::AxisEstimate {
angle: axes[1].angle,
sigma: axes[1].sigma,
},
]
},
);
chessboard::ChessCorner::new(Point2::new(c.x, c.y), axes, c.response)
}