v2.0.0
Loading...
Searching...
No Matches
artifact_detect.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "artifact_detect.h"
18#include "iirfilter.h"
19
20//=============================================================================================================
21// FIFF INCLUDES
22//=============================================================================================================
23
24#include <fiff/fiff_constants.h>
25#include <fiff/fiff_ch_info.h>
26
27//=============================================================================================================
28// EIGEN INCLUDES
29//=============================================================================================================
30
31#include <Eigen/Core>
32
33//=============================================================================================================
34// QT INCLUDES
35//=============================================================================================================
36
37#include <QDebug>
38
39//=============================================================================================================
40// C++ INCLUDES
41//=============================================================================================================
42
43#include <cmath>
44#include <algorithm>
45
46//=============================================================================================================
47// USED NAMESPACES
48//=============================================================================================================
49
50using namespace UTILSLIB;
51using namespace FIFFLIB;
52using namespace Eigen;
53
54//=============================================================================================================
55// PRIVATE HELPERS
56//=============================================================================================================
57
58RowVectorXd ArtifactDetect::bandpassFilter(const RowVectorXd& vecSignal,
59 double dSFreq,
60 double dLow,
61 double dHigh,
62 int iOrder)
63{
64 QVector<IirBiquad> sos;
65
66 if (dLow < 1e-3) {
67 // Low-pass only
68 sos = IirFilter::designButterworth(iOrder, IirFilter::LowPass, dHigh, 0.0, dSFreq);
69 } else {
70 sos = IirFilter::designButterworth(iOrder, IirFilter::BandPass, dLow, dHigh, dSFreq);
71 }
72
73 return IirFilter::applyZeroPhase(vecSignal, sos);
74}
75
76//=============================================================================================================
77
78QVector<int> ArtifactDetect::findPeaks(const RowVectorXd& vecSignal,
79 double dThreshold,
80 int iMinDist)
81{
82 QVector<int> peaks;
83 const int N = static_cast<int>(vecSignal.size());
84 if (N < 3)
85 return peaks;
86
87 int lastPeak = -iMinDist - 1;
88
89 for (int i = 1; i < N - 1; ++i) {
90 double v = vecSignal(i);
91 if (v < dThreshold)
92 continue;
93
94 // Local maximum check
95 if (v > vecSignal(i - 1) && v >= vecSignal(i + 1)) {
96 // Enforce minimum distance
97 if (i - lastPeak >= iMinDist) {
98 peaks.append(i);
99 lastPeak = i;
100 } else if (!peaks.isEmpty() && vecSignal(peaks.last()) < v) {
101 // Replace last peak if this one is higher and within the distance window
102 peaks.last() = i;
103 lastPeak = i;
104 }
105 }
106 }
107
108 return peaks;
109}
110
111//=============================================================================================================
112// PUBLIC DEFINITIONS
113//=============================================================================================================
114
115QVector<int> ArtifactDetect::detectEcg(const MatrixXd& matData,
116 const FiffInfo& fiffInfo,
117 double dSFreq,
118 const EcgParams& params)
119{
120 // ---- Find ECG channel ----
121 int ecgIdx = -1;
122 for (int i = 0; i < fiffInfo.nchan; ++i) {
123 if (fiffInfo.chs[i].kind == FIFFV_ECG_CH) {
124 ecgIdx = i;
125 break;
126 }
127 }
128
129 RowVectorXd ecgSignal;
130
131 if (ecgIdx >= 0) {
132 // Use dedicated ECG channel (already calibrated)
133 ecgSignal = matData.row(ecgIdx).cast<double>();
134 } else {
135 // ---- Synthetic ECG from MEG channels ----
136 // The cardiac artifact is visible on most MEG channels. A robust synthetic ECG is
137 // obtained by summing the absolute values of all magnetometer channels (gradiometer
138 // baseline artefact reduces them; magnetometers show the global field pattern).
139 qWarning() << "ArtifactDetect::detectEcg: No ECG channel found. "
140 "Synthesising ECG proxy from MEG magnetometers.";
141
142 QVector<int> magIdx;
143 for (int i = 0; i < fiffInfo.nchan; ++i) {
144 if (fiffInfo.chs[i].kind == FIFFV_MEG_CH) {
145 // Distinguish magnetometers from gradiometers by coil type:
146 // magnetometers have a single integration point — heuristically identified
147 // as FIFFV_COIL_MAG type; use channel unit as a proxy (T vs T/m).
148 // Use unit: FIFF_UNIT_T = 112, FIFF_UNIT_T_M = 201
149 if (fiffInfo.chs[i].unit == 112) { // Tesla — magnetometer
150 magIdx.append(i);
151 }
152 }
153 }
154
155 if (magIdx.isEmpty()) {
156 // Fall back: any MEG channel
157 for (int i = 0; i < fiffInfo.nchan; ++i) {
158 if (fiffInfo.chs[i].kind == FIFFV_MEG_CH) {
159 magIdx.append(i);
160 }
161 }
162 }
163
164 if (magIdx.isEmpty()) {
165 qWarning() << "ArtifactDetect::detectEcg: No MEG channels found. Cannot detect ECG.";
166 return {};
167 }
168
169 const int nSamp = static_cast<int>(matData.cols());
170 ecgSignal = RowVectorXd::Zero(nSamp);
171 for (int idx : magIdx) {
172 ecgSignal += matData.row(idx).cwiseAbs().cast<double>();
173 }
174 ecgSignal /= static_cast<double>(magIdx.size());
175 }
176
177 // ---- Band-pass filter to isolate QRS complex ----
178 RowVectorXd filtered = bandpassFilter(ecgSignal, dSFreq,
179 params.dFilterLow, params.dFilterHigh,
180 params.iFilterOrder);
181
182 // ---- Adaptive threshold: fraction of the peak-to-peak amplitude ----
183 double sigMin = filtered.minCoeff();
184 double sigMax = filtered.maxCoeff();
185 double threshold = params.dThreshFactor * (sigMax - sigMin) + sigMin;
186
187 // Minimum inter-peak distance in samples
188 int iMinDist = static_cast<int>(std::round(params.dMinRRSec * dSFreq));
189 iMinDist = std::max(iMinDist, 1);
190
191 return findPeaks(filtered, threshold, iMinDist);
192}
193
194//=============================================================================================================
195
196QVector<int> ArtifactDetect::detectEog(const MatrixXd& matData,
197 const FiffInfo& fiffInfo,
198 double dSFreq,
199 const EogParams& params)
200{
201 // ---- Find EOG channel(s) ----
202 QVector<int> eogIdx;
203 for (int i = 0; i < fiffInfo.nchan; ++i) {
204 if (fiffInfo.chs[i].kind == FIFFV_EOG_CH) {
205 eogIdx.append(i);
206 }
207 }
208
209 if (eogIdx.isEmpty()) {
210 qWarning() << "ArtifactDetect::detectEog: No EOG channel found.";
211 return {};
212 }
213
214 // Select the EOG channel with the largest peak-to-peak amplitude
215 int bestIdx = eogIdx[0];
216 double bestPtp = 0.0;
217 for (int idx : eogIdx) {
218 RowVectorXd row = matData.row(idx).cast<double>();
219 double ptp = row.maxCoeff() - row.minCoeff();
220 if (ptp > bestPtp) {
221 bestPtp = ptp;
222 bestIdx = idx;
223 }
224 }
225
226 RowVectorXd eogSignal = matData.row(bestIdx).cast<double>();
227
228 // ---- Low-pass filter ----
229 RowVectorXd filtered = bandpassFilter(eogSignal, dSFreq,
230 0.0, params.dFilterHigh,
231 params.iFilterOrder);
232
233 // ---- Detect excursions above ±threshold ----
234 // Work on the absolute value so both positive (downward blinks, depending on EOG polarity)
235 // and negative deflections are detected.
236 RowVectorXd absFiltered = filtered.cwiseAbs();
237
238 int iMinDist = static_cast<int>(std::round(params.dMinGapSec * dSFreq));
239 iMinDist = std::max(iMinDist, 1);
240
241 return findPeaks(absFiltered, params.dThresholdV, iMinDist);
242}
FIFF channel descriptor record (FIFF_CH_INFO): per-channel logical/scanner numbers,...
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_EOG_CH
#define FIFFV_MEG_CH
#define FIFFV_ECG_CH
Declaration of ArtifactDetect — ECG and EOG physiological artifact event detection.
Butterworth IIR filter design and application via numerically stable second-order sections.
FIFF file I/O, in-memory data structures and high-level readers/writers.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
static QVector< int > detectEcg(const Eigen::MatrixXd &matData, const FIFFLIB::FiffInfo &fiffInfo, double dSFreq, const EcgParams &params=EcgParams())
ArtifactDetectEogParams EogParams
ArtifactDetectEcgParams EcgParams
static QVector< int > detectEog(const Eigen::MatrixXd &matData, const FIFFLIB::FiffInfo &fiffInfo, double dSFreq, const EogParams &params=EogParams())
static Eigen::RowVectorXd applyZeroPhase(const Eigen::RowVectorXd &vecData, const QVector< IirBiquad > &sos)
static QVector< IirBiquad > designButterworth(int iOrder, FilterType type, double dCutoffLow, double dCutoffHigh, double dSFreq)
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:90
QList< FiffChInfo > chs