
做信号处理的人早晚都会碰到“找峰”这件事。不管是分析心电图的R波、色谱图的成分峰还是振动信号里的故障特征频率第一步几乎都是先把这些“突出来的点”精确地从原始波形里抠出来。MATLAB里专门干这个活的函数就叫findpeaks它在Signal Processing Toolbox里一行代码就能返回峰值的位置、大小、宽度和显著性比手动写差分判断逻辑要省事得多而且边界情况处理得也更稳妥。这篇文章我打算把findpeaks讲透。不只是告诉你“这个函数怎么用”更关键的是它那十几个参数分别对应什么物理含义什么时候该调哪个为什么调了之后结果会变。我会带上可复现的代码和波形分析按我们从粗到细、从简到繁的实际排查顺序来写。适合刚接触信号处理的学生也适合那些已经用了findpeaks但总感觉“参数调不对、峰老是漏检或多检”的工程师。1. 先从应用场景聊起findpeaks到底在解决什么问题1.1 峰值检测的通用难点不是“找出来”而是“怎么定义才算峰”你要是在网上搜过峰值检测一定会看到一堆代码片段常用的土办法无非是拿diff求一阶差分再看符号变化或者用滑动窗口判断“中间点是不是比两边都大”。这种办法对付干净的无噪声信号勉强能用但一遇到真实采集来的数据就露馅了噪声毛刺会制造大量假峰基线漂移会让矮的小峰被淹没两个靠得很近的峰可能被合并成一个而一个宽缓的肩峰又可能被误判成两个。findpeaks之所以好用是因为它把“判定峰”的逻辑分成了多层。它不只比较相邻三个点而是允许你同时设置高度门槛、间距门槛、显著性门槛、宽度门槛让“峰”的定义清晰化。这才是工程实践里真正需要的状态——参数配置清楚后算法在批量数据上的表现可以复现、可以审计而不是每个文件都靠肉眼看一遍再手动改阈值。1.2 掌握findpeaks的核心思路输入一个向量得到一组结构化描述findpeaks的基本调用格式很简单但每个返回值都值得理解透彻[pks, locs, w, p] findpeaks(x)pks检测到的峰值幅值也就是峰顶的高度。locs峰值在原始信号中的索引位置。w峰的宽度默认用半峰全宽FWHM来估计也就是峰值高度一半处的横向宽度。p峰的显著性prominence指的是这个峰相对于它左右两侧“最低谷”能高出多少这个量对基线漂移不敏感。你可以只用[pks, locs] findpeaks(x)拿到最核心的“峰在哪里、有多高”需要更多信息时再把w和p接出来用。我自己在项目里经常是先用findpeaks(x)快速画图看一眼默认效果再逐项加参数约束这属于正常工作流后面会详细说。2. 参数拆解调参之前先把每个旋钮的“物理含义”弄明白findpeaks提供了十来个参数新手最容易犯的毛病是把所有参数一股脑堆上去结果峰倒是少了真峰也没了。其实每个参数都对应一类物理约束理解了你就能根据信号特征精准选择。2.1 MinPeakHeight——最简单的绝对高度门槛MinPeakHeight, h的含义很直观只保留幅值大于h的局部极大值点。它适合信号幅值相对稳定、噪声基线清楚的场景比如一个从0到5V变化、噪声不超过0.1V的传感器输出你设一个MinPeakHeight, 2就能滤掉大部分底噪。但它有一个致命弱点对基线漂移没招。如果信号整体在缓慢上升半小时后基线从0飘到了3V那你固定一个绝对高度阈值就废了——要么漏掉漂移后的矮峰要么把漂移本身的毛刺全检出来。所以在实际项目中我很少单独依赖这个参数它一般是和其他条件组合使用的。2.2 MinPeakDistance——最常用的“去重”参数MinPeakDistance, d设定的是两个被识别峰之间的最小水平间距。原理上findpeaks会先找出一批候选峰然后对所有候选峰按显著性从高到低排序并逐个接收同时拒绝那些落在已接收峰周围d范围内的其他候选峰。这就相当于加了一个“局部去重窗口”。这个参数特别好用因为它直接对应采样率和物理事件间隔。比如采样率是1000Hz你研究的对象是心率信号R波之间最短间隔不可能小于0.4秒那MinPeakDistance就应该设为0.4 * 1000 400个采样点。换算关系是距离采样点 最短物理间隔秒× 采样率Hz。这是新手最容易算错的地方单位别搞混。2.3 MinPeakProminence——最抗漂移的参数也是我最推荐的底仓参数Prominence这个概念单纯靠文字描述容易绕我用个生活类比你站在山顶上想衡量这座山有多“突出”不是看它绝对海拔有多高那是MinPeakHeight而是看你要下到哪个山口才能爬上一座更高的山——这个“相对落差”就是显著性。数学上findpeaks会找到峰的左边界和右边界往左右两侧走只要碰到一个比当前峰更高的峰或下降到指定参照位置就停止然后分别在左右区间找最低点取这两个最低点中较大的那个当前峰高减去它就是prominence值。所以MinPeakProminence, p表示“峰比周围最近的低谷至少高出p才被保留”。这意味着什么即使整个信号在一段斜坡上只要局部相对突出峰就能检出来反过来即使幅值很高如果旁边有个更高的峰把它“压住”了它的显著性照样很低会被过滤掉。这个特性对处理基线漂移、趋势项叠加的信号几乎是量身定做的。2.4 Threshold——只对相邻样本差生效的轻量过滤器严格说来Threshold, t约束的是峰顶和它紧挨着的那两个相邻采样点的最小差值。它跟MinPeakProminence的本质区别在于Threshold只看局部一两步的突变幅度而Prominence看的是整个峰相对两翼谷底的落差。假如信号里有很多“矮胖的毛刺”它们和相邻点的差值不大但整体幅值可能高过阈值此时用Threshold可以有效剔除假如信号里的干扰是高频细毛刺每个毛刺和旁边点的差值其实很大用Threshold反而滤不掉得靠MinPeakProminence或者先做平滑。实际调参时我一般把Threshold设成噪声幅度的1到2倍用它来剔除细微抖动导致的伪峰。2.5 MinPeakWidth 与 MaxPeakWidth——按峰的宽度做筛选有时候噪声的幅度并不小但它持续的时间短也就是峰的宽度很窄有时候基线隆起造成的“假峰”幅度不高但特别宽。这两种情况靠高度和距离都很难过滤宽度参数正好派上用场。MinPeakWidth, wmin和MaxPeakWidth, wmax分别设定允许的最小、最大峰宽。需要注意这里宽度的单位和信号横轴的单位一致是采样点个数不是时间秒数。如果你关心的是物理时间宽度要乘以采样率换算。半峰全宽的估计在findpeaks内部是通过峰两侧下降到半高位置的插值来完成的所以它也能处理离散采样的量化误差在数据点不多时依然能给出亚采样点精度的宽度估计。我在处理雷达脉冲信号时经常这么干脉冲宽度已知在80到120个采样点之间借助MaxPeakWidth和MinPeakWidth两个约束可以把很多宽度异常的高幅值尖峰直接滤掉效果比单纯看高度可靠得多。2.6 NPeaks、SortStr 和 Annotate——输出层面的控制项NPeaks, n只保留前n个峰。它一般配合SortStr, descend使用也就是先按幅值或显著性降序排列再取前n名。这在做“找最强的几个峰”时很顺手。SortStr, ascend/descend不设NPeaks时它会改变返回峰的排序默认是按出现先后顺序返回。注意设置了排序后返回的locs顺序也会跟着变别拿它当原信号索引直接用而不看文档。Annotate, extents影响的是画图辅助线的类型有peaks、prominences、widths几种只对findpeaks自动绘图模式生效。它不改变返回数据纯粹是可视化辅助。参数作用对象对基线漂移的耐受度常见误区MinPeakHeight绝对幅值差基线飘了之后完全失效MinPeakDistance峰间距好单位忘记换算成采样点MinPeakProminence相对谷底的落差很好和Threshold混淆Threshold相邻采样点差值中等无法滤除高频细毛刺MinPeakWidth/MaxPeakWidth半峰宽度好宽度单位是采样点不是秒3. 实操案例从无噪声合成信号到真实场景的逐步调参理论讲再多不如直接跑几个例子。下面的代码你用MATLAB R2018b及以上版本都能直接跑通老版本只要支持findpeaks的参数就能用。3.1 基线案例一个干净的多峰信号先看理想情况。我们构造一个由三个高斯峰叠加合成的信号fs 1000; t 0:1/fs:5; x 1.5*exp(-(t-1).^2/0.002) ... 2.0*exp(-(t-2.5).^2/0.005) ... 1.0*exp(-(t-4).^2/0.001); [pks, locs] findpeaks(x); findpeaks(x)默认情况下findpeaks会用红色倒三角在图上标出所有它认为的峰。三个高斯峰互不重叠每个峰都高于周围一大截所以此时三个峰都会被准确识别出来pks会返回[1.5, 2.0, 1.0]locs对应的位置也正好是峰顶所在的索引。这个例子虽然简单但有一个值得注意的细节findpeaks默认只要局部极大值比相邻点高就算一个峰所以哪怕信号里混入了幅值极其微小的数值误差也可能被识别出来。干净信号没问题真实数据几乎必然有噪声所以下一步我们就进入带噪场景。3.2 带噪信号三参数组合拳识别心电R波现在模拟一段带噪声的心电信号。心电R波的特点是幅值明显高于其他波段、出现频率有一定生理上限、形态相对陡峭。针对这三个特点我们正好用上三种参数fs 250; % 采样率250Hz t 0:1/fs:10; % 10秒数据 % 模拟R波每0.8秒一个幅值1mV左右叠加工频干扰和随机噪声 rng(42); xecg 0; for k 0.8:0.8:9.6 idx round(k*fs); xecg xecg exp(-((t - k).^2) / 0.0005); end xecg xecg 0.08*sin(2*pi*50*t) 0.05*randn(size(t)); [pks, locs] findpeaks(xecg, MinPeakHeight, 0.4, ... MinPeakDistance, round(0.3*fs), ... MinPeakProminence, 0.3);三行参数的用意MinPeakHeight, 0.4幅值低于0.4mV的噪声波动直接忽略。如果噪声幅度更大这个门槛就跟着抬。MinPeakDistance, round(0.3*fs)生理上心率最快约200次/分对应R波间隔0.3秒所以间隔小于0.3秒的峰必然来自噪声或误检直接排除。这里用round把秒换算成采样点避免出现小数索引。MinPeakProminence, 0.3即使有个高幅值毛刺通过了前两个关卡但它若没有R波那么突出的相对落差也会被这一步拦下来。实测下来这种组合在信噪比不太差的心电信号上能稳定检出全部R波误检率很低。三参数组合拳比单独用任何一个参数都可靠得多这种“距离限制生理范围、高度过滤绝对幅值、显著性滤除局部毛刺”的思路在绝大多数周期性生理信号上都可以复用。3.3 难啃的骨头重叠峰的分离真实色谱、光谱数据里最头疼的情况是两个峰靠得很近视觉上只有一个宽包但本质上确实有两个物理成分。这种问题findpeaks能不能解决取决于峰之间的“谷”是否真实存在。t 0:0.01:10; x exp(-(t-4).^2/(2*0.3^2)) 0.9*exp(-(t-5).^2/(2*0.3^2)); [pks1, locs1] findpeaks(x); % 默认情况 [pks2, locs2] findpeaks(x, MinPeakProminence, 0.05);两个高斯峰中心间隔1标准差0.3默认情况下findpeaks只返回一个峰因为两个峰之间的极小值并不比相邻点低多少算法可能把中间那个凹陷点漏掉了。加上MinPeakProminence之后它会重新评估两个峰的显著性从而把两个峰都识别出来。这里有个物理分辨率极限当两个峰的中心间距小于约2倍标准差时中间几乎不再有局部极小值这时候任何基于一阶导数的检测算法都无能为力findpeaks也无解。你只能靠更高分辨率的采集设备或专门的解卷积算法来分离。所以别抱着“调参能解决一切”的幻想findpeaks在严重重叠峰面前是有物理边界的。3.4 带基线漂移的信号为什么MinPeakProminence能赢下面这个例子直接对比三种参数在基线漂移信号上的表现t 0:0.01:20; trend 0.02 * t.^2; % 二次趋势项模拟基线漂移 x trend 0.8*sin(2*pi*0.5*t) 0.3*sin(2*pi*1.3*t) 0.05*randn(size(t));如果直接用findpeaks(x, MinPeakHeight, 1)早期基线很低可能检出一堆噪声峰后期基线已经飘到8真信号幅值反而被淹没高度阈值完全失灵。换成findpeaks(x, MinPeakProminence, 0.25)则会好很多因为prominence计算的是局部相对落差基线怎么飘都不影响峰的相对突出程度。这也是我前面说你完全可以把它当底仓参数的原因。当然如果漂移本身太快、太剧烈导致局部谷底都被填平了prominence也会失效那时就得先做高通滤波或基线校正预处理。3.5 峰宽和显著性的后续用途前面提到的返回值w和p很多人拿到[pks, locs]就够了但我建议你多看一眼w和p。它们在某些场景下比峰高更有用在色谱定量分析中峰面积近似为pks .* w往往比峰高更能代表物质浓度因为峰宽变化会影响峰高。在粒度分析中峰的宽度分布直接反映样品粒径分布的宽窄宽度窄意味着颗粒更均匀。在振动故障诊断里某个频率分量的prominence如果持续增大往往意味着故障在发展中而绝对幅值可能受转速波动影响不稳定。所以返回四个值的完整调用方式是工程中更推荐的写法。即使你暂时用不上把数据存下来也有备无患。4. 实际问题排查误检、漏检和性能瓶颈的应对经验4.1 一张速查表帮你定位问题真刀真枪跑数据时遇到问题先别急着堆参数。我习惯按下面的表来定位现象最可能的原因优先排查的参数峰太多了噪声毛刺全是峰缺乏显著性或阈值约束加 MinPeakProminence、Threshold真峰漏检MinPeakHeight设得过高或MinPeakDistance过大降低 MinPeakHeight缩小 MinPeakDistance两个靠很近的峰被合并成一个中间极小值不明显默认参数不敏感加 MinPeakProminence适当调小距离限制基线漂移后峰检不准依赖绝对高度的参数失效改用 MinPeakProminence或先做去趋势/高通滤波宽缓的鼓包也被当成峰缺少宽度上限加 MaxPeakWidth返回结果为空约束过严没有任何候选峰满足条件逐一放宽参数先用默认参数定位峰值大概在哪4.2 我踩过的一个坑MinPeakDistance和采样率单位的错位有一回分析振动数据采样率是51200Hz故障特征频率在1200Hz左右相邻两个特征峰间隔大概42个采样点。我图省事直接写了MinPeakDistance, 0.0008结果返回了一堆莫名其妙的峰后来才反应过来这个参数的单位是采样点不是秒。写成MinPeakDistance, round(51200/1200)才恢复正常。再强调一遍凡是包含时间信息的物理参数最终都要换算成采样点个数换算公式就是“物理时间×采样率”。另一个容易忽略的点是MinPeakDistance不宜设得比MinPeakWidth还小。如果宽度已经限定了峰的横向尺度距离阈值还设得特别近就可能出现同一个峰被分割成两个候选点的情况这个组合会互相打架。4.3 大数据量场景findpeaks跑得慢怎么办findpeaks在处理百万点级别的信号时通常没问题但如果你的信号是几十个通道、每通道千万点还要跑几十个文件速度就可能成为瓶颈。我这里分享两个优化经验第一如果对全局峰值位置不敏感只关心局部脉冲可以先对信号做一次粗降采样或分块处理再在每一块内部用findpeaks最后合并结果。注意降采样前要抗混叠滤波否则峰值位置会偏移。第二如果只想要最高的前几个峰千万别把全部候选峰排完序再做直接在findpeaks里带上NPeaks, 10, SortStr, descend它会内部做剪枝效率高很多。我测过对一个5000万点的信号带NPeaks限制比返回全部候选峰再自己排序耗时能少一个数量级。4.4 边界效应信号端点附近的峰要不要算findpeaks对端点处的处理是端点默认不识别为峰因为它至少要有一个左邻或右邻点来做比较。大多数情况下这是合理的因为你无法确定端点的数据是不是被截断了。但有一种情况需要注意如果信号本身的峰就在边界附近比如心电数据开始时正好落在R波中间那么findpeaks会把它的高度略微低估因为左侧数据不完整会影响宽度和prominence的计算。这种情况下建议在信号前后各延长一段镜像填充数据检测完成后再把边界外的峰剔除。这个操作看着麻烦但对定量分析来说很有必要尤其在做峰值面积积分时边界截断会带来不小的误差。4.5 自制简易峰值检测代码的对比总有人问能不能不依赖工具箱自己写峰值检测能但代价是你要自己处理大量边界问题。给你看一个最基础的实现function [pks, locs] simplePeaks(x, minDist) dx diff(x); locs find(dx(1:end-1) 0 dx(2:end) 0) 1; hgt x(locs); % 按距离去重伪代码思路 ... end这个代码在无噪声、峰间距足够大的信号上没问题但一旦遇到毛刺、漂移、重叠峰你要不断往里面加逻辑最后写出来的东西性能、稳定性都不一定能超过findpeaks。我的建议是没有特殊需求比如嵌入式环境无法装MATLAB就不要重复造轮子把时间花在理解参数和信号特征上性价比高得多。自己写算法真正有价值的地方在于你可以完全控制判定逻辑比如实时流式处理时用状态机逐点扫描或者需要检测的是低谷而不是峰当然findpeaks对-x做一次也能找谷。这些属于特殊场景不在本文讨论范围内。4.6 更进阶的用法自动估计噪声底来设定Prominence前面多次强调MinPeakProminence好用但它也有个尴尬点你并不知道该设多少合适。我常用的一个土办法是先对信号做一次差分算出差分序列的标准差然后用这个标准差的一个倍数作为prominence初值。因为差分信号的std大致能反映噪声的局部波动幅度而真实的峰在局部造成的落差至少要是噪声波动的数倍才能肉眼分辨。noiseLevel std(diff(x)) / sqrt(2); prominenceThresh 5 * noiseLevel; [pks, locs] findpeaks(x, MinPeakProminence, prominenceThresh);系数5不是铁律如果检出的峰还是太多就提到8漏检就降到3最好结合可视化结果调整。这种“先从数据里估出噪声底再设定相对阈值”的做法要比每次凭感觉填一个数字靠谱得多在批量处理多个数据集时尤其好用。写在最后的个人经验我从第一次用findpeaks到现在最大的体会是这个函数最强大的地方不在于“找出峰”本身而在于它逼着你把“什么叫峰”想清楚。你得先搞清楚信号里哪些是真目标、哪些是干扰它们的高度范围、距离范围、宽度范围、相对突出程度分别是什么样然后才能把参数填对。这恰恰是做信号处理最有价值的部分。如果你现在正准备处理一批新数据我的建议很简单先取一小段有代表性的数据用默认参数跑一遍画出来看哪里多检哪里漏检再逐个参数纠正最后把参数组合固定下来跑全量数据。千万别一上来就追求“万能参数”那是标准的工作方式也不是没有风险而是风险会被你亲手埋进看不见的地方。另外每次换了采样率或换了采集设备一定要重新校准一遍参数不要迷信上一波数据调好的值还能继续用。先看数据、再谈参数、最后才谈算法这个顺序能帮你少走很多弯路。