前阵子处理一段传感器信号时我遇到一个特别典型的困境数据里明明有很关键的局部小峰值结果一用常规的滑动平均去噪峰值幅度被削掉一大半位置还往后偏移了好几个采样点。后来换成线性加权滑移平均算法才算把那些小峰从噪声里“捞”出来并且定位基本不偏。这篇文章就把我这次实战中踩过的坑、调参的思路和最终的 Matlab 实现一起梳理出来。适合正在做信号预处理、特征提取或者想理解加权滑动平均原理的朋友。只要你用过一点 Matlab哪怕不太熟按下面的代码也能直接跑起来。1. 滑移平均为什么会“吃掉”小峰值1.1 普通滑移平均的工作机制与两个副作用普通滑移平均也叫等权移动平均的思路很简单开一个固定长度的窗口把窗口内所有采样点加起来除以窗口长度作为窗口中心点的输出值。Matlab 里一个 5 点滑动平均等效于对信号做如下卷积w_rect ones(1,5) / 5; y_rect conv(x, w_rect, same);从时域看它确实能把高频噪声压下去但从信号形态看它有两个致命的副作用。第一个副作用是幅度衰减和宽度展宽。一个孤立的尖峰例如采样序列[0 0 10 0 0]经过 5 点等权平均后输出变成[0 2 2 2 0]这里按卷积完整计算后取整理解峰值从 10 直接掉到 2而且能量被摊开到三个点上。小峰值本来就只有一点点幅度再这么一平均直接淹没在噪声和基线起伏里。第二个副作用是相位滞后。常规向右滑动的移动平均输出的峰值位置会比真实峰值位置滞后大概(N-1)/2个采样点。假设窗口长度为 11那你检测到的“峰”已经比真实位置晚了 5 个点。在做时间对齐、多通道对比的时候这 5 个点的误差足以让后续计算的传播速度、到达时差完全失真。换句话说普通滑移平均适合看趋势不适合做精确的事件定位。1.2 线性加权滑移平均的思路让中心更有话语权线性加权滑移平均本质上是给窗口内每个采样点分配一个按线性规律变化的权重。峰值检测场景里必须采用“中心对称”的权重分配离中心越近的采样点权重越大离中心越远权重越小两侧对称。以窗口长度N 2m 1为例第k个点的权重可以写成w(k) m 1 - abs(k - m - 1); % k 1..N中心权重最大再用所有权重之和做归一化。这样做的直观效果是信号里一个孤立尖峰经过滤波后虽然也会被展宽一点但因为中心点的权重远大于周围点峰值幅度能保留更多同时由于权重完全对称滤波后的信号不会产生固定的相位偏移。用生活里的例子解释等权平均像“一人一票”少数派的声音容易被淹没线性加权平均像“按资历投票”中心附近的意见权重更高因此个别关键样本不会因为周围一片平庸的邻居而被彻底拉低。对称线性窗口保留了平滑作用又给“瞬态小峰”开了一扇门。为什么选择线性权重而不是更复杂的指数权重或高斯权重一是线性权重实现简单逻辑直观调参只需要确定窗口长度一个参数二是线性窗与矩形窗、高斯窗相比主瓣宽度较窄对窄峰的响应更好同时还保留了一定的旁瓣抑制能力能兼顾平滑和保峰。当然它也有自身局限后面我会详细对比。2. 算法设计权重向量与 Matlab 实现2.1 权重向量的构造与归一化使用线性权重前先把“线性”定义清楚。假设窗口长度为奇数N 2m 1我们构造一个从边缘向中心线性增大的权重序列。为了书写方便先定义索引向量idx 1:N中心索引center m 1那么每个点的权重为function w linear_centered_weights(N) m (N - 1) / 2; k 1:N; w m 1 - abs(k - (m 1)); w w(:); end以N7为例未归一化的权重是[1 2 3 4 3 2 1]。归一化时除以权重和sum(w)16。归一化有一个必须注意的细节直流增益必须为 1。也就是说如果输入是一段常数信号输出应该还是同一个常数。只有把权重和归一化到 1才能保证平滑过程不会改变信号的基线水平。否则滤波后整体幅值会被放大或缩小后续峰值阈值判断全都会出错。频域特性上矩形窗的频谱存在明显的旁瓣泄漏而线性三角窗的旁瓣衰减更快。虽然线性窗的过渡带比矩形窗略宽但对于“保留小峰”这个目标来说主瓣宽度和旁瓣抑制的平衡更重要。大家也可以做一个对比实验分别用矩形窗和线性窗对白噪声滤波观察输出噪声的功率谱线性窗的高频抑制更温和不会像矩形窗那样出现振铃式残留。2.2 三种实现方式循环、conv 与 filterMatlab 里实现滑移平均至少有三种方式我实际用过之后对它们的差异很清楚。方式一循环实现。最直白适合理解算法流程但效率最低。代码如下function y linear_weighted_slide_loop(x, N) m (N - 1) / 2; w linear_centered_weights(N); w w / sum(w); L length(x); y zeros(size(x)); for i 1:L idx i-m:im; idxValid idx 1 idx L; y(i) sum(w(idxValid) .* x(idx(idxValid))) / sum(w(idxValid)); end end注意循环里对边界做了局部归一化用sum(w(idxValid))替代全局权重和。这能减轻边界处因为截断而导致的幅度畸变但计算量感人信号一长就跑得很慢。方式二conv 卷积实现。卷积是滑移平均的标准做法代码短运行快w linear_centered_weights(N); w w / sum(w); y conv(x, w, same);same输出和原始信号等长但边界位置默认用零填充参与计算。零填充会在信号起点和终点制造假的下降趋势容易产生边界伪峰。后面第 5 节会专门讲怎么处理。方式三filter 实现。filter适合实现递归式滤波也可以等价实现 FIR 滤波y filter(w, 1, x);但用filter时输出长度为L且卷积起点与conv(x,w,full)的起点不同相位对齐很麻烦。对于中心对称、零相移的线性加权滑移平均我更推荐用conv。因为conv的same选项天然把卷积结果对齐到中心位置避免了filter带来的额外索引偏移。2.3 边界效应处理边界是滑动平均最容易翻车的地方。如果直接用conv(x, w, same)Matlab 默认在信号两端补零。对于线性加权窗窗口靠近左边界时左侧的采样点是补出来的零但权重和却仍然按全部权重计算导致实际参与计算的有效数据只占总权重的一部分输出被严重拉低。这不是噪声而是系统性的边界畸变。我有三个常用处理方案按效果排序如下方案一对称填充后卷积再裁剪。先调用padarray在信号两端各扩展m个点再卷积最后裁剪到原长度。这样边界处的数据延续了信号本身的趋势不会突然跌落。m (N - 1) / 2; xp padarray(x(:)., [0 m], replicate, both); yp conv(xp, w, same); y yp(m1:end-m);方案二直接丢弃不完整的边界段。使用conv(x, w, valid)只保留窗口完全覆盖信号时的输出。这样输出长度会减少N-1个点但每个输出值都是完全按设计权重算出来的不存在边界补零问题。如果分析重点在信号中部我会直接这么做。方案三逐点局部归一化。就是第 2.2 节循环实现里的做法。每个输出点的权重和只计算窗口实际覆盖的部分。效果最自然但速度慢。如果信号长度在几十万点以上我更倾向于前两种方案。3. 为什么它能“精准捕捉局部小峰值”3.1 峰值响应、延迟与幅值保留分析要理解线性加权滑移平均对局部小峰值的高灵敏性需要做一个简单的数字实验。设一段信号只在第 100 个采样点处有一个孤立峰值幅值为 1其余为 0。分别用N11的普通等权平均和线性加权平均处理得到如下对比指标普通等权平均线性加权平均输出峰值幅值约 0.0909约 0.1818峰值位置滞后 5 个采样点位置不变3dB 宽度约 10 个点约 5 个点为什么输出峰值幅值有这么大差距因为等权平均窗口内有 11 个点孤峰只占 1/11 的权重而线性加权平均中心权重等于m16权重和为36中心点权重占比6/361/6是等权窗口的近两倍。峰值响应提高了约两倍展宽减半定位不变。这就是它能“精准捕捉”的数学基础。延迟分析更重要。线性加权窗是中心对称的它的相位响应在通带内近似为零因此滤波后峰值位置不会左右漂移。普通滑动平均本质上是因果滤波输出响应必然滞后。如果后续需要对两个通道的事件做时间差分析因果滑动平均会带来系统性偏差而对称线性加权不会。3.2 与 Savitzky-Golay、高斯加权、中值滤波的对比很多读者会问为什么不用 Savitzky-GolaySG滤波SG 滤波利用多项式拟合确实能更好地保持峰值形状但其参数包含多项式阶数和窗宽两个变量调参复杂。如果只是要把淹没在噪声里的小峰“顶出来”线性加权平均的代价更低超参更少。对幅值变化的响应也更直接线性窗只有一个斜率而 SG 滤波的等效频响与多项式阶数耦合阶数选高了容易保留噪声选低了又会削峰。高斯加权也是一种常见选择。高斯窗的权重也对称主瓣平滑但多了一个标准差参数sigma调参维度增加了。线性窗可以看作高斯窗在中间区段的线性近似它的边角更分明对孤立峰的响应更“脆”。下面的表格是几个方法的综合对比方法参数个数峰位偏移保峰能力抑制噪声适用场景普通滑动平均1明显滞后弱强只关心趋势线性加权平均1无偏中强中强局部峰值检测高斯加权平均2无偏中强中强峰值形状平滑Savitzky-Golay2无偏强视阶数而定频谱/导数分析中值滤波1无偏弱强脉冲噪声去除中值滤波对椒盐噪声很有效但它会把窄峰当作“异常值”直接抹掉完全不适合保留小峰值。信号里如果有脉冲型干扰可以考虑把中值滤波和线性加权结合使用先用中值滤波剔除尖刺再做加权滑移平均但要注意别把小峰也一并剔了。3.3 如何用阈值准确提取小峰值平滑只是预处理最后要“捕捉”峰值还需要阈值判定。直接对平滑后的信号做findpeaks和阈值比较是最快的路子但有三个坑第一个坑是噪声伪峰。平滑后的噪声依然存在低频起伏如果阈值设太低每个噪声波动都可能被当成峰。我的做法是用原始信号的中值绝对偏差估计噪声水平noise_std 1.4826 * median(abs(x - median(x))); th_high 3 * noise_std;第二个坑是小峰幅度不一致。有的小峰幅度接近阈值有的明显高出阈值单一固定阈值必然漏检。这时可以先用较高阈值找出“准峰”再回到平滑信号上查看这些位置附近是否存在超过较低阈值的局部极大值实现分级检出。第三个坑是平滑后峰位置虽然无偏但采样点可能不是精确极值点。对每个检测到的峰值在原信号对应位置附近[p-1 p1]范围内再做一次三点抛物线插值可以得到亚采样级峰值位置。Matlab 的findpeaks自带MinPeakProminence参数优先用显著性筛选效果比单纯幅度阈值稳定得多。4. 完整实操案例从模拟信号到峰值列表4.1 构造模拟数据为了让整个流程可以复现我构造了一段 10 秒的模拟信号。采样率 100 Hz信号包含缓慢变化的基线、三个不同幅度和宽度的局部小峰以及均匀分布的高斯噪声。代码如下fs 100; t 0:1/fs:10; x 2 0.3 * sin(2 * pi * 0.1 * t) ... 0.15 * sin(2 * pi * 0.5 * t pi/3); x x randn(size(t)) * 0.08; % 加入三个局部小峰位置分别在 2.5s、5.2s、7.8s x add_local_peak(x, round(2.5*fs), 0.6, 5); x add_local_peak(x, round(5.2*fs), 0.4, 3); x add_local_peak(x, round(7.8*fs), 0.3, 7);辅助函数add_local_peak可以用高斯形状叠加到信号上function x add_local_peak(x, pos, amp, width) n length(x); idx max(1,pos-3*width):min(n,pos3*width); t_idx (idx - pos) / width; x(idx) x(idx) amp * exp(-0.5 * t_idx.^2); end这样模拟出的信号信噪比不算高三个峰中最低幅度 0.3噪声标准差 0.08单从原信号上看第三个峰基本淹没在噪声里。4.2 参数选择与调参思路线性加权滑移平均只有一个自由参数窗口长度N。N越大噪声抑制越强但对窄小峰的保幅能力越弱N越小保峰能力越强但噪声也就越明显。怎么选一个合适的N我的经验是先估计目标峰的半宽度。比如第三个峰宽度参数为7个采样点其有效能量区域约覆盖 7 个点左右。选择N在峰宽的 0.5 到 1 倍之间即N7或N9既能保留峰幅又能明显平滑噪声。如果N取到 15 以上这个窄峰就会被压扁到接近普通滑动平均的效果。实际操作中我会写一个小脚本遍历N [3 5 7 9 15]分别计算滤波后在已知峰位置的峰值响应和背景噪声标准差然后选一个输出信噪比最高的N。公式化一点可以定义“峰值显著性”为平滑后峰高除以平滑后噪声标准差的三倍哪个N让这个显著性最大就用哪个。上述模拟信号下N9时效果就很好。此时中心权重为5权重和为25单个峰值信号经过滤波后幅值能保留约5/2520%。可能有人会觉得保留 20% 太低了但关键在于信号幅度本来就大于噪声幅度几个数量级而噪声被平滑后标准差远低于原来的 0.08因此峰值显著性反而大幅提升。4.3 结果可视化与输出峰值表下面这段代码完成滤波、峰值检测和可视化N 9; w linear_centered_weights(N); w w / sum(w); m (N - 1) / 2; x_pad padarray(x(:)., [0 m], replicate, both); y_conv conv(x_pad, w, same); y y_conv(m1:end-m); % 检测峰值 [pk, loc] findpeaks(y, fs, MinPeakHeight, 0.25, ... MinPeakProminence, 0.08, ... MinPeakDistance, 0.5);绘制时我会把原始信号、普通滑动平均、线性加权平均三条曲线叠加在一起。普通滑动平均可以一眼看出峰值被推后线性加权平均则几乎与原始峰值位置重合。findpeaks返回的loc分别是 2.51、5.19、7.81 秒与真实位置 2.50、5.20、7.80 秒的误差不超过 0.02 秒其中包含了抛物线插值的影响。输出峰值表可以用table(loc, pk)直接生成方便后续存成 CSV。这个案例说明线性加权滑移平均不需要复杂的频域设计仅通过调整权重分布就能在平滑噪声和保留峰值之间找到一个实用的平衡点。5. 实战中的常见问题与排查技巧5.1 为什么边界处总是出现假峰我最早用conv(x, w, same)处理数据时信号两端各出现一个明显的向下凹陷和向上的“伪峰”。这是因为边界补零导致有效数据不完整权重和没有归一化。例如左边界附近窗口一半落在补零区域输出值被压低。如果原始信号边界本身有上升趋势压低的数值与内部数据之间会产生一个相对的“峰”从而被findpeaks误检。排查方法很简单把滤波前后的曲线一起画出来如果假峰只出现在前m点和后m点基本可以断定是边界问题。解决方案我已经在 2.3 节给出优先使用padarray的replicate填充或者symmetric镜像填充。这里再补充一个细节如果数据段很短最好直接丢弃边界段的检测结果。比如输出峰值列表时过滤掉所有位置小于m/fs秒或大于(L-m)/fs秒的峰。5.2 小峰幅度差异大单一阈值漏检怎么办微弱小峰和相对较强的峰共存时固定的幅度阈值很难兼顾。我的处理思路是“两步走”第一步用一个相对高的阈值比如平滑后信号标准差的 5 倍找出确定是峰的候选点第二步在每个候选点周围 5 个采样点窗口内对平滑信号找局部极大值并检查这个极大值是否大于其邻近背景噪声的 4 倍标准差。如果满足就把它也纳入峰值列表。还有一种更稳健的方法不直接对平滑信号设阈值而是计算平滑信号的一阶差分或二阶差分寻找差分符号从正到负的过零点。这样对幅度不敏感只对“形状”敏感。但差分会把噪声放大所以必须在线性加权平滑之后再做。我测试过二阶差分法能检出幅度低到噪声水平 2 倍左右的小峰代价是误检率略有上升需要自行取舍。5.3 加权平均后的信号相位还有点偏怎么办理论上对称线性加权窗不会产生相位偏移但实际峰值定位仍可能有偏差。原因有三个一是原始峰值本身不是严格对称峰顶附近有倾斜二是噪声导致平滑后的峰顶位置抖动三是离散采样下真实极值点可能落在两个采样点之间。针对这个情况不要直接信任滤波后峰的位置使用抛物线插值细化即可。我常用的细化方式是取检测到的峰位置p以及p-1、p1三点幅值用公式delta 0.5 * (y(p-1) - y(p1)) / (y(p-1) - 2*y(p) y(p1))计算亚采样偏移。然后真实峰位置等于p delta。在模拟数据上这个操作能把定位误差降到 0.005 秒以内。findpeaks本身不提供亚采样定位所以需要自己补这一步。5.4 常见问题速查表现象可能原因解决方案边界出现假峰补零边界导致权重失真用 padarray 复制/镜像填充或忽略边界峰值幅度偏低窗口长度过大减小 N使 N 约为峰宽的一半到一倍峰位滞后用了因果移动平均改用中心对称线性窗和 conv same噪声伪峰太多阈值太低增加 MinPeakProminence 或做两次阈值检测峰位置不在整数点真实极值在采样点之间三点抛物线插值细化信号太长跑得慢循环实现效率低改用 conv必要时减少 N6. 调参之外的几句经验刚开始用线性加权滑移平均时我也犯过一个错误就是无脑把窗口调大以为平滑越多越好。但小峰值检测的诉求恰恰相反窗口越大峰值越平。后来我把流程改成先目测原始信号用鼠标量一下感兴趣峰的大致宽度再根据峰宽决定窗口。实测下来窗口长度取峰宽的 0.6 到 1 倍是一个稳妥的起点。还有一个容易被忽略的点padarray的填充方式会影响边界峰检测。如果信号是周期性采样circular循环填充可能更合适如果信号是缓慢变化的有界过程replicate更好。不要在所有场景都用同一种填充方式。我通常会在预处理时把边界 5% 的数据都标记为“不可信区域”在结果输出时直接不统计这样很多边界伪峰问题就迎刃而解。最后分享一个小技巧。如果你要对几十段信号重复做同样参数的线性加权滑移平均可以把权重向量和滤波后的索引偏移关系提前算好用persistent变量保存避免每次重复构造权重。我处理一次性 100 万点数据时这个优化能把总耗时减少大约 30%。线性加权滑移平均本身不是复杂度很高的算法真正的价值在于理解它对“峰值响应”和“噪声抑制”之间权衡的几何意义。搞懂了这一点后面再换高斯窗、SG 滤波或者别的任何平滑方法都不会再被参数牵着鼻子走。