做信号处理的这些年我越来越觉得“标准测试信号”是个好东西。平时写个去噪算法、做个压缩感知实验总不能老拿一段白噪声或者自己现场录的语音来验证效果既没法复现也没法和别人的结果对比。后来我接触到Donoho和Johnstone在经典论文里常用的那组标准测试信号blocks、bumps和doppler一下子觉得实验规范多了。这三个名字在MATLAB领域几乎成了小波去噪、稀疏表示、压缩感知这些方向的“通用语言”你只要说“用blocks信号加噪再用XX算法去噪”同行立刻就知道你在测什么。这篇文章我就把这几个信号的来龙去脉、MATLAB实现代码、参数怎么调、有哪些坑一次性讲清楚。不绕弯子直接上干货保证你照着敲完代码就能跑出结果。1. 三个标准测试信号的数学本质与设计初衷1.1 为什么偏偏是blocks、bumps和doppler很多刚接触的人会问测试信号那么多为什么几十年来大家反复用这三个先说结论因为它们各自代表了信号处理里最典型、最难处理的局部特征用来检验算法非常“刁钻”。blocks是分段常数信号中间有一堆突变的“跳跃点”。这种信号考验的是算法对边缘、跳变的保持能力——你一平滑台阶就糊了你不敢平滑噪声又去不掉就这么烦人。bumps则是几个形状尖锐、幅度差异很大的脉冲峰峰和峰之间还有宽窄变化专门用来检验算法对瞬态成分和局部奇异的捕捉能力。doppler更狠它是一个频率随时间剧烈变化的调频信号而且幅度还跟着时间轴起伏专门考验算法在时频分析上的分辨率尤其是对高频段局部细节的还原。Donoho那批人设计这些信号的时候目的非常明确一个信号里既要有多尺度结构又要有局部突变还要有全局趋势这样算法好不好一测就露馅。这也是为什么这些信号能一直火到现在而不是被随机噪声或者正弦波取代。1.2 数学定义逐项拆解三个信号在MATLAB里的标准实现都有明确公式先看数学定义再写代码不容易出边界错误。blocks信号是若干个不同高度、不同位置的单位阶跃函数的加权叠加。它的定义可以写成如下形式$$ f(t) \sum_{j1}^{m} h_j \cdot \mathbb{I}(t \ge p_j) $$其中$$\mathbb{I}(\cdot)$$是指示函数$$p_j$$是跳变位置$$h_j$$是对应的跳变幅度。原始论文里给了一组固定参数位置$$p$$为 [0.10, 0.13, 0.15, 0.23, 0.25, 0.40, 0.44, 0.65, 0.76, 0.78, 0.81]对应的高度$$h$$为 [4, -5, 3, -4, 5, -4.2, 2.1, 4.3, -3.1, 2.1, -4.2]。这些参数不是随便拍的负高度意味着向下跳高度绝对值大小决定了信号动态范围这样合出来的信号既有大幅跳变又有小幅波动。bumps信号的定义相对复杂一点它是一组以位置$$p_j$$为中心、宽度$$w_j$$为尺度的有理函数峰叠加$$ f(t) \sum_{j1}^{m} \frac{h_j}{\left( 1 \left|\frac{t - p_j}{w_j}\right| \right)^4} $$这里$$h_j$$是峰高$$w_j$$是峰宽。注意分母是四次方而不是平方这样衰减更快峰的形状更尖。位置参数和blocks基本一样但峰的个数、高度、宽度都有微调具体参数后面代码里直接给。doppler信号则是一个典型的调幅调频信号$$ f(t) \sqrt{t(1-t)} \cdot \sin\left( \frac{2\pi \cdot 1.05}{t 0.05} \right) $$其中$$t$$从 $$1/n$$ 取到 1。根号项是一个平滑的幅度包络在两端趋于零正弦项的瞬时频率随$$t$$增大而升高越靠近右端频率越高而且因为分母里加了0.05避免在$$t0$$处产生奇异。这个信号做小波分析的时候高频段的系数会非常密能有效考察时频局部化能力。1.3 三个信号的典型应用场景这三个信号在学术论文里出场频率极高。我列几个最常见的场景供你参考实际用的时候可以直接对号入座。第一类是信号去噪与恢复对比。这是最经典的应用往三个干净信号上叠加不同强度的高斯白噪声然后跑各种去噪算法小波阈值、全变分、字典学习等用信噪比提升、均方误差这些指标来横向对比。因为信号本身就是公开标准谁都能复现结论才有说服力。第二类是压缩感知与稀疏重建。blocks和bumps信号在某个小波基或字典下通常是稀疏的用来验证压缩感知重构算法非常合适。doppler则因为频率变化剧烈更考验基函数的适配程度。第三类是时频分析与特征提取教学。我在高校做技术分享的时候经常用这三个信号当课堂案例因为它们特征清晰、可视化效果好学生一眼就能看出不同算法对局部特征的保留差异。2. MATLAB代码实现与逐行拆解2.1 手把手生成blocks信号代码我直接贴出一份可靠的实现这段代码与文献中常用的Wavelab工具包逻辑一致但去掉了多余依赖适合复制到自己的脚本里。function x sig_blocks(n) % n: 信号长度建议取 2 的幂如 1024、2048 if nargin 1 n 1024; end pos [0.10 0.13 0.15 0.23 0.25 0.40 0.44 0.65 0.76 0.78 0.81]; hgt [4.0 -5.0 3.0 -4.0 5.0 -4.2 2.1 4.3 -3.1 2.1 -4.2]; t (1:n) / n; x zeros(1, n); for j 1:length(pos) x x hgt(j) * ( t pos(j) ); end end这里有个关键设计(t pos(j))返回逻辑数组在位置参数之后为1之前为0。因为MATLAB里逻辑数组能直接参与数值运算所以累加过程非常干净。要注意的是我把时间轴设成了从 $$1/n$$ 到 1而不是从0开始这样能避免在t0处产生不必要的边界台阶。有的版本会把t定义为(0:n-1)/n即从0取到(n-1)/n。两种写法画出来的图形状略有不同从0开始时信号在起点附近正好落在某个跳变沿上从1/n开始时起点附近通常是0值平台。我个人建议使用从1/n开始的版本因为和doppler信号的实现保持一致三套信号的时间轴完全对齐后续叠加加噪会更方便。2.2 bumps信号的MATLAB实现bumps信号的参数比blocks多一组峰宽代码结构也更丰富。function x sig_bumps(n) % n: 信号长度建议取 2 的幂 if nargin 1 n 1024; end pos [0.10 0.13 0.15 0.23 0.25 0.40 0.44 0.65 0.76 0.78 0.81]; hgt [4.0 5.0 3.0 4.0 5.0 4.2 2.1 4.3 3.1 5.1 4.2]; wth [0.005 0.005 0.006 0.01 0.01 0.03 0.01 0.01 0.005 0.008 0.005]; t (1:n) / n; x zeros(1, n); for j 1:length(pos) x x hgt(j) ./ (1 abs((t - pos(j)) / wth(j))).^4; end endbumps信号的不同之处在于每个峰的高度全部为正但峰宽参数差异很大。最窄的峰宽是0.005在1024个采样点上大约只占5个点左右非常尖锐最宽的峰宽是0.03能在图上占30个点左右看起来就平缓很多。这种多尺度特性正好可以用来检验算法对“不同宽度瞬态”的敏感程度。实现的时候注意点运算符./和.^都不能漏。因为t是向量wth(j)是标量分子分母都是数组必须用点除。我第一次写这段代码时把./写成了/MATLAB直接报矩阵维度不匹配排查了半天才发现是符号问题。2.3 doppler信号代码与频率特性分析doppler信号的代码是三段里最简洁的但它的信号特性却最复杂。function x sig_doppler(n) % n: 信号长度建议取 2 的幂 if nargin 1 n 1024; end t (1:n) / n; x sqrt(t .* (1 - t)) .* sin(2 * pi * 1.05 ./ (t 0.05)); end这段代码看起来短但有两处细节值得说。第一t .* (1 - t)是点乘得到的是逐元素的乘积不是矩阵乘法少了点运算符结果就完全不对。第二sin函数内部是2 * pi * 1.05 ./ (t 0.05)其中./把标量除以整个向量得到瞬时角频率的积分效果。从频域角度看doppler信号的瞬时频率可以近似写作$$ f_{\text{inst}}(t) \approx \frac{1.05}{(t 0.05)^2} $$也就是说在t接近0的时候频率极高在t接近1的时候频率较低整个信号呈现出“从高频啁啾到低频”的扫频特性。再加上根号项在两端压低了幅度信号起点和终点附近能量都很弱形成了典型的“两端尖中间鼓”的时频分布形态。2.4 封装一个万能生成函数实际做实验的时候我通常不会一个个调用上面的单信号函数而是封装成一个总入口想生成哪个就生成哪个顺便把默认参数也管起来。function [x, t] gen_test_signal(sigName, n) % 按名称生成标准测试信号 % sigName: blocks / bumps / doppler % n: 采样点数默认1024 % x: 信号值 % t: 对应的时间轴 if nargin 2 n 1024; end t (1:n) / n; switch lower(sigName) case blocks x sig_blocks(n); case bumps x sig_bumps(n); case doppler x sig_doppler(n); otherwise error(未知信号名%s, sigName); end end封装之后以后跑对比实验就非常省事。比如我要比较三种信号在同样噪声水平下的去噪效果直接循环调用这个函数就行不用每个地方都写一遍t的定义。这也是我个人的一个习惯把试验中常用的小工具尽量都包成函数既能减少重复代码也能避免因为某次复制粘贴漏改参数而导致结果对不上。3. 信号可视化与噪声注入的实操细节3.1 画图脚本一张图看出信号结构生成信号之后第一步永远是画图观察。如果图形形态不对劲后面的实验基本没有意义。我提供一个标准画图脚本你可以直接拿来用。n 1024; t (1:n) / n; x_blocks sig_blocks(n); x_bumps sig_bumps(n); x_doppler sig_doppler(n); figure(Color, w, Position, [100 100 1200 700]); subplot(3, 1, 1); plot(t, x_blocks, LineWidth, 1.2); title(Blocks 信号); xlim([0 1]); ylim([-6 6]); grid on; subplot(3, 1, 2); plot(t, x_bumps, LineWidth, 1.2); title(Bumps 信号); xlim([0 1]); ylim([0 7]); grid on; subplot(3, 1, 3); plot(t, x_doppler, LineWidth, 1.2); title(Doppler 信号); xlim([0 1]); grid on;画图时有两个直观检查点。第一个是blocks信号如果画出来台阶边缘是斜的而不是垂直突变说明采样点太少比如n只有64时台阶看起来就有点“拖泥带水”第二个是bumps信号如果峰的最高点不在0.25附近而是跑到别处去了说明pos参数或wth参数敲错了。另外建议把ylim固定下来特别是blocks信号。因为它的动态范围从-5到5如果不固定纵轴每次画图的范围可能不同视觉效果上会造成信号形态“好像变了”的错觉。3.2 加噪声信噪比到底怎么算才规范标准测试信号的一大用途就是加噪实验。加噪的办法本身很简单就是给干净信号叠加上高斯白噪声但信噪比的定义需要严谨。信噪比通常用dB表示定义式是$$ \text{SNR}{\text{dB}} 10 \log{10} \left( \frac{P_{\text{signal}}}{P_{\text{noise}}} \right) $$其中$$P_{\text{signal}}$$是信号的平均功率一般用均方值$$\frac{1}{n}\sum x_i^2$$来估计$$P_{\text{noise}}}$$是噪声的平均功率即噪声方差$$\sigma^2$$。所以给定目标SNR后噪声标准差的计算公式是$$ \sigma \sqrt{\frac{P_{\text{signal}}}{10^{\text{SNR}/10}}} $$MATLAB里面可以这样写function noisy add_awgn_noise(x, snr) % 按指定SNR(dB)添加高斯白噪声 P_signal mean(x.^2); sigma sqrt(P_signal / (10^(snr / 10))); noise sigma * randn(size(x)); noisy x noise; end这里有几个细节必须注意。第一randn每次生成不同的噪声如果要做可复现实验运行前要设随机种子比如rng(2024)否则每次结果都不一样论文里的数据也会对不上。第二我的实现是按照噪声方差等于要加的标准差平方来生成的没有额外缩放所以SNR计算公式严格成立不要在这上面再乘任何系数。第三实际算信噪比的时候有人的习惯是用snr(x, noise)函数但要注意这个函数对噪声功率的计算方式是“估计噪声方差”和我这里直接根据定义生成的逻辑在结果上非常接近但不会完全一致写实验报告时说明清楚就好。3.3 把生成结果保存成文件有些场景下比如要在Python里接着处理需要把MATLAB生成的信号导出成.mat或CSV文件。我最常使用的是.mat格式命令很简单n 2048; x_blocks sig_blocks(n); x_bumps sig_bumps(n); x_doppler sig_doppler(n); save(test_signals_2048.mat, x_blocks, x_bumps, x_doppler);如果要在其他语言里读取我更推荐CSV格式一行代码搞定s [gen_test_signal(blocks, 2048); ... gen_test_signal(bumps, 2048); ... gen_test_signal(doppler, 2048)]; writematrix(s, test_signals_2048.csv);导出的时候要注意决定保存之前先想清楚后续处理需要哪个版本的信号。如果是要对比不同算法的去噪效果最好从同一份干净信号出发“一次性”生成所有加噪版本再分别处理这样才公平。如果你每跑一个算法就重新生成一次干净信号虽然理论上一样但因为随机种子的关系结果反而容易产生无谓的细微差异。4. 常见问题与排查技巧实录4.1 生成的blocks信号为什么和其他论文的图对不上这是被问得最多的一个问题。明明照着网上代码敲的为什么画出来的blocks信号和论文里的台阶位置不太一样原因多半在于时间轴的定义。不同版本的代码有的把t定义成(0:n-1)/n有的定义成(1:n)/n这会导致台阶在零点附近的位置不同。文献里Wavelab工具箱用的是(1:n)/n所以你在复现论文时最好用这个定义。如果你发现自己的信号整体向右或向左偏移了一个采样点那多半是时间轴差了一个1/n。还有一个常见原因是对参数pos和hgt抄错。网上流传的版本很多有些是早期人翻录时把-4.2抄成了-4.1把0.40抄成了0.4数值上等价但倍数没对齐。我建议以本文或Wavelab官方源码中的参数为准。4.2 信号的边界效应怎么处理做小波去噪时经常有人发现信号两端会出现“飞边”或“振荡”比如doppler信号在t接近1的位置波形发生明显畸变。这不是信号生成的问题而是后续算法处理时的边界效应。信号在有限长度上截断就相当于乘了一个矩形窗频率域里会产生旁瓣泄漏。要减轻这种效应有三种常见做法对小波分解时扩展模式改为“对称延拓”MATLAB里用dwtmode(sym)。对信号两端做余弦渐变衰减或反射延拓然后再处理。用周期化延拓方式重构后裁剪掉延拓区域。我个人经验是如果你只是比较算法相对优劣保持所有算法用同一种边界处理方式即可但如果要追求绝对高的重建精度那最好把信号长度取得足够长并把边界区域排除在评价指标之外。4.3 加噪后信号信噪比不对怎么排查很多新手会发现自己加完噪声之后用snr函数算出来的实际信噪比和预设值差了好几个dB于是开始怀疑是不是代码写错了。我排查这类问题一般按顺序检查三件事。第一randn生成的是标准正态分布均值为0、方差为1先确认自己生成的噪声数组满足这个统计特性样本量太小时均值和方差会有随机波动这个正常。第二检查信号功率的估计方式用mean(x.^2)还是sum(x.^2)/n结果基本一样但如果信号包含直流分量比如blocks信号的平均值不为0有人会先减均值再算功率这样得到的噪声方差会偏大或偏小。第三检查计算信噪比的公式里是否用了10还是20。功率比要用10如果误用20算出来的值会翻倍这是最容易犯的错。4.4 多算法对比时的公平性问题最后再说一个做实验时容易忽略的公平性问题。如果你拿这三个信号对比三种算法那么务必保证所有算法读入的是同一份加噪数据而不是每轮重新生成。否则随机噪声的不同会让比较结果产生额外波动搞得算法的好坏根本说不清。好的做法是先一次性生成所有测试信号并加好噪声存成MAT文件然后所有算法都从这个文件里读数据。我在自己的实验里还会额外保存一个seed_info.txt记录当时用到的随机种子和信噪比参数这样一个月之后回头查数据还能准确复现当时的实验条件。5. 一个实用小技巧把三信号做成实时函数可能有人觉得上面封装的gen_test_signal已经够用了我这边再分享一个更灵活的做法。实际做算法调试时我有时会希望直接改参数看效果比如把bumps信号的峰宽整体调大看算法对平滑峰是否更加敏感。所以我平时会把参数表定义成可选项。function x sig_bumps(n, scaleWidth) if nargin 2 scaleWidth 1; end wth [0.005 0.005 0.006 0.01 0.01 0.03 0.01 0.01 0.005 0.008 0.005] * scaleWidth; % ...其余不变 end有了这个参数调峰宽就是个乘法的事。同理doppler信号的频率参数也可以做成输入项。这种小改动对写论文或做演示很有用因为审稿人或听众如果质疑“你的算法只在固定频率范围上有效”你可以直接现场调参数再跑一遍结果说服力比讲一堆理论要强得多。我个人在实际使用中还有一个体会不要把这三个信号神化。它们虽然经典但都是有特定偏好的测试集比如blocks信号对边缘检测算法的挑战大对纯频谱类算法就相对宽松。做实验时最好把它们和随机信号、真实信号配合使用综合作出判断。毕竟标准测试信号的核心价值在于“可复现、可比对”而不是指望一套信号覆盖天下所有问题。最后如果你刚开始接触这一块我的建议很简单先把本文的代码原样跑一遍画出图加个噪声再试着用小波阈值去噪看看效果这一套流程走下来你对这三个信号的脾气基本就摸透了。