Stepped Board Calibration
Calibrate stereo or single-camera setups using a stepped board -- a physical target with two Z-levels that provides genuine depth information for robust 3D camera models.
Overview
A stepped calibration board is a physical target with dots printed on two faces at different Z heights. The “peak” face and “trough” face are separated by a known step height (typically 1–5 mm). When a camera images this board, it sees dots at two distinct depths in a single frame -- giving genuine 3D point correspondences that break the focal-length / translation ambiguity that plagues single-plane calibration at PIV magnification.
PIVTools supports two stepped calibration modes:
| Method | Config Active Value | Best For | Output |
|---|---|---|---|
| Stereo Stepped | stereo_stepped | Two-camera 3D velocity (transmission or same-side) | Stereo model + per-camera pinhole |
Quality Metrics
| Metric | Description | Target |
|---|---|---|
| Per-camera RMS | Reprojection error per camera (pixels). The two cameras are fitted separately and the stereo pose is composed from them, so these are the only reprojection numbers this method produces. | < 0.5 px |
| Detected points per level | How many dots the walk-first separation recovered on each of the two levels, per camera | Both levels well populated |
At PIV magnification, a standard single-plane calibration board provides very weak depth information -- the camera sees an almost perfectly flat field. This creates a mathematical ambiguity between focal length (fx) and Z-translation (tz) that can cause calibration errors of 3% or more. The stepped board solves this by presenting dots at two known Z-levels in every frame, providing real depth variation that constrains all camera parameters simultaneously.
Board Geometry
Three physical measurements define the board. Enter these in the GUI or config.yaml before running detection.
| Parameter | YAML Key | Description | Example |
|---|---|---|---|
| Dot Spacing | dot_spacing_mm | Centre-to-centre distance between adjacent dots (mm) | 15 |
| Step Height | step_height_mm | Height difference between peak and trough faces (mm) | 3 |
| Board Thickness | board_thickness_mm | Total board thickness including both faces (mm) | 14.8 |
| dt | dt | Time between laser pulses (seconds) | 5.0e-06 |
The step height is critical for accuracy. Measure it with a micrometer, not calipers. A 0.1 mm error in step_height_mm will propagate into all Z-depth estimates and affect stereo reconstruction quality.
How Z-Levels Are Computed
The backend computes world Z coordinates from the board geometry, anchored on camera 1. Z = 0 is whichever level you clicked on camera 1, not the peak by definition. In a same-side setup the other level sits one step away, so clicking the trough gives peak = +step_height_mm. In a transmission setup camera 2 views the opposite face, so its levels are offset through the board thickness rather than simply swapped. PIVTools reads the configuration from the chirality of your fiducial clicks on the two cameras, a single deterministic geometric test.
Fiducial Setup
Fiducials tell PIVTools how your board's physical axes map onto the image. You click three points per camera on the datum frame (the reference pose):
Origin
Click any dot to define the (0, 0) grid position
X-Axis Point
Click a dot in the positive X direction from origin
Y-Axis Point
Click a dot in the positive Y direction from origin
Each click snaps to the nearest detected blob, so you don't need pixel-perfect accuracy. The origin click also determines which face (peak or trough) you clicked on -- this is stored as the clicked_level and establishes the reference convention for all subsequent pose labelling.
Fiducials define the world coordinate system. Origin = (0,0), X-axis point = positive X direction, Y-axis point = positive Y direction. If these are wrong, grid indices will be mirrored or rotated, causing grid stitching to fail. The detection overlay shows indexed dots after fiducials are set -- verify the indices increase in the expected directions.
Fiducials in the GUI
In the stepped calibration panel, navigate to the datum frame and use the fiducial click tool. Three clicks per camera: origin, X-axis, Y-axis. The detection overlay updates immediately to show the assigned grid indices. Fiducials persist to config.yaml automatically.
Fiducials for the CLI
The CLI commands require a --fiducials JSON file. You can either export this from the GUI or write it manually:
{
"1": {
"origin": [3311.5, 751.3],
"x_axis": [3904.2, 751.1],
"y_axis": [3307.1, 542.4],
"clicked_level": "peak"
},
"2": {
"origin": [1985.9, 638.5],
"x_axis": [1684.9, 638.5],
"y_axis": [1990.9, 435.2],
"clicked_level": "trough"
}
}Where the clicks go. Fiducial positions and the clicked level are stored in the model's inputs.mat sidecar, not in config.yaml. Clicks snap to the nearest detected dot, and the origin click also records which level you picked, which is what anchors Z = 0.
Click-to-Label
After setting fiducials on the datum frame, each non-datum pose must be labelled to tell the backend whether dots on that pose are on the peak or trough face. This is the most user-intensive step -- but you only need to click one dot per pose per camera.
How It Works
- 1The datum frame is already labelled from the fiducial setup -- no action needed
- 2Navigate to a non-datum pose in the calibration image viewer
- 3Click any dot you can identify as being on the peak face (or trough -- whichever you recognise)
- 4The backend snaps to the nearest detected blob and reports which level (A or B) it belongs to
- 5The frontend maps A/B to peak/trough using the datum convention, and stores the label
- 6Repeat for every non-datum pose, for every camera
- 7Once all poses are verified, the "Generate Model" button becomes enabled
You don't need to be precise -- click anywhere on a dot you recognise. The system snaps to the nearest detected blob automatically. The detection overlay helps: blue dots = peak, red dots = trough. Colours swap in real time when you set a label.
Detection Overlay
The calibration image viewer shows detected dots overlaid on every frame. Dots are colour-coded by level: blue = peak, red = trough. On the datum frame, fiducial markers (origin, X, Y) are also shown. When you set a pose label, the overlay colours update in real time to confirm the assignment.
What Gets Stored
Labels persist to the model's inputs.mat sidecar, alongside your fiducial clicks. A pose with no entry is considered unverified, and the “Generate Model” button stays disabled until every pose has an entry for every camera.
Where the labels go. Pose labels are not config values. They are written to an inputs.mat sidecar beside the saved model, together with your fiducial clicks, so reopening the tab restores them. Generating a model is blocked until every non-datum pose on both cameras is labelled.
CLI note: The CLI reads pose labels directly from config.yaml and fails with a clear error if any frame in the sequence is missing its label. Set up labels in the GUI first, then the CLI can process headlessly.
Stereo Stepped Workflow
Stepped stereo calibration builds a complete stereo model from two cameras viewing a stepped board at multiple poses. It works with both same-side (cameras on the same side) and transmission (cameras on opposite sides) setups.
GUI Workflow
- 1Set board parameters: dot spacing, step height, board thickness, dt
- 2Configure calibration images: format, count, source path, camera subfolders
- 3Set camera pair (e.g. Camera 1 and Camera 2)
- 4Browse to the datum frame and click three fiducial points per camera (origin, X-axis, Y-axis)
- 5Navigate to each non-datum pose and click-to-label one dot per camera
- 6Once all poses show verified labels, click "Generate Model"
- 7Review per-camera RMS error, stereo RMS, relative angle, and baseline distance
- 8Click "Calibrate Vectors" to apply stereo calibration to PIV data
- 9Click "Set as Active" to make stereo_stepped the active calibration method
Stereo Configuration Auto-Detect
PIVTools works out whether your cameras are same-side or transmission from the handedness of the three fiducial clicks on each camera. Because the two cameras view opposite faces in a transmission rig, that handedness flips, and reading it is one deterministic comparison rather than a fit-and-compare search. If either camera's three clicks are too close to collinear to give a reliable sign, calibration stops and tells you rather than guessing. You can also force a configuration with stereo_config: same_side or stereo_config: transmission.
In a transmission setup, each camera sees a different face of the board at a different Z-plane -- there are no common 3D points visible to both cameras. OpenCV's stereoCalibrate requires common points. Instead, PIVTools derives the stereo pose from individual cv2.solvePnP results per camera and computes R_stereo = R2 @ R1.T, T_stereo = t2 - R_stereo @ t1.
Output Directory
├── model/stereo_model_pinhole.mat
├── model/inputs.mat # your fiducial clicks and pose labels
└── figures/
<calibration_source>/calibration/Cam1/stepped_planar/
└── model/model_pinhole.mat # the per-camera fit feeding the stereo pose
CLI Usage
Stepped-board detection has no command-line equivalent. Building the model needs you to click fiducials and label which level each pose sits on, and there is no way to supply that headlessly. Calibrate on the Stereo Stepped tab in the GUI. Once a model is saved, the CLI can apply it like any other.
After the model exists, the rest of the pipeline is ordinary CLI work:
# 1. Calibrate on the Stereo Stepped tab in the GUI, then:
pivtools-cli instantaneous # PIV for both cameras
pivtools-cli apply-stereo --board stepped --all-paths # 3D reconstruction
pivtools-cli statistics --source-endpoint stereoThe flag is --board, and the stepped value isstepped. Note also thatapply-stereo exits with an error unless you give it--all-paths or an explicit directory set.
Self-calibration is available for stepped models on the GUI tab, but pivtools-cli self-calibrate accepts only charuco and dotboard, so there is no headless route to it here either.
Configuration Reference
Almost nothing lives in config.yaml. The calibration: block is a four-key pointer, and anything else written under it is stripped out the next time the config is saved. Board geometry, the rig dt, and the image settings all live in a per-source sidecar at <source>/calibration/settings.yaml. Create one with pivtools-cli init-settings --source <dir> or by filling in the calibration tab in the GUI.
calibration:
calibration_sources:
- /data/experiment/calibration
source: '' # '' means use calibration_sources[source_idx]
source_idx: 0
active: stereo_steppedimage:
image_format: calib%05d.tif # REQUIRED, no default
image_type: standard # REQUIRED
n_views: 19
start_index: 1
rig:
camera_pair: [1, 2]
dt: 0.0006 # REQUIRED before generating a model
datum_frame: 1 # 1-based
interpolator: lanczos
methods:
stepped:
dot_spacing_mm: 5.0
step_height_mm: 3.0
board_thickness_mm: 14.8
level_offset_mm: null # null derives dot_spacing_mm / 2
stepped_stereo:
stereo_config: auto # auto reads it from your clicks
model_type: pinholeYour clicks are not stored in YAML. Fiducial positions, the clicked level, and the per-frame pose labels are written to an inputs.mat sidecar next to the saved model, so reopening the tab restores exactly what you picked.
Troubleshooting
Grid detection fails on some poses
Check dot spacing parameter matches the actual board. Verify illumination is even across both faces. Try cleaning the board surface. Poses with strong foreshortening (> 18 degrees rotation) may lose dots at the edges.
One level recovers far fewer dots than the other
This usually means the fiducial axis clicks are inconsistent between the two faces. Verify that the detection overlay shows grid indices increasing in the expected directions. Re-click fiducials if needed.
"Generate Model" button stays disabled
Not all poses have been click-to-labelled. Check the pose list panel -- any pose without a label prevents model generation. You need one label per pose per camera.
Stereo RMS error > 1.0 px
Step height measurement may be wrong -- verify with a micrometer. Also check that both cameras can resolve individual dots clearly. Blurred or underexposed images increase RMS.
Focal length (fx) error is large
Too few poses. Use at least 5-6 different target positions with varied angles. The multi-image Zhang initialization needs multiple homographies to robustly estimate intrinsics.
Generate Model stays disabled
Some pose is still unlabelled. Every non-datum pose needs a level label on both cameras before a model can be built. Work through the click-to-label step until no pose is left unverified.
Transmission auto-detect picks wrong config
First check your fiducial clicks: the configuration is read from their handedness, so a mis-ordered origin/x/y pick on either camera flips the answer. If the clicks are right, force it with methods.stepped_stereo.stereo_config: transmission (or same_side) in the settings sidecar.
Next: View Your Results
After calibration, visualise your velocity fields and compute statistics.