Planar Calibration
Convert pixel displacements to physical velocity units (m/s). Three target types (scale factor, dotboard, ChArUco) and two camera models (pinhole, polynomial) depending on your optical setup and accuracy requirements.
Quick Recipe
opinionated defaults -- full reference below- 1.Point
calibration_sourcesat the folder holding your calibration images. Per-camera subfolders are opt-in, not the default. Seed the settings sidecar withpivtools-cli init-settingsor fill in the calibration tab. - 2.Pick a method by target type: Scale Factor if you have a known px/mm, Dotboard for circular dot grids, ChArUco for occlusion-tolerant targets, Polynomial as a camera model on top of a dotboard or ChArUco detection when a pinhole fit is not flexible enough.
- 3.Enter board parameters:
pattern_cols,pattern_rows,dot_spacing_mm(dotboard) orsquares_h/v,square_size(ChArUco). Setrig.dtto the time between laser pulses in seconds. It is required before a model can be generated and is never defaulted. - 4.Click Detect One to verify detection on a single frame before running the full sequence. Detected dots/corners are overlaid in the viewer.
- 5.Click Generate Model. Aim for RMS reprojection error below 0.5 px.
- 6.Click Calibrate Vectors then Set as Active. Multi-camera setups with shared features also need Global Coordinates -- see the bottom of this page.
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.
Calibration converts raw PIV results from pixel displacements to physical units. Choose a method based on your target type and required accuracy.
| Method | Type | Best For | Target Required |
|---|---|---|---|
| Scale Factor | Uniform | Telecentric lenses, quick analysis | Known px/mm ratio, no board |
| Dotboard | Spatially-varying | Standard PIV with lens distortion | Circular dot grid, 10-20 images |
| ChArUco | Spatially-varying | Partial occlusion, oblique angles | ChArUco board, multiple images |
Uniform calibration (Scale Factor) applies the same conversion everywhere: velocity = (pixels / px_per_mm) / (dt * 1000). Spatially-varying methods (Dotboard, ChArUco) compute different conversion factors across the image to correct for lens distortion.
Camera Model Choice
The Dotboard and ChArUco tabs share a Camera Model dropdown with two choices. Pinhole (default) is the DaVis-matching PinholeOpenCV model (single focal length fx = fy, free principal point cx/cy, distortion k1, k2, p1, p2 with k3 fixed at 0). Polynomial is a single-plane 3rd-order pixel-to-mm map -- see the Polynomial section below. Scale Factor is its own tab and involves no camera model fit.
Multi-camera is automatic. With two or more cameras configured, the Dotboard and ChArUco tabs run a joint multi-camera solve -- there is no toggle. A single camera is a mono solve in the same tab. See the Global Coordinates page for the joint solve and the guided wizard.
Calibration Image Setup
Dotboard and ChArUco methods require calibration target images. Configure the image source before running detection.
Directory Structure
calibration_sources holds direct paths to the calibration image locations (the "Calibration Images Location" input on each tab). When use_camera_subfolders is enabled, per-camera folders are appended to the source path.
use_camera_subfolders: false
├── calib00001.tif
├── calib00002.tif
└── ...
use_camera_subfolders: true
├── Cam1/
│ └── calib00001.tif
└── Cam2/
└── calib00001.tif
Image Settings
| Setting | YAML Key | Description |
|---|---|---|
| Calibration Sources | calibration_sources | Direct paths to calibration image locations |
| Image Format | image_format | Filename pattern (e.g. calib%05d.tif) |
| Number of Views | n_views | Calibration images (board poses) to process |
| Image Type | image_type | standard, cine, lavision_set, lavision_im7 (auto-detected from the format if unset) |
| Zero-Based Indexing | zero_based_indexing | Start image numbering from 0 |
| Camera Subfolders | use_camera_subfolders | Append per-camera folders to the source path |
| Subfolder Names | camera_subfolders | Custom folder names (default Cam1, Cam2, ...) |
Image Viewer Tools
The Calibration Image Viewer includes interactive tools for verifying your setup:
- Measure Tool: Click two points on the image to measure distance in pixels (dx, dy, and total length). Useful for verifying dot spacing and image scale.
- Grid Overlay: Toggleable grid with configurable size (8x8 to 64x64) and adjustable line thickness. Helps verify alignment and spacing.
- Magnifier: 2.5x circular zoom lens following the cursor for precise feature inspection.
- Contrast Controls: Dual-slider for manual min/max adjustment, with auto-scale toggle.
LaVision IM7 files can contain multiple cameras in a single file. Set use_camera_subfolders: false for multi-camera IM7 containers. PIVTools extracts each camera frame automatically.
LaVision formats (.im7, .set) are read by a pure-Python, cross-platform reader. No lvpyio dependency -- they work on macOS, Linux, and Windows alike.
Scale Factor
Uniform px-to-mm conversion. No board and no detection -- enter the known pixel-to-mm ratio and the time between laser pulses, then pick the world origin and axis directions on the image. World mm = (pixel - origin) / px_per_mm; velocity = displacement / px_per_mm / dt / 1000.
Parameters
| Parameter | Description | Example |
|---|---|---|
| px_per_mm | Pixels per millimetre | 3.41 |
| dt | Time between frames (seconds) | 0.56 |
| Origin (px / mm) | World-origin pixel + its world position in mm (picked/typed in the GUI, stored in the model, not config) | click, (0, 0) |
| +X / +Y direction | Axis directions (and optional axis swap); a mirrored camera is handled here, not by a velocity flip | right, up |
GUI Workflow
- 1Set the Calibration Images Location (one reference image is enough)
- 2Enter dt (seconds) and px_per_mm -- the two-point measure helper can fill px_per_mm from a known distance
- 3Click "Pick Origin" and click the world origin on the image (or type Origin X/Y in px); optionally type the origin's world position in mm
- 4Set the +X and +Y directions (defaults +X right, +Y up)
- 5Click "Generate Model" -- the model and a proof figure are saved into the calibration source folder
- 6Click "Calibrate Vectors" to apply (instantaneous or ensemble)
- 7Click "Set as Active" to make this the active method
Multiple cameras? Each camera gets its own scale-factor model. To place them all in one shared frame, use the datum + overlap-pair chain on the Global Coordinates page ("Compute + Save Global Frame").
Planar Dotboard
Uses OpenCV camera model to compute intrinsic parameters and distortion coefficients from images of a circular dot grid target. Provides spatially-varying calibration that corrects for lens distortion.
Parameters
| Parameter | Description | Default |
|---|---|---|
| dot_spacing_mm | Physical spacing between dot centres (mm) | 15.0 |
| k_neighbors | Neighbours per dot in the grid walk | 9 |
| model_type | Camera model: pinhole or polynomial | pinhole |
| fix_k2 | Pin the r^4 radial term (only for few-view fits) | false |
Detection Behaviour
Detection needs zero user configuration. Blob detection tries both polarities (dark-on-light and light-on-dark) and keeps whichever finds more blobs, then an Otsu filter on the blob-size histogram rejects small noise blobs (specular reflections). Detected dots are assigned grid indices by a BFS neighbourhood walk -- purely local neighbour relationships, so it is robust to perspective, tilt, and boards partially out of frame -- and a RANSAC homography models the perspective and rejects outliers. Missing interior dots are rescued by predicting their position with a local homography and confirming by template matching against a nearby healthy dot. Grid dimensions are discovered automatically -- only the dot spacing is entered. OpenCV calibrateCamera / calibrateCameraRO then computes the camera model.
World Frame from Clicks
The coordinate system is defined by three clicks on the datum frame -- origin, +X, +Y. Each click snaps to the nearest detected dot, and the buttons auto-advance (origin, then +X, then +Y). The axes are built from the board's orthogonal grid axes, so the +X/+Y clicks only choose which grid axis and its sign -- never skew. A typed origin X/Y in mm places the origin dot at an absolute world position.
GUI Workflow
- 1Configure calibration images (location, format, number of views)
- 2Enter the dot spacing (mm) and choose the camera model (pinhole or polynomial)
- 3On the datum frame, click Set Origin, then +X, then +Y -- clicks snap to the nearest detected dot and the detection overlay appears
- 4Optionally type the origin X/Y in mm
- 5Click "Generate Model" (disabled until the world frame is complete) to run detection on all frames and compute the camera model
- 6Review the RMS reprojection error (target: < 0.5 px) and the proof figures
- 7Click "Calibrate Vectors" to apply calibration to PIV data
- 8Click "Set as Active" to make this the active method
Outputs
Camera Model
- Camera Matrix: focal length f (fx == fy, fixed aspect ratio), cx, cy
- Distortion: 4 coefficients (k1, k2, p1, p2; k3 fixed at 0)
- Extrinsics: Rotation + translation per frame
- RMS Error: Reprojection quality metric
Output Directory
Written into the calibration source folder (with the images), not the PIV base path -- one project can hold many runs, each calibration living with its images.
├── model/model_pinhole.mat (or model_polynomial.mat)
├── model/inputs.mat (detections + clicks sidecar)
└── figures/
Two or more cameras = joint solve. The tab automatically runs the joint multi-camera calibration -- one shared board, one shared world frame, no toggle. The datum and cross-camera ties are picked through the guided "Set Global Coordinates" wizard -- see the Global Coordinates page. A single camera is a mono solve in the same tab.
Planar ChArUco
Combines a chessboard pattern with ArUco markers. Detection uses OpenCV's cv2.CharucoDetector; the markers identify which corners are visible, so detection works with partial occlusion and at oblique viewing angles. Grid indices come directly from the global corner ids -- the grid needs zero clicks.
Parameters
| Parameter | Description | Default |
|---|---|---|
| squares_h | Horizontal square count | 10 |
| squares_v | Vertical square count | 7 |
| square_size | Square size in metres | 0.03 |
| marker_ratio | Marker size relative to square | 0.5 |
| aruco_dict | ArUco dictionary type | DICT_4X4_1000 |
| min_corners | Minimum corners to accept a frame | 6 |
| model_type | Camera model: pinhole or polynomial | pinhole |
ArUco Dictionaries
Available dictionaries: DICT_4X4, DICT_5X5, DICT_6X6, DICT_7X7 -- each with 50, 100, 250, or 1000 markers. Larger dictionaries support more unique markers; smaller bit sizes detect faster.
GUI Workflow
Same as Dotboard: configure images, set board parameters, generate model, review RMS, apply to vectors, and set as active. For a single camera the origin/+X/+Y clicks define the world frame exactly as on the dotboard tab (clicks snap to detected corners). Detection itself needs no clicks -- the corner ids fix the grid.
Joint (multi-camera) ChArUco origin: the joint solve currently uses the default corner-id origin. Picking a chosen corner as the origin is not yet available on the joint ChArUco path -- if you need a clicked origin with multiple cameras, use a dotboard.
Output Directory
├── model/model_pinhole.mat (or model_polynomial.mat)
└── model/inputs.mat
Polynomial
The alternative camera model on the Dotboard and ChArUco tabs (planar only -- the stereo tabs are pinhole-only). It is a direct single-plane 3rd-order map from image pixels to world mm, fitted from the datum view's detected features in the clicked world frame -- 10 coefficients per axis, per camera. The polynomial absorbs strong lens distortion and oblique perspective that a pinhole model cannot. It is fitted output, not an imported file: there is no XML and no DaVis import.
The Polynomial Model
Pixel coordinates are normalised about the image centre and scaled by the image half-dimensions (so s, t span roughly [-1, 1]), then mapped to world mm by a 10-term cubic basis per axis: 1, s, s2, s3, t, t2, t3, st, s2t, st2. The clicked origin/+X/+Y and origin-mm are baked into the coefficients exactly as for pinhole. Everything is produced by the fit -- the user enters none of it. Fit quality is reported as the per-axis RMS residual in mm (rms_x_mm, rms_y_mm).
Two deliberate differences from pinhole: the fit uses only the datum view (it is a single-plane map), and it extrapolates silently outside the fitted region -- values stay finite but are unconstrained beyond the board. Keep the board covering the measurement region.
GUI Workflow
- 1On the Dotboard or ChArUco tab, select "Polynomial" in the Camera Model dropdown
- 2Complete the world-frame clicks (origin, +X, +Y) on the datum frame as usual
- 3Click "Generate Model" -- detection runs, then the two 10-coefficient least-squares fits
- 4Review the per-axis RMS in mm on the results card (it shows coefficients + RMS instead of intrinsics)
- 5Click "Calibrate Vectors" to apply, then "Set as Active"
CLI, Single Camera (ChArUco)
Run detect-charuco --model-type polynomial. Detects ChArUco corners and fits the 10-coefficient polynomial per axis. Dotboard polynomial fits are GUI-only (they need the world-frame clicks).
CLI, Multi-Camera (joint)
Run detect-joint --model-type polynomial. Fits every camera's datum view in the shared global frame, producing per-camera polynomial records that are mutually consistent.
Persistence & Caching
Everything needed to reproduce a model is saved beside it in the calibration source folder. You never re-detect or re-click to regenerate.
The inputs.mat Sidecar
The fitted model record (.mat) is written to <source>/calibration/Cam{N}/<board>_planar/model/. Beside it, an inputs.mat sidecar stores the detected points, the clicked world frame, and the board geometry. Generate re-solves from the sidecar with no re-detecting and no re-clicking -- even after deleting the model file, one Generate press rebuilds it. The model is also self-describing: the board geometry that produced it is stamped inside, and the GUI seeds its parameter panel from the loaded model.
Where each thing lives
There are three homes, and it is worth knowing which is which. config.yaml holds only a four-key pointer saying which source and which method are active. The per-source settings.yaml sidecar holds everything you type before detecting: image settings, the rig dt, board geometry, model-type selections, and the global-coordinates block. The model's inputs.mat sidecar holds everything you click.
Writing board geometry under calibration: in config.yaml has no effect. Those keys are stripped the next time the config is saved, and the values you expected to be used silently disappear.
Detection Caching
Detections are cached in memory for the session and persisted on disk in the inputs.mat sidecar, so previews and re-opens are instant. The Re-detect button forces a fresh detection after the images on disk change.
Tolerant Detection
A view that fails detection is dropped and reported per-view -- it never aborts the solve. A camera only fails if no image detects (almost always a wrong path, format, or board parameter).
Auto-Load
Visiting a tab restores its saved model automatically -- the origin/+X/+Y markers, detection overlay, and results card all repaint. There is no "Load Saved" button.
Setting the Datum Afterwards
After applying any calibration, the vector viewer's Set Datum control shifts a dataset's coordinate grid in place -- x/y offsets applied to the coordinates of all runs in the selected data type. Velocities are untouched.
CLI Usage
Planar calibration uses two CLI steps: detect targets to generate a camera model, then apply the calibration to PIV vectors.
Step 1: Generate Camera Model
# ChArUco detection (mono detection on the CLI is ChArUco-only)
pivtools-cli detect-charuco
# Process a specific camera
pivtools-cli detect-charuco --camera 1
# Choose the camera model and distortion model
pivtools-cli detect-charuco --model-type polynomial
pivtools-cli detect-charuco --model-type pinhole --distortion rational
# Joint multi-camera solve (ChArUco headless; dotboard clicks come from the GUI wizard)
pivtools-cli detect-joint --cameras 1,2,3
# Dotboard detection is GUI-only -- it needs the world-frame clicks.Step 2: Apply Calibration
# Use the active method from config.yaml
pivtools-cli apply-calibration --all-paths
# Choose the board / method
pivtools-cli apply-calibration --board dotboard --all-paths
pivtools-cli apply-calibration --board charuco --all-paths
pivtools-cli apply-calibration --board scale_factor --all-paths
# Specific camera + data type
pivtools-cli apply-calibration --camera 1 --type-name ensemble --all-paths
# All source paths
pivtools-cli apply-calibration --all-pathsapply-calibration Options
| Flag | Description | Default |
|---|---|---|
| --board | Board / method: charuco, dotboard, stepped, scale_factor | From config |
| --camera | Camera number | All cameras |
| --source | Calibration source dir (where the models live) | From config |
| --type-name | Data type (instantaneous / ensemble) | instantaneous |
| --model-type | Which record to load when several exist: pinhole, polynomial, polynomial3d, scale_factor | From model |
| --dt | Time between frames (seconds) | From config |
| --all-paths | Apply to all source paths | Off |
Complete Workflow
# 1. Detect calibration targets (ChArUco; dotboard is GUI-only)
pivtools-cli detect-charuco
# 2. Run PIV processing
pivtools-cli instantaneous
# 3. Apply calibration to vectors
pivtools-cli apply-calibration --board dotboard --all-pathsComplete YAML Reference
# ============================================================
# config.yaml -- a four-key pointer, nothing more.
# Anything else written under calibration: is stripped on save.
# ============================================================
calibration:
calibration_sources:
- /data/experiment/calibration
source: '' # '' means use calibration_sources[source_idx]
source_idx: 0
active: dotboard # charuco | dotboard | stepped | scale_factor
# | stereo_charuco | stereo_dotboard | stepped_stereo
# ============================================================
# <source>/calibration/settings.yaml -- everything else.
# Seed it with: pivtools-cli init-settings --source <dir>
# ============================================================
image:
image_format: calib%05d.tif # REQUIRED at read; no default
image_type: standard # REQUIRED. standard | cine | lavision_set | lavision_im7
n_views: 19 # optional; frame-count auto-detect is the fallback
start_index: 1
zero_based_indexing: false
use_camera_subfolders: true
camera_subfolders: ["Cam1", "Cam2"]
rig:
camera: 1
dt: 0.0057553 # REQUIRED before generate; never defaulted
datum_frame: 1 # 1-based
interpolator: lanczos
piv_type: instantaneous # data type for Calibrate Vectors
fit:
distortion_model: standard
fix_aspect_ratio: true # forces fx == fy
# 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; no default
k_neighbors: 9
model_type: pinhole # pinhole | polynomial
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
scale_factor:
px_per_mm: null # REQUIRED at generate
# Multi-camera global frame (scale-factor path -- see Global Coordinates)
global_coordinates:
enabled: false
datum_camera: 1
datum_pixel: null
datum_physical: [0.0, 0.0]
datum_frame: 1
overlap_pairs: []
# World-frame clicks and detections are in neither file -- they persist in
# the model's inputs.mat sidecar in the calibration source folder.Measure Tool
The calibration image viewer includes a pixel measurement tool for determining distances in your images. This is useful for verifying dot spacing or estimating px_per_mm for the Scale Factor method.
- Toggle measure mode with the ruler button in the viewer toolbar
- Click two points on the image to define start and end
- A line is drawn between the points showing the pixel distance
- Displays dx, dy, and total Euclidean length in pixels
- Click again to start a new measurement
Calculating px_per_mm
If you know a physical distance in your image (e.g., a ruler or known feature), measure it in pixels with this tool, then divide by the known length in mm to get your px_per_mm value for Scale Factor calibration.
Next: Stereo Calibration
For three-component velocity measurements using stereo camera pairs.
Continue to Stereo Calibration