Projection Uncertainty
The covariance of a camera's parameters implies how precisely it places a
ray in the image. For the ray through a pixel, the pixel it projects to
moves with the parameters θ as J = ∂q/∂θ, so its covariance is the 2×2
J Σ Jᵀ. projection_uncertainty reports the major-axis standard deviation
of that ellipse, in pixels, on a grid of 16 × 12 nodes from pixel (0, 0) to
(width − 1, height − 1), together with its value at the image centre, the
largest of the four corners and the largest on the grid. The export does not
record the image size, so the call takes it:
#![allow(unused)] fn main() { use vision_calibration::common::CalibratedCameras; let export = session.export()?; let map = export.projection_uncertainty(0, [1280, 1024])?; println!( "σ: centre {:.3} px, corners up to {:.3} px", map.center, map.corner.unwrap_or(f64::NAN) ); }
p = vc.projection_uncertainty(result, (1280, 1024), camera=0)
print(f"σ: centre {p.center:.3f} px, max {p.max:.3f} px")
heatmap = p.grid() # 12 rows of 16 values, top row first
camera is the camera index (0 for a single camera). A node no ray reaches,
as where a strong distortion folds the image over, has no value.
For the whole ellipse at pixels of your choice, projection_covariance
returns J Σ Jᵀ itself, in px², one entry per pixel (None where no ray
reaches it). It is Rust only:
#![allow(unused)] fn main() { let ellipses = export.projection_covariance(0, &[[640.0, 512.0], [10.0, 10.0]])?; }
Reading the map:
- Where the views were. σ is smallest where the board covered the image and grows toward corners the views did not reach, where the distortion is extrapolated. A large corner value is a reason to add views with the board in that corner; Planning the Next View ranks candidate views by how much each would reduce it.
- Camera frame. The ray is held fixed in the camera's own frame and only the camera's parameters vary: intrinsics, distortion, sensor tilt. A pinhole camera maps every point of a ray to one pixel, so the map does not depend on range. It does not include the uncertainty of the camera's pose.
- Conservative where the pose is re-estimated. A principal-point shift
and a small rotation of the camera move the image almost alike. A use that
estimates the camera's pose again, such as a pose from a known target,
absorbs that part, so the map overstates the error such uses see. A
Scheimpflug camera shows this strongly: its sensor tilt and principal point
trade off, so σ of
cxandcy, and with them the whole map, can be several pixels while the corners still reproject to a fraction of one. - Conditional. Parameters the final solve held fixed count as exact.
The rig and rig hand-eye workflows refine each camera's intrinsics in
their final solve by default, so each camera has its map; with
rig.refine_intrinsics(or the rig hand-eye'shandeye_optimize.refine_intrinsics) set tofalsethe intrinsics are held, the result has no covariance to project and the call returns an error saying so. - The image size matters. Pass the size of the images the camera was calibrated on. Nodes beyond the area the views covered are extrapolated through the distortion, and far outside the field of view σ grows without bound.