Calibration chains
The six chains, their order and dependencies, what each one is judged on, and why "chain" is the right word.
Why "chain" and not "a pile of calibrations"
Because they depend on each other in order, and when an earlier one is wrong the later one does not fail — it produces a plausible wrong number.
┌──────────────────────────┐
│ C3 Intrinsics & distortion│ make what the camera sees geometrically correct
└────────────┬─────────────┘
↓
┌──────────────────────────┐
│ C1 Pixel size │ convert pixels to millimetres
└────┬────────────────┬────┘
↓ ↓
┌──────────────────┐ ┌──────────────────────┐
│ C2 Hand-eye │ │ C4 Galvo field │
└────────┬─────────┘ └──────────────────────┘
↓
┌──────────────────┐ ┌──────────────────────┐
│ C6 Stage geometry │ │ C5 Z focus (separate)│
└──────────────────┘ └──────────────────────┘
The six
| # | Name | Solves for | Depends on | Typical criterion |
|---|---|---|---|---|
| C1 | Pixel size | mm/px, rotation, non-orthogonality | C3 | deviation < 0.05 mm over 20 mm |
| C2 | Hand-eye | T_Tool_Cam or T_World_Cam |
C1 | nine-point residual RMS < 0.03 mm |
| C3 | Intrinsics & distortion | fx, fy, cx, cy, k₁, k₂, p₁, p₂ | — | reprojection error < 0.15 px |
| C4 | Galvo field correction | field distortion correction table | C1 | in-field residual RMS < 15 µm |
| C5 | Z focus | best focal plane, depth of field | — | focus repeatability < 10 µm |
| C6 | Stage geometry | squareness, centre of rotation, flatness | C1, C2 | squareness error < 50 µrad |
Those numbers are the default tolerances of the demo-2axis-vision machine. On real hardware
you set them from your own error budget — in machine configuration, not in code.
Every chain has the same five steps
All of them implement one interface, so the UI, logging, archiving and re-run logic are shared:
public interface ICalibrationProcedure
{
string Id { get; }
IReadOnlyList<string> DependsOn { get; }
Task<CalibResult> RunAsync(CalibContext ctx, IProgress<CalibProgress> p, CancellationToken ct);
}| Step | Does | On failure |
|---|---|---|
| prepare | Check dependencies, fixtures and parameters | Stop here. Never let someone spend five minutes to discover a missing dependency |
| acquire | Collect data at planned poses | A single bad pose is re-acquired; the run continues |
| solve | Fit, report parameters and residual | An odd residual must come with likely causes, not just a number |
| verify | Re-project data held out of the solve | Produces the deviation and PASS/FAIL |
| persist | Versioned write | Append only, never overwrite, always revertible |
Verify data must not have been used in the solve. Fitting and validating on the same data produces a flattering residual and proves nothing — the most common piece of self-deception in calibration reports.
Ground-truth scoring: only on virtual devices
On real hardware you have a residual but no truth. The virtual device knows the truth, so it prints one more line:
solve: mm/px = 0.019823
truth: mm/px = 0.019841
verify: deviation 0.018 mm over 20 mm, tolerance 0.05 mm → PASS
The point is not prettier numbers. It is learning how small a residual has to be before the result is actually correct. Most people only really understand calibration the first time they see "residual 0.02 px, actual error 0.3 mm".
Where results live
%ProgramData%\PreciSim\calibration\
demo-2axis-vision\
C1-pixel-size\
2026-09-21T10-24-13.json ← one record per run, never overwritten
latest.json ← points at the active record
Each record holds parameters, residual, the truth comparison (virtual only), a reference to the raw acquisition data, the software version and the operator. When something goes wrong, "which calibration did it start with" is answerable only because of this archive.