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. 1.Point calibration_sources at the folder holding your calibration images. Per-camera subfolders are opt-in, not the default. Seed the settings sidecar with pivtools-cli init-settings or fill in the calibration tab.
  2. 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. 3.Enter board parameters: pattern_cols, pattern_rows, dot_spacing_mm (dotboard) or squares_h/v, square_size (ChArUco). Set rig.dt to the time between laser pulses in seconds. It is required before a model can be generated and is never defaulted.
  4. 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. 5.Click Generate Model. Aim for RMS reprojection error below 0.5 px.
  6. 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.

MethodTypeBest ForTarget Required
Scale FactorUniformTelecentric lenses, quick analysisKnown px/mm ratio, no board
DotboardSpatially-varyingStandard PIV with lens distortionCircular dot grid, 10-20 images
ChArUcoSpatially-varyingPartial occlusion, oblique anglesChArUco 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
calibration_source/
├── calib00001.tif
├── calib00002.tif
└── ...
use_camera_subfolders: true
calibration_source/
├── Cam1/
│ └── calib00001.tif
└── Cam2/
    └── calib00001.tif

Image Settings

SettingYAML KeyDescription
Calibration Sourcescalibration_sourcesDirect paths to calibration image locations
Image Formatimage_formatFilename pattern (e.g. calib%05d.tif)
Number of Viewsn_viewsCalibration images (board poses) to process
Image Typeimage_typestandard, cine, lavision_set, lavision_im7 (auto-detected from the format if unset)
Zero-Based Indexingzero_based_indexingStart image numbering from 0
Camera Subfoldersuse_camera_subfoldersAppend per-camera folders to the source path
Subfolder Namescamera_subfoldersCustom 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.
IM7 Container Format

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.

Platform Note

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

ParameterDescriptionExample
px_per_mmPixels per millimetre3.41
dtTime 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 directionAxis directions (and optional axis swap); a mirrored camera is handled here, not by a velocity flipright, up

GUI Workflow

  1. 1Set the Calibration Images Location (one reference image is enough)
  2. 2Enter dt (seconds) and px_per_mm -- the two-point measure helper can fill px_per_mm from a known distance
  3. 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
  4. 4Set the +X and +Y directions (defaults +X right, +Y up)
  5. 5Click "Generate Model" -- the model and a proof figure are saved into the calibration source folder
  6. 6Click "Calibrate Vectors" to apply (instantaneous or ensemble)
  7. 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

ParameterDescriptionDefault
dot_spacing_mmPhysical spacing between dot centres (mm)15.0
k_neighborsNeighbours per dot in the grid walk9
model_typeCamera model: pinhole or polynomialpinhole
fix_k2Pin 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

  1. 1Configure calibration images (location, format, number of views)
  2. 2Enter the dot spacing (mm) and choose the camera model (pinhole or polynomial)
  3. 3On the datum frame, click Set Origin, then +X, then +Y -- clicks snap to the nearest detected dot and the detection overlay appears
  4. 4Optionally type the origin X/Y in mm
  5. 5Click "Generate Model" (disabled until the world frame is complete) to run detection on all frames and compute the camera model
  6. 6Review the RMS reprojection error (target: < 0.5 px) and the proof figures
  7. 7Click "Calibrate Vectors" to apply calibration to PIV data
  8. 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.

<source>/calibration/Cam1/dotboard_planar/
├── 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

ParameterDescriptionDefault
squares_hHorizontal square count10
squares_vVertical square count7
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
model_typeCamera model: pinhole or polynomialpinhole

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

<source>/calibration/Cam1/charuco_planar/
├── 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

  1. 1On the Dotboard or ChArUco tab, select "Polynomial" in the Camera Model dropdown
  2. 2Complete the world-frame clicks (origin, +X, +Y) on the datum frame as usual
  3. 3Click "Generate Model" -- detection runs, then the two 10-coefficient least-squares fits
  4. 4Review the per-axis RMS in mm on the results card (it shows coefficients + RMS instead of intrinsics)
  5. 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

Detection Commands
# 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

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-paths

apply-calibration Options

FlagDescriptionDefault
--boardBoard / method: charuco, dotboard, stepped, scale_factorFrom config
--cameraCamera numberAll cameras
--sourceCalibration source dir (where the models live)From config
--type-nameData type (instantaneous / ensemble)instantaneous
--model-typeWhich record to load when several exist: pinhole, polynomial, polynomial3d, scale_factorFrom model
--dtTime between frames (seconds)From config
--all-pathsApply to all source pathsOff

Complete Workflow

Full Planar Calibration 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-paths

Complete 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
PIVtools - High-Performance PIV Processing