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.
| Quantity | Meaning | Reference 3-camera set | Why it matters |
|---|---|---|---|
| Board tilt | Angle between the board normal and the camera axis, per view | 2.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 direction | Which way the board leans in the image, across the set | One 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 range | How far the board moved towards and away from the camera | 58 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 coverage | Where the board appeared in the image | -- | Distortion coefficients act where the board went. Corners never covered are extrapolated. |
| View count | Number of board positions per camera | 13 to 18 | Five 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
- 1Fix the cameras and let the rig settle. Nothing on the rig moves again until the last calibration image is taken.
- 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.
- 3Tilt the board 20 to 45 degrees towards the camera, then away, then left, then right. Tilts about two different axes are the goal.
- 4Move the board towards and away from the camera by 10 to 20 % of the standoff, tilted as you go.
- 5Keep every camera that shares a view in focus on the board; a blurred camera raises its RMS and hides a real pose problem.
- 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.
- 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
| Field | Where | Meaning | How to read it |
|---|---|---|---|
| rms_px | joint | Root-mean-square reprojection error over every corner of every camera and view, in pixels | Screening only. High means something is wrong. Low proves nothing about the parameters. |
| per_camera_rms / cam_rms | joint, stereo, mono | The same, per camera | A camera well above the others has a detection or focus problem, or its rig pose moved between views. |
| per_view_rms | mono, stereo | Reprojection error per calibration image | Read the spread, not the mean. One view far above the rest was shot before something settled. |
| board_meta.converged | joint | The alternation between pose fit and board release met its tolerance | Always 0 under board_release: none, because nothing alternates. That is not a failure. |
| board_meta.n_board_dots | joint | Size of the shared board: every grid point any camera detected in any view | Not 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_mm | joint | Distance between the boards each camera reconstructed | Exactly 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_diversity | joint | The determinacy diagnostic described below | Read 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 / max | Board tilt per camera, degrees |
| n_views_tilt_above_floor | Views tilted more than tilt_floor_deg (1.0). Below two, the direction metrics are NaN and the report says why |
| tilt_azimuth_spread_deg | Arc of tilt axes, modulo 180. Leaning one hinge forward and back is one axis |
| tilt_anisotropy | 0 means every tilt about one axis, 1 means all directions equally |
| standoff_mm_min / median / max | Median camera-frame depth of the board per view |
| standoff_range_fraction | (max minus min) over median standoff |
| depth_ratio_max | Depth 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_tilt | Fewer than two tilted views, median tilt below 5 degrees, or depth ratio below 0.02. Fires the DEGENERATE warning |
| flag_single_azimuth | Tilt axes within 30 degrees or anisotropy below 0.10. Printed as a re-shoot hint |
| flag_constant_standoff | Standoff range below 2 % of median. Printed as a re-shoot hint |
| degenerate | Rig-level: true when any camera has flag_low_tilt. Under a released board one untilted camera pulls every focal length |
| view_rms_px / view_flagged | Reprojection error per (camera, view) and a flag for views above twice their camera median |
| board_planarity_rms_mm | Out-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)
| Field | Meaning |
|---|---|
| camera_matrix | 3x3 K: fx, fy, cx, cy in pixels |
| dist_coeffs | OpenCV distortion vector k1 k2 p1 p2 k3; the standard model fixes k3 at 0 |
| R, t, rvec | World to camera. X_cam = R X_world + t. The camera centre is minus R transpose t, not t |
| image_width, image_height | Sensor size the model was fitted at, pixels |
| rms | This camera's reprojection RMS, pixels |
Stereo record
| Field | Meaning |
|---|---|
| cam1, cam2, model1, model2 | Camera numbers and their camera blocks as above |
| R_stereo, T_stereo | Camera 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_rms2 | Reprojection error per view, per camera |
| self_cal | Self-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
| Field | Meaning |
|---|---|
| cameras, camera_models | Camera numbers and one camera block per camera (the rig pose in the datum board frame) |
| board_index, board_xyz | Global grid index and released position of every board point, mm |
| board_release, spacing_mm | Which board coordinates were freed (full3d, z_only, none) and the nominal pitch |
| per_camera_rms, rms_px | Reprojection RMS per camera and overall |
| pose_diversity | The diagnostic block. Parallel arrays ordered by its own cameras field; empty when the record predates it |
| world_frame, board_meta, contract_version | The clicked origin and axes; the solve inputs plus converged, n_board_dots and cross_camera_board_agreement_mm; and the file-format version (1) |
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 mmfrom 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 mmThe 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