Calibration Quality

What to shoot, how to read the numbers, and what the model file means. This page covers the three questions the method pages do not.

Overview

Each calibration page tells you how to run its method. None of them answers these questions, so they live here:

  • Did I shoot the right set of board positions? Count is not the measure; variety is.
  • Does a low RMS mean the model is right? No. It means the model fits the corners it was shown. Whether the parameters are determined is a separate question, and the joint solve now reports on it.
  • What do R, t, and the other fields in the .mat file mean, and which way do they point? The file does not say. This page is the reference.

Capturing a pose set

A pinhole calibration recovers focal length, principal point, distortion and the camera pose from where the board corners land in the image. A board facing the camera squarely, at one distance, gives a scale and nothing else: a longer lens further away produces the same picture. Tilting the board is what breaks that tie (the stepped board page explains the same fx / tz ambiguity for its own method). The table lists what matters and what a healthy reference set measured.

QuantityMeaningReference 3-camera setWhy it matters
Board tiltAngle between the board normal and the camera axis, per view2.9 to 64 degrees, median 30 (13 to 18 views per camera)The only thing that separates focal length from standoff. A board that never tilts leaves fx undetermined at any RMS.
Tilt directionWhich way the board leans in the image, across the setOne hinge axis leaned both ways (anisotropy 0.06 to 0.08)Tilting about two axes constrains the principal point and the distortion centre along both image axes. One axis leaves the other weak.
Standoff rangeHow far the board moved towards and away from the camera58 to 94 mm per camera, at 830 to 920 mm (7 to 11 %)Helps distortion and the released board. On its own it does not rescue an untilted set: each view then adds one unknown distance and one equation.
Frame coverageWhere the board appeared in the image--Distortion coefficients act where the board went. Corners never covered are extrapolated.
View countNumber of board positions per camera13 to 18Five to six is the floor. Beyond that, variety beats count: ten near-identical views add nothing a sixth different one would.

Reference set: three cameras, 15 mm ChArUco, 4064 x 3040 px, about 850 mm standoff, 0.048 mm per pixel. The tilt-direction row shows that even a good set can be one-sided; the diagnostic below reports it so you can decide whether it matters for your lens.

Long lenses need more tilt, not more views. The conditioning the fit sees is the depth a tilted board spans divided by its standoff, roughly W sin(tilt) / Z. A 16 degree field of view at 850 mm gets a quarter of what a 60 degree lens gets from the same 30 degree tilt. The diagnostic reports this as depth_ratio_max.

Recipe

  1. 1Fix the cameras and let the rig settle. Nothing on the rig moves again until the last calibration image is taken.
  2. 2Fill the frame with the board in one view, then move it so that over the set every corner of every camera's image has seen the board.
  3. 3Tilt the board 20 to 45 degrees towards the camera, then away, then left, then right. Tilts about two different axes are the goal.
  4. 4Move the board towards and away from the camera by 10 to 20 % of the standoff, tilted as you go.
  5. 5Keep every camera that shares a view in focus on the board; a blurred camera raises its RMS and hides a real pose problem.
  6. 6Shoot the datum view last, or at least after the rig has settled. Its pose defines the world plane, and a datum shot while a camera was still creeping puts the world plane where that camera was, not where it is.
  7. 7Aim for 12 to 20 views per camera. If two views look alike, one of them is wasted.

The datum view. On the dotboard path it carries the clicked origin, +X and +Y, and on every joint path its board plane is the world z = 0. Make it the cleanest, most settled frame. It does not have to be seen by every camera: the joint solve needs only a connected chain of shared views across the rig, so a traverse where no single view is common to all cameras still solves.

Judging a calibration

RMS measures whether the model fits the corners. It does not prove the parameters are determined. A board that never tilted gives the same 0.4 px RMS as a good set while the focal length is off by a factor of ten. High RMS means something is definitely wrong. Low RMS means nothing has been proven yet.

What the reported numbers are

FieldWhereMeaningHow to read it
rms_pxjointRoot-mean-square reprojection error over every corner of every camera and view, in pixelsScreening only. High means something is wrong. Low proves nothing about the parameters.
per_camera_rms / cam_rmsjoint, stereo, monoThe same, per cameraA camera well above the others has a detection or focus problem, or its rig pose moved between views.
per_view_rmsmono, stereoReprojection error per calibration imageRead the spread, not the mean. One view far above the rest was shot before something settled.
board_meta.convergedjointThe alternation between pose fit and board release met its toleranceAlways 0 under board_release: none, because nothing alternates. That is not a failure.
board_meta.n_board_dotsjointSize of the shared board: every grid point any camera detected in any viewNot the released count. Points seen by fewer than two rays cannot be triangulated and stay nominal; the CLI prints released rows out of this total.
board_meta.cross_camera_board_agreement_mmjointDistance between the boards each camera reconstructedExactly 0.0 by construction. Every camera references the one shared board, so there is no second board to disagree with. It is not a check.
pose_diversityjointThe determinacy diagnostic described belowRead alongside rms_px. Absence of a flag is not a certificate of quality.

Checks you can run today

  • Read boards_3d.png. The released boards from every view should stack into one consistent scene; a board that floats away from the rest was shot after something moved.
  • Compare the solved baseline and standoff against a tape measure. The joint solve constrains camera positions over every view, so a 5 mm disagreement on a 300 mm baseline is a real finding, not noise.
  • Re-fit with two views dropped and watch fx. A determined focal length moves by a fraction of a percent; an undetermined one moves by whatever the dropped views were holding in place.
  • Read the per-view RMS spread, not its mean. The joint report flags views above twice their camera's median.
  • Distrust distortion coefficients acting where the board never went. Coverage is in the detection overlay figures.
  • Under full3d, read board_planarity_rms_mm. A flat board that bowed by millimetres has absorbed a constraint the poses did not supply.

The pose-diversity report

After a joint pinhole solve the CLI prints a pose-diversity block and the record stores it under pose_diversity. It is computed from the converged poses, changes no fit result, and is a detector rather than a grade: the flags fire only on the unambiguous corner, and there is no GOOD verdict because a wrong GOOD would be as misleading as the zero agreement number above.

Field (per camera unless stated)Meaning
tilt_deg_min / median / maxBoard tilt per camera, degrees
n_views_tilt_above_floorViews tilted more than tilt_floor_deg (1.0). Below two, the direction metrics are NaN and the report says why
tilt_azimuth_spread_degArc of tilt axes, modulo 180. Leaning one hinge forward and back is one axis
tilt_anisotropy0 means every tilt about one axis, 1 means all directions equally
standoff_mm_min / median / maxMedian camera-frame depth of the board per view
standoff_range_fraction(max minus min) over median standoff
depth_ratio_maxDepth spanned by one view over its standoff, roughly W sin(tilt) / Z. This is the conditioning the fit sees; it folds in the lens angle
flag_low_tiltFewer than two tilted views, median tilt below 5 degrees, or depth ratio below 0.02. Fires the DEGENERATE warning
flag_single_azimuthTilt axes within 30 degrees or anisotropy below 0.10. Printed as a re-shoot hint
flag_constant_standoffStandoff range below 2 % of median. Printed as a re-shoot hint
degenerateRig-level: true when any camera has flag_low_tilt. Under a released board one untilted camera pulls every focal length
view_rms_px / view_flaggedReprojection error per (camera, view) and a flag for views above twice their camera median
board_planarity_rms_mmOut-of-plane rms of the released board. A flat board bowing by millimetres is absorbing a missing constraint

When it prints DEGENERATE. The report says which cameras never tilted the board and that the RMS above is not evidence the intrinsics are identifiable. Re-shoot those cameras' views with the board tilted 15 degrees or more in several directions. Do not lower the bar by switching to board_release: none: a converging rig can rescue one untilted camera under a fixed board, but the pose set is still wrong and the next rig will not be so lucky.

Two things that look wrong and are not

  • The final rms_px is above the rms after alternation, which the CLI prints on its stages line (it is not stored in the record). The alternation fits a free pose per camera and view; the final bundle holds every camera on one rigid rig, and a rigid rig cannot follow a camera that crept between views. The rise is the price of a physical model, and the per-view flags show where it landed.
  • converged is 0 under board_release: none. Nothing alternates when the board is fixed, so the flag has nothing to report.

Reported baselines changed in this release. Camera positions and baselines from the joint solve are now constrained over every view rather than read from one, so they are worth checking against the rig. Joint models saved before this release used a free pose per view and must be re-run; the stored file has no pose_diversity block, which is how you can tell.

Reading the model file

Models are MATLAB v5 .mat files under the calibration source: model_*.mat per camera, stereo_model_*.mat per pair, joint_model_*.mat per rig. The convention is the same in all three and is documented only here.

Convention. R and t map world to camera: X_cam = R * X_world + t. The camera centre in world coordinates is C = -R' * t; it is not t. The stereo pose maps camera 1 to camera 2: X_cam2 = R_stereo * X_cam1 + T_stereo. Pixels are 0-based, origin top-left, +y down. Lengths are mm; self-calibration tilts are radians. The one exception: coordinates.mat stores 1-based pixel positions, so subtract 1 before feeding them to a model.

Camera block (mono record, and inside stereo and joint)

FieldMeaning
camera_matrix3x3 K: fx, fy, cx, cy in pixels
dist_coeffsOpenCV distortion vector k1 k2 p1 p2 k3; the standard model fixes k3 at 0
R, t, rvecWorld to camera. X_cam = R X_world + t. The camera centre is minus R transpose t, not t
image_width, image_heightSensor size the model was fitted at, pixels
rmsThis camera's reprojection RMS, pixels

Stereo record

FieldMeaning
cam1, cam2, model1, model2Camera numbers and their camera blocks as above
R_stereo, T_stereoCamera 1 frame to camera 2 frame: X_cam2 = R_stereo X_cam1 + T_stereo. The norm of T_stereo is the baseline
per_view_rms1, per_view_rms2Reprojection error per view, per camera
self_calSelf-calibration block when it has run. The fitted sheet is fitted_z_offset (mm), fitted_tilt_x and fitted_tilt_y (radians); the correction is baked into the stored models, so z_offset, tilt_x and tilt_y read 0 with baked = 1

Joint record

FieldMeaning
cameras, camera_modelsCamera numbers and one camera block per camera (the rig pose in the datum board frame)
board_index, board_xyzGlobal grid index and released position of every board point, mm
board_release, spacing_mmWhich board coordinates were freed (full3d, z_only, none) and the nominal pitch
per_camera_rms, rms_pxReprojection RMS per camera and overall
pose_diversityThe diagnostic block. Parallel arrays ordered by its own cameras field; empty when the record predates it
world_frame, board_meta, contract_versionThe clicked origin and axes; the solve inputs plus converged, n_board_dots and cross_camera_board_agreement_mm; and the file-format version (1)
MATLAB
m = load('joint_model_pinhole.mat'); cam = m.camera_models(1); % first camera in m.cameras K = cam.camera_matrix; % 3x3, pixels R = cam.R; t = cam.t(:); % world -> camera C = -R' * t % camera centre, world mm
Python
from scipy.io import loadmat m = loadmat("joint_model_pinhole.mat", squeeze_me=True, struct_as_record=False) import numpy as np cam = np.atleast_1d(m["camera_models"])[0] # first camera in m["cameras"]; atleast_1d survives a one-camera file K, R, t = cam.camera_matrix, cam.R, cam.t.reshape(3) C = -R.T @ t # camera centre, world mm

The file does not record any of this. There is no convention field, and none is planned this release; this page is the reference. What the file does carry is contract_version (currently 1), stamped into every record.

Checklist

  • Rig settled before the first image; datum view shot after it settled
  • Tilts of 20 to 45 degrees about two different axes, and 10 to 20 % standoff change
  • Every image corner of every camera has seen the board
  • RMS read as screening: high is wrong, low is not yet right
  • Pose-diversity report read: no DEGENERATE line, flagged views explained or re-shot
  • Baseline and standoff checked against a tape measure
  • Model file consumed with X_cam = R X_world + t and C = -R' t, pixels 0-based, coordinates.mat minus 1

Next: Global Coordinates

Put every camera in one shared world frame with the joint multi-camera model.

Continue to Global Coordinates
PIVtools - High-Performance PIV Processing