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:

MethodConfig Active ValueBest ForOutput
Stereo Steppedstereo_steppedTwo-camera 3D velocity (transmission or same-side)Stereo model + per-camera pinhole

Quality Metrics

MetricDescriptionTarget
Per-camera RMSReprojection 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 levelHow many dots the walk-first separation recovered on each of the two levels, per cameraBoth levels well populated
Why Stepped Boards?

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.

ParameterYAML KeyDescriptionExample
Dot Spacingdot_spacing_mmCentre-to-centre distance between adjacent dots (mm)15
Step Heightstep_height_mmHeight difference between peak and trough faces (mm)3
Board Thicknessboard_thickness_mmTotal board thickness including both faces (mm)14.8
dtdtTime between laser pulses (seconds)5.0e-06
Measurement Precision

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):

1
Origin

Click any dot to define the (0, 0) grid position

2
X-Axis Point

Click a dot in the positive X direction from origin

3
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.

Getting Fiducials Right

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:

fiducials.json (stereo example)
{ "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

  1. 1The datum frame is already labelled from the fiducial setup -- no action needed
  2. 2Navigate to a non-datum pose in the calibration image viewer
  3. 3Click any dot you can identify as being on the peak face (or trough -- whichever you recognise)
  4. 4The backend snaps to the nearest detected blob and reports which level (A or B) it belongs to
  5. 5The frontend maps A/B to peak/trough using the datum convention, and stores the label
  6. 6Repeat for every non-datum pose, for every camera
  7. 7Once all poses are verified, the "Generate Model" button becomes enabled
Tip: Approximate Clicks Are Fine

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

  1. 1Set board parameters: dot spacing, step height, board thickness, dt
  2. 2Configure calibration images: format, count, source path, camera subfolders
  3. 3Set camera pair (e.g. Camera 1 and Camera 2)
  4. 4Browse to the datum frame and click three fiducial points per camera (origin, X-axis, Y-axis)
  5. 5Navigate to each non-datum pose and click-to-label one dot per camera
  6. 6Once all poses show verified labels, click "Generate Model"
  7. 7Review per-camera RMS error, stereo RMS, relative angle, and baseline distance
  8. 8Click "Calibrate Vectors" to apply stereo calibration to PIV data
  9. 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.

Why Not cv2.stereoCalibrate?

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

<calibration_source>/calibration/stereo_cam1_cam2/
├── 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:

End-to-End Stepped Stereo
# 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 stereo

The 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_stepped
image: 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: pinhole

Your 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.

PIVtools - High-Performance PIV Processing