Skip to main content

vision_calibration_pipeline/laserline_device/
problem.rs

1//! [`ProblemType`] implementation for single laserline device calibration.
2
3use crate::Error;
4use serde::{Deserialize, Serialize};
5use vision_calibration_core::ScheimpflugParams;
6use vision_calibration_linear::prelude::*;
7use vision_calibration_optim::{
8    BackendSolveOptions, LaserlineDataset, LaserlineEstimate, LaserlineResidualType,
9    LaserlineSolveOptions, LaserlineStats,
10};
11
12use crate::session::{InvalidationPolicy, ProblemType};
13
14use super::state::LaserlineDeviceState;
15
16/// Laserline device calibration problem (single camera + laser plane).
17#[derive(Debug)]
18pub struct LaserlineDeviceProblem;
19
20/// Input type for laserline device calibration.
21pub type LaserlineDeviceInput = LaserlineDataset;
22
23/// Configuration for laserline device calibration.
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25#[non_exhaustive]
26pub struct LaserlineDeviceConfig {
27    /// Initialization options.
28    pub init: LaserlineDeviceInitConfig,
29    /// Shared solver options.
30    pub solver: LaserlineDeviceSolverConfig,
31    /// Bundle-adjustment options.
32    pub optimize: LaserlineDeviceOptimizeConfig,
33}
34
35/// Initialization options for laserline device calibration.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[non_exhaustive]
38pub struct LaserlineDeviceInitConfig {
39    /// Number of iterations for iterative intrinsics estimation.
40    pub iterations: usize,
41    /// Fix k3 during initialization (recommended for typical lenses).
42    pub fix_k3: bool,
43    /// Fix tangential distortion during initialization.
44    pub fix_tangential: bool,
45    /// Enforce zero skew during initialization.
46    pub zero_skew: bool,
47    /// Initial Scheimpflug sensor parameters (use zeros for pinhole/identity).
48    pub sensor_init: ScheimpflugParams,
49}
50
51impl Default for LaserlineDeviceInitConfig {
52    fn default() -> Self {
53        Self {
54            iterations: 2,
55            fix_k3: true,
56            fix_tangential: false,
57            zero_skew: true,
58            sensor_init: ScheimpflugParams::default(),
59        }
60    }
61}
62
63/// Shared solver options for laserline device calibration.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[non_exhaustive]
66pub struct LaserlineDeviceSolverConfig {
67    /// Maximum iterations for the optimizer.
68    pub max_iters: usize,
69    /// Verbosity level (0 = silent, 1 = summary, 2+ = detailed).
70    pub verbosity: usize,
71}
72
73impl Default for LaserlineDeviceSolverConfig {
74    fn default() -> Self {
75        Self {
76            max_iters: 50,
77            verbosity: 0,
78        }
79    }
80}
81
82/// Bundle-adjustment options for laserline device calibration.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[non_exhaustive]
85pub struct LaserlineDeviceOptimizeConfig {
86    /// Robust loss for calibration reprojection residuals.
87    pub calib_loss: vision_calibration_optim::RobustLoss,
88    /// Robust loss for laser residuals.
89    pub laser_loss: vision_calibration_optim::RobustLoss,
90    /// Global weight for calibration residuals.
91    pub calib_weight: f64,
92    /// Global weight for laser residuals.
93    pub laser_weight: f64,
94    /// Fix camera intrinsics during optimization.
95    pub fix_intrinsics: bool,
96    /// Fix distortion parameters during optimization.
97    pub fix_distortion: bool,
98    /// Fix k3 distortion parameter during optimization.
99    pub fix_k3: bool,
100    /// Fix Scheimpflug sensor parameters during optimization.
101    pub fix_sensor: bool,
102    /// Indices of poses to fix (e.g., \[0\] to fix first pose).
103    pub fix_poses: Vec<usize>,
104    /// Fix laser plane parameters during optimization.
105    pub fix_plane: bool,
106    /// Laser residual type.
107    pub laser_residual_type: LaserlineResidualType,
108}
109
110impl Default for LaserlineDeviceOptimizeConfig {
111    fn default() -> Self {
112        Self {
113            calib_loss: vision_calibration_optim::RobustLoss::Huber { scale: 1.0 },
114            laser_loss: vision_calibration_optim::RobustLoss::Huber { scale: 0.01 },
115            calib_weight: 1.0,
116            laser_weight: 1.0,
117            fix_intrinsics: false,
118            fix_distortion: false,
119            fix_k3: true,
120            fix_sensor: true,
121            fix_poses: vec![0],
122            fix_plane: false,
123            laser_residual_type: LaserlineResidualType::LineDistNormalized,
124        }
125    }
126}
127
128impl LaserlineDeviceConfig {
129    /// Convert to vision-calibration-linear initialization options.
130    pub fn init_opts(&self) -> IterativeIntrinsicsOptions {
131        IterativeIntrinsicsOptions {
132            iterations: self.init.iterations,
133            distortion_opts: DistortionFitOptions {
134                fix_k3: self.init.fix_k3,
135                fix_tangential: self.init.fix_tangential,
136                iters: 8,
137            },
138            zero_skew: self.init.zero_skew,
139        }
140    }
141
142    /// Convert to vision-calibration-optim solve options.
143    pub fn solve_opts(&self) -> LaserlineSolveOptions {
144        LaserlineSolveOptions {
145            calib_loss: self.optimize.calib_loss,
146            calib_weight: self.optimize.calib_weight,
147            laser_loss: self.optimize.laser_loss,
148            laser_weight: self.optimize.laser_weight,
149            fix_intrinsics: self.optimize.fix_intrinsics,
150            fix_distortion: self.optimize.fix_distortion,
151            fix_k3: self.optimize.fix_k3,
152            fix_sensor: self.optimize.fix_sensor,
153            fix_poses: self.optimize.fix_poses.clone(),
154            fix_plane: self.optimize.fix_plane,
155            laser_residual_type: self.optimize.laser_residual_type,
156        }
157    }
158
159    /// Convert to backend solver options.
160    pub fn backend_opts(&self) -> BackendSolveOptions {
161        BackendSolveOptions {
162            max_iters: self.solver.max_iters,
163            verbosity: self.solver.verbosity,
164            ..Default::default()
165        }
166    }
167}
168
169/// Pipeline output including optimized parameters and summary statistics.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct LaserlineDeviceOutput {
172    /// Optimized camera/laser parameters and backend report.
173    pub estimate: LaserlineEstimate,
174    /// Aggregated reprojection and laser residual statistics.
175    pub stats: LaserlineStats,
176}
177
178/// Export type for laserline device calibration.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[non_exhaustive]
181pub struct LaserlineDeviceExport {
182    /// Pipeline output including optimized parameters and summary statistics.
183    pub estimate: LaserlineEstimate,
184    /// Laserline statistics payload.
185    pub stats: LaserlineStats,
186    /// Mean reprojection error (pixels).
187    pub mean_reproj_error: f64,
188    /// Per-camera reprojection errors (single element for single-camera workflows).
189    pub per_cam_reproj_errors: Vec<f64>,
190}
191
192impl ProblemType for LaserlineDeviceProblem {
193    type Config = LaserlineDeviceConfig;
194    type Input = LaserlineDeviceInput;
195    type State = LaserlineDeviceState;
196    type Output = LaserlineDeviceOutput;
197    type Export = LaserlineDeviceExport;
198
199    fn name() -> &'static str {
200        "laserline_device_v1"
201    }
202
203    fn validate_input(input: &Self::Input) -> Result<(), Error> {
204        if input.len() < 3 {
205            return Err(Error::InsufficientData {
206                need: 3,
207                got: input.len(),
208            });
209        }
210
211        for (i, view) in input.iter().enumerate() {
212            if view.obs.len() < 4 {
213                return Err(Error::invalid_input(format!(
214                    "view {} has too few points (need >= 4 for homography, got {})",
215                    i,
216                    view.obs.len()
217                )));
218            }
219            view.meta
220                .validate()
221                .map_err(|e| Error::invalid_input(format!("view {}: {}", i, e)))?;
222        }
223
224        Ok(())
225    }
226
227    fn validate_config(config: &Self::Config) -> Result<(), Error> {
228        if config.solver.max_iters == 0 {
229            return Err(Error::invalid_input("max_iters must be positive"));
230        }
231        if config.init.iterations == 0 {
232            return Err(Error::invalid_input("init_iterations must be positive"));
233        }
234        if config.optimize.calib_weight <= 0.0 {
235            return Err(Error::invalid_input("calib_weight must be positive"));
236        }
237        if config.optimize.laser_weight <= 0.0 {
238            return Err(Error::invalid_input("laser_weight must be positive"));
239        }
240        Ok(())
241    }
242
243    fn on_input_change() -> InvalidationPolicy {
244        InvalidationPolicy::CLEAR_COMPUTED
245    }
246
247    fn export(output: &Self::Output, _config: &Self::Config) -> Result<Self::Export, Error> {
248        Ok(LaserlineDeviceExport {
249            estimate: output.estimate.clone(),
250            stats: output.stats.clone(),
251            mean_reproj_error: output.stats.mean_reproj_error,
252            per_cam_reproj_errors: vec![output.stats.mean_reproj_error],
253        })
254    }
255}