☰
MATLAB findpeaks全解析:从原理到参数调优的峰值检测实战指南
2026/10/4 14:40:18 网站建设 项目流程

做信号处理的人,早晚都会碰到“找峰”这件事。不管是分析心电图的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到现在,最大的体会是:这个函数最强大的地方不在于“找出峰”本身,而在于它逼着你把“什么叫峰”想清楚。你得先搞清楚信号里哪些是真目标、哪些是干扰,它们的高度范围、距离范围、宽度范围、相对突出程度分别是什么样,然后才能把参数填对。这恰恰是做信号处理最有价值的部分。

如果你现在正准备处理一批新数据,我的建议很简单:先取一小段有代表性的数据,用默认参数跑一遍,画出来看哪里多检哪里漏检,再逐个参数纠正,最后把参数组合固定下来跑全量数据。千万别一上来就追求“万能参数”,那是标准的工作方式,也不是没有风险,而是风险会被你亲手埋进看不见的地方。另外,每次换了采样率或换了采集设备,一定要重新校准一遍参数,不要迷信上一波数据调好的值还能继续用。先看数据、再谈参数、最后才谈算法,这个顺序能帮你少走很多弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询