Planning the Next View
A planar calibration's covariance says where its camera is poorly
determined; the projection map shows where in the image that matters. The
next question during capture is which view to take next. predict_with_views
answers it without a re-solve: it predicts the covariance the calibration
would have with candidate views of its target added, and rank_candidates
orders candidate target poses by how much each would reduce the largest
projection σ.
#![allow(unused)] fn main() { use vision_calibration::analysis::{CandidateGrid, candidate_poses, predict_with_views, rank_candidates}; let export = session.export()?; let board = vision_calibration::synthetic::planar::grid_points(9, 7, 0.03); let image = [1280, 960]; // 3 × 3 image regions, the board 0.55 m away, fronto-parallel and tilted // by ±0.4 rad about each of its axes: 45 candidates. let grid = CandidateGrid::new(0.55, 0.4, [3, 3]); let candidates = candidate_poses(&export.params.camera, &board, image, &grid)?; let ranked = rank_candidates(&export, &candidates, &board, image)?; let best = &ranked[0]; println!( "take candidate {}: max projection σ {:.2} px (−{:.2} px)", best.index, best.max_projection_sigma_px, best.projection_sigma_reduction_px ); // The covariance with the best three taken together. let picks: Vec<_> = ranked[..3].iter().map(|r| candidates[r.index]).collect(); let predicted = predict_with_views(&export, &picks, &board, image)?; println!("σ(cx) {:.3} px", predicted.covariance.std_dev("cx").unwrap()); }
board = [(0.03 * i, 0.03 * j, 0.0) for j in range(7) for i in range(9)]
image = (1280, 960)
candidates = vc.candidate_poses(result.camera, board, image, vc.CandidateGrid(0.55, 0.4, (3, 3)))
ranked = vc.rank_candidates(result, candidates, board, image)
best = ranked[0]
print(f"take candidate {best.index}: max σ {best.max_projection_sigma_px:.2f} px")
predicted = vc.predict_with_views(result, [candidates[r.index] for r in ranked[:3]], board, image)
print(f"σ(cx) {predicted.covariance.std_dev('cx'):.3f} px")
A candidate is a target pose, camera_se3_target; any pose works, so an
application can propose its own (a pose the operator can reach, a robot
station) instead of the grid. target_points are the target's points in its
own frame, as the calibration observed them.
How the prediction works
Each candidate's target points are projected through the current estimate;
points behind the camera or outside the image are dropped. What the view
would teach the camera is its Fisher information JᵀJ, over the camera's
parameters and the view's own pose. A re-solve would estimate that pose too,
so it is marginalized out (a Schur complement), and the rest is added to the
calibration's information:
Σ′ = (Σ⁻¹ + Σ_c S_c / σ²)⁻¹
Σ is the result's covariance of its estimated parameters, so the
prediction is conditional on the same fixed parameters and every existing
view's pose is already accounted for. σ is the pixel noise that covariance
implies, read off the result itself (effective_noise_px): the existing
views' information is rebuilt from the export's observed corners and poses,
and σ² is the scalar that relates it to Σ. A robust loss enters the
covariance as that one scalar, so the prediction is on the same scale as the
result, whatever loss the calibration used.
A candidate that sees fewer than four target points, or whose points do not determine its pose, adds nothing: a calibration would reject the view.
Reading the ranking
- The score is the largest projection σ over the image after adding the
candidate alone, on the same 16 × 12 grid as the projection map and the
quality check's limit.
projection_sigma_reduction_pxis how much it drops. log_det_reductionisln det Σ − ln det Σ′, the shrinkage of the whole covariance's volume. Unlike a trace, it does not add pixels² of the focal length to squared distortion coefficients, and a change of units does not change it. A candidate can reduce it a lot while leaving the worst corner where it was.- Candidates are scored one at a time. Two candidates that each fix the
same corner are not worth twice one of them. To plan several views, call
predict_with_viewswith the views already chosen plus each further candidate and compareprojection.max, or capture, calibrate and rank again. - Ties keep the order of the candidate list, and the result is deterministic: the same export and candidates give the same ranking.
How far to trust it
On synthetic scenes (a 9 × 7 board, six views over the central part of a 1280 × 960 image, 0.1–0.3 px noise), the predicted σ of each parameter and the predicted largest projection σ match a re-solve with the view actually added to within 0.1–2.1 % on average, for Brown-Conrady, the division model and a Scheimpflug camera, and for one or three views at once.
The prediction is first order around the current estimate. It holds while the current uncertainty is small against the distortion's curvature; a calibration whose corners are extrapolated to tens of pixels of σ is far enough from the truth that its Jacobians are not the re-solve's, and the prediction can then be off by tens of percent. Take a few spread views first; plan from there.
Planning the next robot pose
A rig hand-eye calibration estimates more than its cameras: the rig, the
hand-eye and the target pose. Its next view is a robot pose, and the target
pose follows from the calibration's own chain. predict_with_robot_poses
predicts the covariance with views at candidate robot poses added, and
rank_robot_poses orders the candidates.
#![allow(unused)] fn main() { use vision_calibration::analysis::{predict_with_robot_poses, rank_robot_poses}; let export = session.export()?; // a rig hand-eye export let board = vision_calibration::synthetic::planar::grid_points(9, 7, 0.03); let image_sizes = [[1280, 960], [1280, 960]]; // one per camera // Robot poses the arm can reach (`base_se3_gripper`), from the application. let ranked = rank_robot_poses(&export, &reachable, &board, &image_sizes)?; let best = &ranked[0]; println!( "move to pose {}: ln det Σ −{:.2}, max projection σ {:.2} px", best.index, best.log_det_reduction, best.max_projection_sigma_px ); let picks: Vec<_> = ranked[..3].iter().map(|r| reachable[r.index]).collect(); let predicted = predict_with_robot_poses(&export, &picks, &board, &image_sizes)?; println!( "σ(hand-eye tz) {:.3} mm", 1e3 * predicted.covariance.std_dev("gripper_se3_rig.tz").unwrap() ); }
ranked = vc.rank_robot_poses(result, reachable, board, [(1280, 960), (1280, 960)])
best = ranked[0]
print(f"move to pose {best.index}: ln det Σ −{best.log_det_reduction:.2}")
picks = [reachable[r.index] for r in ranked[:3]]
predicted = vc.predict_with_robot_poses(result, picks, board, [(1280, 960), (1280, 960)])
print(f"σ(hand-eye tz) {1e3 * predicted.covariance.std_dev('gripper_se3_rig.tz'):.3f} mm")
A candidate is a base_se3_gripper pose. Each camera sees the target
through the result's chain at that pose: eye-in-hand, the rig on the gripper
and the target in the base; eye-to-hand, the rig in the base and the target
on the gripper. Points behind a camera or outside its image are dropped, and
a camera that keeps fewer than four adds nothing.
What the view adds. Its information is exact over every parameter the
result estimated: each camera's intrinsics and distortion, each camera's
cam_se3_rig, the hand-eye and the target pose, in the coordinates the
covariance reports them. A robot pose adds no unknown when the calibration
took the robot poses as exact. A calibration that corrected them
(robot_poses.refine, the default) corrects the candidate's too: the
correction is marginalized under the result's robot pose error, weighed
against its reprojection noise as the solve weighed it.
The scale is read off the result as for a planar calibration: the
existing views' information is rebuilt from the export's observed target
points at the robot poses it solved with, and σ² relates it to Σ. With
exact robot poses that relation is exact. With corrected ones it holds when
the corner noise is Gaussian: the solve weighs its robot pose priors at a
median-based estimate of that noise, which overstates a heavy-tailed noise's
σ.
The ranking is by log_det_reduction, the shrinkage of the whole
covariance's volume. A view that pins down the hand-eye counts as much as one
that pins down a lens, and the score does not depend on units. The largest
projection σ over every camera's image is reported alongside, for the lenses
alone.
How far to trust it. On synthetic two-camera rigs (a 9 × 7 board, six
base robot poses, 0.2 px Gaussian noise, both modes, with and without robot
pose corrections), the predicted σ of each parameter matches a re-solve with
the views actually added to within 0.1–1.2 % on average, for the best-,
median- and worst-ranked candidate and the best three at once; the largest
single-parameter difference is 9.6 %. The best-ranked candidate's real
reduction of ln det Σ beats the worst one's in every scene.
A rig hand-eye laserline calibration is not planned this way: its laser residuals are not part of the prediction.