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
Stepped Stereostepped_boardTwo-camera 3D velocity (transmission or same-side)Stereo model + per-camera pinhole
Stepped Planarstepped_planarSingle-camera 3D calibration with real depthPer-camera pinhole model

Quality Metrics

MetricDescriptionTarget
Per-camera RMSReprojection error per camera (pixels)< 0.5 px
Stereo RMSOverall stereo reprojection error< 0.5 px
Cross-level consensusAgreement between peak/trough grid stitching> 90%
Cross-level RMSPixel RMS of stitched level overlay< 2.0 px
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. For a same-side configuration (both cameras on the same side of the board): the peak face is at Z = 0 and the trough face is at Z = -step_height_mm. For transmission (cameras on opposite sides): Z assignments are reversed for one camera. PIVTools auto-detects the correct configuration by trying both assignments and picking whichever gives lower RMS.

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" } }

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 config.yaml as cam1_pose_levels / cam2_pose_levels entries. A pose with no entry is considered unverified. The “Generate Model” button remains disabled until every pose has an entry for every camera.

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 stepped_board the active calibration method

Stereo Configuration Auto-Detect

PIVTools auto-detects whether your cameras are in same-side or transmission configuration. It fits camera 2 twice (once per configuration) and picks whichever gives lower RMS. The result is surfaced as stereo_config_resolved in the calibration output. You can also force a specific configuration via 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

base_path/calibration/stereo_cam1_cam2/
├── model/stereo_model.mat
├── Cam1/model/camera_model.mat
├── Cam2/model/camera_model.mat
├── indices/
├── figures/
└── camera_placement.html    # Interactive Plotly visualisation

Stepped Planar Workflow

Stepped planar calibration fits a per-camera 3D pinhole model using both Z-levels of the stepped board. Unlike standard planar calibration (which sees a flat field), each pose provides genuine non-coplanar 3D points — dots at two Z-planes give real depth information that breaks the fx/tz ridge without needing a stereo pair.

Standard Planar (1 Z-level)

  • All dots at one Z plane per pose
  • Depth constrained only by multiple poses
  • Fragile at PIV magnification (fx error 1–3%)

Stepped Planar (2 Z-levels)

  • Dots at two Z planes in every single pose
  • Real depth variation constrains all parameters
  • Robust at PIV magnification (fx error < 0.25%)

GUI Workflow

  1. 1Set board parameters: dot spacing, step height, board thickness, dt
  2. 2Configure calibration images: format, count, source path
  3. 3Select target level (peak or trough — which face to use as reference)
  4. 4Click three fiducial points on the datum frame (origin, X-axis, Y-axis)
  5. 5Navigate to each non-datum pose and click-to-label one dot
  6. 6Click "Generate Model" once all poses are verified
  7. 7Review per-camera RMS reprojection error (target: < 0.5 px)
  8. 8Click "Calibrate Vectors" to apply

Output Directory

base_path/calibration/Cam1/stepped_planar/
├── model/camera_model.mat
├── indices/
└── figures/

CLI Usage

Both stepped commands require a --fiducials JSON file containing the origin, axis, and clicked_level for each camera. The easiest workflow: set up fiducials and pose labels in the GUI, then run detection headlessly via CLI.

detect-stepped-stereo

Stereo Detection
# Basic usage (reads board params from config.yaml) pivtools-cli detect-stepped-stereo --fiducials fiducials.json # Explicit stereo config and pose count pivtools-cli detect-stepped-stereo -f fiducials.json --stereo-config transmission -n 11 # Custom calibration source directory pivtools-cli detect-stepped-stereo -f fiducials.json -cs /path/to/calibration/images
FlagShortDescriptionDefault
--fiducials-fPath to fiducials JSON file (required)--
--active-paths-pComma-separated path indicesFrom config
--calibration-source-csDirect path to calibration imagesFrom config
--num-frames-nNumber of poses in the sequenceFrom config or 11
--start-frame-sFirst frame index1
--datum-frame-dDatum frame indexSame as start-frame
--stereo-config--auto, same_side, or transmissionauto

detect-stepped-planar

Planar Detection
# All cameras in fiducials file pivtools-cli detect-stepped-planar --fiducials fiducials.json # Single camera only pivtools-cli detect-stepped-planar -f fiducials.json --camera 1 # Custom pose count pivtools-cli detect-stepped-planar -f fiducials.json -n 6
FlagShortDescriptionDefault
--camera-cSingle camera number to processAll in fiducials
--fiducials-fPath to fiducials JSON file (required)--
--active-paths-pComma-separated path indicesFrom config
--calibration-source-csDirect path to calibration imagesFrom config
--num-frames-nNumber of posesFrom config or 11
--start-frame-sFirst frame index1
--datum-frame-dDatum frame indexSame as start-frame

Pose labels from config: The CLI reads stepped_board.cam1_pose_levels and cam2_pose_levels (stereo) or stepped_planar.pose_levels (planar) directly from config.yaml. If any frame in the sequence is missing its label, the CLI exits with a clear error message listing the missing frames.

Complete Stereo Workflow

End-to-End Stepped Stereo
# 1. Set up board params in config.yaml (or use GUI) # 2. Set fiducials + pose labels in GUI, or create fiducials.json manually # 3. Generate stereo model from stepped board pivtools-cli detect-stepped-stereo -f fiducials.json # 4. Run PIV processing for both cameras pivtools-cli instantaneous # 5. Apply stereo calibration (3D reconstruction) pivtools-cli apply-stereo --method stepped_board # 6. Compute statistics on stereo data pivtools-cli statistics --source-endpoint stereo # 7. Create visualisation video pivtools-cli video --data-source stereo -v uz

Complete Planar Workflow

End-to-End Stepped Planar
# 1. Generate per-camera model from stepped board pivtools-cli detect-stepped-planar -f fiducials.json # 2. Run PIV processing pivtools-cli instantaneous # 3. Apply calibration pivtools-cli apply-calibration --method stepped_board # 4. Compute statistics pivtools-cli statistics

Complete YAML Reference

calibration: active: stepped_board piv_type: instantaneous # Board geometry stepped_board: dot_spacing_mm: 15 # Centre-to-centre dot distance (mm) step_height_mm: 3 # Peak-to-trough face height (mm) board_thickness_mm: 14.8 # Total board thickness (mm) dt: 5.0e-06 # Time between laser pulses (seconds) # Camera setup camera_pair: [1, 2] stereo_config: transmission # auto | same_side | transmission datum_frame: 1 # Reference pose frame number datum_camera: 1 # Reference camera num_calibration_frames: 11 # Total poses in the sequence # Fiducials (auto-populated by GUI click tool) cam1_fiducials: origin: [3311.5, 751.3] x_axis: [3904.2, 751.1] y_axis: [3307.1, 542.4] cam1_clicked_level: peak # Which face the origin click landed on cam2_fiducials: origin: [1985.9, 638.5] x_axis: [1684.9, 638.5] y_axis: [1990.9, 435.2] cam2_clicked_level: trough # Pose labels (set via click-to-label in GUI) cam1_pose_levels: '1': peak # Frame 1 verified as peak '2': peak cam2_pose_levels: '1': trough # Frame 1 verified as trough '2': trough

GUI to YAML Field Mapping

GUI ControlYAML FieldValues
Active Methodcalibration.activestepped_board or stepped_planar
Dot Spacing (mm)stepped_board.dot_spacing_mmFloat (mm)
Step Height (mm)stepped_board.step_height_mmFloat (mm)
Board Thickness (mm)stepped_board.board_thickness_mmFloat (mm)
dt (seconds)stepped_board.dtFloat (seconds)
Camera Pairstepped_board.camera_pair[int, int]
Stereo Configstepped_board.stereo_configauto | same_side | transmission
Datum Framestepped_board.datum_frameInteger (1-based)
Fiducial Origin Clickstepped_board.cam*_fiducials.origin[x, y] pixels
Clicked Levelstepped_board.cam*_clicked_levelpeak | trough
Pose Label Clickstepped_board.cam*_pose_levels{ frame: peak|trough }

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.

Cross-level consensus below 50%

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.

CLI fails with "missing pose labels" error

The CLI reads pose labels from config.yaml. Run the GUI click-to-label workflow first, or manually add entries to cam1_pose_levels / cam2_pose_levels in config.yaml.

Transmission auto-detect picks wrong config

Force the correct configuration with stereo_config: transmission (or same_side) in config.yaml, or --stereo-config on the CLI.

Next: View Your Results

After calibration, visualise your velocity fields and compute statistics.