简介:面向MATLAB超声探伤学习者的小型示例包,适合无损检测初学者与相关课程实践。压缩包内共两个文件,均为M脚本,整体大小仅5KB,精简易读。其中主要脚本用于生成高斯余弦脉冲信号,模拟超声波短脉冲发射波形;配套测试脚本则负责调用信号模型,完成回波生成、滤波降噪、傅里叶频谱分析及缺陷位置推算。两个脚本共同构成一段可运行的超声探伤模拟流程,覆盖信号产生、介质传播、回波处理和数据可视化等关键环节。虽代码体量不大,却集中演示了超声检测中常用的高斯脉冲设计、反射波时间差法定位以及MATLAB绘图函数的使用方法,可作为进一步开发探伤算法或开展仿真实验的入门底稿。由于文件为源码形式,读者可根据实际需求调整参数并观察波形变化,便于动手实践。已有492人学习浏览,适合需要快速了解MATLAB在超声探伤中应用的读者参考。
1. 超声探伤用 MATLAB 做,到底在解决什么
超声探伤(ultrasonic-testing)落到 MATLAB 里,核心就一句话:从一段回波信号里把缺陷反射找出来,算准它的深度和幅度。很多人拿到 ultrasonic-testing.rar 这类源码包跑不出预期效果,问题不在算法深,而在采样率、声速、探头频率全用默认值,仿真图和真实探伤完全对不上。这条路实际是一条完整处理链:A 扫信号生成、带通滤波、包络检测、飞行时间换算深度、扫查数据成像。适合信号处理方向毕设、无损检测从业者做算法验证,以及想用 MATLAB 复现探伤仪界面的同学。这个方向的核心处理思路二十来年没大改,matlab 从老版本装到 2026b,处理逻辑依然相同,工具版本升级不影响你该怎么算。
2. 从零生成可信的 A 扫回波信号:脉冲回波模型与参数
2.1 脉冲回波模型:高斯包络正弦波为什么是默认选择
超声探伤里最常碰到的信号是 A 扫,横轴时间、纵轴幅度。探头激励脉冲碰到界面后反射回来,被同一个探头接收,形成一串回波。真实的换能器受高压电脉冲激励后,压电晶片会在谐振频率附近做衰减振荡,用高斯包络的正弦波去近似这个振荡,不是玄学,而是工程上公认的做法:
s(t) = A · exp(-(t - t0)^2 / τ²) · sin(2πfc(t - t0))
其中 A 是回波幅度,t0 是往返传播时间,τ 控制脉冲宽度,fc 是探头中心频率。高斯包络衰减快,旁瓣小,和真实探头输出波形很像,而且参数少,调起来直观。
常见探伤场景里,回波按到达时间排列:始波(探头表面的电泄漏信号)在 t=0 附近,缺陷回波在 t=2d/c 处,底面回波在探头到远端面这个固定距离处。缺陷深度和往返时间的关系是 d = c·t/2,除以 2 是因为声波走了一个来回,这条公式后面所有计算都绕不开。
2.2 一发一收 A 扫仿真:函数设计与参数表
我一般会把 A 扫生成写成一个独立函数,参数全部从外部传入,避免脚本里到处藏着魔法数。代码这样组织:
function [t, a_scan] = gen_ascan(fs, fc, c, defects) % 生成一发一收 A 扫信号 % fs : 采样率 (Hz) % fc : 探头中心频率 (Hz) % c : 材料纵波声速 (m/s) % defects : 结构体数组, 每个元素含 depth(m) 与 amp(相对幅度) T = 30e-6; % 采集时间窗 30 us t = (0:1/fs:T).'; % 时间轴, 注意是列向量 a_scan = zeros(size(t)); for k = 1:numel(defects) t0 = 2 * defects(k).depth / c; % 往返时间 if t0 > T, continue; end pulse = defects(k).amp * exp(-((t - t0) / 6e-7).^2) ... .* sin(2 * pi * fc * (t - t0)); a_scan = a_scan + pulse; end end调用时构造两个反射体:一个 20 mm 深的缺陷,一个 60 mm 深处的底面回波,采样率开到 100 MHz:
fs = 100e6; fc = 5e6; c = 5920; % 钢中纵波声速约 5920 m/s defects(1) = struct('depth', 20e-3, 'amp', 0.4); % 20 mm 缺陷回波 defects(2) = struct('depth', 60e-3, 'amp', 0.8); % 60 mm 底面回波 [t, a_scan] = gen_ascan(fs, fc, c, defects); plot(t * 1e6, a_scan); xlabel('时间 (us)'); ylabel('幅度');逻辑说明:循环里每个缺陷生成一个高斯包络正弦波,叠加到总信号上。t0 用往返时间计算,超过采集时间窗的直接跳过。τ 取 600 ns,按 5 MHz 中心频率算是 3 个周期左右,脉冲宽度和真实窄脉冲探头接近。
| 参数 | 典型值 | 作用 | 调参影响 |
|---|---|---|---|
| fs | 10~100 MHz | 采样率 | 低于 2~3 倍 fc 时包络会失真 |
| fc | 2~10 MHz | 探头中心频率 | 高频分辨力好但衰减大 |
| c | 材料声速 | 深度换算基准 | 偏一点所有深度整体偏移 |
| τ | 0.3~1 us | 脉冲宽度 | 太宽会把相邻反射叠加 |
| amp | 0~1 | 回波幅度 | 只影响显示,不影响时间 |
参数说明:采样率不要卡着奈奎斯特采样定理选。探伤要的是包络峰值准确,不是单纯不混叠。100 MHz 采 5 MHz 探头,一个周期 20 个点,包络检测才有足够分辨率。如果采样率只有 10 MHz,峰值位置量化误差会直接变成深度误差。
2.3 叠加噪声和材料衰减:让仿真更像真实探伤
真实探头信号里一定有噪声,而且越深处回波越弱。材料对超声的衰减随距离指数增加,常见做法是给每个回波乘一个距离相关的衰减因子,再叠加热噪声:
att = exp(-0.02 * defects(k).depth * 2); % 0.02 Np/mm 衰减系数 pulse = defects(k).amp * att * exp(-((t - t0)/6e-7).^2) ... .* sin(2*pi*fc*(t-t0));噪声用 randn 生成后按幅度比例叠加:
noise_level = 0.03; % 噪声幅度约为信号峰值 3% a_scan = a_scan + noise_level * randn(size(t));这里有个细节:衰减系数不能随便乱给。钢的纵波衰减通常在 0.005~0.05 Np/mm 之间,和频率强相关,频率越高衰减越厉害。仿真时光考虑扩散衰减(幅度按 1/d 衰减)也能出形,但做 DAC 补偿时就要把材料衰减单独建模,否则补偿曲线形状不对。
噪声幅度也别开太大。3% 噪声在包络检测后基本不影响峰值位置,但开到 20% 以上,浅缺陷的峰值会被噪声抬起来,后面阈值检测就分不清到底哪个峰是真回波。
3. 从 A 扫到缺陷参数:滤波、包络、飞行时间与 DAC 补偿
3.1 带通滤波与希尔伯特包络:两个常用函数解决一件事
原始 A 扫信号是高频振荡,直接找峰值很不稳。标准做法分两步:先带通滤波抑制带外噪声,再取包络把高频振荡变成平滑的单峰。
滤波器我用 designfilt 设计,filtfilt 做零相位滤波:
d = designfilt('bandpassiir', ... 'FilterOrder', 4, ... 'HalfPowerFrequency1', 2e6, ... 'HalfPowerFrequency2', 8e6, ... 'SampleRate', fs); y = filtfilt(d, a_scan); env = abs(hilbert(y));逻辑说明:带通范围设在探头中心频率附近,5 MHz 探头给 2~8 MHz 的带宽,把低频振动和高频干扰都拦掉。filtfilt 是零相位滤波,波形不发生时间偏移,这对飞行时间计算很关键。hilbert 求解析信号幅度,得到包络曲线,峰值位置对应回波到达时刻。
参数说明:HalfPowerFrequency1 和 HalfPowerFrequency2 是 -6 dB 截止点,带宽太宽噪声滤不净,太窄会把回波尾巴拉长导致两个相邻缺陷分不开。FilterOrder 取 4 够用,太高会出现数值不稳定,尤其是采样率高的时候。
3.2 计算飞行时间并换算缺陷深度:声速校准是定量跳不过去的一关
包络求出来后,峰值检测用 findpeaks。先设定最小峰高阈值过滤噪声,再用 MinPeakDistance 避免同一个回波的几个振荡毛刺被重复检测:
[peaks, locs] = findpeaks(env, ... 'MinPeakHeight', 0.15, ... 'MinPeakDistance', fs / fc / 2); % 间隔至少半个周期 tof_us = (locs - 1) / fs * 1e6; % 换算成微秒 depth_mm = c * tof_us * 1e-3 / 2; % 声速换算深度, 注意除以2逻辑说明:locs 是峰值所在的采样点索引,减 1 乘采样间隔得到时间。深度换算必须用 c/2,这是探伤里最容易算错的地方之一,往返距离才是真实深度的两倍。
参数说明:MinPeakDistance 取 fc 半个周期,意思是两个峰至少隔这么远,否则算同一个回波。实际使用时这个值要根据探头带宽微调,带宽大的探头包络窄,可以把间隔进一步缩小。
到这里如果只做单点测厚,工作就完成了。但探伤不是测厚,缺陷波后面还有底面波,而且缺陷波幅度受深度影响很大,不补偿会出现浅缺陷报警深缺陷漏检的问题。
3.3 DAC 曲线补偿:让不同深度缺陷都有可比较的波幅
DAC(距离-波幅曲线)补偿要解决的问题是:同样大小的缺陷,放得越深回波越弱,如果不做补偿,仪器上显示的波幅不能真实反映缺陷尺寸。
常见做法是准备一组不同深度的标准试块,测出每个深度平底孔的最大回波波幅,然后把波幅-深度关系拟合成曲线。没有试块时,可以用衰减模型近似建一条参考曲线:
R = linspace(5, 150, 100); % 深度范围 5~150 mm A0 = 1; alpha = 0.02; % 材料衰减系数 Np/mm DAC_ref = A0 ./ R .* exp(-2 * alpha * R); % 扩散衰减+材料衰减 plot(R, DAC_ref); xlabel('深度 (mm)'); ylabel('参考波幅');逻辑说明:扩散衰减按 1/R 衰减,材料衰减按 exp(-2αR),两者相乘得到参考曲线。实际探伤中这个曲线由仪器自动补偿,但用 MATLAB 做离线数据处理时,可以把各回波幅度投影到这个曲线上,得到归一化幅度。
参数说明:alpha 是虚拟参数,直接影响曲线形状,它只用于没有试块数据时的近似。真正要出定量结论,DAC 曲线必须用标准试块实测数据拟合,仿真模型只能用来理解原理和验证算法流程。
得到 DAC 曲线后,每个缺陷回波的等效波幅是 original_amplitude / DAC_ref(depth),这个数值在不同深度间可比,缺陷定量的主人才能看明白。
4. B 扫与 C 扫成像:把一条信号线变成二维探伤图
4.1 B 扫灰度图:imagesc 的正确用法与坐标轴换算
A 扫只管一个点,实际探伤要沿直线扫查。探头每移动一步存一条 A 扫,一组 A 扫按位置堆叠成矩阵就是 B 扫矩阵,用 imagesc 显示成灰度图:
ascan_matrix = []; % 每一行是一条 A 扫 probe_pos = []; % 每个位置坐标, 单位 m for k = 1:numel(filelist) a_scan = load_a_scan(filelist{k}); % 读取一条 A 扫 ascan_matrix(k, :) = a_scan.'; probe_pos(k) = (k - 1) * 1e-3; % 步进 1 mm end depth_mm = c * t * 1e3 / 2; % 时间轴换算深度 imagesc(probe_pos * 1e3, depth_mm * 1e3, ... 20 * log10(ascan_matrix + eps)); set(gca, 'YDir', 'reverse'); % 深度越大在图像下方 xlabel('扫查位置 (mm)'); ylabel('深度 (mm)'); colorbar;逻辑说明:imagesc 的第二个参数是 y 轴,第三个是强度矩阵。这里把时间轴换算成深度轴,再用 set 反转向 y 轴,让图像上浅下深,符合探伤习惯。显示幅度用 20*log10 转成 dB,否则微弱回波在灰度图上完全看不出来。
参数说明:ascan_matrix 的每一行对应一个探头位置,所以调用 imagesc 时矩阵不用转置。如果你发现图像横竖反了,先查维度是 k 行还是 k 列,这是 B 扫显示最常见的错误来源。
4.2 C 扫峰值与深度成像:二维分布怎么组织数据
C 扫是在一个矩形区域内扫查,每个位置取回波包络的峰值和峰值时间,生成两幅图:峰值幅度图和深度图。
[peaks, locs] = max(env_matrix, [], 2); % 每条 A 扫的最大峰 tof_us = (locs - 1) / fs * 1e6; depth_mm = c * tof_us * 1e-3 / 2; figure; subplot(1, 2, 1); imagesc(x_mm, y_mm, reshape(peaks, ny, nx)); % 峰值图 xlabel('x (mm)'); ylabel('y (mm)'); title('峰值幅度 C 扫'); colorbar; subplot(1, 2, 2); imagesc(x_mm, y_mm, reshape(depth_mm, ny, nx)); % 深度图 xlabel('x (mm)'); ylabel('y (mm)'); title('缺陷深度 C 扫'); colorbar;逻辑说明:env_matrix 的行是每条 A 扫,列是时间采样点。max 沿第二维取每行的最大值和位置,得到两个列向量。reshape 是把一维扫描序列按扫查网格还原成二维矩阵,顺序要和扫查路径一致,不然图像会错乱。
常见做法是把扫查路径设计成蛇形或光栅形,数据存储顺序和物理位置一一对应。如果用了蛇形路径,每隔一行的数据要翻转,否则图像会出现锯齿状错位。
4.3 图像增强处理:借用 matlab 图像处理思路看 B 扫
B 扫原始灰度图噪点多,缺陷边界模糊。后续定量处理前我会先做两步图像处理:中值滤波去噪,形态学闭运算把断裂的小缺陷连起来。
img_db = 20 * log10(ascan_matrix + eps); img_f = medfilt2(img_db, [3 3]); % 3x3 中值滤波 mask = img_f > -12; % 阈值分割, 根据噪底调 mask = bwmorph(mask, 'close', 2); % 闭运算补洞 figure; imagesc(probe_pos * 1e3, depth_mm * 1e3, mask); set(gca, 'YDir', 'reverse'); xlabel('扫查位置 (mm)'); ylabel('深度 (mm)');参数说明:medfilt2 的 [3 3] 窗口适合 B 扫这种行密列密的图,窗口太大会抹掉小缺陷。阈值 -12 dB 是从灰度直方图的噪底估计的,实际每批数据不一样。bwmorph 的 close 操作把相邻的白色区域连接,2 是迭代次数,小缺陷相隔很近时才需要开到这个值。
图像处理在探伤里的边界要清楚:它可以改善显示质量、辅助人工判读,但不能用来证明一个小缺陷是否存在。缺陷定论还得回到 A 扫原始包络特征上去。
5. MATLAB 超声探伤最容易翻车的 5 个坑:现象、原因与解决
5.1 采样率看着够,深度计算却系统性偏差
现象:仿真时用 20 MHz 采样 5 MHz 探头,波形不混叠,但计算出的缺陷深度总比真实值偏大或偏小几十微米。
原因:采样率只满足波形显示,不满足峰值定位精度。20 MHz 采样间隔 50 ns,声速 5920 m/s 时对应深度误差约 0.15 mm。包络峰值在采样点之间,直接用离散峰值位置就会带量化误差。
解决:对包络做插值后再找峰,或者提高采样率。插值用 spline 对包络做 4 倍过采样:
env_interp = interp1(t, env, ... linspace(t(1), t(end), length(t) * 4), 'spline');插值后重新找峰,深度精度明显提升。这个方法不增加硬件成本,适合离线处理。
5.2 中文注释乱码加中文路径,脚本直接崩
现象:从老项目里拷贝的 .m 文件用 MATLAB 打开,中文注释变成乱码,某些版本直接报语法错误。脚本所在路径含中文时 load 命令找不到文件。
原因:Windows 中文系统下,MATLAB 默认按系统编码读文件。现代编辑器把 .m 存成 UTF-8,旧版脚本是 GBK,两者不匹配就是乱码。
解决:项目内统一英文注释,文件命名只用字母、数字和下划线。中文说明单独放 README.txt,不要混进代码文件。如果手头脚本已经乱码,用编辑器另存为 UTF-8,并在 MATLAB 里执行:
feature('DefaultCharacterSet', 'UTF-8');这个设置重启后失效,所以更要靠代码文件本身统一编码。这是我踩过最莫名的坑,一个中文注释能让你排查半天语法错误。
5.3 阈值选全局最大值,底面回波被当成缺陷
现象:把 MinPeakHeight 设成全局最大包络值的一部分后,检测出来的第一个峰不是缺陷,而是底面回波。
原因:底面回波通常比缺陷回波强得多,全局阈值一高,弱的缺陷峰直接被滤掉。阈值看似合理,实际没结合探伤场景。
解决:只允许在缺陷可能出现的时间窗内找峰。探伤里叫闸门,MATLAB 里实现就是加一个索引范围:
gate = (t > 5e-6) & (t < 30e-6); % 从始波后到底面波前 env_gate = env; env_gate(~gate) = 0; [peaks, locs] = findpeaks(env_gate, ... 'MinPeakHeight', 0.1, ... 'MinPeakDistance', fs/fc/2);闸门范围要基于样品厚度算:底面波到达时间 tB = 2·d/c,缺陷检测窗口设在 tB 之前。窗口开太大把底面波包进来,开太小漏检靠近底面的缺陷。
5.4 声速用默认值没改,所有缺陷整体偏向同一方向
现象:用钢的 5920 m/s 去算铝试块,所有缺陷深度偏差约 15%,方向完全一致。
原因:探伤定量是拿声速当尺子。不同材料纵波声速差异很大:钢约 5920 m/s,铝约 6300 m/s,有机玻璃约 2700 m/s。用错材料相当于拿错误的尺子量长度。
解决:每个项目用已知厚度试块做一次实测校准。取试样底面回波时间 tB,反推实际声速:
d_known = 25e-3; % 标准试块已知厚度 tB = 8.43e-6; % 实测底面回波时间 c_eff = 2 * d_known / tB; % 校准后的声速 fprintf('校准声速: %.1f m/s\n', c_eff);一次实测校准,后面所有深度换算都走这个值。不要用材料手册的理论值当默认值用,实测才是真值。
5.5 批量 load 回波数据内存爆掉
现象:几百个 .mat 文件用 load 循环读入,文件稍大点 MATLAB 直接提示内存不足,命令行失去响应。
原因:load 把整个变量载入内存,循环里又不清理临时变量,数据积累到一定量就爆了。
解决:改成逐条处理、保存结果、立即清变量:
results = zeros(numel(filelist), 2); for k = 1:numel(filelist) data = load(filelist{k}, 'a_scan'); % 只读需要字段 [p, loc] = process_one_scan(data.a_scan); results(k, :) = [p, loc]; clear data; endload 指定字段名只读该变量,clear 及时释放内存。真遇到超大文件,用 memmapfile 做内存映射,只按需读取数据段,这是最后一招。
6. 批量处理管道与校准验证:一台普通电脑也能扛两百个文件
批量探伤数据处理,我一般用 parfeval 建并行池,把每个文件的处理任务分给 worker:
p = gcp('nocreate'); if isempty(p) p = parpool('Processes'); % 新版 MATLAB 默认进程池 end futures = parallel.Future.empty; for k = 1:numel(filelist) futures(k) = parfeval(p, @process_one_scan, 2, filelist{k}); end all_results = zeros(numel(filelist), 2); for k = 1:numel(filelist) [idx, p, loc] = fetchNext(futures); all_results(idx, :) = [p, loc]; end逻辑说明:parfeval 每调用一次提交一个任务,fetchNext 按完成顺序取结果,idx 告诉你结果属于哪个文件。这个模式把 CPU 多核用满,两百个文件的分批处理从几分钟压到几十秒。
验证技巧不能省:批量跑完先抽查三个已知深度缺陷,对比检测深度和真实深度,偏差超过 1% 就要回查声速校准值和采样率设置。我每次跑批量前会先单文件跑通链路,确认参数没被改坏再上全量。这个习惯让我少浪费了很多等待时间,希望帮到你。
本文还有配套的精品资源,点击获取