v2.0.0
Loading...
Searching...
No Matches
peak_finder.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "peak_finder.h"
18
19//=============================================================================================================
20// STL INCLUDES
21//=============================================================================================================
22
23#include <algorithm>
24#include <cmath>
25#include <limits>
26#include <vector>
27
28//=============================================================================================================
29// USED NAMESPACES
30//=============================================================================================================
31
32using namespace UTILSLIB;
33using namespace Eigen;
34
35//=============================================================================================================
36// DEFINE FUNCTIONS
37//=============================================================================================================
38
39VectorXd UTILSLIB::peakProminences(const VectorXd& data, const QList<int>& peakIndices)
40{
41 const int n = static_cast<int>(data.size());
42 VectorXd proms(peakIndices.size());
43
44 for (int p = 0; p < peakIndices.size(); ++p) {
45 int idx = peakIndices[p];
46 double peakVal = data(idx);
47
48 // Search left for highest valley before a higher peak
49 double leftMin = peakVal;
50 for (int i = idx - 1; i >= 0; --i) {
51 if (data(i) > peakVal)
52 break;
53 leftMin = std::min(leftMin, data(i));
54 }
55
56 // Search right for highest valley before a higher peak
57 double rightMin = peakVal;
58 for (int i = idx + 1; i < n; ++i) {
59 if (data(i) > peakVal)
60 break;
61 rightMin = std::min(rightMin, data(i));
62 }
63
64 // Prominence = peak height minus highest of the two valleys
65 double highestValley = std::max(leftMin, rightMin);
66 proms(p) = peakVal - highestValley;
67 }
68
69 return proms;
70}
71
72//=============================================================================================================
73
74QList<QPair<int, double>> UTILSLIB::peakFinder(const VectorXd& data,
75 const PeakFinderParams& params)
76{
77 const int n = static_cast<int>(data.size());
78 QList<QPair<int, double>> peaks;
79
80 if (n < 3) {
81 return peaks;
82 }
83
84 // Step 1: Find all local maxima (strictly greater than both neighbours)
85 QList<int> peakIdx;
86 for (int i = 1; i < n - 1; ++i) {
87 if (data(i) > data(i - 1) && data(i) > data(i + 1)) {
88 if (data(i) >= params.dMinHeight) {
89 peakIdx.append(i);
90 }
91 }
92 }
93
94 // Step 2: Filter by prominence
95 if (params.dProminence > 0.0 && !peakIdx.isEmpty()) {
96 VectorXd proms = peakProminences(data, peakIdx);
97 QList<int> filtered;
98 for (int i = 0; i < peakIdx.size(); ++i) {
99 if (proms(i) >= params.dProminence) {
100 filtered.append(peakIdx[i]);
101 }
102 }
103 peakIdx = filtered;
104 }
105
106 // Step 3: Filter by minimum distance (keep highest peaks)
107 if (params.iMinDistance > 1 && !peakIdx.isEmpty()) {
108 // Sort peaks by height descending
109 std::vector<int> sortedIdx(peakIdx.begin(), peakIdx.end());
110 std::sort(sortedIdx.begin(), sortedIdx.end(),
111 [&data](int a, int b) { return data(a) > data(b); });
112
113 QList<int> kept;
114 std::vector<bool> suppressed(static_cast<size_t>(n), false);
115
116 for (int idx : sortedIdx) {
117 if (suppressed[static_cast<size_t>(idx)])
118 continue;
119 kept.append(idx);
120
121 // Suppress nearby peaks
122 int lo = std::max(0, idx - params.iMinDistance);
123 int hi = std::min(n - 1, idx + params.iMinDistance);
124 for (int s = lo; s <= hi; ++s) {
125 if (s != idx) {
126 suppressed[static_cast<size_t>(s)] = true;
127 }
128 }
129 }
130
131 // Re-sort by index
132 std::sort(kept.begin(), kept.end());
133 peakIdx = kept;
134 }
135
136 // Build output
137 for (int idx : peakIdx) {
138 peaks.append(qMakePair(idx, data(idx)));
139 }
140
141 return peaks;
142}
143
144//=============================================================================================================
145
146QList<QPair<int, double>> UTILSLIB::peakFinderMne(const VectorXd& data, double thresh, int extrema)
147{
148 // Adapted from mne.preprocessing.peak_finder (MNE-Python, BSD-3-Clause).
149 QList<QPair<int, double>> result;
150 const Index s = data.size();
151 if (s == 0)
152 return result;
153 if (std::isnan(thresh))
154 thresh = (data.maxCoeff() - data.minCoeff()) / 4.0;
155 const VectorXd x0 = extrema < 0 ? VectorXd(-data) : data;
156
157 // Turning points (the first of repeated values) plus both end points
158 std::vector<Index> ind{0};
159 for (Index i = 1; i + 1 < s; ++i) {
160 double left = x0(i) - x0(i - 1);
161 double right = x0(i + 1) - x0(i);
162 if (left == 0.0)
163 left = -std::numeric_limits<double>::epsilon();
164 if (right == 0.0)
165 right = -std::numeric_limits<double>::epsilon();
166 if (left * right < 0.0)
167 ind.push_back(i);
168 }
169 ind.push_back(s - 1);
170 std::vector<double> x(ind.size());
171 for (std::size_t i = 0; i < ind.size(); ++i)
172 x[i] = x0(ind[i]);
173
174 const double minMag = *std::min_element(x.begin(), x.end());
175 std::vector<Index> peakInds;
176 std::vector<double> peakMags;
177 if (x.size() > 2) {
178 double tempMag = minMag;
179 double leftMin = minMag;
180 bool foundPeak = false;
181 Index tempLoc = 0;
182 const auto sign = [](double v) {
183 return (v > 0.0) - (v < 0.0);
184 };
185 const int sign0 = sign(x[1] - x[0]);
186 const int sign1 = sign(x[2] - x[1]);
187 long ii = 0;
188 if (sign0 <= 0) {
189 ii = -1;
190 if (sign0 == sign1) {
191 x.erase(x.begin() + 1);
192 ind.erase(ind.begin() + 1);
193 }
194 } else if (sign0 == sign1) {
195 x.erase(x.begin());
196 ind.erase(ind.begin());
197 }
198 const long length = static_cast<long>(x.size());
199 // mne compares with the last slot of its preallocated peak array, which stays 0 until it is filled
200 const std::size_t maxPeaks = static_cast<std::size_t>(std::ceil(length / 2.0));
201 const auto lastSlot = [&]() {
202 return peakMags.size() == maxPeaks ? peakMags.back() : 0.0;
203 };
204 while (ii < length - 1) {
205 ++ii;
206 if (foundPeak && (x[ii] > lastSlot() || leftMin < lastSlot() - thresh)) {
207 tempMag = minMag;
208 foundPeak = false;
209 }
210 if (ii == length - 1)
211 break;
212 if (x[ii] > tempMag && x[ii] > leftMin + thresh) {
213 tempLoc = ii;
214 tempMag = x[ii];
215 }
216 ++ii;
217 if (!foundPeak && tempMag > thresh + x[ii]) {
218 foundPeak = true;
219 leftMin = x[ii];
220 peakInds.push_back(tempLoc);
221 peakMags.push_back(tempMag);
222 } else if (x[ii] < leftMin) {
223 leftMin = x[ii];
224 }
225 }
226 if (x.back() > tempMag && x.back() > leftMin + thresh) {
227 peakInds.push_back(length - 1);
228 peakMags.push_back(x.back());
229 } else if (!foundPeak && tempMag > minMag) {
230 peakInds.push_back(tempLoc);
231 peakMags.push_back(tempMag);
232 }
233 for (std::size_t k = 0; k < peakInds.size(); ++k)
234 result.append(qMakePair(static_cast<int>(ind[peakInds[k]]), extrema < 0 ? -peakMags[k] : peakMags[k]));
235 } else {
236 const auto best = std::max_element(x.begin(), x.end());
237 if (*best > minMag + thresh)
238 result.append(qMakePair(static_cast<int>(ind[best - x.begin()]), extrema < 0 ? -*best : *best));
239 }
240 return result;
241}
Local-maxima peak detection in 1-D signals with prominence filtering.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
DSPSHARED_EXPORT QList< QPair< int, double > > peakFinder(const Eigen::VectorXd &data, const PeakFinderParams &params=PeakFinderParams())
Find peaks in a 1D signal.
DSPSHARED_EXPORT Eigen::VectorXd peakProminences(const Eigen::VectorXd &data, const QList< int > &peakIndices)
Compute prominence of each peak.
DSPSHARED_EXPORT QList< QPair< int, double > > peakFinderMne(const Eigen::VectorXd &data, double thresh=std::numeric_limits< double >::quiet_NaN(), int extrema=1)
Noise-tolerant extrema search of mne.preprocessing.peak_finder.