New: Earn 20% as an AgentAlley affiliate. Share your link and earn on every sale you refer. Join now New: Earn 20% as an affiliate. Join now

Particle Image Velocimetry analysis with OpenPIV

by K-Dense-AI · MIT license · ★ 45,977 on GitHub · updated Sep 21

Openpiv cover

Particle Image Velocimetry (PIV) analysis with OpenPIV. Use when extracting velocity fields from PIV image pairs, analyzing fluid dynamics or flow visualization experiments, cross-correlating interrogation windows, validating and…

---
name: openpiv
description: Particle Image Velocimetry (PIV) analysis with OpenPIV. Use when extracting velocity fields from PIV image pairs, analyzing fluid dynamics or flow visualization experiments, cross-correlating interrogation windows, validating and replacing spurious PIV vectors, or computing vorticity, strain rate, and turbulence statistics from measured velocity fields.
license: BSD-3-Clause
compatibility: Requires Python 3.10+ with openpiv installed (uv pip install openpiv). numpy, scipy, scikit-image, and matplotlib arrive as dependencies. No network access needed after install.
allowed-tools: Read Write Edit Bash
metadata:
  version: "1.1"
  skill-author: OpenPIV Team
  tested-against: "openpiv 0.25.4"
---

# OpenPIV

## Overview

OpenPIV (Open Particle Image Velocimetry) analyzes fluid flow from PIV image pairs. It covers
preprocessing, cross-correlation, vector validation, outlier replacement, smoothing, and scaling to

Overview

OpenPIV (Open Particle Image Velocimetry) analyzes fluid flow from PIV image pairs. It covers preprocessing, cross-correlation, vector validation, outlier replacement, smoothing, and scaling to physical units.

What you get

  • vectors.txttab-delimited, %.4e formatted, with a # x y u v flags mask comment header
  • params.npzNumPy archive with x, y, u, v, flags arrays
  • vector_field.pngvector field drawn over the first frame
Manual install (npx, ZIP)
Claude Codenpx skills add K-Dense-AI/scientific-agent-skills --skill openpiv -g -a claude-code
Claude Code (manual)mkdir -p ~/.claude/skills && curl -sL https://codeload.github.com/K-Dense-AI/scientific-agent-skills/tar.gz/49c6e97775ea \ | tar -xz -C ~/.claude/skills --strip-components=2 scientific-agent-skills-49c6e97775ea/skills/openpiv
Windows (PowerShell)New-Item -ItemType Directory -Force "$HOME\.claude\skills" | Out-Null; Invoke-WebRequest "https://m.agentalley.io/zip/67214c3213d8dac1f14b/openpiv.zip" -OutFile "$env:TEMP\openpiv.zip"; Expand-Archive "$env:TEMP\openpiv.zip" -DestinationPath "$HOME\.claude\skills" -Force
Claude.ai / desktopDownload the ZIP and upload it under Settings → Capabilities → Skills.
Codex, Cursor, other agentsPoint them at SKILL.md. The other files are plain text.

Connects with Agent Alley MCP · agentalley.io/mcp

Files included

  • SKILL.md16.6 KB
  • references/1 file · 10.4 KB
  • scripts/4 files · 15.0 KB
Read the files here
K-Dense-AI/49c6e976 files
SKILL.md
Show the full text470 lines

OpenPIV

Overview

OpenPIV (Open Particle Image Velocimetry) analyzes fluid flow from PIV image pairs. It covers preprocessing, cross-correlation, vector validation, outlier replacement, smoothing, and scaling to physical units.

Everything below is verified against openpiv 0.25.4. The API moves between releases — check inspect.signature() before trusting a snippet against a different version.

When to use

Use this skill when working with experimental PIV or flow-visualization image pairs: measuring 2D velocity fields, tuning interrogation-window parameters, validating vectors, or deriving vorticity, strain rate, and turbulence statistics. For simulating flow rather than measuring it, use a CFD skill instead.

Quick Start

Install OpenPIV:

uv pip install openpiv

# Pin it when the analysis needs to be reproducible -- this is the version every
# snippet below was checked against.
uv pip install "openpiv==0.25.4"

Run PIV analysis on an image pair:

import numpy as np
from openpiv import tools, pyprocess, validation, filters, scaling

frame_a = tools.imread("image_a.bmp")
frame_b = tools.imread("image_b.bmp")

# Cross-correlate. Returns (u, v, s2n) whenever sig2noise_method is not None.
u, v, s2n = pyprocess.extended_search_area_piv(
    frame_a.astype(np.int32),
    frame_b.astype(np.int32),
    window_size=32,
    overlap=12,
    dt=0.02,
    search_area_size=38,
    correlation_method="linear",   # required for search_area_size > window_size
    sig2noise_method="peak2peak",
)

x, y = pyprocess.get_coordinates(
    image_size=frame_a.shape,
    search_area_size=38,
    overlap=12,
)

# flags is a boolean array: True marks a spurious vector.
flags = validation.sig2noise_val(s2n, threshold=1.05)
u, v = filters.replace_outliers(u, v, flags, method="localmean", max_iter=3, kernel_size=2)

# Scale to physical units, then flip to image coordinates for plotting.
x, y, u, v = scaling.uniform(x, y, u, v, scaling_factor=96.52)
x, y, u, v = tools.transform_coordinates(x, y, u, v)

tools.save("vectors.txt", x, y, u, v, flags)

Or use the bundled CLI, which wraps exactly that pipeline:

python skills/openpiv/scripts/runner.py \
    --image frame_a.bmp --image frame_b.bmp --output_dir results --verbose

Core Concepts

PIV Fundamentals

Particle Image Velocimetry is an optical method for measuring fluid velocity by tracking illuminated tracer particles between two images.

Process flow:

  1. Capture an image pair (frame_a, frame_b) separated by a known time dt.
  2. Divide the images into interrogation windows.
  3. Cross-correlate matching windows to find peak displacement.
  4. Validate vectors (signal-to-noise, global range, local median).
  5. Replace spurious vectors with interpolated values.
  6. Scale pixel displacements to physical units.
Interrogation Window Parameters

window_size — correlation window in pixels (typically 16–128). Larger windows give better correlation but coarser spatial resolution.

overlap — pixels shared between adjacent windows (typically 50–75% of window_size). Higher overlap raises vector density and cost, but adjacent vectors become correlated rather than independent.

search_area_size — the window searched in the second frame. Must be ≥ window_size; a few pixels larger accommodates larger displacements. Pair an extended search area with correlation_method="linear" — the default "circular" relies on FFT wrap-around and aliases large displacements into small ones. See references/advanced_algorithms.md.

Rules of thumb: keep the largest displacement under about a quarter of window_size, and aim for 5–10 particles per window.

Signal-to-Noise Ratio

s2n measures how distinct the correlation peak is. sig2noise_method controls how it is computed — "peak2mean" (the function default) or "peak2peak". The two are on different scales, so a threshold tuned for one is meaningless for the other. Typical peak2peak thresholds are 1.05–1.3.

flags = validation.sig2noise_val(s2n, threshold=1.05)
# flags is bool: True == spurious. `~flags` selects the good vectors.

Common Operations

Dynamic Masking

Masking lives in openpiv.preprocess, not in an openpiv.masking module. It returns an (image, mask) tuple and expects a float image.

from openpiv import preprocess

# method="edges" for dark, sharp-edged objects; "intensity" for high-contrast objects.
frame_a_masked, mask_a = preprocess.dynamic_masking(
    frame_a.astype(np.float64), method="intensity", filter_size=7, threshold=0.005
)
frame_b_masked, mask_b = preprocess.dynamic_masking(
    frame_b.astype(np.float64), method="intensity", filter_size=7, threshold=0.005
)

Feed the returned image into the correlation step — it already has the masked region zeroed. Do not multiply the original frame by mask: masking is already applied, and for method="edges" the mask comes back as uint8 0/255 rather than boolean, so multiplying rescales the image by 255.

Multi-Pass Processing

Multi-pass (window deformation) lives in openpiv.windef, driven by a PIVSettings dataclass. pyprocess has no multi-pass entry point.

import numpy as np
from openpiv import scaling, windef

settings = windef.PIVSettings()
settings.windowsizes = (64, 32, 16)   # one entry per pass, decreasing (this is also the default)
settings.overlap = (32, 16, 8)        # same length as windowsizes
settings.num_iterations = 3           # number of passes to actually run
settings.sig2noise_threshold = 1.05

x, y, u, v, flags = windef.simple_multipass(
    frame_a.astype(np.int32), frame_b.astype(np.int32), settings
)

# Output is in PIXELS PER FRAME -- convert yourself. scaling.uniform only divides
# by scaling_factor, so apply dt separately.
dt = 0.02
x, y, u, v = scaling.uniform(x, y, u, v, scaling_factor=96.52)
u, v = u / dt, v / dt

simple_multipass already validates, replaces outliers, fills remaining NaNs with zeros, and calls transform_coordinates — do not repeat those steps.

Units trap: PIVSettings has dt and scaling_factor fields, but windef never uses either — first_pass calls extended_search_area_piv without dt, so the whole multi-pass chain works in pixels per frame. Setting settings.dt = 0.02 changes nothing about the returned values. Convert after the fact, as above.

For control over individual passes, windef.first_pass and windef.multipass_img_deform are the lower-level building blocks.

Validation and Post-Processing

Validation Methods

Every validator returns a boolean array where True marks a spurious vector.

# Signal-to-noise
flags = validation.sig2noise_val(s2n, threshold=1.05)

# Global range -- takes (min, max) TUPLES, positionally or as u_thresholds/v_thresholds.
flags = validation.global_val(u, v, (-300, 300), (-300, 300))

# Local median -- u_threshold and v_threshold are REQUIRED; size is the neighbourhood half-width.
flags = validation.local_median_val(u, v, u_threshold=30.0, v_threshold=30.0, size=1)

# Combine with boolean OR (not np.maximum -- these are bool arrays).
flags = (
    validation.sig2noise_val(s2n, threshold=1.05)
    | validation.global_val(u, v, (-300, 300), (-300, 300))
    | validation.local_median_val(u, v, u_threshold=30.0, v_threshold=30.0)
)

Set these thresholds in the units of u and v, not in pixels per frame. extended_search_area_piv divides by dt, so with dt=0.02 a 3 px/frame displacement arrives as 150 px/s. The thresholds above suit that case; the (-30, 30) figure that PIV literature and PIVSettings.min_max_u_disp use is a px/frame limit, and applying it to px/s output rejects the entire field. Either validate before scaling, or scale the thresholds by 1/dt too.

Outlier Replacement
u, v = filters.replace_outliers(
    u, v, flags, method="localmean", max_iter=3, tol=1e-3, kernel_size=2
)

method accepts "localmean", "disk", or "distance" — and only those three. An unrecognized name is not rejected; it falls through to an all-zero kernel and silently returns a useless field. Note that replacement fills the flagged positions with interpolated values — if you then overwrite them with NaN, the replacement was wasted. Choose one or the other:

# Keep flagged vectors out of the analysis entirely, instead of interpolating them.
u = np.where(flags, np.nan, u)
v = np.where(flags, np.nan, v)
Smoothing

Smoothing is openpiv.smoothn.smoothn; there is no openpiv.smooth module. It returns a tuple whose first element is the smoothed field, and it does not accept NaN input.

from openpiv.smoothn import smoothn

u_smooth, *_ = smoothn(np.nan_to_num(u), s=0.5)  # s: larger == smoother
v_smooth, *_ = smoothn(np.nan_to_num(v), s=0.5)
u_smooth = np.asarray(u_smooth)

Visualization

Vector Field Plotting

display_vector_field reads a saved vectors file and calls plt.show() internally, so select a non-interactive backend for batch runs.

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from openpiv import tools

fig, ax = plt.subplots(figsize=(8, 8))
tools.display_vector_field(
    "vectors.txt",
    ax=ax,
    scaling_factor=96.52,   # same factor used in scaling.uniform, to map back onto the image
    scale=50,
    width=0.0035,
    on_img=True,
    image_name="frame_a.bmp",
)
fig.savefig("vector_field.png", dpi=150, bbox_inches="tight")
plt.close(fig)
Custom Visualization
import numpy as np
import matplotlib.pyplot as plt

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

mag = np.sqrt(u**2 + v**2)
for ax, field, title, cmap in [
    (axes[0], mag, "Velocity Magnitude", "viridis"),
    (axes[1], u, "U Velocity", "RdBu_r"),
    (axes[2], v, "V Velocity", "RdBu_r"),
]:
    im = ax.imshow(field, cmap=cmap)
    ax.set_title(title)
    plt.colorbar(im, ax=ax)

fig.tight_layout()
fig.savefig("velocity_components.png")
plt.close(fig)

Analysis Functions

scripts/analyze.py bundles these against a params.npz written by runner.py. It infers the physical grid spacing from the saved coordinates, so the derivatives come out per unit length:

import sys
sys.path.insert(0, "skills/openpiv/scripts")
from analyze import PIVAnalyzer

piv = PIVAnalyzer("results/params.npz")
vorticity = piv.compute_vorticity()          # dv/dx - du/dy
exx, eyy, exy = piv.compute_strain()
stats = piv.compute_statistics()             # u_mean, v_mean, rms_u, rms_v, tke
piv.plot_vector_field(save_path="quiver.png")

The standalone forms, if you would rather compute them inline:

Vorticity
def compute_vorticity(u, v, dx=1.0, dy=None):
    """Out-of-plane vorticity dv/dx - du/dy. Pass the physical grid spacing, not 1.0."""
    dy = dx if dy is None else dy
    return np.gradient(v, dx, axis=1) - np.gradient(u, dy, axis=0)

The grid spacing is (window_size - overlap) / scaling_factor in physical units, so leaving dx=1.0 yields vorticity per grid cell, not per unit length.

Sign convention: runner.py ends with transform_coordinates, which relabels the grid into a right-handed y-up frame but leaves the rows in image order, so the saved y decreases as the row index grows. The standalone forms above assume the opposite, so on a params.npz field they return -du/dy and flip the sign of the vorticity and the shear strain — negate the axis=0 derivatives, or use PIVAnalyzer, which reads the orientation off the saved coordinates.

Strain Rate
def compute_strain(u, v, dx=1.0, dy=None):
    """Return (exx, eyy, exy) of the 2D strain-rate tensor."""
    dy = dx if dy is None else dy
    du_dx = np.gradient(u, dx, axis=1)
    du_dy = np.gradient(u, dy, axis=0)
    dv_dx = np.gradient(v, dx, axis=1)
    dv_dy = np.gradient(v, dy, axis=0)
    return du_dx, dv_dy, 0.5 * (du_dy + dv_dx)
Turbulence Statistics
def compute_statistics(u, v):
    """Single-frame spatial statistics. NOT Reynolds decomposition."""
    u_prime = u - np.nanmean(u)
    v_prime = v - np.nanmean(v)
    rms_u, rms_v = np.nanstd(u_prime), np.nanstd(v_prime)
    return {
        "u_mean": np.nanmean(u),
        "v_mean": np.nanmean(v),
        "rms_u": rms_u,
        "rms_v": rms_v,
        "tke": 0.5 * (rms_u**2 + rms_v**2),
    }

Caveat: subtracting the spatial mean of one frame measures spatial variance, which equals turbulent intensity only for a homogeneous field. Genuine Reynolds decomposition needs an ensemble of image pairs: average over the time axis, then subtract that mean field from each realization.

CLI Usage

# Basic run
python skills/openpiv/scripts/runner.py \
    --image img1.bmp --image img2.bmp --output_dir results --verbose

# Tuned parameters with dynamic masking
python skills/openpiv/scripts/runner.py \
    --image frame_a.bmp \
    --image frame_b.bmp \
    --output_dir results \
    --window_size 32 \
    --overlap 12 \
    --search_area 38 \
    --dt 0.02 \
    --scaling 96.52 \
    --threshold 1.05 \
    --mask dynamic \
    --mask_method intensity \
    --verbose
CLI Options
Option Default Description
--image required Image file; specify exactly twice for the pair
--output_dir results Output directory (created if absent)
--window_size 32 Interrogation window size (px)
--overlap 12 Window overlap (px)
--search_area 38 Search area size (px), must be ≥ --window_size
--dt 0.02 Time between frames (s)
--scaling 96.52 Scaling factor, pixels per physical unit (e.g. px/mm)
--threshold 1.05 peak2peak signal-to-noise threshold
--mask none none or dynamic (openpiv.preprocess.dynamic_masking)
--mask_method intensity edges or intensity, used only with --mask dynamic
--drop_invalid off NaN out flagged vectors instead of keeping interpolated values
--verbose off Print progress messages

Verify an install end to end against OpenPIV's own bundled image pair:

python skills/openpiv/scripts/run_example.py --output_dir /tmp/openpiv-demo

Output Files

  • vectors.txt — tab-delimited, %.4e formatted, with a # x y u v flags mask comment header
  • params.npz — NumPy archive with x, y, u, v, flags arrays
  • vector_field.png — vector field drawn over the first frame
# x	y	u	v	flags	mask
2.1757e-01	3.5226e+00	-6.2220e-02	-2.7081e+00	0.0000e+00	0.0000e+00
4.8695e-01	3.5226e+00	-3.1587e-01	-2.9800e+00	0.0000e+00	0.0000e+00

flags is written as a float, 0 for a valid vector and 1 for a flagged one.

Best Practices

Parameter Selection
  1. Window size — 32×32 suits most cases. 64/128 for better correlation at coarser resolution; 16/24 for finer resolution at the cost of noise.
  2. Overlap — 50–75% of window size.
  3. Threshold — raise it to reject more vectors; always re-tune after switching sig2noise_method.
  4. Scaling factor — calibrate against a known reference such as a calibration grid, and keep the units straight (96.52 in OpenPIV's test1 tutorial data is px/mm).
Image Quality
  • Particles visible and evenly distributed, 5–10 per interrogation window
  • No saturated or overexposed regions
  • Minimal background noise; consider background subtraction across a run
Processing Tips
  1. Start from the defaults, then tune against the vector field you get.
  2. Inspect the s2n distribution — a low median means poor correlation, not a bad threshold.
  3. Visualize early; obvious problems (uniform vectors, edge artifacts) show up immediately.
  4. Use multi-pass (windef) for flows with large velocity gradients or displacements.
  5. Mask reflections and solid boundaries rather than letting them generate vectors.

Resources

references/
  • advanced_algorithms.md — correlation and subpixel methods, multi-pass window deformation, PIVSettings fields, 3D and phase-separation modules

Load the reference when detailed algorithm or settings information is needed.

1---
2name: openpiv
3description: Particle Image Velocimetry (PIV) analysis with OpenPIV. Use when extracting velocity fields from PIV image pairs, analyzing fluid dynamics or flow visualization experiments, cross-correlating interrogation windows, validating and replacing spurious PIV vectors, or computing vorticity, strain rate, and turbulence statistics from measured velocity fields.
4license: BSD-3-Clause
5compatibility: Requires Python 3.10+ with openpiv installed (uv pip install openpiv). numpy, scipy, scikit-image, and matplotlib arrive as dependencies. No network access needed after install.
6allowed-tools: Read Write Edit Bash
7metadata:
8 version: "1.1"
9 skill-author: OpenPIV Team
10 tested-against: "openpiv 0.25.4"
11---
12 
13# OpenPIV
14 
15## Overview
16 
17OpenPIV (Open Particle Image Velocimetry) analyzes fluid flow from PIV image pairs. It covers
18preprocessing, cross-correlation, vector validation, outlier replacement, smoothing, and scaling to
19physical units.
20 
21Everything below is verified against **openpiv 0.25.4**. The API moves between releases — check
22`inspect.signature()` before trusting a snippet against a different version.
23 
24## When to use
25 
26Use this skill when working with experimental PIV or flow-visualization image pairs: measuring 2D
27velocity fields, tuning interrogation-window parameters, validating vectors, or deriving vorticity,
28strain rate, and turbulence statistics. For *simulating* flow rather than measuring it, use a CFD
29skill instead.
30 
31## Quick Start
32 
33Install OpenPIV:
34 
35```bash
36uv pip install openpiv
37 
38# Pin it when the analysis needs to be reproducible -- this is the version every
39# snippet below was checked against.
40uv pip install "openpiv==0.25.4"
41```
42 
43Run PIV analysis on an image pair:
44 
45```python
46import numpy as np
47from openpiv import tools, pyprocess, validation, filters, scaling
48 
49frame_a = tools.imread("image_a.bmp")
50frame_b = tools.imread("image_b.bmp")
51 
52# Cross-correlate. Returns (u, v, s2n) whenever sig2noise_method is not None.
53u, v, s2n = pyprocess.extended_search_area_piv(
54 frame_a.astype(np.int32),
55 frame_b.astype(np.int32),
56 window_size=32,
57 overlap=12,
58 dt=0.02,
59 search_area_size=38,
60 correlation_method="linear", # required for search_area_size > window_size
61 sig2noise_method="peak2peak",
62)
63 
64x, y = pyprocess.get_coordinates(
65 image_size=frame_a.shape,
66 search_area_size=38,
67 overlap=12,
68)
69 
70# flags is a boolean array: True marks a spurious vector.
71flags = validation.sig2noise_val(s2n, threshold=1.05)
72u, v = filters.replace_outliers(u, v, flags, method="localmean", max_iter=3, kernel_size=2)
73 
74# Scale to physical units, then flip to image coordinates for plotting.
75x, y, u, v = scaling.uniform(x, y, u, v, scaling_factor=96.52)
76x, y, u, v = tools.transform_coordinates(x, y, u, v)
77 
78tools.save("vectors.txt", x, y, u, v, flags)
79```
80 
81Or use the bundled CLI, which wraps exactly that pipeline:
82 
83```bash
84python skills/openpiv/scripts/runner.py \
85 --image frame_a.bmp --image frame_b.bmp --output_dir results --verbose
86```
87 
88## Core Concepts
89 
90### PIV Fundamentals
91 
92Particle Image Velocimetry is an optical method for measuring fluid velocity by tracking illuminated
93tracer particles between two images.
94 
95**Process flow:**
96 
971. Capture an image pair (`frame_a`, `frame_b`) separated by a known time `dt`.
982. Divide the images into interrogation windows.
993. Cross-correlate matching windows to find peak displacement.
1004. Validate vectors (signal-to-noise, global range, local median).
1015. Replace spurious vectors with interpolated values.
1026. Scale pixel displacements to physical units.
103 
104### Interrogation Window Parameters
105 
106**`window_size`** — correlation window in pixels (typically 16–128). Larger windows give better
107correlation but coarser spatial resolution.
108 
109**`overlap`** — pixels shared between adjacent windows (typically 50–75% of `window_size`). Higher
110overlap raises vector density and cost, but adjacent vectors become correlated rather than
111independent.
112 
113**`search_area_size`** — the window searched in the second frame. Must be ≥ `window_size`; a few
114pixels larger accommodates larger displacements. Pair an extended search area with
115`correlation_method="linear"` — the default `"circular"` relies on FFT wrap-around and aliases large
116displacements into small ones. See `references/advanced_algorithms.md`.
117 
118Rules of thumb: keep the largest displacement under about a quarter of `window_size`, and aim for
1195–10 particles per window.
120 
121### Signal-to-Noise Ratio
122 
123`s2n` measures how distinct the correlation peak is. `sig2noise_method` controls how it is computed —
124`"peak2mean"` (the function default) or `"peak2peak"`. **The two are on different scales**, so a
125threshold tuned for one is meaningless for the other. Typical `peak2peak` thresholds are 1.05–1.3.
126 
127```python
128flags = validation.sig2noise_val(s2n, threshold=1.05)
129# flags is bool: True == spurious. `~flags` selects the good vectors.
130```
131 
132## Common Operations
133 
134### Dynamic Masking
135 
136Masking lives in `openpiv.preprocess`, **not** in an `openpiv.masking` module. It returns an
137`(image, mask)` tuple and expects a float image.
138 
139```python
140from openpiv import preprocess
141 
142# method="edges" for dark, sharp-edged objects; "intensity" for high-contrast objects.
143frame_a_masked, mask_a = preprocess.dynamic_masking(
144 frame_a.astype(np.float64), method="intensity", filter_size=7, threshold=0.005
145)
146frame_b_masked, mask_b = preprocess.dynamic_masking(
147 frame_b.astype(np.float64), method="intensity", filter_size=7, threshold=0.005
148)
149```
150 
151Feed the **returned image** into the correlation step — it already has the masked region zeroed. Do
152not multiply the original frame by `mask`: masking is already applied, and for `method="edges"` the
153mask comes back as `uint8` 0/255 rather than boolean, so multiplying rescales the image by 255.
154 
155### Multi-Pass Processing
156 
157Multi-pass (window deformation) lives in `openpiv.windef`, driven by a `PIVSettings` dataclass.
158`pyprocess` has no multi-pass entry point.
159 
160```python
161import numpy as np
162from openpiv import scaling, windef
163 
164settings = windef.PIVSettings()
165settings.windowsizes = (64, 32, 16) # one entry per pass, decreasing (this is also the default)
166settings.overlap = (32, 16, 8) # same length as windowsizes
167settings.num_iterations = 3 # number of passes to actually run
168settings.sig2noise_threshold = 1.05
169 
170x, y, u, v, flags = windef.simple_multipass(
171 frame_a.astype(np.int32), frame_b.astype(np.int32), settings
172)
173 
174# Output is in PIXELS PER FRAME -- convert yourself. scaling.uniform only divides
175# by scaling_factor, so apply dt separately.
176dt = 0.02
177x, y, u, v = scaling.uniform(x, y, u, v, scaling_factor=96.52)
178u, v = u / dt, v / dt
179```
180 
181`simple_multipass` already validates, replaces outliers, fills remaining NaNs with zeros, and calls
182`transform_coordinates` — do not repeat those steps.
183 
184**Units trap:** `PIVSettings` has `dt` and `scaling_factor` fields, but `windef` never uses either —
185`first_pass` calls `extended_search_area_piv` without `dt`, so the whole multi-pass chain works in
186pixels per frame. Setting `settings.dt = 0.02` changes nothing about the returned values. Convert
187after the fact, as above.
188 
189For control over individual passes, `windef.first_pass` and `windef.multipass_img_deform` are the
190lower-level building blocks.
191 
192## Validation and Post-Processing
193 
194### Validation Methods
195 
196Every validator returns a boolean array where **True marks a spurious vector**.
197 
198```python
199# Signal-to-noise
200flags = validation.sig2noise_val(s2n, threshold=1.05)
201 
202# Global range -- takes (min, max) TUPLES, positionally or as u_thresholds/v_thresholds.
203flags = validation.global_val(u, v, (-300, 300), (-300, 300))
204 
205# Local median -- u_threshold and v_threshold are REQUIRED; size is the neighbourhood half-width.
206flags = validation.local_median_val(u, v, u_threshold=30.0, v_threshold=30.0, size=1)
207 
208# Combine with boolean OR (not np.maximum -- these are bool arrays).
209flags = (
210 validation.sig2noise_val(s2n, threshold=1.05)
211 | validation.global_val(u, v, (-300, 300), (-300, 300))
212 | validation.local_median_val(u, v, u_threshold=30.0, v_threshold=30.0)
213)
214```
215 
216**Set these thresholds in the units of `u` and `v`, not in pixels per frame.**
217`extended_search_area_piv` divides by `dt`, so with `dt=0.02` a 3 px/frame displacement arrives as
218150 px/s. The thresholds above suit that case; the `(-30, 30)` figure that PIV literature and
219`PIVSettings.min_max_u_disp` use is a px/frame limit, and applying it to px/s output rejects the
220entire field. Either validate before scaling, or scale the thresholds by `1/dt` too.
221 
222### Outlier Replacement
223 
224```python
225u, v = filters.replace_outliers(
226 u, v, flags, method="localmean", max_iter=3, tol=1e-3, kernel_size=2
227)
228```
229 
230`method` accepts `"localmean"`, `"disk"`, or `"distance"` — and only those three. An unrecognized
231name is not rejected; it falls through to an all-zero kernel and silently returns a useless field.
232Note that replacement *fills* the flagged
233positions with interpolated values — if you then overwrite them with NaN, the replacement was
234wasted. Choose one or the other:
235 
236```python
237# Keep flagged vectors out of the analysis entirely, instead of interpolating them.
238u = np.where(flags, np.nan, u)
239v = np.where(flags, np.nan, v)
240```
241 
242### Smoothing
243 
244Smoothing is `openpiv.smoothn.smoothn`; there is no `openpiv.smooth` module. It returns a tuple
245whose first element is the smoothed field, and it does not accept NaN input.
246 
247```python
248from openpiv.smoothn import smoothn
249 
250u_smooth, *_ = smoothn(np.nan_to_num(u), s=0.5) # s: larger == smoother
251v_smooth, *_ = smoothn(np.nan_to_num(v), s=0.5)
252u_smooth = np.asarray(u_smooth)
253```
254 
255## Visualization
256 
257### Vector Field Plotting
258 
259`display_vector_field` reads a saved vectors file and calls `plt.show()` internally, so select a
260non-interactive backend for batch runs.
261 
262```python
263import matplotlib
264matplotlib.use("Agg")
265import matplotlib.pyplot as plt
266from openpiv import tools
267 
268fig, ax = plt.subplots(figsize=(8, 8))
269tools.display_vector_field(
270 "vectors.txt",
271 ax=ax,
272 scaling_factor=96.52, # same factor used in scaling.uniform, to map back onto the image
273 scale=50,
274 width=0.0035,
275 on_img=True,
276 image_name="frame_a.bmp",
277)
278fig.savefig("vector_field.png", dpi=150, bbox_inches="tight")
279plt.close(fig)
280```
281 
282### Custom Visualization
283 
284```python
285import numpy as np
286import matplotlib.pyplot as plt
287 
288fig, axes = plt.subplots(1, 3, figsize=(15, 5))
289 
290mag = np.sqrt(u**2 + v**2)
291for ax, field, title, cmap in [
292 (axes[0], mag, "Velocity Magnitude", "viridis"),
293 (axes[1], u, "U Velocity", "RdBu_r"),
294 (axes[2], v, "V Velocity", "RdBu_r"),
295]:
296 im = ax.imshow(field, cmap=cmap)
297 ax.set_title(title)
298 plt.colorbar(im, ax=ax)
299 
300fig.tight_layout()
301fig.savefig("velocity_components.png")
302plt.close(fig)
303```
304 
305## Analysis Functions
306 
307`scripts/analyze.py` bundles these against a `params.npz` written by `runner.py`. It infers the
308physical grid spacing from the saved coordinates, so the derivatives come out per unit length:
309 
310```python
311import sys
312sys.path.insert(0, "skills/openpiv/scripts")
313from analyze import PIVAnalyzer
314 
315piv = PIVAnalyzer("results/params.npz")
316vorticity = piv.compute_vorticity() # dv/dx - du/dy
317exx, eyy, exy = piv.compute_strain()
318stats = piv.compute_statistics() # u_mean, v_mean, rms_u, rms_v, tke
319piv.plot_vector_field(save_path="quiver.png")
320```
321 
322The standalone forms, if you would rather compute them inline:
323 
324### Vorticity
325 
326```python
327def compute_vorticity(u, v, dx=1.0, dy=None):
328 """Out-of-plane vorticity dv/dx - du/dy. Pass the physical grid spacing, not 1.0."""
329 dy = dx if dy is None else dy
330 return np.gradient(v, dx, axis=1) - np.gradient(u, dy, axis=0)
331```
332 
333The grid spacing is `(window_size - overlap) / scaling_factor` in physical units, so leaving `dx=1.0`
334yields vorticity per grid cell, not per unit length.
335 
336**Sign convention:** `runner.py` ends with `transform_coordinates`, which relabels the grid into a
337right-handed y-up frame but leaves the rows in image order, so the saved `y` *decreases* as the row
338index grows. The standalone forms above assume the opposite, so on a `params.npz` field they return
339`-du/dy` and flip the sign of the vorticity and the shear strain — negate the `axis=0` derivatives, or
340use `PIVAnalyzer`, which reads the orientation off the saved coordinates.
341 
342### Strain Rate
343 
344```python
345def compute_strain(u, v, dx=1.0, dy=None):
346 """Return (exx, eyy, exy) of the 2D strain-rate tensor."""
347 dy = dx if dy is None else dy
348 du_dx = np.gradient(u, dx, axis=1)
349 du_dy = np.gradient(u, dy, axis=0)
350 dv_dx = np.gradient(v, dx, axis=1)
351 dv_dy = np.gradient(v, dy, axis=0)
352 return du_dx, dv_dy, 0.5 * (du_dy + dv_dx)
353```
354 
355### Turbulence Statistics
356 
357```python
358def compute_statistics(u, v):
359 """Single-frame spatial statistics. NOT Reynolds decomposition."""
360 u_prime = u - np.nanmean(u)
361 v_prime = v - np.nanmean(v)
362 rms_u, rms_v = np.nanstd(u_prime), np.nanstd(v_prime)
363 return {
364 "u_mean": np.nanmean(u),
365 "v_mean": np.nanmean(v),
366 "rms_u": rms_u,
367 "rms_v": rms_v,
368 "tke": 0.5 * (rms_u**2 + rms_v**2),
369 }
370```
371 
372**Caveat:** subtracting the *spatial* mean of one frame measures spatial variance, which equals
373turbulent intensity only for a homogeneous field. Genuine Reynolds decomposition needs an ensemble of
374image pairs: average over the time axis, then subtract that mean field from each realization.
375 
376## CLI Usage
377 
378```bash
379# Basic run
380python skills/openpiv/scripts/runner.py \
381 --image img1.bmp --image img2.bmp --output_dir results --verbose
382 
383# Tuned parameters with dynamic masking
384python skills/openpiv/scripts/runner.py \
385 --image frame_a.bmp \
386 --image frame_b.bmp \
387 --output_dir results \
388 --window_size 32 \
389 --overlap 12 \
390 --search_area 38 \
391 --dt 0.02 \
392 --scaling 96.52 \
393 --threshold 1.05 \
394 --mask dynamic \
395 --mask_method intensity \
396 --verbose
397```
398 
399### CLI Options
400 
401| Option | Default | Description |
402|--------|---------|-------------|
403| `--image` | required | Image file; specify exactly twice for the pair |
404| `--output_dir` | `results` | Output directory (created if absent) |
405| `--window_size` | 32 | Interrogation window size (px) |
406| `--overlap` | 12 | Window overlap (px) |
407| `--search_area` | 38 | Search area size (px), must be ≥ `--window_size` |
408| `--dt` | 0.02 | Time between frames (s) |
409| `--scaling` | 96.52 | Scaling factor, pixels per physical unit (e.g. px/mm) |
410| `--threshold` | 1.05 | `peak2peak` signal-to-noise threshold |
411| `--mask` | `none` | `none` or `dynamic` (`openpiv.preprocess.dynamic_masking`) |
412| `--mask_method` | `intensity` | `edges` or `intensity`, used only with `--mask dynamic` |
413| `--drop_invalid` | off | NaN out flagged vectors instead of keeping interpolated values |
414| `--verbose` | off | Print progress messages |
415 
416Verify an install end to end against OpenPIV's own bundled image pair:
417 
418```bash
419python skills/openpiv/scripts/run_example.py --output_dir /tmp/openpiv-demo
420```
421 
422## Output Files
423 
424- **vectors.txt** — tab-delimited, `%.4e` formatted, with a `# x y u v flags mask` comment header
425- **params.npz** — NumPy archive with `x`, `y`, `u`, `v`, `flags` arrays
426- **vector_field.png** — vector field drawn over the first frame
427 
428```text
429# x y u v flags mask
4302.1757e-01 3.5226e+00 -6.2220e-02 -2.7081e+00 0.0000e+00 0.0000e+00
4314.8695e-01 3.5226e+00 -3.1587e-01 -2.9800e+00 0.0000e+00 0.0000e+00
432```
433 
434`flags` is written as a float, `0` for a valid vector and `1` for a flagged one.
435 
436## Best Practices
437 
438### Parameter Selection
439 
4401. **Window size** — 32×32 suits most cases. 64/128 for better correlation at coarser resolution;
441 16/24 for finer resolution at the cost of noise.
4422. **Overlap** — 50–75% of window size.
4433. **Threshold** — raise it to reject more vectors; always re-tune after switching
444 `sig2noise_method`.
4454. **Scaling factor** — calibrate against a known reference such as a calibration grid, and keep the
446 units straight (`96.52` in OpenPIV's `test1` tutorial data is px/mm).
447 
448### Image Quality
449 
450- Particles visible and evenly distributed, 5–10 per interrogation window
451- No saturated or overexposed regions
452- Minimal background noise; consider background subtraction across a run
453 
454### Processing Tips
455 
4561. Start from the defaults, then tune against the vector field you get.
4572. Inspect the `s2n` distribution — a low median means poor correlation, not a bad threshold.
4583. Visualize early; obvious problems (uniform vectors, edge artifacts) show up immediately.
4594. Use multi-pass (`windef`) for flows with large velocity gradients or displacements.
4605. Mask reflections and solid boundaries rather than letting them generate vectors.
461 
462## Resources
463 
464### references/
465 
466- `advanced_algorithms.md` — correlation and subpixel methods, multi-pass window deformation,
467 `PIVSettings` fields, 3D and phase-separation modules
468 
469Load the reference when detailed algorithm or settings information is needed.
470 
View all files on GitHub →

Details

Updated
Sep 21, 2026
License
MIT
Stars on the repo
★ 45,977
Checked
Source
K-Dense-AI/scientific-agent-skills ↗
Creator
K-Dense-AI · 168 listings
View creator

Similar skills

See all →
SkillCore Python library for astronomy and astrophysics workflowsAstropyK-Dense-AI/scientific-agent-skills · by K-Dense-AICore Python library for astronomy and astrophysics workflows that need Astropy APIs, including units/quantities, coordinates, FITS I/O, tables, time systems, WCS, and cosmology. Use when implementing or debugging astronomical data analysis code with Astropy.★ 46k GitHub starsSkillGoogle quantum computing frameworkCirq - Quantum Computing with PythonK-Dense-AI/scientific-agent-skills · by K-Dense-AIGoogle quantum computing framework. Use when targeting Google Quantum AI hardware, designing noise-aware circuits, or running quantum characterization experiments. Best for Google hardware, noise modeling, and low-level circuit design. For IBM hardware use qiskit; for quantum ML with autodiff use pennylane; for physics simulations use qutip.★ 46k GitHub starsSkillPlan, configure, inspect, restartFluidsimK-Dense-AI/scientific-agent-skills · by K-Dense-AIPlan, configure, inspect, restart, and analyze bounded FluidSim computational-fluid-dynamics simulations with explicit numerical-validity and HPC safety checks. Use for FluidSim solver selection, parameter review, FFT/MPI setup, output diagnostics, or restart compatibility.★ 46k GitHub starsSkillHardware-agnostic quantum ML framework with automaticPennylaneK-Dense-AI/scientific-agent-skills · by K-Dense-AIHardware-agnostic quantum ML framework with automatic differentiation. Use when training quantum circuits via gradients, building hybrid quantum-classical models, or needing device portability across IBM/Google/Rigetti/IonQ. Best for variational algorithms (VQE, QAOA), quantum neural networks, and integration with PyTorch or JAX. For hardware-specific optimizations use qiskit (IBM) or cirq (Google); for open quantum systems use qutip.★ 46k GitHub starsSkillVersion-aware guidance for PufferLib reinforcement-learningK-Dense-AI/scientific-agent-skills · by K-Dense-AIVersion-aware guidance for PufferLib reinforcement-learning environments, vectorization, policies, PuffeRL training, evaluation, and safe checkpoint review. Use when adapting Gymnasium/PettingZoo environments to published PufferLib 3.0.0 or working with the redesigned native 4.0 source line.★ 46k GitHub starsSkillBuild, simulate, transpileQiskitK-Dense-AI/scientific-agent-skills · by K-Dense-AIBuild, simulate, transpile, and execute quantum circuits with Qiskit and IBM Quantum Runtime. Use for Qiskit 2.x circuits and operators, V2 Sampler or Estimator primitives, target-aware transpilation, local or noisy simulation, IBM QPU execution, Runtime sessions or batches, error mitigation, and Qiskit ecosystem packages.★ 46k GitHub stars

More from K-Dense-AI

SkillHow to use the Adaptyv Bio Foundry API and Python SDKK-Dense-AI/scientific-agent-skills · by K-Dense-AIHow to use the Adaptyv Bio Foundry API and Python SDK for protein experiment design, submission, and results retrieval. Use this skill whenever the user mentions Adaptyv, Foundry API, protein binding assays, protein screening experiments, BLI/SPR assays, thermostability assays, or wants to submit protein sequences for experimental characterization. Also trigger when code imports `adaptyv`, `adaptyv_sdk`, or `FoundryClient`, or references `foundry-api-public.adaptyvbio.com`.★ 46k GitHub starsSkillAeon Time Series Machine LearningK-Dense-AI/scientific-agent-skills · by K-Dense-AIThis skill should be used for time series machine learning tasks including classification, regression, clustering, forecasting, anomaly detection, segmentation, and similarity search. Use when working with temporal data, sequential patterns, or time-indexed observations requiring specialized algorithms beyond standard ML approaches. Particularly suited for univariate and multivariate time series analysis with scikit-learn compatible APIs.★ 46k GitHub starsSkillPlan, execute, and document validation, verificationAnalytical method validationK-Dense-AI/scientific-agent-skills · by K-Dense-AIPlan, execute, and document validation, verification, and transfer of analytical procedures under the governing framework - ICH Q2(R2) and Q14, USP <1220>/<1225>/<1226>, ICH M10 bioanalytical, CLSI EP, or ISO/IEC 17025. Use for HPLC, LC-MS/MS, GC, CE, ICP-MS, dissolution, qNMR, qPCR, NIR, and ligand binding or cell-based assays whenever the question is whether a procedure is fit for its intended purpose. Triggers include "method validation", "analytical method validation", "AMV", "validation protocol", "acceptance criteria", "linearity", "reportable range", "accuracy and precision", "repeatability", "intermediate precision", "recovery", "LOD", "LOQ", "detection limit", "quantitation limit", "specificity", "robustness", "method transfer", "method comparison", "Deming", "Passing-Bablok", "Bland-Altman", "equivalence testing", "OOS investigation", "ICH Q2", "Q2(R2)", "Q14", "USP 1225", "ICH M10", "incurred sample reanalysis", "ISR", "CLSI EP", and any request to show that an assay works.★ 46k GitHub starsSkillData structure for annotated matrices in single-cellAnndataK-Dense-AI/scientific-agent-skills · by K-Dense-AIData structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.★ 46k GitHub starsSkillAutonomously improve a real artifact against an objectiveArbor — Autonomous Optimization via Hypothesis Tree RefinementK-Dense-AI/scientific-agent-skills · by K-Dense-AIAutonomously improve a real artifact (code, training recipe, agent harness, data pipeline, prompt) against an objective and an evaluator, using Hypothesis Tree Refinement (HTR) from the Arbor paper. Use this whenever someone wants to iteratively optimize something over many experiments without overfitting — e.g. "get my model's eval score up", "improve this agent/harness", "tune this pipeline", "beat the baseline on this benchmark", "run a search over approaches and keep the best", "do an MLE-bench / Kaggle-style optimization", or any long-horizon "make this artifact better and don't just memorize the dev set" task. Trigger it even when the user doesn't say "Arbor" or "hypothesis tree" but describes repeated experiment-and-evaluate loops, branching exploration of competing ideas, or worries about a dev/test gap. Runs Claude itself as the coordinator with subagent executors in isolated git worktrees; for the standalone `arbor` CLI tool see references/arbor-upstream.md.★ 46k GitHub starsSkillInfer gene regulatory networks from gene expression dataArboretoK-Dense-AI/scientific-agent-skills · by K-Dense-AIInfer gene regulatory networks (GRNs) from gene expression data using scalable algorithms (GRNBoost2, GENIE3). Use when analyzing transcriptomics data (bulk RNA-seq, single-cell RNA-seq) to identify transcription factor-target gene relationships and regulatory interactions. Supports distributed computation for large-scale datasets.★ 46k GitHub stars