PIV实验做完,拿着一堆互相关算出来的速度矢量,很多人第一反应是直接出图。但矢量图密密麻麻的箭头,或者只用颜色显示速度大小的散点图,其实都不够直观。真正能拿出去展示、能用来分析流场结构的,往往是流速云图。而MATLAB里画云图的主力函数就是contour,尤其是contourf。这篇东西就把我从PIV数据处理到最终出云图的完整经验整理出来,包括contour函数的各种细节坑,适合刚接触PIV数据后处理、被一堆矢量文件搞得头疼的同学参考。
1. 从PIV实验到流速云图:为什么云图是最后一步的关键
1.1 PIV实验数据到底长什么样
PIV(Particle Image Velocimetry,粒子图像测速)的核心原理不复杂:在流场里撒示踪粒子,用激光片光照明,用高速相机连续拍两张或一组照片,然后对图像划分成许多查询窗口,对每个窗口做互相关计算,找到粒子团在Δt时间内的位移,除以时间间隔就得到速度矢量。
但真正拿到手的数据,和教材上画的简图差距很大。一套常规二维PIV实验做下来,你会得到这样的东西:一个文件夹里塞满了几百上千对图片,经过Davis、PIVlab、OpenPIV这类软件计算后,导出的是若干组数据文件。常见的导出格式是x,y,u,v四列,有时候还带一个表示矢量质量的参数(比如信噪比、相关性峰值)。这里的x和y是每个查询窗口中心的空间坐标,u和v是对应位置的流向速度和法向速度。
也就是说,你手里的数据本质上是分布在离散网格上的矢量场,不是一张现成的云图。横竖网格点数量通常取决于查询窗口大小和重叠率。比如相机分辨率是2048×2048像素,查询窗口是64×64,步长是32像素,那算出来大概是63×63个矢量点。这些点在空间上虽然规则,但因为实验过程中可能存在标定系数变化、坐标方向设定等问题,直接拿去画图经常出各种幺蛾子。
1.2 云图和矢量图怎么选,别一上来就裸画箭头
有些同学拿到数据后第一件事就是quiver(x,y,u,v),画出一大片箭头。箭头图适合看方向,比如旋涡的旋转方向、剪切层的流动方向,确实一目了然。但箭头图有两个很明显的问题。
第一个问题是信息密度太低。63×63个矢量点,画出来的箭头密密麻麻,屏幕上全是黑色的线条和箭头尾巴,根本分不清哪里速度快哪里速度慢。你得把图放大,一个个箭头去比长度,非常费眼睛。第二个问题是无法直观反映速度大小的空间分布。流速云图(等值线图)恰恰解决了这个需求:横纵坐标代表空间位置,颜色或等值线代表速度大小,整个流场的高低速区域一眼就能看出来,哪里是射流核心区、哪里是回流区、哪里是边界层低速带,全部清清楚楚。
所以我的习惯是:先用云图看整体分布,确定关键区域后再叠加箭头图看方向,两张图配合使用。这也是为什么contour函数在PIV后处理里几乎是必用工具。
2. contour函数的核心细节与参数玩法
2.1 contour与contourf,一字之差效果天差地别
MATLAB里画等值线的函数有一对孪生兄弟:contour和contourf。很多新手第一次用,直接contour(x,y,U),画出来的图只有一圈一圈的黑线,没有颜色填充,看起来像地形图,而不是"云图"。
contour函数默认只画等值线,线条本身不带颜色填充。如果你想要那种颜色连续变化的云图效果,两个办法:
- 用contourf(x,y,U,levels),f是fill的意思,会在相邻等值线之间填充颜色。
- 用contour(x,y,U,'Fill','on','LineColor','none'),这是老版本MATLAB的常用写法,效果和contourf类似。
新版MATLAB(R2014b之后)里,contourf的行为也有一些调整。现在contourf(x,y,U)默认画出来的是填充的云图,但注意,不同版本对levels参数的解释略有差异。我建议明确写levels,不要靠默认值。
另外还有一个常见误区:contourf默认会在等值线之间画出黑色的边界线,这在云图上会显得很杂乱。通常我们不需要这些黑线,所以我画云图时基本固定搭配:
contourf(xq, yq, U_mag, 50, 'LineColor', 'none');如果你还在用特别老的MATLAB版本(R2014a之前),可能不支持LineColor参数,那就用:
contourf(xq, yq, U_mag, 50, 'LineStyle', 'none');或者直接:
[c,h] = contourf(xq, yq, U_mag, 50); set(h, 'EdgeColor', 'none');这三种写法在功能上是等价的,选一种自己习惯的就行。我的建议是记住第一种,够用了。
2.2 levels参数:等值线数量和控制列表
levels这个参数是contour函数里最容易让人困惑的一个点。它有两种完全不同的含义,取决于你传进去的是单个数字还是向量。
- 如果你传的是单个数字,比如contourf(x,y,U,30),意思是让MATLAB自动生成30条等值线。这个数字不一定是精确的等值线数量,更准确地说,是颜色的分层数。MATLAB会在数据的最小值和最大值之间自动划分30个颜色级别。
- 如果你传的是向量,比如contourf(x,y,U,[0.1:0.02:0.5]),意思是让MATLAB在指定的数值处画等值线。这个等值线列表可以是不等间距的,完全由你控制。
实际做PIV云图时,我强烈建议用向量方式,不要用数字。原因很简单:如果你用contourf(x,y,U,30),颜色范围是由当前数据min和max决定的。当你对比不同实验工况时,每个图的颜色范围都不一样,看起来就无法直接对比。统一用clim把色标范围固定之后,再用数字levels也可以,但最好还是用向量,把等值线间距设成一个固定步长,图与图之间才有可比性。
举例说明,假设你要对比三个不同流速下的流场,速度范围分别是0~2 m/s、0~4 m/s和0~6 m/s。如果都用30条等值线,第一张图的颜色从0到2渐变,第三张图从0到6渐变,视觉上完全没法对比。正确的做法是设定统一的levels向量:
levels = 0:0.1:6; contourf(xq, yq, U1, levels, 'LineColor', 'none');这样三张图的颜色级别完全对应,放一起看才有意义。
2.3 颜色映射与色标范围:云图好看的关键
级别和颜色范围的设定直接决定了云图的信息表达。这里有三个东西要分开理解:colormap、clim(老版本叫caxis)、colorbar。
colormap是颜色映射表,MATLAB里最常用的包括:
- parula:MATLAB默认的颜色映射,蓝到黄渐变,颜色区分度好,对色盲相对友好,做科研出图推荐。
- jet:经典的彩虹色,蓝→青→绿→黄→红,视觉冲击强,但存在两个问题:黄色区域亮度过高容易造成视觉假象,且两端深蓝色和深红色对于色盲人群不友好。现在很多期刊已经明确不推荐用jet。
- turbo:新版MATLAB提供的jet替代方案,比jet更平滑,色彩分布更均匀。
- hot:黑→红→黄→白,适合热场。
- cmocean系列:专门为海洋和大气科学设计的颜色映射,比如cmocean('balance')、cmocean('thermal'),如果装了Climate Data Toolbox可以直接用,效果非常专业。
我个人的习惯是:内部处理用jet方便观察细节,出正式图用parula或turbo,投期刊再按期刊要求调整。在代码里用colormap命令设置:
colormap(parula); % 或者 colormap(turbo); % 或者自定义 cmap = cmocean('thermal'); colormap(cmap);clim控制的是颜色映射的范围,也就是数据的最小值和最大值对应到色标的最小值和最大值。很多时候数据里有异常的大值或小值,如果不限制clim,云图的颜色范围会被这些异常值拉得很宽,导致主体区域颜色没有区分度。这时候就得手动固定:
clim([0 5]); % 新版本写法 % 老版本用 caxis([0 5]);固定clim之后,数据超出范围的部分会显示成最红或最蓝的颜色,这样反而能把异常区域标出来,方便你回到原始数据排查。
2.4 从离散矢量到连续场:网格化的关键一步
这一步是PIV云图最容易翻车的环节,也是我一开始踩坑最多的地方。PIV软件导出的x,y坐标虽然来自规则网格,但由于标定、畸变矫正等原因,这些点往往不是严格等间距的矩形网格。而contour函数对输入数据格式是有要求的,它需要的是矩阵形式的数据,也就是xq、yq各自是网格坐标矩阵,U_mag和它们维度一致。
很多同学直接做contour(x,y,U_mag),发现报错说维度不对,或者画出来的图乱七八糟,就是因为x、y、U_mag都是向量而不是矩阵。解决办法就是对原始离散点做网格插值。
MATLAB里常用的插值函数有两个:
- griddata:老牌函数,用法是[xq,yq] = meshgrid(xi,yi);Uq = griddata(x,y,U_mag,xq,yq,'linear')。简单直接,缺点是复杂数据时速度较慢。
- scatteredInterpolant:推荐使用,速度快且内存效率高。用法:
F = scatteredInterpolant(x, y, U_mag, 'natural', 'linear'); Uq = F(xq, yq);scatteredInterpolant支持三种插值方法:linear(线性插值,速度快,但会在数据点周围产生折线效果)、natural(自然邻域插值,效果好,光滑,适合PIV数据)、nearest(最近邻插值,速度快但结果块状感强)。
对于PIV数据,我推荐用natural,它能比较好地保留流场细节,又不会像linear那样出现明显的插值痕迹。如果你用的是griddata,那对应的方法是'v4',效果也还可以,但数据量大的时候非常慢。
还有一点要注意:插值的目标网格范围不能超出原始数据的凸包范围,否则插值结果是NaN,云图上会出现空洞。这个我在后文的常见问题部分再细说。
3. 完整实操:从原始矢量场到一张能进论文的流速云图
3.1 准备数据与坏矢量处理
假设你手里已经有一个从PIV软件导出的数据文件,比如是CSV格式,包含x、y、u、v四列。第一步自然是读进来:
data = readmatrix('piv_data.csv'); x = data(:,1); y = data(:,2); u = data(:,3); v = data(:,4);但这里有一个大坑:PIV互相关计算出来的原始矢量场,几乎必定含有坏矢量。所谓坏矢量,就是明显错误的计算值,可能是由于粒子图像质量差、窗口内粒子数量少、跨帧丢失等原因造成的。这些坏矢量的典型特征是速度大小异常大、方向突变,和周围矢量完全不一致。
如果不处理坏矢量直接插值,后果非常严重。因为插值会把坏矢量的错误值扩散到周围区域,云图上会出现一块假的低速或高速区,整个流场看起来就像长了疮一样。
所以读入数据后,必须先做坏矢量检测和剔除。我常用的方法有三个:
第一个是速度阈值法。根据实验条件估计一个合理的速度上限。比如水管实验流速估计最大不超过2 m/s,那么abs(sqrt(u.^2+v.^2)) > 3就可以判断为坏矢量。
speed = sqrt(u.^2 + v.^2); max_valid = 3; % 根据实验情况调整 valid = speed < max_valid;第二个是邻域中值法。把每个矢量点与周围8个邻近点的中值比较,如果偏差超过中值的两到三倍,判定为坏矢量。这个在PIVlab里叫standard deviation filter或者median filter。
第三个是最原始的人工检查。把矢量场画出来,用鼠标一个个把明显的错误箭头挑出来。这个办法适用于矢量点很少的情况,数据量大了不现实。
在实际处理中,我一般先用阈值法粗筛,再对剩余的矢量做一次邻域中值滤波,双保险。处理完之后,被剔除的位置暂时是NaN,后面插值时把这些点排除掉:
u_clean = u(valid); v_clean = v(valid); x_clean = x(valid); y_clean = y(valid);3.2 插值、速度合成与平滑
坏矢量剔除后,就可以开始插值了。推荐用scatteredInterpolant,因为它对NaN点的处理比较自然。先把有效点构造成插值对象:
F_u = scatteredInterpolant(x_clean, y_clean, u_clean, 'natural', 'linear'); F_v = scatteredInterpolant(x_clean, y_clean, v_clean, 'natural', 'linear');然后定义插值网格。注意网格的定义范围要尽量贴近原始数据的有效区域,不要随便包一个巨大的范围,否则边缘会有大片NaN:
xi = linspace(min(x_clean), max(x_clean), 100); yi = linspace(min(y_clean), max(y_clean), 100); [xq, yq] = meshgrid(xi, yi); uq = F_u(xq, yq); vq = F_v(xq, yq);插值完成之后,计算速度大小:
U_mag = sqrt(uq.^2 + vq.^2);这里还可以顺带计算涡量,用MATLAB的curl函数:
[curlz, cav] = curl(xq, yq, uq, vq);涡量云图在某些场合比速度云图更能反映流场结构,比如边界层分离、旋涡脱落,涡量云图一看就明白。
插值之后还有一个细节值得注意:要不要做平滑。PIV原始数据本身带有一定的噪声,插值后这些噪声会被保留下来,云图上表现为颜色过渡不自然、有颗粒感。我一般会做一次二维平滑,最简单的是移动平均,但要注意不能过度平滑导致小尺度结构被抹掉。用imgaussfilt做高斯平滑是最省事的:
U_mag_smooth = imgaussfilt(U_mag, 1.5);不过要记住,平滑是一种修图手段,不要为了好看而失真。论文里如果用平滑,最好在方法部分写清楚。
3.3 绘制云图与叠加矢量
数据准备好了,画图本身其实很快:
figure; contourf(xq, yq, U_mag, 50, 'LineColor', 'none'); hold on; quiver(xq(1:5:end,1:5:end), yq(1:5:end,1:5:end), ... uq(1:5:end,1:5:end), vq(1:5:end,1:5:end), 2, 'k'); hold off;quiver叠加箭头时,注意两个技巧:
第一个技巧是抽稀。云图本身已经足够密了,箭头不需要每格都画,否则黑压压一片。用1:5:end每隔5个点画一个箭头即可,具体间隔看网格密度调整。
第二个技巧是颜色。箭头用黑色或者白色比较合适,在彩色云图上识别度最高。如果用红色蓝色,反而会和云图颜色混淆。箭头密度和长短也要调,可以用quiver的缩放参数,比如上面的2表示箭头自动缩放2倍,调一次到看起来疏密合适为止。
如果不想用quiver,MATLAB的streamline函数可以做流线图,在某些场景下比箭头更优雅。但对PIV数据来说,streamline需要在规则网格上工作,我们的插值网格正好满足要求:
h = streamline(xq, yq, uq, vq, startx, starty); set(h, 'Color', 'k', 'LineWidth', 0.5);画出来是流线叠加云图,效果非常专业。startx和starty是流线起点的坐标,可以按需设置几个。
3.4 坐标、色标与出图设置
云图画出来后,还有一系列细节需要处理。很多PIV数据在采集时,X方向是流向(水平),Y方向垂直向下,因为相机图像坐标习惯从左上角开始。但流场分析的惯例是Y轴向上,所以画出来的图经常是上下颠倒的。解决办法很简单:
set(gca, 'YDir', 'reverse');或者反过来,如果你希望图像和实际流场对应,用set(gca, 'YDir', 'normal')。这个方向问题没有绝对标准,关键是确保你知道自己的Y轴方向代表什么,标注清楚。
接下来设置色标:
colormap(parula); clim([min_val max_val]); colorbar; ylabel(colorbar, 'Velocity (m/s)');这里重点强调统一范围的问题。你如果要出一组不同工况的图,务必用同一个clim,否则放在一起没有可比性。我通常在代码前部定义一个变量:
v_range = [0 2]; % 统一速度范围 % 每张图都写 clim(v_range);坐标轴单位换算也要留意。PIV软件输出速度的单位取决于标定设置,常见的单位有m/s、mm/s、pixel/frame。如果是pixel/frame,还需要乘以标定系数和时间间隔才能换算成实际速度。换算公式是:
U_mag_ms = U_mag * calib_mm_per_pixel / dt_s / 1000;calib_mm_per_pixel是每个像素对应的毫米数,dt_s是两帧之间的时间间隔(秒),除以1000是把毫米换算成米。这一步很容易被忽略,导致云图上的数值量级完全不对。
坐标轴的标注文字也不要偷懒。xlabel、ylabel写清楚物理量和单位,标题里写上实验工况编号。这个习惯在写论文时能省很多事。
最后是出图,导出高清图片有两个办法:
% 老办法 print(gcf, 'result.png', '-dpng', '-r300'); % 新办法 exportgraphics(gcf, 'result.png', 'Resolution', 300);exportgraphics是R2020a之后引入的,更好地保留了图的原始样式,推荐使用。300 dpi是期刊投稿的最低标准,如果要放大打印,建议600 dpi。
4. 常见问题与排查技巧实录
4.1 云图出现空洞或锯齿状边界
这个问题的根本原因是插值范围超出了原始数据的凸包范围。contourf对NaN数据很敏感,如果你的插值网格上有大片NaN,云图就会出现空洞。
排查思路:
- 先用isnan检查插值结果中有多少NaN:sum(isnan(U_mag(:)))。
- 检查插值网格范围是否超出原始数据范围。
- 如果确实需要在整个矩形区域显示云图,可以考虑用scatteredInterpolant的'linear'方法并配合外推,但是要谨慎,外推出来的数据并不代表真实流场。
解决方法是把网格范围缩小到数据覆盖范围内。还有一个办法是生成网格后,把数据范围以外的点直接置为NaN,再用contourf绘制,得到有边界的云图。
4.2 云图颜色乱七八糟,和速度对不上
这种情况十有八九是clim没设置。特别是数据里有几个坏矢量没清干净的情况下,速度最大值可能是一个几十甚至几百的错误值,导致整张图颜色全部被这个异常值"拉走",真实流场区域全是同一个颜色。
处理思路很简单:设置合理的clim,比如clim([0 prctile(U_mag(:),98)]),用98分位数作为上界,能保留大部分有效信号,又不会被尾部极值干扰。
4.3 坐标轴方向反了,图上下颠倒
这个很常见。PIV软件导出的Y方向可能是从上往下,也可能从下往上,取决于你的标定设置。如果你发现云图的上下和实验流场的上下对不上,直接set(gca,'YDir','reverse')翻转即可。如果在某些情况下坐标轴的tick标签也需要翻转,可能需要手动设置YTickLabel。
一个更隐蔽的问题是:有些同学把图像坐标系的y和PIV物理坐标系的y搞混了。图像坐标系是左上角为原点,y向下;物理坐标系是左下角为原点(或者根据实际布置定义),y向上。画图前先确认数据里的y到底是哪种,避免出现瀑布往下流但云图上显示往上的笑话。
4.4 性能优化与批量出图
试验数据一多,比如要画100个工况的云图,如果每个都手动调参数,工作量巨大。我的习惯是写一个批处理脚本,把所有数据文件放在一个文件夹里,循环处理自动出图。
files = dir('*.csv'); for k = 1:length(files) data = readmatrix(files(k).name); % ... 处理、插值、画图 ... exportgraphics(gcf, [files(k).name(1:end-4) '_cloud.png'], ... 'Resolution', 300); end批量出图要注意内存管理。每次循环结束用close all关掉图形窗口,否则积攒几十个figure会把内存耗尽。
另外,如果数据量特别大(比如几百MB的PIV数据),建议用matfile或者只读入所需列,避免MATLAB卡死。我试过一次性读入5GB的CSV文件,MATLAB直接内存不足。后来改用readmatrix读取一列处理完再释放,就流畅多了。
最后再分享一个提高出图速度的小技巧:在做云图调试阶段,先用低分辨率网格(比如50×50)快速画图,调整好表现参数后,再用高分辨率(200×200)出正式图。不要一上来就用超高分辨率,否则每调整一次参数都要等半天,非常浪费时间。