Stereo Calibration

Calibrate stereo camera pairs for three-component (3C) velocity measurements. Three methods available: Dotboard, ChArUco, and Stepped.

Quick Recipe

opinionated defaults -- full reference below
  1. 1.Note your configuration: same-side (both cameras on one side of the light sheet) or transmission (on opposite sides). If your setup is high-magnification or fragile at single-plane calibration, consider the stepped board instead.
  2. 2.Pick detection: Stereo Dotboard (circular dot grids) or Stereo ChArUco (occlusion-tolerant). Configure images for both cameras in the calibration tab.
  3. 3.Click Generate Model. Targets: stereo RMS < 0.5 px, relative angle 30–60°. Verify the baseline matches your physical setup.
  4. 4.Run PIV on both cameras first (pivtools-cli instantaneous or ensemble).
  5. 5.Click Reconstruct 3D (GUI) or run pivtools-cli apply-stereo to produce ux, uy, uz.
  6. 6.Recommended: run Self-Calibration afterwards to correct for laser-sheet Z-offset and tilt. Adds a few minutes, meaningfully improves accuracy.

Overview

Calibration settings live beside your images, not in config.yaml. The calibration: block in config.yaml is a four-key pointer saying which source and which method are active. Image format, board geometry, and the rig dt 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. The image format is required when the settings are read and rig.dt when a model is generated, so a missing value stops the run instead of quietly defaulting.

Stereo calibration computes the geometric relationship between two cameras viewing the same measurement plane. The stereo model contains intrinsic parameters for each camera and extrinsic parameters (rotation and translation) describing their relative geometry.

MethodBest ForTarget Required
Stereo DotboardStandard stereo PIV setupsCircular dot grid, 10-20 positions
Stereo ChArUcoPartial target visibility, oblique anglesChArUco board, multiple positions
Stereo SteppedTransmission rigs where cameras image different board facesDual-level (stepped) dot board, 1+ positions (3+ recommended)
Stereo is pinhole only

The stereo tabs offer no polynomial camera model. A single-plane polynomial has no out-of-plane sensitivity, so it cannot recover the W component. The model-type dropdown that appears on the planar tabs is absent on the stereo tabs.

Quality Metrics

After calibration, review these metrics to assess quality.

MetricDescriptionTarget
Stereo RMS ErrorJoint reprojection error from the stereoCalibrate pose fit (pixels). Dotboard and ChArUco only< 0.5 px
Stereo ViewsNumber of shared views used in the stereo pose fit (n_stereo_views)10+ typical
Cam1 / Cam2 RMS ErrorPer-camera reprojection error (pixels)< 0.5 px each
Relative AngleAngle between camera optical axes (degrees)30-60 degrees typical
Baseline DistancePhysical camera separation (mm)Verify against setup

Stereo Model Outputs

Intrinsic Parameters (per camera)

  • Camera Matrix: fx and fy (focal length, constrained equal by the default fix_aspect_ratio), cx, cy (principal point)
  • Distortion: 4 coefficients -- k1, k2 radial and p1, p2 tangential, with k3 held at zero

Extrinsic Parameters

  • R_stereo: 3x3 rotation from Cam1 to Cam2
  • T_stereo: Baseline translation (mm)
  • self_cal block: Laser-sheet z_offset and tilt from self-calibration (empty until run)

Persistence

The model record is written into the calibration source folder, not the output base path. Beside it, an inputs.mat sidecar caches the detected points, the clicked world frame, and the board geometry, so the model regenerates without re-detecting or re-clicking. Clicks and detections are never stored in config.yaml -- config holds only what you type or select before detecting (image settings, camera pair, dt, geometry seed).

<calibration_source>/calibration/stereo_cam1_cam2/
├── model/stereo_model_pinhole.mat (includes the self_cal block)
├── model/inputs.mat (cached detections + clicks)
└── figures/ (proof figures; self_cal/ subfolder after self-calibration)

Calibration Image Setup

Stereo calibration requires synchronised images from both cameras at each target position. Image settings are shared with planar calibration.

Directory Structure

Each entry in calibration_sources is a direct path to a folder of calibration images. Per-camera subfolders are opt-in: set image.use_camera_subfolders in the settings sidecar and name them in image.camera_subfolders. There is no layout-ordering switch.

use_camera_subfolders: true
<calibration_source>/
├── Cam1/
│   └── calib_001.tif
└── Cam2/
    └── calib_001.tif
use_camera_subfolders: false
<calibration_source>/
├── calib_001.tif
└── calib_002.tif

(or one IM7 container holding both cameras)
IM7 Container Format

LaVision IM7 files can contain both stereo cameras in a single file. Set use_camera_subfolders: false and PIVTools will extract each camera frame automatically.

Image Synchronisation

Each image index must show the target at the same position for both cameras. Misaligned images produce incorrect stereo geometry.

Stereo Dotboard

Both cameras view a circular dot grid at multiple target positions. Detection uses the same algorithm as planar dotboard (blob detection, neighbour walk, RANSAC filtering) -- grid dimensions are found automatically, so no row/column counts are configured.

Model Fitting

Each camera's intrinsics are fitted independently. Object-point release (calibrateCameraRO) is on by default and falls back to plain calibrateCamera per camera when that camera's views are not identical (partial boards). The cross-camera pose then comes from cv2.stereoCalibrate(CALIB_FIX_INTRINSIC) over all shared views -- a joint estimate that replaced the old compose of two single-view solvePnP poses. The fit reports the stereo RMS (px) and the shared-view count (n_stereo_views).

Parameters

ParameterDescriptionDefault
camera_pairCamera numbers forming the stereo pair[1, 2]
dot_spacing_mmPhysical spacing between dot centres (mm)15.0
datum_frameImage index defining the world frame (1-based)1
dtTime between frames (seconds)1.0
fix_k2Pin the r^4 radial term to zero (toggle appears below 3 frames; on by default there)false

GUI Workflow

  1. 1Configure calibration images (format, count, subfolder)
  2. 2Set camera pair (e.g. Camera 1 and Camera 2) and dot spacing
  3. 3Browse images to verify both camera views show the target
  4. 4Detect dots on the datum frame, then click origin / +X / +Y on camera 1 to define the world frame
  5. 5Click "Generate Model" to compute stereo calibration
  6. 6Review stereo RMS error, shared-view count, relative angle, and baseline distance
  7. 7Click "Calibrate Vectors" to apply calibration to PIV data

Output Directory

<calibration_source>/calibration/stereo_cam1_cam2/
├── model/stereo_model_pinhole.mat
├── model/inputs.mat
└── figures/

Detection cache: detections persist in inputs.mat beside the model, so Generate reuses them on a reopened tab. The Re-detect button ignores the cache and detects fresh before recalibrating.

Stereo ChArUco

Combines ArUco marker detection with stereo geometry computation. ArUco markers identify which corners are visible, so detection works with partial occlusion and oblique viewing angles. Model fitting is identical to Stereo Dotboard -- independent per-camera intrinsics, then cv2.stereoCalibrate(CALIB_FIX_INTRINSIC) over all shared views, with corners matched across cameras by their global corner ids.

Parameters

ParameterDescriptionDefault
camera_pairCamera numbers forming the stereo pair[1, 2]
squares_hHorizontal square count10
squares_vVertical square count9
square_sizeSquare size in metres0.03
marker_ratioMarker size relative to square0.5
aruco_dictArUco dictionary typeDICT_4X4_1000
min_cornersMinimum corners to accept a frame6
dtTime between frames (seconds)0.0057553

ArUco Dictionaries

Available dictionaries: DICT_4X4, DICT_5X5, DICT_6X6, DICT_7X7 -- each with 50, 100, 250, or 1000 markers.

GUI Workflow

Same as Stereo Dotboard: configure images, set camera pair and board parameters, generate model, review quality metrics, and apply to vectors. Detections cache in inputs.mat with the same Re-detect override.

Shared board parameters: ChArUco board settings (squares_h, squares_v, etc.) are read from the calibration.charuco section, shared with the planar ChArUco method.

Stereo Stepped

A stepped (dual-level) board carries dots on two Z planes, so a single view is non-coplanar and constrains depth on its own. The cameras may image different board faces (transmission rigs), which means they share no features and cv2.stereoCalibrate cannot run. Instead, two mono pinhole fits are composed into one rig. This composed pose is the DaVis-matching un-bundled method, not a weaker fallback.

Model Fitting

Each camera is fitted into the shared plate frame independently, then the rig pose is composed as

R_stereo = R2 · R1ᵀ
T_stereo = t2 − R_stereo · t1

Because composition has no joint reprojection step, there is no stereo RMS for this method -- the results card shows the two per-camera RMS values plus the self-calibration residual disparity. Stereo stepped is strictly a two-camera pair; there is no multi-camera stitching on this tab. The camera model is pinhole only.

Parameters

ParameterDescriptionDefault
camera_pairCamera numbers forming the stereo pair (Camera 1 is the world reference)[1, 2]
dot_spacing_mmPhysical spacing between dot centres (mm)15.0
step_height_mmHeight between peak and trough levels (mm)3.0
board_thickness_mmTotal thickness of the board (mm)14.8
datum_frameWorld origin image, 1-based, both cameras1
stereo_configRig geometry: auto (from clicks), same_side, or transmissionauto
dtTime between frames (seconds)1.0
fix_k2Pin the r^4 radial term to zero (toggle appears below 3 usable poses)on when shown
Fewer than 3 poses: keep fix k2 on

One near-planar view has no leverage on the r^4 radial term. A free k2 runs away into a degenerate basin, dragging focal length, principal point, stereo angle, and baseline with it. The toggle defaults on below 3 poses. In-plane vectors stay usable either way, but W picks up a bias from the wrong angle -- use 3 or more views for quantitative 3C.

GUI Workflow

  1. 1Configure calibration images and the camera pair (both cameras, same frame count)
  2. 2Set the board geometry (dot spacing, step height, thickness) and datum frame
  3. 3Run detection -- dots on both Z levels are found for every frame of both cameras
  4. 4Click the three fiducials (origin / +X / +Y) on each camera's datum frame
  5. 5Verify every usable pose: click one peak or trough dot per pose to label its face
  6. 6Click "Generate Model" and review the per-camera RMS values
  7. 7Optionally run self-calibration, then "Calibrate Vectors" for 3C reconstruction

Peak/trough verification is enforced: Generate stays disabled until every usable pose has its face labelled on both cameras. The detection figure colours peaks blue and troughs red so labels can be checked at a glance. Detections, fiducials, and labels persist per-pick to inputs.mat, so a reopened tab regenerates the model with one Generate press.

No polynomial3d: the stereo stepped model is pinhole only. A per-camera polynomial pair builds no usable stereo baseline, so the UI drops the option and the backend rejects it.

3D Velocity Reconstruction

After computing the stereo model, reconstruction converts the 2D PIV velocity fields from each camera into 3C velocity vectors (u, v, w) in world coordinates. The solve is the Willert/Soloff method -- both cameras' projection Jacobians are stacked into a 4x3 least-squares system per grid point and solved for (U, V, W).

Process

1
2D PIV (Cam 1)

ux, uy in image plane

2
2D PIV (Cam 2)

ux, uy in image plane

3
3D Velocity

ux, uy, uz in world coords

Requirements

  • Valid stereo calibration model
  • 2D PIV vectors from Camera 1
  • 2D PIV vectors from Camera 2
  • Overlapping camera views on the sheet plane (empty overlap raises)

Output Variables

  • ux: x-velocity (m/s)
  • uy: y-velocity (m/s)
  • uz: out-of-plane velocity (m/s)
  • b_mask: validity mask (0 = valid, non-zero = excluded)
  • x, y: world coordinates (mm)

Output Grid and Conventions

  • Output lands on a regular world-mm grid spanning the two cameras' overlap on the sheet plane. Spacing is automatic -- the median world-space vector pitch of both PIV grids. To change the output resolution, change the PIV window/overlap and re-run.
  • Row 0 is the top of the view (y descending down the rows), matching every other calibrated product.
  • The frame is right-handed with +Z = +X x +Y, so W shares its axis with the world z coordinate. The legacy "+w toward cameras" flip (z_toward_cameras) has been removed.
  • A point is masked in b_mask where either camera's projection leaves its PIV grid or either camera flagged its contributing vector.
Old stereo files are rejected

The vector reader requires b_mask. Stereo results written before it existed (ux/uy/uz only) raise on load and must be re-reconstructed.

Stereo ensemble reconstructs mean flow only

Applying stereo calibration to ensemble results reconstructs the mean velocity (ux/uy/uz/b_mask) per pass -- the 4x3 solve is linear in the displacements, so the ensemble mean reconstructs exactly. Reynolds stresses are not reconstructed through stereo (they transform quadratically and require a separate method).

GUI Workflow

  1. 1Ensure a stereo model has been generated (Dotboard, ChArUco, or Stepped)
  2. 2Run PIV processing for both cameras
  3. 3Select data type (instantaneous or ensemble)
  4. 4Click "Reconstruct 3D" to start reconstruction
  5. 5View reconstructed vectors in the Vector Viewer with the Stereo data source

Output Directory

base_path/stereo_calibrated/{N}/Cam1_Cam2/instantaneous/
├── B00001.mat ... B0NNNN.mat (ux, uy, uz, b_mask per frame)
└── coordinates.mat (regular world-mm grid)

base_path/stereo_calibrated/{N}/Cam1_Cam2/ensemble/
└── per-pass coordinates.mat + ensemble_result.mat (ux, uy, uz, b_mask)

Note: Reconstruction quality depends on stereo calibration accuracy and proper overlap between the PIV fields from both cameras.

Self-Calibration (Wieneke 2005)

Self-calibration detects and corrects laser-sheet misalignment from the recorded particle images, not the calibration board. The laser sheet may be offset from the calibration plane (z_offset) or tilted relative to it (tilt_x, tilt_y). Self-calibration measures the disparity between camera views and iteratively refines the sheet parameters until the corrections stabilise. It is available on the stereo board tabs and on stereo stepped, for pinhole models only.

Algorithm Steps

  1. 1Dewarp both camera images to a common reference plane using the existing stereo model
  2. 2Cross-correlate Camera 1 vs Camera 2 (same time instant, different viewpoints)
  3. 3Extract the disparity field -- residual displacement between the two dewarped views
  4. 4Fit the disparity to a plane model, extracting Z-offset and tilt angles
  5. 5Update the dewarping maps and repeat until the per-iteration corrections (Z-offset and tilt) stabilise below threshold

Parameters

ParameterDescriptionDefault
n_imagesNumber of image pairs used for disparity estimation20
window_sizeCorrelation window size (pixels)64
overlapWindow overlap percentage50.0

Output Parameters

Estimated Parameters

  • z_offset: Laser sheet Z-offset (mm)
  • tilt_x: Tilt about X-axis (radians)
  • tilt_y: Tilt about Y-axis (radians)

Quality Metrics

  • RMS disparity: Residual disparity (pixels)
  • Iteration count: Number of refinement iterations
  • Convergence status: Whether threshold was reached

GUI Workflow

  1. 1Complete stereo calibration (Dotboard, ChArUco, or Stepped pinhole)
  2. 2Open the Self-Calibration panel below the stereo model results
  3. 3Select the PIV dataset (base path) providing the particle images
  4. 4Preview the dewarp alignment (red-cyan overlay shows before/after)
  5. 5Set number of images, window size, and overlap
  6. 6Click "Run Self-Calibration" to start the iterative process
  7. 7Review convergence history, final RMS disparity, and the diagnostic figures

Storage and Application

Results are stored in the stereo record's self_cal block inside stereo_model_pinhole.mat -- not a sidecar file and not config.yaml. Subsequent stereo reconstruction applies the stored sheet automatically, so no extra step is needed. Regenerating the stereo model clears the block -- re-run self-calibration after recalibrating.

Diagnostic Output

Seven diagnostic figures plus correlation_planes.mat are written beside the model, shareable with the dataset.

<calibration_source>/calibration/stereo_cam1_cam2/figures/self_cal/

CLI Usage

Stereo calibration uses two CLI steps: detect targets to generate a stereo model, then apply-stereo to reconstruct 3D velocities.

Step 1: Generate Stereo Model

Detection Commands
# Stereo ChArUco detection (CLI detection is ChArUco-only -- # dotboard and stepped need interactive clicks, so they are GUI-only) pivtools-cli detect-stereo # Specify the camera pair explicitly pivtools-cli detect-stereo --camera-pair 1,2

Step 2: Reconstruct 3D Velocity

Apply Stereo Calibration
# Reconstruct using the ChArUco stereo model pivtools-cli apply-stereo --board charuco --all-paths # Specify the camera pair explicitly pivtools-cli apply-stereo --board charuco --camera-pair 1,2 --all-paths # Process every configured path (like the GUI) pivtools-cli apply-stereo --board charuco --all-paths # Choose the resample kernel pivtools-cli apply-stereo --board charuco --interpolator lanczos --all-paths

Step 3: Self-Calibration (Optional)

Self-Calibration Command
# Run self-calibration to correct laser-sheet misalignment # (--base-path-idx selects the PIV dataset providing particle images) pivtools-cli self-calibrate --board dotboard --camera-pair 1,2 \ --base-path-idx 0 --n-images 20 --window-size 64 --overlap 50

apply-stereo Options

FlagDescriptionDefault
--boardStereo board: charuco, dotboard, or steppedFrom config
--camera-pairCamera pair as "1,2"From config
--dtTime between frames (seconds)From model / config
--all-pathsDerive every base_path from config (like the GUI)off
--type-namePIV result type for --all-paths: instantaneous or ensembleinstantaneous
--interpolatorResample kernel: cubic or lanczoslanczos
--model-typeWhich stereo record to load when several exist in the model dirpinhole

Complete Workflow

Full Stereo PIV Workflow
# 1. Generate stereo calibration model (ChArUco; dotboard/stepped via GUI) pivtools-cli detect-stereo # 2. Run PIV processing for both cameras pivtools-cli instantaneous # 3. Reconstruct 3D velocities pivtools-cli apply-stereo --board charuco --camera-pair 1,2 --all-paths # 4. Compute statistics on stereo data pivtools-cli statistics --source-endpoint stereo

Complete YAML Reference

# ============================================================ # config.yaml -- a four-key pointer, nothing more. # Anything else under calibration: is stripped on the next save. # ============================================================ calibration: calibration_sources: - /data/experiment/calibration source: '' source_idx: 0 active: stereo_dotboard # stereo_dotboard | stereo_charuco | stepped_stereo # ============================================================ # <source>/calibration/settings.yaml # Seed it with: pivtools-cli init-settings --source <dir> # ============================================================ image: image_format: calib_%03d.tif # REQUIRED at read image_type: standard # REQUIRED at read n_views: 15 # calibration views (NOT images.num_images) zero_based_indexing: false use_camera_subfolders: true camera_subfolders: ["Cam1", "Cam2"] rig: camera_pair: [1, 2] dt: 0.0006 # REQUIRED before generate; never defaulted datum_frame: 1 # 1-based interpolator: lanczos # cubic | lanczos piv_type: instantaneous fit: distortion_model: standard fix_aspect_ratio: true # forces fx == fy self_cal_n_images: 20 # One block per physical board, shared by that board's mono and stereo # flows. The mono/stereo distinction lives in calibration.active. methods: dotboard: dot_spacing_mm: null # REQUIRED at generate k_neighbors: 9 model_type: pinhole charuco: squares_h: null # REQUIRED at generate squares_v: null # REQUIRED at generate square_size: null # REQUIRED at generate; metres marker_ratio: 0.5 aruco_dict: DICT_4X4_1000 min_corners: 6 model_type: pinhole stepped: dot_spacing_mm: null step_height_mm: null board_thickness_mm: null level_offset_mm: null # null derives dot_spacing_mm / 2 stepped_stereo: stereo_config: auto # auto | same_side | transmission model_type: pinhole # fix_k2 and use_release_object are deliberately not templated: their # defaults depend on context, so a stored value would override both. # Set either explicitly only if you mean to. # World-frame clicks, detections, and self-calibration results are in # neither file -- they live in the model record + inputs.mat sidecar.

GUI to YAML Field Mapping

GUI ControlYAML FieldValues
Active Methodconfig.yaml -> calibration.activestereo_dotboard, stereo_charuco, or stepped_stereo
Camera 1settings.yaml -> rig.camera_pair[0]Integer (1-based)
Camera 2settings.yaml -> rig.camera_pair[1]Integer (1-based)
dtsettings.yaml -> rig.dtFloat (seconds); required, never defaulted
Datum Framesettings.yaml -> rig.datum_frameInteger (1-based)
Dot Spacing (mm)settings.yaml -> methods.dotboard.dot_spacing_mmFloat (mm); required
Fix radial k2 = 0settings.yaml -> methods.<board>.fix_k2Boolean. Not templated: absent means false, and it only appears in the GUI below 3 usable poses
Squares Hsettings.yaml -> methods.charuco.squares_hInteger; required
Squares Vsettings.yaml -> methods.charuco.squares_vInteger; required
Square Sizesettings.yaml -> methods.charuco.square_sizeFloat (metres); required
ArUco Dictionarysettings.yaml -> methods.charuco.aruco_dictDICT_{4-7}X{4-7}_{50-1000}
Step Height (mm)settings.yaml -> methods.stepped.step_height_mmFloat (mm); required
Board Thickness (mm)settings.yaml -> methods.stepped.board_thickness_mmFloat (mm)
Stereo Geometrysettings.yaml -> methods.stepped_stereo.stereo_configauto, same_side, or transmission

Next: Create Visualisation Videos

Visualise your calibrated velocity fields with animated videos.

Continue to Video Maker
PIVtools - High-Performance PIV Processing