
MATLAB生成blocks、bumps和doppler标准测试信号做信号处理、小波分析或者压缩感知的朋友一定绕不开这几类经典测试信号blocks、bumps、doppler。它们最早由Donoho和Johnstone在1994年前后提出用来评估小波去噪、稀疏表示、阈值收缩等算法的性能这几十年来几乎成了这个领域的“标准benchmark”。不管你是刚入门的学生还是需要快速验证算法效果的研究者手头有一套能一键生成这三类信号的MATLAB代码绝对能省下大量时间。这篇文章我会先把这三类信号各自的特点和用途讲清楚再给出一套完整的MATLAB实现从数学表达式到代码逐行解读最后配合去噪对比实验演示怎么用这套信号做标准测试。过程中会用我实际调试中踩过的坑和积累的小技巧帮助你把代码改到自己项目里直接能用。1. 内容整体设计与思路拆解1.1 为什么偏偏是这三类信号刚接触这个领域时我也不理解为什么大家都默认用blocks、bumps、doppler而不是随手造几个正弦波叠加或者白噪声。实际用下来才发现这三类信号在设计上非常“刁钻”各自针对算法性能的不同侧面。blocks是分段常数信号在少量位置发生突变其余位置完全平坦。它的特点是小波域表示非常稀疏大部分小波系数接近零只有跨越跳变点的少数系数很大。这正好用来检验算法对“边缘”和“跳变”的保持能力——如果去噪后跳变被磨平了说明阈值处理过度如果跳变附近出现振荡说明噪声没有被有效抑制。bumps信号由多个不对称的高斯型峰叠加而成峰的宽度、高度各不相同叠加后形成一个形态复杂的连续曲线。相比blocksbumps没有尖锐的跳变但峰的陡峭程度差异很大有些峰很窄、有些很宽。它考察的是算法对“局部结构”和“峰值”的恢复能力特别是在峰值密集区域算法很容易把两个相邻峰混淆成一个。doppler信号是一个频率随时间变化的调频信号起始频率很高、振动密集后段频率越来越低、振动稀疏。它的麻烦在于高频段在固定采样率下只有很少几个采样点信号“振”得很快在视觉上几乎连成一片。这用来考验算法对“时频局部化”能力也就是能否在保留高频细节的同时抑制高频段的噪声。把这三类放在一起几乎覆盖了非平稳信号的主要难点突变边缘、幅值陡变、频率漂移。所以算法在它们上面的表现可以比较全面地反映实际应用中的鲁棒性。许多论文里展示去噪效果对比图用的也是这组信号就是因为标准统一、结论可信。1.2 这套方案的选型考量生成这几类信号的实现方法有很多MATLAB File Exchange上有现成脚本Wavelab工具箱里也有完整实现。但我建议自己动手写一套函数原因主要有三个。第一现成脚本往往包含大量无关代码。很多工具箱为了通用性加入了信号增强、边界延拓、多分辨率分析等功能传到自己项目里既占空间又容易和已有变量名冲突。自己写一个精简版只保留核心逻辑后期维护和调试都轻松。第二自己写可以自由调整参数。标准版blocks只有11个跳变点、bumps只有11个峰但某些实验需要更多突变、更密集的峰或者更宽的频带。自己实现后改一个数组就能搞定不用去翻工具箱内部代码。第三生成函数本身就是理解信号结构的捷径。blocks为什么是分段常数因为它的数学定义就是常数乘以指示函数。bumps为什么看起来高低不平因为每个峰都是四次有理函数分母趋近零时峰陡峭上升。这些细节靠“看别人代码”是很难消化的自己写一遍才能刻进脑子里。我在下文的实现中将n采样点数作为函数唯一输入参数输出长度为n的列向量。这样无论做512点还是4096点实验都是调用一次函数的事实验对比不同采样率时非常方便。另外信号都做了一定程度的归一化处理幅值范围大致在[-6, 6]之间这个区间在实际实验里比较好用——既不会让信号淹没在噪声里也不会因为幅值过大导致数值溢出。2. 核心细节解析与实操要点2.1 blocks信号的数学原理与代码实现blocks信号的数学定义是一个加权指示函数之和。首先在[0,1]区间上取一组跳变位置pos_j每个位置对应一个高度h_j那么blocks信号在任意点t处的值等于所有满足t大于等于pos_j的项的高度之和。这里的关键是“指示函数”的思想位置在跳变点之前该项为0位置在跳变点之后该项为1再乘以高度。这个过程用MATLAB的sign函数可以非常简洁地实现。其中pos_j的经典取值如下跳变点位置对应高度0.1040.13-50.1530.23-40.2550.40-4.20.442.10.654.30.76-3.10.782.10.81-4.2下面给出完整函数代码function y blocks(n) % BLOCKS 生成Donoho-Johnstone blocks标准测试信号 % 输入n为采样点数输出y为n行1列的列向量 t (0:n-1) / n; % 跳变点位置和对应高度 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 -5 3 -4 5 -4.2 2.1 4.3 -3.1 2.1 -4.2]; y zeros(n, 1); for j 1:length(pos) y y hgt(j) * (1 sign(t - pos(j))) / 2; end end这里最关键的一行是(1 sign(t - pos(j))) / 2。当t小于pos(j)时sign返回-1这一整项为0当t大于pos(j)时sign返回1这一整项为1。在t恰好等于pos(j)时sign返回0该项是0.5倍高度形成一个“半台阶”。实际离散采样中这种情况极少发生可以忽略。运行y blocks(1024); plot(y);就能看到典型的阶梯状信号。从0.10位置开始抬升到0.13突然下坠再到0.15回升整条曲线由若干个不同高度的平台拼接而成。值得注意的是最后一个跳变点0.81之后信号一直保持一个非零的直流分量这是正常现象不代表信号有偏置问题。2.2 bumps信号的数学原理与代码实现bumps信号是多个四次有理函数的叠加。每个峰的形状由三个参数决定位置pos_j、高度hgt_j、宽度wth_j。第j个峰的表达式是hgt_j / (1 ((t - pos_j)/wth_j)^4)。之所以用四次而不是二次是因为四次函数在远离中心时衰减更快峰的“尾巴”更窄彼此之间的重叠比高斯函数少更容易看到独立的峰形。经典参数如下峰位置峰高度峰宽度0.1040.0050.1350.0050.1530.0060.2340.0100.2550.0100.404.20.0300.442.10.0100.654.30.0100.763.10.0050.785.10.0080.814.20.005这里要注意峰宽度的取值0.005和0.010在数值上相差一倍但实际信号形态差异很大。宽度越小峰越尖锐对采样率的要求也越高。如果在n256时生成bumps信号最窄的峰可能只占两三个采样点就会产生明显的走样。我建议做实验时n至少取1024才能看到平滑完整的峰形。function y bumps(n) % BUMPS 生成Donoho-Johnstone bumps标准测试信号 t (0:n-1) / n; 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 5 3 4 5 4.2 2.1 4.3 3.1 5.1 4.2]; wth [0.005 0.005 0.006 0.010 0.010 0.030 0.010 0.010 0.005 0.008 0.005]; y zeros(n, 1); for j 1:length(pos) y y hgt(j) ./ (1 ((t - pos(j)) / wth(j)).^4); end end这里用了./和.^而不是/和^是因为t是列向量要对每个元素做运算。pos数组和t做减法时会自动广播维度MATLAB R2016b及以后版本都支持这个特性老版本需要改用bsxfun(minus, t, pos(j))。运行y bumps(1024); plot(y);可以看到约11个高矮不同、宽窄不一的峰几个宽的峰在0.40附近叠成一个较大的隆起窄峰则在各自位置画出尖锐的顶点。这个信号不像blocks那样有明显的跳变但局部梯度的变化非常剧烈对小波系数来说相当于在高频子带里布满了能量。2.3 doppler信号的数学原理与代码实现doppler信号的定义是在t从0到1范围内取sqrt(t .* (1 - t)) .* sin(2 * pi * 1.05 ./ (t 0.05))。这个式子看着简单但里面有几个关键设计不注意很容易在实现时出错。首先是前面的sqrt(t .* (1 - t))它是一个在0到1之间呈拱形的包络在两端趋近于0。没有这个包络的话doppler信号在高频段会保持恒定的振幅视觉效果和图谱特征都会变差。有了它信号在t靠近0时振幅被压得很低噪声掩盖信号的效果就会更明显——这正是我们想要测试的困难场景。其次是分母上的t 0.05。当t趋近0时分母趋近0.05频率参数2 * pi * 1.05 / 0.05约为131.9对应的角频率并不算极度夸张但如果直接除以t在t0处会出现无穷大频率MATLAB会给出NaN或Inf警告整个信号就没法用了。加一个很小的偏移量0.05本质上是一种“截断”策略让最高频率可控。标准定义中频率调制系数alpha通常取1.05这是Donoho论文里的经验值。如果你需要更高频的测试信号可以把这个系数调大如果希望低频段更平缓可以减小它。但改的时候要注意观察图形不要调到信号在视觉上完全糊成一片。function y doppler(n) % DOPPLER 生成Donoho-Johnstone doppler标准测试信号 t (0:n-1) / n; alpha 1.05; % 频率调制系数经典取值 y sqrt(t .* (1 - t)) .* sin(2 * pi * alpha ./ (t 0.05)); end运行y doppler(1024); plot(y);可以看到前半段信号振得非常密几乎是一团黑线后半段逐渐稀疏能看出清晰的起伏。如果n取4096前半段的高频细节会更丰富但整体形态与1024点版本一致。2.4 统一封装一键生成三类信号实际做实验时经常需要同时生成多类信号做对比。我习惯把它们封装成一个函数用字符串指定类型function [y, t] genSignal(type, n) % GENSIGNAL 统一生成blocks、bumps、doppler标准测试信号 % type: blocks、bumps 或 doppler % n: 采样点数 t (0:n-1) / n; switch lower(type) case blocks y blocks(n); case bumps y bumps(n); case doppler y doppler(n); otherwise error(未知信号类型%s, type); end end我还习惯另加一个addNoise函数把高斯白噪声按指定信噪比叠加到信号上这部分在下一节详细展开。这样从信号生成到加噪再到去噪实验全程只需两三个函数调用代码非常清爽。3. 实操过程与核心环节实现3.1 加噪信号生成SNR怎么算、噪声怎么加标准测试流程是先把干净信号加上高斯白噪声形成含噪观测再运行待测试的去噪算法。这里最关键的步骤是控制信噪比SNR因为不同论文里SNR的定义方式可能不同有的用dB有的直接用噪声标准差如果没对齐对比实验就不公平。我习惯用dB作为统一单位计算公式是SNR_dB 10 * log10(var(signal) / var(noise))。给定目标SNR_dB可以反推出噪声标准差function noisy addNoise(y, snr_db, seed) % ADDNOISE 给信号叠加高斯白噪声返回带噪信号 % snr_db: 目标信噪比单位dB if nargin 3 rng(seed); % 固定随机种子让实验可复现 end noise randn(size(y)); noise noise / std(noise); % 归一化到单位标准差 signal_power var(y); noise_power signal_power / (10^(snr_db / 10)); noisy y sqrt(noise_power) * noise; end这里先对噪声做了一次归一化确保std(noise)严格等于1然后乘以目标标准差。这样写的优点是无论randn生成多少数据噪声的实际功率都能精确匹配目标SNR。如果不做这步归一化直接用randn乘系数由于有限样本的方差波动实际SNR可能偏差0.2~0.5dB在小样本实验里会影响结论。我在实验里通常生成三组数据每组先生成干净信号再用addNoise(clean, 5, 1)、addNoise(clean, 10, 2)分别得到低信噪比和高信噪比的含噪信号。固定随机种子保证了每次运行结果完全一致这一点在写论文投稿时特别重要——审稿人可以复现你的实验数据一致性问题会少很多。3.2 小波去噪实验从“能跑”到“跑得对”有了标准测试信号最经典的验证方式就是做小波去噪对比。MATLAB的Wavelet Toolbox提供了一整套函数我要演示的是基于Donoho-Johnstone阈值的软阈值去噪流程这也是该领域最基础的指标性方法。去噪过程分三步小波分解、阈值处理、小波重构。% 以blocks信号为例SNR10dB n 1024; [clean, t] genSignal(blocks, n); noisy addNoise(clean, 10, 42); % 第1步小波分解使用db4小波分解层数5 wname db4; level 5; [C, L] wavedec(noisy, level, wname); % 第2步对每一层细节系数做软阈值处理 sigma median(abs(C(L(1)1:L(2)))) / 0.6745; % 噪声标准差估计 thr sigma * sqrt(2 * log(n)); % 通用阈值 % 提取各层细节系数 for k 1:level idx L(k)1 : L(k1); % 第k层细节系数的索引范围 C(idx) wthresh(C(idx), s, thr); % 软阈值 end % 第3步重构 denoised waverec(C, L, wname); % 计算去噪后的SNR snr_out 10 * log10(var(clean) / mean((denoised - clean).^2)); fprintf(输入SNR: %.2f dB, 去噪后SNR: %.2f dB\n, 10, snr_out);这段代码里有两个细节值得展开说。第一个是噪声标准差估计。Donoho和Johnstone提出来的做法是取第一层细节系数即最高频子带的绝对中位差再除以0.6745。0.6745是标准正态分布的第75百分位数这样得到的估计不受信号本身结构的影响对含有尖锐跳变的blocks信号非常稳健。如果你改用std直接估计跳变处的系数会被当成噪声导致阈值偏大把真实信号细节也一起滤掉。第二个是软阈值与硬阈值的区别。wthresh(x, s, thr)返回的是sign(x) * max(abs(x) - thr, 0)也就是把所有系数朝零方向收缩wthresh(x, h, thr)则是保留绝对值大于阈值的原值其余置零。硬阈值重构出来的信号在跳变附近容易产生小幅振荡软阈值更光滑但也会让峰的幅值整体缩小。做实验时两种都跑一下对比效果有助于理解各自特点。同样流程可以用在bumps和doppler上。你会发现一个有意思的现象blocks信号去噪后跳变处的对比度保持得最好bumps信号去噪后窄峰幅值可能被压低宽峰恢复得比较准doppler信号去噪后高频段细节要么保留较好但背景仍有噪声要么背景干净但高频细节丢失。不同算法在这三类信号上的最优参数往往不一样这正是它们作为“标准测试信号”的价值——用一套固定参数跑三类信号谁的表现更均衡谁就更适合实际应用。3.3 从对比结果里能读出什么做完去噪实验不要只看输出SNR数字我强烈建议把干净信号、含噪信号、去噪信号画在同一张图上对比figure; subplot(3,1,1); plot(t, clean); title(Clean); subplot(3,1,2); plot(t, noisy); title(Noisy); subplot(3,1,3); plot(t, denoised); title(Denoised);观察的重点有三个位置blocks的跳变点、bumps的窄峰处、doppler的高频段。跳变点旁边如果出现成对的尖峰说明阈值偏低、噪声泄漏窄峰幅值如果明显变矮说明软阈值收缩过度doppler高频段如果出现周期性波纹说明重构时系数截断得太狠。这三类问题分别对应“欠去噪”“过去噪”和“伪影”是任何去噪算法都会面临的三个基本矛盾。4. 常见问题与排查技巧实录4.1 信号形状不对先检查这四处我在给学生和同事调试代码时发现生成信号环节的问题主要集中在四个地方。第一采样点n取得太小。blocks信号在n64时还能看出大台阶但bumps信号最窄的峰只有0.005宽度在64个点上根本显示不出来看起来就像几个离散点。doppler信号如果n小于256高频段会完全糊成一条线。建议n至少取1024做精细对比时用4096。第二sign函数误用导致台阶位置偏移。blocks里如果写成sign(t - pos(j))而漏掉了除以2那么跳变前是-hgt/2而不是0信号整体会有一个偏移。运行后如果发现信号最低点不是0多半就是这个原因。第三doppler的t 0.05这一项被人为抽掉。有些教程为了“数学严谨”直接除以t结果在t0处产生NaN整个信号全是NaN。如果看见plot出来一片空白先查这一句。第四bumps函数里忘记用./和.^。如果写成1 ((t - pos(j)) / wth(j))^4MATLAB会尝试做矩阵除法报错维度不一致或者给出一个完全错误的结果。这属于点运算和矩阵运算的混淆初学者尤其容易犯。4.2 去噪效果差别急着调参数去噪实验效果不理想时先不要堆参数。我发现很多人第一步就去改阈值倍数结果调了半天也不知道哪个方向是对的。建议按下面顺序排查先确认噪声功率是否正确匹配。打印10*log10(var(noise))看是否接近10*log10(signal_power) - snr_db。如果偏差超过0.1dB检查addNoise函数里的归一化步骤有没有生效。再确认分解层数是否合理。层数太少高频噪声滤不干净层数太多低频逼近系数也被当成噪声处理信号会被整体削平。对n1024的信号db4小波分解5层基本够用n4096可以到7层。这个经验值适用于大部分光滑信号。最后检查阈值是否对每一层都用了同一个值。Donoho-Johnstone的通用阈值sigma * sqrt(2 * log(n))是一个全局阈值对每一层细节系数都做相同收缩。但在实际信号中不同层的噪声能量可能不同同一层内部不同位置的系数也有差异。如果发现某一层噪声残留很重可以换用层内独立估计的阈值。4.3 从“能跑”到“跑得对”的几条经验第一固定随机种子是实验可复现的生命线。别觉得rng(42)这种写法是小题大做一旦论文被要求补充实验、或者审稿人要复现你的结果同样的随机种子会省掉无数解释和纠纷。我在封装addNoise函数时特地设置了第三个可选参数seed默认不指定时用系统时间指定时固定结果兼顾灵活与可复现。第二信号长度尽量取2的幂。小波分解对长度为2的幂的信号最友好wavedec和waverec在边界处理上会更干净。n1024、2048、4096都是常用值。如果你的实验要求任意长度wavedec也能处理但边界延拓方式可能影响首尾效果需要额外留意。第三保存数据时顺便保存参数。别只存一个y.mat至少要在文件名里带上信号类型、采样点数、信噪比和随机种子。我见过太多次实验数据堆在一起事后完全想不起某个文件对应哪组参数最后只能重新跑实验。用genSignal配合命名规则比如blocks_n1024_snr10_seed42.mat既清晰又不会出错。5. 测试信号在更多场景里的扩展用法如果你觉得只做去噪实验不够过瘾这三类信号还能用在很多其他场景。我把这几年用到的几个扩展方向列在这里供你参考。压缩感知方向blocks在DCT基或小波基下是稀疏的bumps在过完备字典下也有稀疏表示doppler则在时频域具有稀疏结构。用它们作为稀疏性先验的测试信号可以验证不同测量矩阵、不同重构算法在有限采样率下的表现。我做过一组实验在1024点信号中只取256个随机观测值再用OMP和BP算法重构对比重构误差和算法耗时效果非常直观。深度学习方向现在很多论文用合成信号训练去噪网络。你可以写一个循环用genSignal批量生成2000组不同参数、不同SNR的blocks/bumps/doppler信号合成训练集和测试集再丢给一个小型卷积网络训练。相比用真实采集的信号合成数据的好处是标签完全已知损失函数可以直接用输出与干净信号之间的MSE训练曲线特别干净。算法评估方向除了信噪比还可以计算结构相似性指标SSIM、峰值信噪比PSNR以及边缘保持指数EPI。不同指标反映的侧重点不同比如SSIM更接近人眼感知PSNR对噪声幅值敏感EPI则直接衡量跳变位置的保持程度。用三类信号分别算这些指标可以画出一张雷达图或者柱状图直观比较不同算法的综合性能。我自己实际体会最深的一点是标准测试信号最大的价值不在于“标准”而在于它制造困难的方式是已知的、可控的。你知道blocks有几个跳变点、bumps每个峰在什么位置、doppler的频率变化规律所以你能够精确地定位算法在哪里失败、为什么会失败。这种“可解释的失败”比单纯追求指标数值更能推动算法改进。最后分享一个我在项目里一直用的小技巧把这几个函数和去噪脚本放在同一个文件夹起名signal_benchmark/然后在主脚本里用addpath(signal_benchmark)引入。以后无论是写新论文还是复现师兄的旧代码都不需要到处找工具包一套小工具就够用了。如果你在实际使用中遇到什么奇怪的报错或者效果问题欢迎回来交流我大概率遇到过类似的坑。