PIV and Flow Field Analysis(PIV 与流场分析)
PIV and Flow Field Analysis - User Manual
Dragonfly Prototype Apps · PIV and Flow Field Analysis...
版本 Version 1.0 · 2026-08-03
第一部分 中文手册
目录
1. 简介
2. 适用场景
3. 安装与启用
4. 运行环境与首次配置
5. 界面说明
6. 使用步骤
7. 参数说明
8. 输出结果
9. 常见问题与故障排除
10. 注意事项与已知限制
11. 参考资料
1. 简介
给两张前后拍摄的示踪粒子图像,算出画面上每一点的流体往哪个方向流、流多快,再由速度场导出涡量、散度、应变率与最大剪切率。做流动实验、微流控、风洞的人用。这是相关法 PIV:把图像切成互相重叠的询问窗口,对每个窗口做归一化 FFT 互相关,取相关峰的位置作为该窗口的位移。窗口逐级细化,默认 64/32 → 32/16 → 16/8;每一遍都用上一遍的场做预测并按对称半程平移变形窗口,只测量剩下的残差,报告值始终等于已施加的位移加残差。
物理单位来自你,而不是来自图像。 Δt(秒)与像素间距(mm)在第 2 步填写,两者缺一,运行与预览按钮就保持禁用——没有它们只有体素位移,没有速度。速度以 mm/s 给出,涡量、散度与应变率以 1/s 给出,环量以 mm²/s 给出。Δt 绝不会被悄悄替你填上:Dragonfly 的 T 间距没有文档说明单位,因此需要你按下「从 T 间距读取」,并读到随之出现的那句提醒。
没有任何输出被叫作「不确定度」。 本版给出的是 SNR(主峰比次峰、峰值比均方根)、有效与实测矢量占比、亚像素拟合残差,以及邻域离散度——每一项都按自己的本名标注。一个站得住脚的逐矢量不确定度需要粒子配对与图像匹配(Sciacchitano 与 Wieneke 的方法),本版没有实现,因此也不用别的量去冒名。SNR 是 SNR。
被替换的矢量是插值,不是测量。 校验剔除后用邻域中值填上的节点带有替换标记,这个标记一路传到 CSV、VTK 与 HTML 报告;任何差分模板只要触及一个被替换、无效或缺失的矢量,该节点的导出场就是 NaN 并被计入丢弃计数,绝不悄悄改用单侧差分。因此报告的头条数字是实测占比而不是有效占比:一个 40% 由插值构成的场,和一个 100% 实测的场是两种结果,尽管两者的「有效率」都是 100%。
峰值锁定是实测的,不是假设的。 每个三点亚像素估计器都对相关峰的形状做了假设,假设不成立时误差会把位移拉向最近的整体素。实测(窗口 32、双体素扫描的去趋势幅值):粒子像斑直径 2 体素时,2×3 点高斯 0.0076 体素、9 点二维高斯 0.0107、抛物线 0.105、质心 0.0745——高斯胜出,因为高斯粒子像斑产生高斯相关峰,此时三点高斯拟合是精确的。而直径降到约 1 体素时,四种估计器全部落在 0.15–0.5 体素,没有一个可用;插件因此在第 2 步给出醒目告警,而不是照算一个看起来正常的场。
2. 适用场景
- 水槽、微流控芯片、风洞里的示踪粒子实验:测出速度场,并给出涡量、散度、最大剪切率与应变率模。
- 判断一次拍摄本身是否合适:粒子像斑直径与小数部分直方图的锁定比,直接回答「时间间隔选得对不对、粒子够不够大」这个问题。
- 把速度场发布回 Dragonfly——u/v/速度模通道、涡量等导出场通道、矢量场箭头或流线 Graph——与同一次扫描的其它数据叠在一起看。
- 把整场结果连同溯源记录导出:长表 CSV、逐遍质量 CSV、VTK 序列(ParaView)与单文件 HTML 报告。
3. 安装与启用
1. 安装 Prototype Apps 完整包(或在 App Store 中勾选本插件)。
2. 本插件默认未启用,请在 App Store 或菜单项管理器中启用。
3. 完全重启 Dragonfly(插件只在启动时被发现)。菜单位置:Prototype Apps ▸ Measurements & Analysis ▸ PIV and Flow Field Analysis…
4. 运行环境与首次配置
无需任何安装或配置。 完全运行在 Dragonfly 自带的 Python 中,使用其已附带的 NumPy 与 SciPy;skimage.exposure.equalize_adapthist(仅 CLAHE)与 pyevtk(仅 VTK 导出)都是延迟导入,不用这两个功能就完全不会碰它们。不联网、不下载、不需要 GPU、不创建虚拟环境。
面板顶部的运行环境一行显示当前 Dragonfly 版本能否读取图像对、能否发布通道、能否把多个图像对写成一个 4-D 时间序列、能否发布矢量场、能否发布 Graph。做不到的操作会直接置灰,并把原因写进日志,而不是让你点下去再报错;即使一样都发布不了,四个导出按钮依然可用。
所有输入都必须是已发布的对象:三个下拉框都用 getAllRepresentableInstances() 列举。每一次 ORSModel 调用都由插件自己调度到 Qt 的界面线程上,因此相关计算、发布与导出都跑在工作线程里,窗口不会假死,也随时可取消。
5. 界面说明
1. 输入
图像对来源三选一:同一通道的两个时间步(默认)、两个通道、同一通道的两个切片。下面依次是通道(帧 A)、通道(帧 B,仅两通道模式可用)、可选的掩膜(ROI / MultiROI / 通道)、图像平面(XY / XZ / YZ,决定行轴、列轴与面外轴)、切片序号与时间步。配对方式三选一:跨帧曝光 (0,1) (2,3) …(默认,每对是一次独立的曝光事件)、连续 (0,1) (1,2) …(会明确告知相邻结果共用帧,因此不能当独立样本平均)、固定参考帧(界面上直接标注「这是 DIC 的模式」:它测的是相对参考帧的总位移,不是相邻帧之间的速度)。底部两行分别给出对象的形状、间距、原点、时间步数、T 间距与该平面的切片数,以及本次将要形成的图像对清单与相关告警。
2. 标定与预处理
dt(秒)在这里填写,或按「从 T 间距读取」;随后总会出现一句提醒:Dragonfly 未说明其 T 间距的单位,请先确认它是秒。像素间距默认从对象读取(按所选平面取行、列两个方向的体素尺寸);勾选「手动指定像素间距(标定靶)」后可分别填写 y、x。像素间距各向异性时会出现提示,并且下面一行永远显示一个换算:h = 像素间距 × 窗口步长——矢量间距是窗口步长乘以像素间距,不是像素间距本身。预处理依次是背景去除(无 / 序列最小值 / 局部最小值)、CLAHE(默认关闭)、高通 sigma、强度截顶、联合归一化与滤波边界处理;每一个数据相关的标量(截顶阈值、归一化范围)都取自合并双帧的统计量,因此预处理对两帧是对称的。最后一组是「估计粒子像斑直径」:由图像自相关拟合得出 d_p,并分别给出 y、x 两个方向的值。预处理只作用于副本,输入对象不会被修改。
3. 参数
最上面是多遍方案表:四行,每行一个复选框加窗口、步长与该遍的窗口变形;默认启用前三行 64/32、32/16、16/8,第四行 8/4 默认关闭。中间是相关计算:亚像素估计器(默认 2×3 点高斯)、窗函数(默认无)、默认窗口变形(默认对称半程平移)、插值阶数(默认 5)、FFT 工作线程数、信噪比排除半径与「遍与遍之间平滑预测场」。再下面是矢量校验:全局上下限、幅值上限、n 倍标准差检验(可改用中位数与 MAD)、归一化中值检验(默认开启,阈值 2.0,epsilon 0.1 体素)、按信噪比剔除,以及「用邻域值替换被剔除的矢量」。最下面是导出场:有限差分方案(默认五点最小二乘),选定后旁边立刻显示按你当前矢量间距算出的噪声放大倍数,网格各向异性时明确说出来;再是小数部分直方图分箱数,以及那条永远显示的符号约定。
4. 预览与质检
在一个图像对的居中裁剪区域(默认 256 体素见方)上跑一遍完整流程,画出矢量图与位移小数部分直方图,并给出一行质检读数:实测 / 内插 / 空缺占比、信噪比中位数、锁定比及其判定(≤1.35 正常、≤2.0 警告、更高为差),以及一个窗口内位移变化的最大值与 3 体素护栏的比较。不发布任何对象,也不会写入正式结果,导出按钮保持禁用。裁剪会改变视场,所以预览只用于检查形态与质量,其数值不可作为报告值。
5. 运行
先复述一遍本次计划(来源、平面、多遍方案、亚像素估计器、差分格式、dt、像素间距、是否用掩膜),再给出代价估计:按实测的「1024×1024 三遍约 1.10 秒每对」按图像对数、遍数与像素数缩放,并算出单遍相关平面的内存峰值——那些平面在每个图像对结束后立即释放,绝不累积。整个运行随时可取消,运行本身同样不发布任何对象:每一次发布都是第 6 步里的一个按钮。
6. 结果与导出
上面是逐图像对表(帧号、遍数、实测 / 内插 / 空缺占比、信噪比中位数、锁定比、窗口内位移变化最大值、是否被取消),下面是所选图像对的导出场表(场、单位、中位数、均方根、绝对最大值),再下面一行给出所用差分格式及其噪声放大倍数、矢量间距 h、第一个矢量的世界坐标与符号约定,然后是全部告警。四个发布按钮:u/v/速度模通道(可同时发布有效性掩膜)、四个导出场通道(标题里带差分格式名)、矢量场(箭头,可按 x、y 抽稀)、流线 Graph(RK4 积分,每个顶点带速度标量)。四个导出按钮:CSV(长表加逐遍表)、VTK(每对一个文件加一个 .pvd 集合)、JSON 溯源、HTML 报告。任何输入或参数的改动都会立即作废上一次结果并禁用全部导出与发布按钮。
分享报告
第七个标签页 分享报告 不是流程步骤,它只把已生成的报告临时发布到互联网,不做任何计算。它通过 Cloudflare 临时快速隧道,直接从本机提供刚生成的报告,并返回一个临时的公网地址;不上传任何数据,也不需要账号,而报告页面本身就带着您的图像数据。该地址不是立刻可用的:隧道建立之后大约还要 40 秒它才开始响应(本机实测:一次为 45 秒,另有两次始终没能解析),所以状态转绿之前不要把链接发出去。分享期间,拿到链接的任何人都能读取报告,没有密码。点击停止分享、关闭窗口或退出 Dragonfly 后,该地址立即失效且永不重发。只有在本机确认地址已可访问、或明确告知无法确认时,才会把链接交给您。
6. 使用步骤
1. 第 1 步选好图像对来源、通道与图像平面,确认底部列出的图像对清单就是你要的。
2. 第 2 步填 dt(秒)与像素间距(mm)——两者缺一,运行按钮不会亮。
3. 第 2 步按「估计粒子像斑直径」。低于 1.5 体素时先回到采集,不要指望后处理救回来。
4. 第 3 步确认多遍方案与校验设置;换差分格式时看一眼旁边的噪声放大倍数。
5. 第 4 步跑预览:矢量图形态是否合理、锁定比是否正常、护栏有没有报警。
6. 第 5 步全幅运行。
7. 第 6 步读实测占比与告警,然后再发布或导出。
7. 参数说明
输入与标定
参数 | 默认值 | 说明 |
图像对来源 | 同一通道的两个时间步 | 另有两个通道、同一通道的两个切片;只有时间步模式能从对象推出 dt |
图像平面 | XY | 另有 XZ、YZ;决定哪两个轴是行与列,哪个轴是面外方向 |
切片序号 | 0 | 越界时自动改用中间切片并写入提示 |
时间步 | 0 | 仅在两个通道 / 两个切片模式下可编辑 |
配对方式 | 跨帧曝光 (0,1) (2,3) … | 连续模式必然告知相邻结果共用帧;固定参考帧是 DIC 的模式,测总位移而非速度 |
参考帧 | 0 | 仅固定参考帧模式可用;越界时回退到范围内第一帧并说明 |
起始帧 / 结束帧 | 0 / -1 | -1 表示最后一帧;两帧相同会被拒绝 |
dt(秒) | 0(必须填写) | 为 0 时预览与运行按钮保持禁用;从 T 间距读取时必然附带单位提醒 |
手动指定像素间距 | 关闭 | 关闭时按所选平面从对象读取;开启后用于标定靶 |
像素间距 y / x(mm) | 1.0 / 1.0 | 仅在开启手动指定后可编辑;各向异性时给出提示 |
背景去除 | 无 | 另有序列最小值(只用这两帧时会吃掉重叠粒子,故给出告警)与局部最小值 |
局部最小值尺寸(体素) | 15 | 强制为不小于 3 的奇数:偶数最小值滤波不居中,会把背景平移半个体素 |
CLAHE | 关闭 | 非线性且按分块进行,开启后必然写入一条告警——两帧局部直方图不同就会使相关峰偏移 |
CLAHE 核尺寸 / 截断限值 | 32 / 0.01 | 分箱数固定为 256 |
高通 sigma(体素) | 0(关闭) | 去掉缓变照明;默认把结果在 0 处截断 |
强度截顶(n 倍标准差) | 0(关闭) | 阈值取自合并双帧的均值与标准差,不是各帧自己的——那正是要避免的不对称 |
按双帧联合范围归一化到 [0, 1] | 开启 | 同一个仿射映射作用于两帧 |
滤波边界处理 | nearest | 另有 wrap、reflect。只有 wrap 能让整幅图像的平移等变性精确成立(周期性幻影用),但它把对边粘在一起,对真实图像是错的 |
相关计算
参数 | 默认值 | 说明 |
多遍方案 | 64/32 → 32/16 → 16/8(第 4 遍 8/4 关闭) | 窗口应逐级变小;后一遍窗口更大时给出告警。窗口下限 8 体素,装不进图像的遍次会被跳过并说明 |
每遍窗口变形 | 使用默认值 | 可逐遍覆盖为无 / 整像素平移 / 对称半程平移 / 仿射 |
亚像素估计器 | 2 × 3 点高斯 | 另有 9 点二维高斯、2 × 3 点抛物线、3 点质心。实测锁定幅度(d_p = 2 体素):0.0076 / 0.0107 / 0.105 / 0.0745 体素 |
窗函数 | 无 | 另有汉宁窗;加窗后会再次去均值,否则相关平面出现宽基座并拉偏峰位 |
默认窗口变形 | 对称半程平移 | 两帧各朝相反方向移动半个预测位移,相关只测残差;仿射再叠加局部位移梯度 |
插值阶数 | 5 | 范围 1..5。自相关对照测得的重采样偏差:阶数 5 为 0.031 体素、阶数 3 为 0.049、阶数 1 为 0.105;代价是多约 28% 运行时间。阶数 5 在噪声超过约 5%(相对粒子峰幅值)后不再同时占优,但偏差优势始终存在,这才是它作为默认值的理由 |
FFT 工作线程数 | -1(全部核心) | |
信噪比排除半径(体素) | 2 | 先排除主峰周围该半径内的圆盘,再取次峰与均方根 |
遍与遍之间平滑预测场 | 开启 | 3 × 3 中值,只作用于送入下一遍的预测场;报告值仍是校验器给出的值 |
矢量校验、导出场与第 4/6 步选项
参数 | 默认值 | 说明 |
全局上下限(u、v) | 关闭(各为 -32 … 32 体素/帧) | 实验本身允许的硬限;非有限值不在此计数,它从未被测到 |
幅值上限 | 关闭(32 体素/帧) | |
n 倍标准差检验 | 关闭(3.0) | PIVlab 的标准差滤波;少数极端矢量会把标准差抬到足以藏住自己,这正是需要中值检验的原因 |
改用中位数与 MAD | 关闭 | 稳健变体,仅在 n 倍标准差检验开启时可用 |
归一化中值检验 | 开启 | Westerweel 与 Scarano 的通用异常检测,两个分量的归一化残差按 hypot 合并 |
阈值 | 2.0 | 作用在合并残差上 |
epsilon(体素) | 0.1 | 邻域完全均匀时中值绝对残差恰为 0;有了 epsilon,判据化为「偏差不超过 阈值 × epsilon = 0.2 体素 就绝不算异常」 |
邻域半宽(节点) | 1 | 要求 8 个邻居全部可用,因此一圈边界节点标为未检验而不是被剔除:邻域只有一半时在任何有梯度的场上都系统性偏置,实测把最小邻居数放宽到 3 会让一个精确线性场的两个角点越过阈值(合并残差 2.8517),也就是用插值换掉两个完好的测量 |
低于信噪比即剔除 | 关闭(1.3) | 用主峰比次峰;SNR 为非有限值时按剔除处理 |
用邻域值替换被剔除的矢量 | 开启 | 同步(Jacobi)填充,最多 8 轮,因此结果与扫描顺序无关;邻居不足的节点留空,替换标记也不置位 |
有限差分方案 | 五点最小二乘 | 另有中心差分、Richardson(四阶)、环量回路;噪声放大倍数分别为 0.447 / 1.000 / 1.344 / 0.612 |
小数部分直方图分箱数 | 21 | 奇数把零放在分箱中心;偶数分箱会把对称的锁定峰劈到相邻两箱,实测 30000 个锁定样本下 20 箱读作 10.03/20,而 21 箱读作 20.63/21 |
预览裁剪尺寸(体素) | 256 | 上限 320,居中裁剪;预览只跑第一个图像对 |
矢量图抽稀 | 1 | 只影响预览与结果页的画法,不影响数值 |
同时发布有效性掩膜 | 开启 | 随速度通道一起发布一个 uint8 掩膜:1 表示该节点有矢量 |
箭头抽稀(x、y) | 1 / 1 | 矢量场的 glyph 采样,按 ORS 的 x、y、z 顺序;任何一项为 0 会被直接拒绝 |
流线种子数 / 步数 | 36 / 400 | 在节点索引空间做 RK4 积分,步长固定为半个节点;线走到无矢量的节点即停止 |
8. 输出结果
输出 | 含义 |
逐图像对表 | 每对的帧号、遍数、实测 / 内插 / 空缺占比、信噪比中位数、锁定比、一个窗口内位移变化的最大值,以及是否被取消 |
导出场表 | 涡量 ω_z、散度、最大剪切率、应变率模、ε̇_xx、ε̇_yy、ε̇_xy,单位均为 1/s,给出中位数、均方根与绝对最大值 |
矢量图 | 按物理长宽比绘制;箭头颜色始终表示真实速度,超过两格的箭头被截断并加第二个箭头头;内插矢量画成虚线加空心方底,无矢量的节点画灰色叉 |
小数部分直方图 | 位移小数部分的分布与锁定比。平坦即无锁定,两端出现峰即锁定 |
通道:u、v、速度模(mm/s) | 多个图像对写成一个 4-D |
通道:有效性掩膜 | uint8,1 表示该节点有矢量。发布无测量值的节点前,先发布这个掩膜 |
通道:涡量 / 散度 / 最大剪切率 / 应变率模(1/s) | 标题里带所用差分格式名。NaN 被保留而不是填 0——边界处 NaN 是正常状态,而 0 涡量是一个可信的测量值 |
矢量场(箭头) | 由 X(图像列)、Y(图像行)、Z(恒为 0,这是二维场)与速度模四个分量通道组成;分量通道默认作为临时对象 |
Graph:流线 | RK4 积分的折线,顶点带 mm/s 速度标量(0 号槽);只针对第 6 步选中的那一个图像对 |
CSV(长表) | 每节点每遍一行,40 列:帧号、遍次、窗口与步长、节点行列与世界坐标、体素位移、mm/s 速度、四个导出场、两个 SNR、相关峰值、亚像素拟合残差、邻域离散度、窗口内位移变化,以及 valid / measured / replaced / outlier / clamped 与标记位。导出场只在最后一遍填写 |
CSV(逐遍表) | 29 列的逐遍质量摘要;实测、内插与空缺三个占比精确构成一个划分,加起来正好是 1 |
VTK | 每个图像对一个 |
JSON 溯源 | 几何(像素间距、矢量间距、原点、源原点、切片、符号约定,并逐项说明哪个是给定的、哪个是推出来的)、多遍方案、相关设置、差分格式及其噪声放大倍数与边界策略、校验设置与逐检验剔除数、预处理、粒子像斑、质量定义与逐对质量、全部参数及其单位、种子说明、软件版本、许可来源与全部告警 |
HTML 报告 | 单文件,四幅图(矢量图、锁定直方图、逐遍有效率堆叠柱、SNR 直方图)全部为内联 SVG,不含任何外部请求——无 CDN、无网络字体、无远程图片、无脚本。自带浅色/深色两套配色 |
9. 常见问题与故障排除
- 「运行」和「运行预览」是灰的 —— 缺三样之一:第 1 步没选输入、第 2 步 dt 仍为 0、或者拿不到像素间距。按钮的提示文字会逐条说明缺哪一样。
- 速度的数值明显不对 —— 先查 dt。「从 T 间距读取」拿到的是 Dragonfly 的原始 T 间距,其单位在 SDK 里没有任何文档,插件原样传递并提醒你确认它是秒。两个切片模式根本不存在 dt,速度尺度完全来自你填的那个数。
- 涡量比另一个工具算出来的大 1.75 倍 —— 典型的「一个间距用在两个方向」:在 h_y/h_x = 2.5 的网格上,两个导数都用 h_x 会让涡量偏大恰好 (1 + h_y/h_x)/2 = 75.0%,而散度仍然精确为 0,场看起来还是一个干净的涡。另一个同样常见的错误是用像素间距代替节点间距,那会把每个导出场放大一个窗口步长(64/32 时是 32 倍)。本插件两处都是对的:矢量间距永远是
窗口步长 × 像素间距,两个方向各用自己的间距。 - 锁定比偏高(大于 2) —— 先看粒子像斑直径。低于 1.5 体素时任何亚像素估计器都会把位移拉向整体素,这是采集问题而不是后处理问题;直径 2 体素以上请用默认的 2×3 点高斯,不要用抛物线(实测差 14 倍)。
- 导出场在边界和空洞周围是空的 —— 这是设计如此。任何差分模板只要伸出网格,或触及一个被替换、无效或缺失的矢量,该节点就是 NaN,并被计入丢弃计数(边界、支撑不良、两者兼有三项精确构成一个划分)。边界退化成单侧差分会在一个二阶或四阶场里塞进一个一阶值,而输出里什么都不会说。
- 最外一圈矢量带着「预测被削减」的标记 —— 在对称半程平移方案里,最外圈窗口本来就无法再平移,所以一个完好的场也会在边界报出这个标记。真正需要警惕的是未施加的那部分超过窗口的四分之一,那会另外置一个超范围标记。被削减时报告的是削减后的位移加残差,绝不会悄悄按原预测记账。
- 提示「一个窗口内位移变化超过 3 体素护栏」 —— 此时一个相关峰已经不能代表整个窗口的运动。缩小窗口、增加一遍,或把窗口变形改成仿射。实测一个 0.20 的剪切在 64 体素窗口上变化 12.8 体素时,拟合梯度偏低 7.5%、散度 3.2 体素、主峰比次峰中位数降到 1.19(温和情形是 6.2)——护栏和 SNR 会同时报警。
- 有效率 100% 但实测占比低得多 —— 差额就是插值。被替换的矢量在「有效」里算数,在「实测」里不算,报告的头条数字是实测占比。插值或空缺超过 25% 时会额外给出一句提醒,请把导出场只当作指示性结果。
- 图像里有 NaN 或 inf —— 这是正常输入(配准填充、取过对数、上一个插件的输出都可能产生)。每个非有限体素都会先被换成 0,告警里写明每一帧各有几个 NaN、几个 +inf、几个 -inf;任何读到被修补体素的窗口都会被标为 nonfinite、位移记为 0 并报为无效。实测:整幅全 NaN 的一对,有效率 0.000、225 个窗口全部带平坦标记;单独一个 NaN 体素只让 225 个窗口里的 9 个失效,有效率 0.960——损伤是局部的而不是致命的。
- 「连续」配对给出的相邻结果看起来相关 —— 它们确实相关:每一个中间帧都被用了两次。插件必然给出这句告警,因为这些结果不能当独立样本去平均。要独立的图像对,请用跨帧曝光。
- 固定参考帧模式的结果不像速度 —— 因为它不是速度。固定参考帧测的是相对参考帧的总位移,也就是 DIC 的模式;随着图案离参考越远,相关本身还会逐渐失配。界面上就这么标注。
- Dragonfly 自己不是已经有光流了吗 —— 有,但那是另一件事。Dragonfly 自带的光流一族(切片配准用到,且在免费 SDK 里)是基于亮度守恒的微分法估计器:没有 Δt、没有物理单位、没有逐矢量 SNR,也不给导出场。本插件是相关法 PIV。要做固体表面的斑点图案位移,请用 muDIC 那个插件;两者方法上有重叠,用途不同。
- VTK 导出报错说需要 pyevtk —— pyevtk 由 Dragonfly 附带且只在导出时延迟导入;若当前解释器导入失败,错误信息会原样给出。CSV、JSON 与 HTML 三个导出与它无关。
- 某个发布按钮是灰的 —— 顶部的运行环境一行说明了原因:没有从数组建通道的工厂就无法发布任何通道;没有
setTSize就无法把多对写成一个 4-D 序列;VectorField缺少fromChannels就无法发布箭头;Graph.setEdges缺失就无法发布流线。导出在任何情况下都可用。 - 发布出来的场位置不对 —— 结果页会把原因原文写出来:矢量间距没能写入(导出场的物理尺度就错了)、半个窗口的原点没能写入(每个矢量都偏了半个窗口加半个节点)、源对象的朝向没能复制(旋转的源会按世界轴发布)。三句话各对应一次失败的写入,而不是一次猜测。
10. 注意事项与已知限制
- 逐矢量不确定度没有实现。 本版给出 SNR(主峰比次峰、峰值比均方根)、有效与实测占比、亚像素拟合残差与邻域离散度,每一项都按本名标注;没有任何一项是不确定度,也没有任何一项被这样标注。基于图像匹配的不确定度需要粒子配对,属于后续版本。
- 这是二维平面内的测量。 发布的矢量场 Z 分量恒为 0;散度不为零意味着有面外运动或噪声,而不是一个可用的第三分量。
- Δt 与像素间距是必填项,不是可选的精修项:没有它们就只有体素位移。插件不会替你从对象里悄悄填上 Δt。
- 预览是一个图像对的居中裁剪,不发布任何对象,也不启用导出;它是形态与质量检查,其数值不可作为报告值。运行同样不发布任何对象。
- 整帧读入内存;相关平面是一遍里最大的一块分配(1024 见方、窗口 32 时约 65 MB),每个图像对结束后立即释放,绝不累积。第 5 步的估计值是量级参考,不是承诺。
- 归一化中值检验默认不检验外面一圈节点:只有一半的邻域在任何有梯度的场上都系统性偏置。因此检出率必须与未检验节点数一并引用——实测在 sigma = 0.10 的算例上,被检验节点的检出率是 100%(假阳性 1.15%),而按全部注入节点计只有 93.3%。
- CLAHE 是唯一对两帧不对称的预处理步骤,开启即写入告警。其余每一步都是逐点运算或平移不变滤波,且所有数据相关的标量都取自合并双帧,因此交换两帧的输出是逐位对称的。
- 滤波边界的平移等变性只在
wrap下于整幅图像上精确成立;默认的nearest在滤波器覆盖范围伸出图像的那一圈上不等变(实测 256 见方、位移 (3, -5)、高通 sigma 3 加 3 倍标准差截顶时,边界处最大差 2.664e-01,向内超过 17 体素后精确为 0)。这是边界规则的性质,不是流水线的缺陷;但两遍之后的矢量误差在nearest下反而更小(边界 0.0778 对 0.0894),所以默认不改。 - 流线只在第 6 步选中的那一个图像对的矢量网格上积分,且在无矢量的节点处停止;它是可视化输出,不是一条迹线的物理轨迹。
- 本插件不发布 ROI 或 MultiROI。掩膜只被读取;掩膜边缘是一条很强的直线特征,跨越它的窗口会相关在边缘上而不是粒子上,插件为此给出告警。
- 本插件不重复别处已有的能力。 固体表面斑点图案的位移测量属于 Digital Image Correlation 2D (muDIC);Dragonfly 自带的光流一族是微分法亮度守恒估计器,没有 Δt、没有物理单位、没有逐矢量 SNR、也没有导出场。本插件是相关法 PIV。
- 所有质量数字都只描述这一次相关计算本身:它们说不出示踪粒子跟随流体的程度、标定靶的精度或者光路的畸变,那些只能靠实验设计。
11. 参考资料
算法来源是 PIVlab(MATLAB,MIT 许可,Copyright (c) 2024 William Thielicke,Thielicke 与 Stamhuis):https://github.com/Shrediquette/PIVlab 。多阶段 FFT 相关与窗口变形、预处理、矢量校验与后处理各量的取舍,参照其 piv_FFTmulti.m、PIVlab_preproc.m、filtervectors.m、PIVlab_postproc.m 的公开方法与已记录行为,用 NumPy/SciPy 重新实现;未复制、移植、翻译或打包任何 .m、.fig、.mat 文件,运行时不需要 MATLAB、MATLAB Runtime 或 Octave。有一处刻意的偏离:PIVlab 把相关平面重标到 0..255,本插件返回真正的相关系数(落在 [-1, 1] 内),因为那是用户唯一能解读的数值。
方法引用(与代码无关):Westerweel 与 Scarano,Universal outlier detection for PIV data,Exp. Fluids 39 (2005) 1096(归一化中值检验);Scarano 2002(窗口变形);Richardson 外推;Raffel、Willert、Scarano、Kähler、Wereley 与 Kompenhans,Particle Image Velocimetry: A Practical Guide;Sciacchitano 与 Wieneke 关于 PIV 不确定度量化的工作,仅用于记录本版刻意未实现它。
运行时调用的库都由 Dragonfly 附带,本插件不随包分发它们:NumPy 1.22.3、SciPy 1.9.3、scikit-image 0.19.3(均为 BSD-3-Clause),以及 VTK 导出用到的 pyevtk 1.5.0(BSD-2-Clause,Copyright 2010 - 2016 Paulo A. Herrera)。matplotlib 与 vtk 都没有被导入:图表全部为手写内联 SVG。
不使用任何 GPL 或 LGPL 代码。 OpenPIV(GPL-3.0)不以任何形式被使用——不导入、不复制、不打包,也刻意不出现在本仓库中;本插件测试套件里承担交叉校验角色的是自己独立写的暴力参考实现(手工求和的 3×3 回路积分、numpy.polyfit 斜率、显式 Richardson 外推与手工求积的回路)。edt(LGPLv3+)与 cc3d(LGPL)虽然装在 Dragonfly 的 Python 里,但从不导入;orix 与 Gmsh 不以任何形式使用。完整声明见插件目录下的 THIRD_PARTY_NOTICES.md。
Part II English Manual
Contents
1. Introduction
2. Use cases
3. Installation & enabling
4. Runtime environment & first-run setup
5. Interface
6. How to use
7. Parameters
8. Output
9. FAQ & troubleshooting
10. Notes & known limitations
11. References
1. Introduction
Given two frames of a tracer-particle experiment, this measures which way the fluid is moving at every point of the image and how fast, then derives vorticity, divergence, strain rate and maximum shear rate from that field. It is for flow experiments, microfluidics and wind tunnels. This is correlation PIV: the image is cut into overlapping interrogation windows, each pair of windows is cross-correlated by a normalised FFT, and the position of the correlation peak is that window's displacement. The window shrinks pass by pass — 64/32 → 32/16 → 16/8 by default — and each pass takes the previous field as a predictor, deforms both windows by half of it and measures only what is left, so a reported displacement is always exactly the applied offset plus the residual.
The physical units come from you, not from the image. Δt in seconds and the pixel pitch in mm are entered in step 2, and without both the Preview and Run buttons stay disabled — without them there is a displacement in voxels and no velocity. Velocity is reported in mm/s, vorticity, divergence and strain rate in 1/s, circulation in mm²/s. Δt is never filled in for you silently: Dragonfly's T spacing has no documented unit, so you press "Read from T spacing" and read the notice that comes with it.
Nothing here is called "uncertainty". What this version reports is SNR (the primary-to-second-peak ratio and the peak-to-RMS ratio), the valid and measured vector fractions, the sub-pixel fit residual and the local neighbourhood dispersion — each under its own name. A defensible per-vector uncertainty needs particle pairing and image matching (the Sciacchitano & Wieneke method); it is not implemented in this version, so no other quantity is dressed up as it. SNR is SNR.
A replaced vector is an interpolation, not a measurement. A node that a validation test rejected and the neighbourhood median filled carries a replaced flag, and that flag travels into the CSV, the VTK file and the HTML report. Any difference stencil touching a replaced, invalid or missing vector yields NaN at that node and is counted — never a silent one-sided difference. That is why the report's headline number is the measured fraction and not the valid fraction: a field that is 40 % interpolated is a different result from one that is 100 % measured, even though both are 100 % "valid".
Peak locking is measured, not assumed. Every three-point sub-pixel estimator assumes a shape for the correlation peak, and where the assumption is wrong the error pulls the estimate onto the nearer whole voxel. Measured (detrended amplitude over a two-voxel sweep, window 32): at a particle-image diameter of 2 voxels the 2 x 3-point Gaussian locks by 0.0076 voxel, the 9-point 2-D Gaussian by 0.0107, the parabola by 0.105 and the centroid by 0.0745 — the Gaussian wins because a Gaussian particle image produces a Gaussian correlation peak, which makes a three-point Gaussian fit exact. At a diameter near 1 voxel all four sit between 0.15 and 0.5 voxel and none of them works; the plugin therefore warns in step 2 instead of computing a field that would look perfectly normal.
2. Use cases
- Tracer-particle experiments in a water channel, a microfluidic chip or a wind tunnel: the velocity field, plus vorticity, divergence, maximum shear rate and strain-rate magnitude.
- Deciding whether the acquisition itself was right: the particle-image diameter and the locking ratio of the fractional-part histogram answer "was the frame separation sensible and are the particles big enough" directly.
- Publishing the field back into Dragonfly — u / v / speed Channels, derived-field Channels, a Vector Field of glyphs or a streamline Graph — so it can be viewed against the rest of the same acquisition.
- Exporting a whole run with its provenance: a long-format CSV, a per-pass quality CSV, a VTK series for ParaView and a self-contained HTML report.
3. Installation & enabling
1. Install the Prototype Apps package (or tick this plugin in the App Store).
2. This plugin is disabled by default — enable it in the App Store or the Menu Item Manager.
3. Restart Dragonfly completely (plugins are discovered at startup only). Menu: Prototype Apps ▸ Measurements & Analysis ▸ PIV and Flow Field Analysis…
4. Runtime environment & first-run setup
Nothing to install or configure. It runs entirely in Dragonfly's own Python using the NumPy and SciPy it already ships; skimage.exposure.equalize_adapthist (CLAHE only) and pyevtk (the VTK export only) are both imported lazily, so neither is touched unless you use that feature. No internet, no download, no GPU, no virtual environment.
The Environment line at the top of the panel reports whether this build can read an image pair, publish a Channel, write several pairs as one 4-D time series, publish a Vector Field and publish a Graph. Anything it cannot do is greyed out with the reason written into the log, rather than left to fail on click — and all four export buttons stay available even when nothing can be published.
Every input must be a published object: all three pickers use getAllRepresentableInstances(). Every ORSModel call is marshalled onto Qt's UI thread by the plugin itself, which is why the correlation, the publishing and the exports all run on a worker thread: the window never freezes and everything is cancellable.
5. Interface
1. Inputs
Three pair sources: two timesteps of one Channel (the default), two Channels, or two slices of one Channel. Then the Channel for frame A, the Channel for frame B (enabled in two-Channel mode only), an optional mask (ROI / MultiROI / Channel), the image plane (XY / XZ / YZ, which fixes the row axis, the column axis and the out-of-plane axis), the slice index and the timestep. Three pairings: frame-straddling (0,1) (2,3) … (the default; each pair is one independent exposure event), consecutive (0,1) (1,2) … (which always warns that neighbouring results share a frame and must not be averaged as independent samples), and fixed reference, labelled in the interface as "this is DIC's mode" because it measures total displacement from the reference frame rather than the velocity between neighbours. Two lines at the bottom give the object's shape, spacing, origin, timestep count, T spacing and the number of slices in that plane, then the list of pairs that will actually be formed plus any warning about them.
2. Calibration & Preprocessing
Δt in seconds is typed here, or read with "Read from T spacing" — after which a notice always appears: Dragonfly does not document the unit of its T spacing, so confirm it is seconds. The pixel pitch is read from the object by default (the voxel size of the row and column axes of the chosen plane); ticking "Override the pixel pitch (calibrated target)" lets you enter y and x yourself. An anisotropic pitch raises a notice, and the line below always shows the conversion h = pixel pitch x window step — the vector spacing is the window step times the pixel pitch, not the pixel pitch itself. The preprocessing is background removal (none / sequence minimum / local minimum), CLAHE (off by default), a high-pass sigma, intensity capping, the joint rescale and the filter border mode; every data-derived scalar (the cap threshold, the rescale range) is computed over the pooled pair, which is what makes the preprocessing symmetric between the two frames. The last group estimates the particle-image diameter d_p from the image autocorrelation and reports it per axis as well. All preprocessing acts on a working copy — the input object is never modified.
3. Parameters
At the top is the pass schedule table: four rows, each a tick box plus the window, the step and that pass's deformation; the first three (64/32, 32/16, 16/8) are on by default and the fourth (8/4) is off. In the middle, the correlation: sub-pixel estimator (2 x 3-point Gaussian by default), window function (none), the default window deformation (symmetric half shift), the interpolation order (5), the FFT worker count, the SNR exclusion radius and "smooth the predictor between passes". Below that, the validation: global limits, a magnitude limit, the n-sigma test (optionally on the median and MAD), the normalised median test — on by default, threshold 2.0, epsilon 0.1 voxel — rejection below an SNR, and "replace rejected vectors from their neighbours". At the bottom, the derived fields: the finite-difference scheme (least-squares 5-point by default), whose noise amplification for the vector spacing you actually have is shown next to the choice and says so explicitly when the grid is anisotropic; then the fractional-part histogram bin count and the sign convention, which is always on screen.
4. Preview & QC
The whole pipeline runs on one pair, on a centred crop (256 voxels square by default), and draws the quiver plot and the histogram of the displacement's fractional part, plus one line of QC: the measured / interpolated / empty fractions, the median SNR, the locking ratio with its verdict (≤ 1.35 ok, ≤ 2.0 warn, above that bad) and the largest displacement variation across one window against the 3-voxel guardrail. Nothing is published, no result is recorded, and the export buttons stay disabled. The crop changes the field of view, so a Preview is a shape and quality check and its numbers must not be reported.
5. Run
The plan is restated first (source, plane, pass schedule, sub-pixel estimator, difference scheme, Δt, pixel pitch, whether a mask is used), then the cost estimate: the measured constant of about 1.10 s per pair for three passes at 1024 x 1024, scaled by pairs, passes and pixels, together with the peak memory of one pass's correlation planes — which are freed after every pair and never accumulated. The run is cancellable throughout, and a Run publishes nothing either: every publish is a button in step 6.
6. Results & Export
The upper table is one row per pair (frame indices, passes, measured / interpolated / empty fractions, median SNR, locking ratio, the largest displacement variation across one window, and whether it was cancelled). Below it, the derived fields of the selected pair (field, unit, median, RMS, largest magnitude), then one line giving the difference scheme with its noise amplification, the vector spacing h, the world position of the first vector and the sign convention, and then every warning. Four publish buttons: u / v / speed Channels (with the validity mask alongside), the four derived-field Channels (their titles carry the scheme name), a Vector Field of glyphs (with x / y decimation), and a streamline Graph (RK4, with a speed scalar on every vertex). Four export buttons: CSV (the long table plus the per-pass table), VTK (one file per pair plus a .pvd collection), the JSON provenance and the HTML report. Any change to an input or a parameter drops the previous result and disables every export and publish button immediately.
Share Report
A seventh tab, Share Report, is not a workflow step — it puts a finished report on the internet temporarily and does no analysis. It serves the report you just generated from this computer through a temporary Cloudflare quick tunnel and hands back a temporary public address; nothing is uploaded, no account is needed, and the page carries your image data itself. The address is not usable immediately: it only starts answering roughly 40 s AFTER the tunnel is started (measured from this machine: 45 s once, while two other tunnels never resolved at all), so do not send the link before the status turns green. While it is running anyone who has the link can read the report — there is no password. Pressing Stop sharing, closing the window or quitting Dragonfly makes the address stop working permanently; it is never reissued. The link is only offered once this machine has confirmed the address answers, or has said plainly that it could not confirm it.
6. How to use
1. In step 1, choose the pair source, the Channel and the image plane, and check that the pair list at the bottom is the one you meant.
2. In step 2, enter Δt in seconds and the pixel pitch in mm — without both, the Run button never lights up.
3. In step 2, press "Estimate the particle-image diameter". Below 1.5 voxel, go back to the acquisition; no post-processing recovers it.
4. In step 3, confirm the pass schedule and the validation, and glance at the noise amplification beside the difference scheme when you change it.
5. In step 4, run a preview: is the quiver plausible, is the locking ratio sane, did the guardrail fire?
6. In step 5, run at full frame.
7. In step 6, read the measured fraction and the warnings, then publish or export.
7. Parameters
Inputs and calibration
Parameter | Default | Meaning |
Image pair source | two timesteps of one Channel | two Channels and two slices of one Channel are the others; only the timestep mode can derive Δt from the object |
Image plane | XY | XZ and YZ are the others; it fixes which two axes are rows and columns and which is out of plane |
Slice index | 0 | out of range falls back to the middle slice, with a note |
Timestep | 0 | editable in the two-Channel and two-slice modes only |
Pairing | frame-straddling (0,1) (2,3) … | consecutive always warns that neighbouring results share a frame; fixed reference is DIC's mode and measures total displacement, not velocity |
Reference frame | 0 | fixed-reference mode only; out of range falls back to the first frame in the range and says so |
First / last frame | 0 / -1 | -1 means the last frame; two identical frame indices are refused |
Δt (s) | 0 (required) | at 0 the Preview and Run buttons stay disabled; reading it from the T spacing always brings the unit notice with it |
Override the pixel pitch | off | off reads it from the object for the chosen plane; on is for a calibrated target |
Pixel pitch y / x (mm) | 1.0 / 1.0 | editable only with the override on; an anisotropic pitch raises a notice |
Background removal | none | sequence minimum (which warns that two frames alone eat overlapping particle images) and local minimum are the others |
Local-minimum size (voxels) | 15 | coerced to an odd number of at least 3: an even minimum filter is off-centre and shifts the background estimate by half a voxel |
CLAHE | off | non-linear and per-tile; turning it on always adds a warning, because a region whose local histogram differs between the frames is re-mapped differently in each |
CLAHE kernel / clip limit | 32 / 0.01 | the bin count is fixed at 256 |
High-pass sigma (voxels) | 0 (off) | removes the slowly varying illumination; the result is clipped at zero by default |
Intensity cap (n sigma) | 0 (off) | the threshold comes from the pooled mean and standard deviation of both frames, never from each frame's own — that is exactly the asymmetry to avoid |
Rescale the pair jointly to [0, 1] | on | one affine map, derived from the pooled pair, applied to both frames |
Filter border mode | nearest | wrap and reflect are the others. Only wrap makes shift equivariance exact over the whole frame (what a periodic phantom needs), but it glues opposite edges together and is wrong for a real image |
Correlation
Parameter | Default | Meaning |
Pass schedule | 64/32 → 32/16 → 16/8 (a fourth pass 8/4, off) | the windows should shrink; a later pass with a larger window is warned about. The minimum window is 8 voxels, and a pass that does not fit the image is skipped with a note |
Per-pass deformation | use the default | overridable per pass to none / integer shift / symmetric half shift / affine |
Sub-pixel estimator | 2 x 3-point Gaussian | 9-point 2-D Gaussian, 2 x 3-point parabola and 3-point centroid are the others. Measured locking amplitude at d_p = 2 voxels: 0.0076 / 0.0107 / 0.105 / 0.0745 voxel |
Window function | none | Hann is the other; the taper's own DC term is removed again afterwards, or the plane picks up a broad pedestal that shifts the peak |
Default deformation | symmetric half shift | each frame moves half the predictor in the opposite direction and the correlation measures only the residual; affine adds the local displacement gradient |
Interpolation order | 5 | range 1..5. Resampling bias measured against a self-correlation control: 0.031 voxel at order 5, 0.049 at order 3, 0.105 at order 1, for about 28 % more runtime. Order 5 stops winning on scatter above roughly 5 % image noise (as a fraction of the particle-peak amplitude), but its bias advantage holds throughout — and bias is the part that does not average away |
FFT worker threads | -1 (all cores) | |
SNR exclusion radius (voxels) | 2 | a disc of this radius around the primary peak is excluded before the second peak and the RMS are taken |
Smooth the predictor between passes | on | a 3 x 3 median, applied only to the field handed to the next pass; the reported values are still what the validator said |
Validation, derived fields and the step 4/6 options
Parameter | Default | Meaning |
Global limits on u and v | off (-32 … 32 voxel/frame each) | the hard limits the experiment itself allows; a non-finite value is not counted here, because it was never measured |
Magnitude limit | off (32 voxel/frame) | |
n sigma test | off (3.0) | PIVlab's standard-deviation filter; a few wild vectors inflate the standard deviation enough to hide themselves, which is precisely why the median test exists |
Use median and MAD instead | off | the robust variant, available only while the n-sigma test is on |
Normalised median test | on | Westerweel & Scarano's universal outlier detection; the two components' normalised residuals are combined as a hypot |
Threshold | 2.0 | applied to the combined residual |
epsilon (voxels) | 0.1 | the median absolute residual of a perfectly uniform neighbourhood is exactly zero; with epsilon the test becomes "a deviation no larger than threshold x epsilon = 0.2 voxel is never an outlier" |
Neighbourhood half-width (nodes) | 1 | all 8 neighbours must be usable, so the one-node border is reported as untested rather than rejected: a partial neighbourhood is systematically biased on any field with a gradient. Measured with the neighbour count relaxed to 3, two corners of an exactly linear field cross the threshold (combined residual 2.8517) — i.e. two perfectly good measurements replaced by interpolations |
Reject below an SNR | off (1.3) | on the primary-to-second-peak ratio; a non-finite SNR counts as a rejection |
Replace rejected vectors from their neighbours | on | a synchronous (Jacobi) fill, at most 8 passes, so the answer cannot depend on scan order; a node with too few known neighbours stays empty and its replaced flag stays clear |
Finite-difference scheme | least-squares 5-point | central, Richardson (4th order) and the circulation contour are the others; their noise amplifications are 0.447 / 1.000 / 1.344 / 0.612 |
Fractional-part histogram bins | 21 | an odd count puts zero at a bin centre; with an even count a symmetric locking peak splits across two adjacent bins — measured on 30 000 locked samples, 20 bins read 10.03 of 20 while 21 read 20.63 of 21 |
Preview crop (voxels) | 256 | 320 at most, centred; a preview runs the first pair only |
Quiver decimation | 1 | affects only how the preview and the results page are drawn, never a number |
Publish the validity mask alongside | on | a uint8 mask published with the velocity Channels: 1 means this node has a vector |
Glyph decimation (x, y) | 1 / 1 | the Vector Field's glyph sampling, in ORS x, y, z order; a zero in any of them is refused outright |
Streamline seeds / steps | 36 / 400 | RK4 in node-index space with a fixed half-cell step; a line stops when it reaches a node with no vector |
8. Output
Output | Meaning |
Per-pair table | each pair's frame indices, pass count, measured / interpolated / empty fractions, median SNR, locking ratio, the largest displacement variation across one window, and whether it was cancelled |
Derived-field table | vorticity ω_z, divergence, maximum shear rate, strain-rate magnitude, ε̇_xx, ε̇_yy and ε̇_xy, all in 1/s, with the median, the RMS and the largest magnitude |
Quiver plot | drawn at the field's physical aspect ratio; an arrow's colour always carries the true speed, an arrow longer than two cells is capped and gets a second head, an interpolated vector is dashed with a hollow square base and a node with no vector is a grey cross |
Fractional-part histogram | the distribution of the displacement's fractional part, with the locking ratio. Flat means no locking; peaks at both ends mean locking |
Channels: u, v and speed (mm/s) | several pairs become one 4-D |
Channel: validity mask | uint8, 1 where the node has a vector. Publish it before reading nodes that hold no measurement |
Channels: vorticity / divergence / maximum shear rate / strain-rate magnitude (1/s) | the title carries the difference scheme. NaN is kept rather than filled with zero — NaN at the boundary is the normal state, and a vorticity of zero is a plausible measurement |
Vector Field (glyphs) | built from four component Channels: X (image columns), Y (image rows), Z (exactly zero, because this is a 2-D field) and the speed; the components are temporary objects by default |
Graph: streamlines | RK4 polylines with an mm/s speed scalar on every vertex (slot 0), for the one pair selected in step 6 |
CSV (long form) | one row per node per pass, 40 columns: frame indices, pass, window and step, node row/column and world position, the displacement in voxels, the velocity in mm/s, the four derived fields, both SNRs, the correlation peak, the sub-pixel fit residual, the neighbourhood dispersion, the displacement variation across one window, and valid / measured / replaced / outlier / clamped plus the flag word. Derived columns are filled on the final pass only |
CSV (per pass) | a 29-column per-pass quality summary; measured, interpolated and empty are an exact partition of the grid, so those three fractions sum to 1 |
VTK | one |
JSON provenance | the geometry (pixel pitch, vector spacing, origin, source origin, slice, sign convention, and per key whether each was supplied or derived), the pass schedule, the correlation settings, the difference scheme with its noise amplification and boundary policy, the validation settings with per-test rejection counts, the preprocessing, the particle image, the quality definitions and per-pair quality, every parameter with its unit, a seed note, the software versions, the licence origin and every warning |
HTML report | one file whose four plots (quiver, locking histogram, per-pass valid-vector stacked bars, SNR histogram) are all inline SVG, with no external request of any kind — no CDN, no web font, no remote image, no script. It carries both a light and a dark palette |
9. FAQ & troubleshooting
- Run and Run preview are greyed out — one of three things is missing: no input in step 1, Δt still 0 in step 2, or no pixel pitch. The button's tooltip names each missing item.
- The velocities are obviously wrong — check Δt first. "Read from T spacing" hands back Dragonfly's raw T spacing, whose unit is documented nowhere in the SDK; the plugin passes it through unconverted and asks you to confirm it is seconds. In the two-slice mode there is no Δt to read at all, and the whole velocity scale comes from the number you type.
- My vorticity is 1.75x another tool's — the classic one-spacing mistake: on a grid with h_y/h_x = 2.5, using h_x for both derivatives makes vorticity too large by exactly (1 + h_y/h_x)/2 = 75.0 %, while the divergence stays exactly 0 and the field still looks like a clean vortex. The other common error is using the pixel pitch instead of the node spacing, which multiplies every derived field by the window step (a factor of 32 at 64/32 windows). This plugin does both correctly: the vector spacing is always
window step x pixel pitch, and each axis uses its own. - The locking ratio is high (above 2) — look at the particle-image diameter first. Below 1.5 voxel every sub-pixel estimator pulls the displacement onto whole voxels, and that is an acquisition problem rather than a post-processing one. Above 2 voxels, use the default 2 x 3-point Gaussian and not the parabola (measured 14x worse).
- The derived fields are empty at the border and around holes — by design. Any stencil that leaves the grid, or that touches a replaced, invalid or missing vector, gives NaN at that node and is counted: boundary-only, bad-support-only and both are an exact partition of the dropped nodes. Falling back to a one-sided difference would put a first-order value into a second- or fourth-order field and nothing in the output would say so.
- The outermost ring of vectors is flagged "predictor clamped" — in the symmetric scheme the outermost windows cannot be shifted at all, so a perfectly good field still flags its border. What actually matters is whether the un-applied part of the predictor exceeds a quarter of the window, which raises a separate range-exceeded flag. When it is clamped, the reported displacement is the clamped offset plus its residual; it is never silently booked against the full predictor.
- It warns that the displacement varies by more than the 3-voxel guardrail across one window — one correlation peak no longer represents that window's motion. Use a smaller window, add a pass, or switch the deformation to affine. Measured: a shear of 0.20 varying 12.8 voxel across a 64-voxel window gives a fitted gradient 7.5 % low, 3.2 voxel of scatter and a median primary-to-second-peak ratio of 1.19 against 6.2 on the mild case — the guardrail and the SNR both say so.
- The valid fraction is 100 % but the measured fraction is much lower — the difference is interpolation. A replaced vector counts as valid and does not count as measured, and the headline number is the measured fraction. Above 25 % interpolated or empty, an extra note asks you to treat the derived fields as indicative only.
- There are NaN or inf values in my image — that is a normal input (a registration fill, a log of zero, the output of a previous plugin). Every non-finite voxel is replaced by zero first, the warning names how many NaN, +inf and -inf were in which frame, and every window that reads a repaired voxel is flagged nonfinite, given a displacement of exactly zero and reported invalid. Measured: an all-NaN pair comes back at valid 0.000 with all 225 windows flagged flat; a single NaN voxel costs 9 of those 225 windows and leaves valid at 0.960 — the damage is localised, not fatal.
- Consecutive pairing gives neighbouring results that look correlated — they are: every interior frame is used twice. The plugin always warns about it, because those results cannot be averaged as independent samples. Use frame-straddling when you need independent pairs.
- The fixed-reference results do not look like a velocity — they are not one. A fixed reference measures total displacement from that frame, which is DIC's mode, and the correlation itself also de-correlates as the pattern moves away from the reference. The interface says so on the control.
- Doesn't Dragonfly already have optical flow? — it does, and it is a different thing. Dragonfly's own optical-flow family (used by slice registration, and present in the free SDK) is a differential brightness-constancy estimator: it has no Δt, no physical units, no per-vector SNR and no derived fields. This plugin is correlation PIV. For the displacement of a speckle pattern on a solid surface, use the Digital Image Correlation 2D (muDIC) plugin instead; the methods overlap, the purposes do not.
- The VTK export says it needs pyevtk — pyevtk is shipped by Dragonfly and imported lazily at export time only; if this interpreter cannot import it, the message says so verbatim. The CSV, JSON and HTML exports do not touch it.
- One of the publish buttons is greyed out — the Environment line at the top says why: with no Channel-from-array factory nothing can be published at all; without
setTSizeseveral pairs cannot become one 4-D series; aVectorFieldmissingfromChannelscannot publish glyphs; a missingGraph.setEdgescannot publish streamlines. The exports remain available in every case. - The published field is in the wrong place — the Results tab prints the reason verbatim: the node spacing could not be written (every derived field in that object is then at the wrong physical scale); the half-window origin could not be written (every vector is drawn at the source's own position, half a window and half a node from where it was measured); or the source's orientation was not copied (a rotated source publishes axis-aligned). Each sentence corresponds to one failed write, not to a guess.
10. Notes & known limitations
- Per-vector uncertainty is not implemented. This version reports SNR (primary-to-second-peak and peak-to-RMS), the valid and measured fractions, the sub-pixel fit residual and the local neighbourhood dispersion, each under its own name; none of them is an uncertainty and none is labelled as one. Uncertainty by image matching needs particle pairing and is a later version.
- This is an in-plane 2-D measurement. The published Vector Field's Z component is exactly zero; a non-zero divergence means out-of-plane motion or noise, not a usable third component.
- Δt and the pixel pitch are mandatory, not optional refinements: without them there is only a displacement in voxels. The plugin will not quietly fill Δt in from the object for you.
- A Preview is one pair on a centred crop, publishes nothing and enables no export; it is a shape and quality check and its numbers must not be reported. A Run publishes nothing either.
- Whole frames are read into memory. The correlation planes are the largest allocation in a pass (about 65 MB at a 32-voxel window on a 1024-square frame) and are freed after every pair, never accumulated. The step-5 estimate is an order-of-magnitude guide, not a promise.
- The normalised-median test leaves the outer ring of nodes untested by default, because a half neighbourhood is systematically biased on any field with a gradient. So a detection rate must be quoted together with the untested count: measured on the sigma = 0.10 case, detection over the tested nodes is 100 % (1.15 % false positives) while detection over all injected nodes is 93.3 %.
- CLAHE is the one preprocessing step that is not symmetric between the two frames, and turning it on always adds a warning. Every other step is either pointwise or a translation-invariant filter, and every data-derived scalar comes from the pooled pair, so exchanging the two frames gives a bitwise-symmetric result.
- Shift equivariance of the filters is exact over the whole frame only under the
wrapborder; under the defaultnearestit does not hold in the ring where a filter reaches outside the image (measured on a 256-square phantom, shift (3, -5), high-pass sigma 3 plus a 3-sigma cap: 2.664e-01 at the border and exactly 0 more than 17 voxels in). That is a property of the border rule, not a defect in the pipeline — and after two passes the border's vector error is actually smaller undernearest(0.0778 against 0.0894), which is why the default stays. - Streamlines are integrated on the vector grid of the one pair selected in step 6 and stop at any node with no vector; they are a visualisation, not the physical path of a tracer.
- This plugin publishes no ROI and no MultiROI. A mask is only read — and a mask edge is a strong straight feature, so windows straddling it correlate on the edge instead of the particles, which the plugin warns about.
- This plugin does not duplicate what already exists elsewhere. Speckle-pattern displacement on a solid surface belongs to Digital Image Correlation 2D (muDIC); Dragonfly's own optical-flow family is a differential brightness-constancy estimator with no Δt, no physical units, no per-vector SNR and no derived fields. This plugin is correlation PIV.
- Every quality number describes this correlation and nothing else. None of them can tell you how faithfully the tracer follows the fluid, how accurate the calibration target was, or how much the optics distorted — those come from the experiment, not from here.
11. References
The algorithmic source is PIVlab (MATLAB, MIT, Copyright (c) 2024 William Thielicke; Thielicke & Stamhuis): https://github.com/Shrediquette/PIVlab . The multi-pass FFT correlation and window deformation, the preprocessing, the vector validation and the choice of post-processing quantities follow the published method and documented behaviour of its piv_FFTmulti.m, PIVlab_preproc.m, filtervectors.m and PIVlab_postproc.m, reimplemented in NumPy/SciPy. No .m, .fig or .mat file was copied, ported, translated or bundled, and no MATLAB, MATLAB Runtime or Octave is required at runtime. One deliberate departure: PIVlab rescales each correlation plane to 0..255, while this plugin returns a true correlation coefficient in [-1, 1], because that is the one number a user can interpret.
Method citations (no code relationship): Westerweel & Scarano, Universal outlier detection for PIV data, Exp. Fluids 39 (2005) 1096 (the normalised-median test); Scarano 2002 (window deformation); Richardson extrapolation; Raffel, Willert, Scarano, Kähler, Wereley & Kompenhans, Particle Image Velocimetry: A Practical Guide; and Sciacchitano & Wieneke on PIV uncertainty quantification, cited only to record that it is deliberately not implemented here.
The libraries called at runtime are all shipped by Dragonfly and are not bundled by this plugin: NumPy 1.22.3, SciPy 1.9.3 and scikit-image 0.19.3 (all BSD-3-Clause), plus pyevtk 1.5.0 for the VTK export (BSD-2-Clause, Copyright 2010 - 2016 Paulo A. Herrera). Neither matplotlib nor vtk is imported: every plot is hand-written inline SVG.
No GPL or LGPL code, in any form. OpenPIV (GPL-3.0) is not used in any way — not imported, not copied, not packaged, and deliberately not present in this repository; the cross-check role in this plugin's own test suite is filled by independently written brute-force references (a hand-summed 3x3 contour integral, numpy.polyfit slopes, an explicit Richardson extrapolation and a hand-quadratured contour loop). edt (LGPLv3+) and cc3d (LGPL) are installed in Dragonfly's Python and are never imported; orix and Gmsh are not used in any form. The full statement is in THIRD_PARTY_NOTICES.md in the plugin folder.