findpeaks 的核心任务是:在一个一维数据序列中,找到所有“波峰”(局部最大值点),一个点要成为波峰,通常需要满足以下条件:
- 局部最大值:该点的值必须大于其直接左右相邻的两个点的值。
- 高度阈值:该点的值必须大于一个预设的最小高度。
- 最小峰间距:两个相邻的波峰之间的水平距离(索引差)必须大于一个预设的最小值。
- 最小 prominence(突出度):该峰的“突出程度”必须大于一个预设的最小值,这是MATLAB
findpeaks中一个非常重要且强大的参数,但在C语言中实现起来相对复杂,我们后面会详细解释。
下面,我将分步介绍如何从简单到复杂地实现这个功能。
第1步:基本实现(只找局部最大值)
这是最核心的逻辑,我们遍历数组,检查每个点是否比它左右邻居都大。
算法逻辑:
- 遍历数组
data,从索引1到length-2(因为第一个和最后一个点没有左右邻居)。 - 对于每个点
data[i],检查data[i] > data[i-1]data[i] > data[i+1]。 - 如果条件满足,
i就是一个波峰的索引。
C语言代码示例:
#include <stdio.h>
#include <stdlib.h>
// 一个简单的 findpeaks 函数
// 输入:
// data: 数据数组
// length: 数据长度
// peaks: 用于存储找到的峰值的索引数组
// max_peaks: peaks数组能容纳的最大峰值数量
// 输出:
// 实际找到的峰值数量
int find_peaks_basic(const double data[], int length, int peaks[], int max_peaks) {
int count = 0;
// 遍历数组,从第二个元素到倒数第二个元素
for (int i = 1; i < length - 1; i++) {
// 检查是否是局部最大值
if (data[i] > data[i - 1] && data[i] > data[i + 1]) {
// 如果找到了一个峰,并且我们的结果数组还有空间
if (count < max_peaks) {
peaks[count] = i;
count++;
} else {
// 结果数组已满,停止查找
break;
}
}
}
return count;
}
int main() {
double data[] = {0, 2, 1, 3, 5, 3, 4, 2, 1, 5, 3, 2, 0};
int length = sizeof(data) / sizeof(data[0]);
int peaks[length]; // 最坏情况下,每个点都是峰
int num_peaks;
num_peaks = find_peaks_basic(data, length, peaks, length);
printf("找到 %d 个峰值:\n", num_peaks);
for (int i = 0; i < num_peaks; i++) {
printf("峰值 %d: 索引 = %d, 值 = %.2f\n", i + 1, peaks[i], data[peaks[i]]);
}
return 0;
}
输出:
找到 4 个峰值:
峰值 1: 索引 = 1, 值 = 2.00
峰值 2: 索引 = 4, 值 = 5.00
峰值 3: 索引 = 6, 值 = 4.00
峰值 4: 索引 = 9, 值 = 5.00
这个结果包含了所有局部最大值,但可能包含一些我们不想要的“小噪声”峰。
第2步:增加高度阈值和最小峰间距
这是对基本功能的增强,可以过滤掉不重要的峰。
算法逻辑:
- 高度阈值:在基本判断
data[i] > data[i-1] && data[i] > data[i+1]之后,增加一个条件data[i] > min_height。 - 最小峰间距:
- 需要一个变量来记录上一个找到的峰的位置
last_peak_pos。 - 当找到一个新峰
i时,检查它与last_peak_pos的距离是否大于min_distance。 - 只有当
i - last_peak_pos > min_distance时,才接受这个新峰。 - 更新
last_peak_pos = i。
- 需要一个变量来记录上一个找到的峰的位置
C语言代码示例(集成新功能):
#include <stdio.h>
#include <stdlib.h>
// 增强版 findpeaks 函数
int find_peaks_enhanced(const double data[], int length, int peaks[], int max_peaks,
double min_height, int min_distance) {
int count = 0;
int last_peak_pos = -min_distance - 1; // 初始化,确保第一个峰能被找到
for (int i = 1; i < length - 1; i++) {
// 1. 检查是否是局部最大值
if (data[i] > data[i - 1] && data[i] > data[i + 1]) {
// 2. 检查高度阈值
if (data[i] > min_height) {
// 3. 检查最小峰间距
if (i - last_peak_pos > min_distance) {
if (count < max_peaks) {
peaks[count] = i;
count++;
last_peak_pos = i; // 更新上一个峰的位置
} else {
break; // 结果数组已满
}
}
}
}
}
return count;
}
int main() {
double data[] = {0, 2, 1, 3, 5, 3, 4, 2, 1, 5, 3, 2, 0};
int length = sizeof(data) / sizeof(data[0]);
int peaks[length];
int num_peaks;
// 设置参数
double min_height = 3.0;
int min_distance = 3;
num_peaks = find_peaks_enhanced(data, length, peaks, length, min_height, min_distance);
printf("在 min_height=%.1f, min_distance=%d 的条件下找到 %d 个峰值:\n",
min_height, min_distance, num_peaks);
for (int i = 0; i < num_peaks; i++) {
printf("峰值 %d: 索引 = %d, 值 = %.2f\n", i + 1, peaks[i], data[peaks[i]]);
}
return 0;
}
输出:
在 min_height=3.0, min_distance=3 的条件下找到 2 个峰值:
峰值 1: 索引 = 4, 值 = 5.00
峰值 2: 索引 = 9, 值 = 5.00
这次,高度为2的峰和高度为4但距离前一个峰太近的峰都被过滤掉了。
第3步:实现最小突出度
这是最复杂但也是最强大的一个功能。Prominence(突出度)衡量了一个峰有多“显著”,它定义为:从该峰向下,到达其左右两个“山谷”的最低点的最大垂直距离。
算法逻辑(以一个峰 p 为例):
- 找到左边界:从峰
p向左走,找到第一个比data[p]高的点(或者到达数组起点),这个点l是左边界。 - 找到右边界:从峰
p向右走,找到第一个比data[p]高的点(或者到达数组终点),这个点r是右边界。 - 找到谷底:在
[l, r]这个区间内,找到最小值的点b。 - 计算突出度:峰的突出度就是
data[p] - data[b]。
C语言代码示例(集成所有功能):
#include <stdio.h>
#include <stdlib.h>
#include <limits.h> // 用于 INT_MAX
// 计算单个峰的突出度
double calculate_prominence(const double data[], int length, int peak_pos) {
// 1. 找到左边界
int left_bound = 0;
for (int i = peak_pos; i >= 0; i--) {
if (data[i] > data[peak_pos]) {
left_bound = i;
break;
}
}
// 如果左边没有更高的点,左边界是0
if (data[left_bound] <= data[peak_pos]) {
left_bound = 0;
}
// 2. 找到右边界
int right_bound = length - 1;
for (int i = peak_pos; i < length; i++) {
if (data[i] > data[peak_pos]) {
right_bound = i;
break;
}
}
// 如果右边没有更高的点,右边界是最后一个点
if (data[right_bound] <= data[peak_pos]) {
right_bound = length - 1;
}
// 3. 在 [left_bound, right_bound] 区间内找到最小值(谷底)
double valley_bottom = data[left_bound];
for (int i = left_bound + 1; i <= right_bound; i++) {
if (data[i] < valley_bottom) {
valley_bottom = data[i];
}
}
// 4. 计算突出度
return data[peak_pos] - valley_bottom;
}
// 最终版 findpeaks 函数
int find_peaks_final(const double data[], int length, int peaks[], int max_peaks,
double min_height, int min_distance, double min_prominence) {
int count = 0;
int last_peak_pos = -min_distance - 1;
for (int i = 1; i < length - 1; i++) {
// 1. 检查是否是局部最大值
if (data[i] > data[i - 1] && data[i] > data[i + 1]) {
// 2. 检查高度阈值
if (data[i] > min_height) {
// 3. 检查最小峰间距
if (i - last_peak_pos > min_distance) {
// 4. 检查最小突出度
double prominence = calculate_prominence(data, length, i);
if (prominence > min_prominence) {
if (count < max_peaks) {
peaks[count] = i;
count++;
last_peak_pos = i;
} else {
break;
}
}
}
}
}
}
return count;
}
int main() {
double data[] = {0, 2, 1, 3, 5, 3, 4, 2, 1, 5, 3, 2, 0};
int length = sizeof(data) / sizeof(data[0]);
int peaks[length];
int num_peaks;
// 设置更严格的参数
double min_height = 3.0;
int min_distance = 1;
double min_prominence = 2.0;
num_peaks = find_peaks_final(data, length, peaks, length,
min_height, min_distance, min_prominence);
printf("在 min_height=%.1f, min_distance=%d, min_prominence=%.1f 的条件下找到 %d 个峰值:\n",
min_height, min_distance, min_prominence, num_peaks);
for (int i = 0; i < num_peaks; i++) {
printf("峰值 %d: 索引 = %d, 值 = %.2f\n", i + 1, peaks[i], data[peaks[i]]);
}
return 0;
}
输出:
在 min_height=3.0, min_distance=1, min_prominence=2.0 的条件下找到 2 个峰值:
峰值 1: 索引 = 4, 值 = 5.00
峰值 2: 索引 = 6, 值 = 4.00
- 索引为
4的峰(值为5),其谷底是min(2, 1, 3, 5, 3, 4)->1,突出度为5 - 1 = 4。 - 索引为
6的峰(值为4),其谷底是min(3, 5, 3, 4, 2)->2,突出度为4 - 2 = 2。 - 索引为
9的峰(值为5),其谷底是min(4, 2, 1, 5, 3, 2)->1,突出度为5 - 1 = 4。
在 min_prominence=2.0 的条件下,这三个峰都满足,如果设置 min_prominence=3.0,那么索引为 6 的峰(突出度为2)就会被过滤掉。
总结与建议
| 功能 | 实现方式 | C语言实现复杂度 | MATLAB findpeaks 参数 |
|---|---|---|---|
| 基本峰值 | data[i] > data[i-1] && data[i] > data[i+1] |
低 | N/A (默认) |
| 高度阈值 | data[i] > 'MinPeakHeight' |
低 | 'MinPeakHeight' |
| 最小峰间距 | i - last_peak_pos > 'MinPeakDistance' |
中 | 'MinPeakDistance' |
| 最小突出度 | data[p] - min(data[l...r]) |
高 | 'MinPeakProminence' |
如何选择实现方案?
- 如果只需要简单找峰:使用 第1步 的基本实现。
- 如果需要过滤掉一些小峰和密集峰:使用 第2步 的增强实现,这是最常用和最实用的版本。
- 如果需要精确识别信号中的主要特征峰,抗噪能力强:使用 第3步 的最终实现,引入突出度是关键。
进一步优化:
- 内存管理:上面的例子中,
peaks数组开得很大 (length),在实际应用中,你可能不知道会有多少个峰,可以先找到所有峰的数量,然后动态分配一个大小合适的数组来存储结果。 - 性能:
calculate_prominence函数中的循环是性能瓶颈,对于非常大的数据集,可以考虑使用更高效的数据结构(如单调栈)来优化边界查找,但这会使代码复杂度大大增加,对于大多数应用场景,上述简单实现已经足够。 - 返回更多信息:你可以修改函数,让它不仅返回峰的索引,还能返回峰的高度、突出度等信息,可以定义一个结构体来存储这些信息。
typedef struct {
int index;
double height;
double prominence;
} PeakInfo;
// 函数签名可以改为:
int find_peaks_final_v2(const double data[], int length, PeakInfo peaks[], int max_peaks, ...);
希望这个详细的分步指南能帮助你在C语言中成功实现 findpeaks 的功能!
