calib_targets_charuco/
io.rs

1//! JSON configuration and report helpers for ChArUco detection.
2
3use crate::{
4    CharucoBoard, CharucoBoardError, CharucoBoardSpec, CharucoDetectError, CharucoDetectionResult,
5    CharucoDetector, CharucoDetectorParams,
6};
7use calib_targets_aruco::{ArucoScanConfig, MarkerDetection};
8use calib_targets_chessboard::{ChessboardParams, GridGraphParams};
9use calib_targets_core::{Corner, GridAlignment, TargetDetection};
10use serde::{Deserialize, Serialize};
11use std::{
12    fs,
13    path::{Path, PathBuf},
14};
15
16#[derive(thiserror::Error, Debug)]
17pub enum CharucoIoError {
18    #[error(transparent)]
19    Io(#[from] std::io::Error),
20    #[error(transparent)]
21    Json(#[from] serde_json::Error),
22}
23
24#[derive(thiserror::Error, Debug)]
25pub enum CharucoConfigError {
26    #[error(transparent)]
27    Board(#[from] CharucoBoardError),
28}
29
30fn default_px_per_square() -> f32 {
31    60.0
32}
33
34/// Configuration for the ChArUco detection example.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct CharucoDetectConfig {
37    pub image_path: String,
38    pub board: CharucoBoardSpec,
39    #[serde(default)]
40    pub output_path: Option<String>,
41    #[serde(default)]
42    pub rectified_path: Option<String>,
43    #[serde(default)]
44    pub mesh_rectified_path: Option<String>,
45    #[serde(default = "default_px_per_square")]
46    pub px_per_square: f32,
47    #[serde(default)]
48    pub min_marker_inliers: Option<usize>,
49    #[serde(default)]
50    pub chessboard: Option<ChessboardParams>,
51    #[serde(default)]
52    pub graph: Option<GridGraphParams>,
53    #[serde(default)]
54    pub aruco: Option<ArucoScanConfig>,
55}
56
57impl CharucoDetectConfig {
58    /// Load a JSON config from disk.
59    pub fn load_json(path: impl AsRef<Path>) -> Result<Self, CharucoIoError> {
60        let raw = fs::read_to_string(path)?;
61        Ok(serde_json::from_str(&raw)?)
62    }
63
64    /// Write this config to disk as pretty JSON.
65    pub fn write_json(&self, path: impl AsRef<Path>) -> Result<(), CharucoIoError> {
66        let json = serde_json::to_string_pretty(self)?;
67        fs::write(path, json)?;
68        Ok(())
69    }
70
71    /// Resolve the output report path.
72    pub fn output_path(&self) -> PathBuf {
73        self.output_path
74            .as_ref()
75            .map(PathBuf::from)
76            .unwrap_or_else(|| PathBuf::from("charuco_detect_report.json"))
77    }
78
79    /// Build a validated ChArUco board from the config.
80    pub fn build_board(&self) -> Result<CharucoBoard, CharucoConfigError> {
81        Ok(CharucoBoard::new(self.board)?)
82    }
83
84    /// Build detector parameters, applying overrides from the config.
85    pub fn build_params(&self) -> CharucoDetectorParams {
86        let mut params = CharucoDetectorParams::for_board(&self.board);
87        params.px_per_square = self.px_per_square;
88        if let Some(min_marker_inliers) = self.min_marker_inliers {
89            params.min_marker_inliers = min_marker_inliers;
90        }
91        if let Some(chessboard) = self.chessboard.clone() {
92            params.chessboard = chessboard;
93        }
94        if let Some(graph) = self.graph.clone() {
95            params.graph = graph;
96        }
97        if let Some(aruco) = self.aruco.as_ref() {
98            if let Some(max_hamming) = aruco.max_hamming {
99                params.max_hamming = max_hamming;
100            }
101            aruco.apply_to_scan(&mut params.scan);
102        }
103        params
104    }
105
106    /// Build a detector from this config.
107    pub fn build_detector(&self) -> Result<CharucoDetector, CharucoConfigError> {
108        let params = self.build_params();
109        Ok(CharucoDetector::new(params)?)
110    }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct CharucoDetectReport {
115    pub image_path: String,
116    pub config_path: String,
117    pub board: CharucoBoardSpec,
118    pub num_raw_corners: usize,
119    pub raw_corners: Vec<Corner>,
120    #[serde(default)]
121    pub detection: Option<TargetDetection>,
122    #[serde(default)]
123    pub markers: Option<Vec<MarkerDetection>>,
124    #[serde(default)]
125    pub alignment: Option<GridAlignment>,
126    #[serde(default)]
127    pub error: Option<String>,
128}
129
130impl CharucoDetectReport {
131    /// Build a base report from the input config and raw corners.
132    pub fn new(cfg: &CharucoDetectConfig, config_path: &Path, raw_corners: Vec<Corner>) -> Self {
133        Self {
134            image_path: cfg.image_path.clone(),
135            config_path: config_path.to_string_lossy().into_owned(),
136            board: cfg.board,
137            num_raw_corners: raw_corners.len(),
138            raw_corners,
139            detection: None,
140            markers: None,
141            alignment: None,
142            error: None,
143        }
144    }
145
146    /// Populate report fields from a successful detection.
147    pub fn set_detection(&mut self, res: CharucoDetectionResult) {
148        self.detection = Some(res.detection);
149        self.markers = Some(res.markers);
150        self.alignment = Some(res.alignment);
151        self.error = None;
152    }
153
154    /// Record a detection error.
155    pub fn set_error(&mut self, err: CharucoDetectError) {
156        self.error = Some(err.to_string());
157    }
158
159    /// Load a report from JSON on disk.
160    pub fn load_json(path: impl AsRef<Path>) -> Result<Self, CharucoIoError> {
161        let raw = fs::read_to_string(path)?;
162        Ok(serde_json::from_str(&raw)?)
163    }
164
165    /// Write this report to disk as pretty JSON.
166    pub fn write_json(&self, path: impl AsRef<Path>) -> Result<(), CharucoIoError> {
167        let json = serde_json::to_string_pretty(self)?;
168        fs::write(path, json)?;
169        Ok(())
170    }
171}