PIV Processing
Configure multi-pass cross-correlation for instantaneous or ensemble analysis.
Quick Recipe
opinionated defaults -- full reference below- 1.Pick a mode: Instantaneous for per-pair velocity fields, Ensemble for time-averaged flow + Reynolds stresses from many pairs.
- 2.Window table
[128,128] → [64,64] → [32,32]at 50% overlap covers the vast majority of setups. Drop the final pass to[16,16]withsinglemode for ensemble at max resolution. - 3.Leave the peak finder at default (
gauss6for both). Leave outlier detection and infilling on. - 4.Ensemble users: leave
fit_method: kspacealone. It is the only ensemble fitter, and it estimates the Reynolds stresses jointly with the noise floor so noise does not bias them. - 5.Click Run PIV (GUI) or
pivtools-cli instantaneous/ensemble. Come back to this page if results look wrong.
Overview
| Instantaneous | Ensemble | |
|---|---|---|
| Output | Velocity field per frame pair | Single mean velocity field |
| Correlation | Per frame pair | Averaged across all pairs |
| Use case | Time-resolved data, turbulence statistics | Mean flow, low seeding, max resolution |
| Pass types | Standard only | Standard + Single mode |
| Peak finder | Configurable (gauss3-6, default gauss6) | Not applicable: the k-space fitter returns displacement directly |
| Live preview | Yes (per-frame) | No (accumulated) |
| Resume | No | Yes (resume_from_pass) |
Window Configuration
Both modes use multi-pass refinement. Each pass refines results from the previous one. Configure passes in the GUI pass table or directly in YAML.
| Parameter | Format | Description |
|---|---|---|
| window_size | [Height, Width] | Interrogation window in pixels per pass. Each axis must be a built FFT size (8, 12, 16, 24, 32, 48, 64, 96, 128). On an ensemble single-mode pass this is instead the Frame-A mask support inside sum_window rather than an FFT length, so 4 and 6 are also legal there and the grid spacing follows it. Rectangular windows supported. |
| overlap | integer (%) | Overlap percentage per pass. 50% doubles grid density. |
| runs | list of pass numbers | Which passes to save (1-based). The list is honoured exactly; the last pass is saved only as a fallback when the list is empty. |
| type (ensemble) | 'std' | 'single' | Standard or single-pixel mode per pass. |
Window size convention: [Height, Width] (row-major). Height = vertical (Y), Width = horizontal (X). Use rectangular windows for directional flows (e.g., [32, 64] for predominantly horizontal flow).
# Typical 3-pass configuration
instantaneous_piv:
window_size:
- [128, 128] # Pass 1: large windows
- [64, 64] # Pass 2: refinement
- [32, 32] # Pass 3: final
overlap:
- 50
- 50
- 50
runs:
- 3 # Save pass 3 onlySingle Mode and Sum Window
Ensemble only. Appears when any pass uses type single.
Single mode uses asymmetric window weighting: Frame A gets a small central window (the configured window_size), Frame B gets the full sum_window. This concentrates the measurement on a small region while computing correlations at the larger FFT size.
| Parameter | Description | Constraint |
|---|---|---|
| sum_window | FFT correlation window size [H, W] (default [32, 32]) | Must be a built FFT size and >= each single-mode pass window |
| sum_fitting_window | Central crop of the summed plane used for fitting (default [32, 32]; falls back to sum_window when unset) | Positive, even, <= sum_window. Exempt from the FFT-size restriction (it is a crop, not an FFT) |
ensemble_piv:
window_size:
- [128, 128] # Pass 1: std
- [64, 64] # Pass 2: std
- [16, 16] # Pass 3: single
type:
- std
- std
- single
sum_window:
- 64
- 64 # [Height, Width] for single passesCamera Selection
Select which cameras to process via numbered chips in the GUI. At least one must be selected. In YAML, set paths.camera_numbers.
Peak Finding
Both modes. Instantaneous and ensemble both default to gauss6.
| Algorithm | DOF | Description |
|---|---|---|
| gauss3 | 3 | Parabolic (1D Gaussian). Fastest. |
| gauss4 | 4 | Circular Gaussian with amplitude. |
| gauss5 | 5 | Elliptical Gaussian. Good for anisotropic flows. |
| gauss6 | 6 | Rotated elliptical Gaussian. Maximum accuracy. |
instantaneous_piv:
peak_finder: gauss6 # Options: gauss3, gauss4, gauss5, gauss6Predictor & Peak Settings
Control how the predictor field is refined between multi-pass iterations and configure multi-peak detection.
| Setting | Default | Mode | Description |
|---|---|---|---|
| predictor_smoothing | true / false | Inst / Ens | Gaussian-smooth the predictor between passes. Recommended for instantaneous (reduces single-pair noise). For ensemble, smoothing can destroy real gradients -- leave disabled unless data is very noisy. |
| image_warp_interpolation | cubic | Both | Interpolation kernel for image warping during predictor deformation. cubic = bicubic (4×4 stencil), lanczos = Lanczos-3 (6×6 stencil, slightly sharper). |
| secondary_peak | false | Instantaneous | Extract the second-highest correlation peak per window. Useful for reverse flow or multiple particle populations. |
| num_peaks | 1 | Instantaneous | Number of correlation peaks to detect per window. Usually 1; increase for multi-peak analysis. |
| save_mode | minimal | Instantaneous | Output fields per vector file. 'minimal' saves ux, uy, b_mask only (fastest). 'full' saves all 11 fields including peak height, sigma, stresses. |
| save_compression | false | Instantaneous | Enable ZLIB compression on .mat output files. Slower writes but smaller file size. |
instantaneous_piv:
predictor_smoothing: true
secondary_peak: false
num_peaks: 1
save_mode: minimal
save_compression: false
image_warp_interpolation: cubic
ensemble_piv:
predictor_smoothing: false
image_warp_interpolation: cubicBoundary Conditions
Ensemble only. Overrides edge-replicated predictor padding near walls with prescribed velocity values.
In wall-bounded flows, the predictor field near boundaries can be corrupted by edge replication. Boundary conditions let you prescribe known velocities (e.g., no-slip: ux=0, uy=0) at specific wall positions.
| Field | Type | Description |
|---|---|---|
| y_position | int (px) | Row position of the wall in the image |
| ux | float (px/frame) | Prescribed horizontal displacement at the wall |
| uy | float (px/frame) | Prescribed vertical displacement at the wall |
| edge | 'bottom' | 'top' | Which image edge the wall is near |
ensemble_piv:
predictor_boundary_conditions:
- y_position: 10
ux: 0
uy: 0
edge: bottom
- y_position: 950
ux: 0
uy: 0
edge: topCorrelation & Fitting
Ensemble only. Advanced controls for peak fitting and memory management.
| Setting | Default | Description |
|---|---|---|
| persist_images | false | Keep all filtered images in worker RAM across passes. Faster on HPC with lots of memory, but significantly increases RAM usage. |
| sum_fitting_window_enabled | true | Extract a central crop from the summed correlation plane before peak fitting. Reduces memory and speeds up fitting. |
| sum_fitting_window | [32, 32] | Size [H, W] of the central crop. Falls back to sum_window when unset. Must be positive, even, and <= sum_window; not restricted to built FFT sizes. |
ensemble_piv:
persist_images: false
sum_fitting_window_enabled: true
sum_fitting_window:
- 32
- 32Ensemble Options
Ensemble only. Available in the collapsible Ensemble Options panel.
Why kspace is the ensemble default: The k-space fitter (ensemble only) jointly estimates displacement, Reynolds stresses, the loss-of-correlation gain and the spectral noise floor in one fit, so noise does not bias the stresses. It is the only ensemble fitter. The defaults are the validated recipe and most users should leave them alone, but three knobs exist for cases the default model does not describe well:kspace_shape,kspace_floor andenvelope_divide, all documented below. Instantaneous PIV always uses the LM Gaussian peak fitter; the k-space option does not apply there.
| Setting | Default | Description |
|---|---|---|
| fit_method | kspace | 'kspace' (Fourier-space one-stage joint LM fit: displacement, stresses, gain, noise floor). This is the only accepted value -- any other raises at config load. |
| kspace_shape | gaussian | Displacement-PDF shape assumed by the fit. 'gaussian', or add quartic kurtosis terms with 'kx4', 'ky4' or 'kx4+ky4'. Use a quartic term when the displacement distribution is measurably non-Gaussian along that axis; the Gaussian assumption otherwise biases the stresses. |
| kspace_floor | coloured | Noise-floor model. 'coloured' fits an analytic N0*P(k) shaped by the imaging and processing pipeline. 'flat' is the pre-2026-07 constant floor, kept for reproducing older runs. |
| envelope_divide | false | Divide AA/BB/AB by the pair-count envelope before fitting. Off leaves the tent attenuation in place, which the fit accounts for. Set true only to reproduce pre-2026-07-27 results. |
| background_subtraction_method | correlation | 'correlation': R = <AB> - <A><B> (single-pass, memory efficient). 'image': R = <(A-μA)(B-μB)> (two-pass, more stable for k-space). 'window_mean': per-pair per-window mean subtraction inside the correlator. 'correlation+window_mean' / 'image+window_mean': stationary-background removal plus per-pair window-mean removal. 'correlation+dc_zero' / 'image+dc_zero': zero the DC bin after correlation in Fourier space, with no per-pair mean. |
| per_pair_normalization | false | Equalises fluctuation-energy weighting across pairs. Only legal when background_subtraction_method is exactly 'window_mean' -- any combined mode raises. |
| gradient_correction | false | Reynolds stress gradient correction near walls |
| store_planes | false | Save AA, BB, AB correlation planes to disk (large files) |
| save_diagnostics | false | Warped-image diagnostics under filters/, plus a per-window fit_diagnostics_pass_N.mat holding gain, N0, cost_per_pt, n_valid, iter, conv and status (with b4x/b4y when a quartic shape is active). Each file also records the settings that produced it: pass_idx, n_pairs, bg_method, per_pair_normalization, gain_normalised, kspace_shape and kspace_floor. |
| resume_from_pass | 0 | Resume from pass N (1-based). 0 = fresh start. Requires existing ensemble_result.mat. |
Two defaults changed. kspace_floor is now coloured rather than flat, and envelope_divide is now off where the divide previously always happened. Both change your numbers, and both can be switched back to reproduce an older run. The other recent additions (window_mean, per_pair_normalization, predictor_rounding, kspace_shape) all default to the legacy or off behaviour, so existing configs otherwise run unchanged.
Brightness controls for difficult experimental data
Three controls address distinct brightness artefacts and compose. The meannorm preprocessing filter (filters: [{type: meannorm}] -- an image filter, not an ensemble_piv key) divides each frame by its spatial mean and fixes multiplicative gain. window_mean removes the additive per-pair pedestal. per_pair_normalization equalises fluctuation-energy weighting across pairs.
Recommended escalation ladder when ensemble results degrade on experimental data: gain_normalisation → meannorm → window_mean → add per_pair_normalization → add predictor_rounding. If the stresses still look wrong and you have reason to think the displacement PDF is non-Gaussian, try a quartic kspace_shape.
preprocessing:
gain_normalisation: true # divide out per-frame laser gain first
filters:
- type: meannorm # image filter: fixes multiplicative gain
ensemble_piv:
background_subtraction_method: window_mean
per_pair_normalization: true # requires exactly window_mean
predictor_rounding: truePerformance Settings
Configure parallel processing resources. Applies to both modes.
| Setting | Default | Description |
|---|---|---|
| backend | cpu | Processing backend |
| omp_threads | 4 | OpenMP threads per worker for C extensions |
| dask_workers_per_node | 1 | Number of parallel Dask workers |
| dask_memory_limit | 12GB | RAM allocation per worker |
| dask_max_in_flight_per_worker | 3 | Max concurrent tasks queued per worker. Higher values (4-6) improve I/O pipelining on HPC with fast storage. |
| open_dashboard | false | Auto-open the Dask performance dashboard in your browser when processing starts |
| cluster_type | local | 'local' or 'slurm' |
| auto_compute_params | false | Auto-compute omp_threads, dask_workers, and dask_memory from system resources |
| n_nodes | 1 | Number of compute nodes (SLURM cluster only) |
| slurm_walltime | 01:00:00 | Job walltime for SLURM submissions |
| slurm_partition | (none) | SLURM partition name |
| post_processing_workers | auto | Max parallel workers for calibration, statistics, merge, and transform. auto = min(cpu_count, 16) |
processing:
backend: cpu
omp_threads: 4
dask_workers_per_node: 1
dask_memory_limit: 12GB
dask_max_in_flight_per_worker: 3
open_dashboard: false
cluster_type: localOutlier Detection
Identify and mark spurious vectors for replacement. Multiple methods can be chained. Instantaneous and ensemble use separate config sections.
| Method | Parameters | Description |
|---|---|---|
| peak_mag | threshold: 0.2 | Reject vectors with correlation peak below threshold |
| median_2d | epsilon: 0.1, threshold: 2, size: 5 | PIVware-style normalised median test on the vector norm over a size x size neighbourhood (default 5, so 24 neighbours). The vector is judged as a whole rather than per component. |
| sigma | sigma_threshold | Reject vectors further than N local standard deviations from the local mean. |
| div_vort | div_thresh, vort_thresh | Reject on divergence and vorticity magnitude. Leave either unset for an automatic MAD-based threshold. CLI only, not exposed in the GUI. |
outlier_detection:
enabled: true
methods:
- type: peak_mag
threshold: 0.2
- type: median_2d
epsilon: 0.1
threshold: 2
size: 5ensemble_outlier_detection:
enabled: true
methods:
- type: median_2d
epsilon: 0.1
threshold: 2
size: 5Infilling
Replace outlier vectors (NaN) with interpolated values. Applied in two phases: mid-pass (between passes, always enabled) and final-pass (on output, optional).
| Method | Parameters | Description |
|---|---|---|
| nearest | (none) | Nearest valid vector via distance transform. Fastest, robust for clustered gaps. |
| biharmonic | (none) | PDE-based smooth interpolation. High quality. |
| knn | n_neighbors: 32, weights: 'distance' | Distance-weighted regression. Sparse data. |
infilling: # or ensemble_infilling:
mid_pass:
method: nearest # no parameters needed
final_pass:
enabled: true
method: biharmonic # no parameters neededNaN Reason Codes
Every interrogation window is assigned a nan_reason code indicating why it was marked invalid, or 0 if it passed all checks. This field is saved in the output .mat file. The table below is the ensemble taxonomy, produced by the k-space fitters and the accumulator. Instantaneous PIV uses its own codes, aligned where the meanings match.
| Code | Stage | Description |
|---|---|---|
| -1 | Pre-fitting | Masked vector (outside ROI / polygon mask) |
| 0 | Success | Fit succeeded and passed all validation checks |
| 1 | Fitting | Fit failed: the LM solve did not converge |
| 2 | Post-fit validation | SNR too low |
| 3 | Post-fit validation | Displacement exceeds 3/4 window rule (peak too far from centre) |
| 5 | Post-fit validation | Negative sigma / variance values (unphysical fit) |
| 6 | Displacement check | Displacement exceeds 3/4 window rule (checked in accumulator) |
| 10 | Outlier detection | Velocity outlier: fit succeeded but flagged by median-based displacement outlier detection |
| 11 | Outlier detection | Stress outlier: fit succeeded but flagged by stress field median test or Cauchy-Schwarz realizability violation (ensemble only) |
Validation pipeline order
- Fitter attempts fit (codes 1, 2)
- Post-fit parameter validation (codes 3, 5)
- Displacement magnitude check in accumulator (code 6)
- Velocity outlier detection via median test (code 10)
- Stress outlier detection + realizability check, ensemble final pass only (code 11)
- Vectors with code 0 after all checks are valid
Codes 3 and 6 both enforce the 3/4 displacement rule but at different stages: code 3 is checked inside the fitter, code 6 in the accumulator after the fitter returns. Vectors flagged with codes 10 or 11 are infilled from neighbours if infilling is enabled.
Instantaneous codes
Instantaneous PIV writes nan_reason in the full save mode with its own taxonomy. Shared meanings keep the same numbers (-1 masked, 0 valid, 1 peak-fit failure, 6 large-displacement rejection, 10 outlier on the primary peak). Instantaneous-only codes are 9 unclassified (in the NaN mask but no stage claimed it), 12 outlier on a substituted secondary peak, and 13 chosen peak magnitude NaN (peaks exhausted).
Running PIV
Start processing from the Run PIV card. Select which source/base path pairs to process via checkboxes.
Workflow
- Check the datasets to include. Same PIV settings apply to all selected paths.
- Click Run PIV. If output already exists, a confirmation dialog asks to clear and recompute.
- Monitor progress via the progress bar (instantaneous) or console logs (both modes). The bar spans every selected dataset, and with more than one selected a line beneath names the dataset currently running.
- Cancel gracefully if needed. Partial results may be saved.
Instantaneous feedback
Progress bar across all selected datasets with a Dataset n of N counter, live vector field preview of the dataset being written (variable selector, colormap), console logs.
Ensemble feedback
Status indicator + console logs only. No preview until all pairs are accumulated.
# Enable one mode at a time
processing:
instantaneous: true
ensemble: falseComplete YAML Reference
| YAML Path | Inst. | Ens. | Description |
|---|---|---|---|
| processing.instantaneous | Y | - | Enable instantaneous mode |
| processing.ensemble | - | Y | Enable ensemble mode |
| *_piv.window_size | Y | Y | List of [H, W] per pass |
| *_piv.overlap | Y | Y | Overlap % per pass |
| *_piv.runs | Y | Y | Passes to save (1-based) |
| *_piv.peak_finder | Y | Y | gauss3 / gauss4 / gauss5 / gauss6 (default gauss6 for both) |
| *_piv.predictor_smoothing | Y | Y | Smooth predictor between passes (inst: true, ens: false) |
| instantaneous_piv.secondary_peak | Y | - | Detect secondary correlation peak (default false) |
| instantaneous_piv.num_peaks | Y | - | Number of peaks to detect (default 1) |
| ensemble_piv.type | - | Y | Per-pass: std or single |
| ensemble_piv.sum_window | - | Y | [H, W] for single mode |
| ensemble_piv.fit_method | - | Y | kspace (one-stage joint LM). Only accepted value. |
| ensemble_piv.kspace_shape | - | Y | gaussian / kx4 / ky4 / kx4+ky4 (default gaussian) |
| ensemble_piv.kspace_floor | - | Y | coloured / flat (default coloured) |
| ensemble_piv.envelope_divide | - | Y | Divide by the pair-count envelope (default false) |
| preprocessing.gain_normalisation | Y | Y | Per-frame laser-gain divide (default false) |
| ensemble_piv.background_subtraction_method | - | Y | correlation, image, window_mean, correlation+window_mean or image+window_mean |
| ensemble_piv.gradient_correction | - | Y | Reynolds stress gradient correction |
| ensemble_piv.persist_images | - | Y | Keep filtered images in worker RAM (default false) |
| ensemble_piv.image_warp_interpolation | - | Y | Image warp kernel: cubic or lanczos |
| instantaneous_piv.image_warp_interpolation | Y | - | Image warp kernel: cubic or lanczos (default cubic) |
| instantaneous_piv.save_mode | Y | - | minimal (3 fields) or full (11 fields) |
| instantaneous_piv.save_compression | Y | - | ZLIB compression on .mat files (default false) |
| ensemble_piv.predictor_boundary_conditions | - | Y | Wall boundary conditions array |
| ensemble_piv.sum_fitting_window_enabled | - | Y | Crop summed plane before fitting (default true) |
| ensemble_piv.sum_fitting_window | - | Y | [H, W] crop size (default [32, 32]; falls back to sum_window) |
| ensemble_piv.store_planes | - | Y | Save correlation planes |
| ensemble_piv.save_diagnostics | - | Y | Warped-image + LM fit diagnostics (default false) |
| ensemble_piv.resume_from_pass | - | Y | 0 = fresh, N = resume from pass N |
| outlier_detection.* | Y | - | enabled, methods: [{type, threshold, epsilon}] |
| ensemble_outlier_detection.* | - | Y | Same structure as outlier_detection |
| infilling.* | Y | - | mid_pass + final_pass config |
| ensemble_infilling.* | - | Y | Same structure as infilling |
| processing.omp_threads | Y | Y | OpenMP threads (default 4) |
| processing.dask_workers_per_node | Y | Y | Dask workers (default 1) |
| processing.dask_memory_limit | Y | Y | Per-worker RAM (default 12GB) |
| processing.dask_max_in_flight_per_worker | Y | Y | Max tasks per worker (default 3) |
| processing.open_dashboard | Y | Y | Auto-open Dask dashboard (default false) |
| processing.cluster_type | Y | Y | local or slurm |
| processing.auto_compute_params | Y | Y | Auto-compute worker/thread settings (default false) |
| processing.post_processing_workers | Y | Y | Parallel post-processing workers (default auto) |
processing:
instantaneous: true
ensemble: false
backend: cpu
omp_threads: 4
dask_workers_per_node: 1
dask_memory_limit: 12GB
dask_max_in_flight_per_worker: 3
open_dashboard: false
cluster_type: local
auto_compute_params: false
post_processing_workers: null
preprocessing:
gain_normalisation: false
instantaneous_piv:
window_size:
- [128, 128]
- [64, 64]
- [32, 32]
overlap:
- 50
- 50
- 50
runs:
- 3
peak_finder: gauss6
predictor_smoothing: true
secondary_peak: false
num_peaks: 1
image_warp_interpolation: cubic
save_mode: minimal
save_compression: false
ensemble_piv:
window_size:
- [128, 128]
- [64, 64]
- [16, 16]
overlap:
- 50
- 50
- 50
type:
- std
- std
- single
runs:
- 3
sum_window:
- 64
- 64
window_type: square
fit_method: kspace
kspace_shape: gaussian
kspace_floor: coloured
envelope_divide: false
background_subtraction_method: correlation
skip_background_subtraction: false
per_pair_normalization: false
predictor_rounding: false
gradient_correction: false
persist_images: false
predictor_smoothing: false
image_warp_interpolation: cubic
predictor_boundary_conditions: []
sum_fitting_window_enabled: true
sum_fitting_window:
- 32
- 32
store_planes: false
save_diagnostics: false
resume_from_pass: 0
outlier_detection:
enabled: true
methods:
- type: peak_mag
threshold: 0.4
- type: median_2d
epsilon: 0.1
threshold: 2
size: 5
ensemble_outlier_detection:
enabled: true
methods:
- type: median_2d
epsilon: 0.1
threshold: 2
size: 5
infilling:
mid_pass:
method: nearest
parameters: {}
final_pass:
enabled: true
method: biharmonic
parameters: {}
ensemble_infilling:
mid_pass:
method: nearest
parameters: {}
final_pass:
enabled: true
method: biharmonic
parameters: {}Command Line Usage
# Run instantaneous PIV
pivtools-cli instantaneous
# Run ensemble PIV
pivtools-cli ensemble
# Process specific paths only (0-indexed)
pivtools-cli instantaneous -p 0,2
# Override active paths
pivtools-cli ensemble --active-paths 0,1| Command | Description |
|---|---|
| pivtools-cli init | Create default config.yaml |
| pivtools-cli instantaneous | Run instantaneous PIV |
| pivtools-cli ensemble | Run ensemble PIV |
| pivtools-cli apply-calibration | Apply calibration to vectors |
| pivtools-cli statistics | Compute statistics |
| pivtools-cli transform | Apply geometric transforms |
| pivtools-cli merge | Merge multi-camera fields |
| pivtools-cli video | Create visualisation videos |
All commands read settings from config.yaml in the current directory. Use -p 0,1 to override paths.active_paths. Use --help for command-specific options.