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) return peaks;
85
86 int lastPeak = -iMinDist - 1;
87
88 for (int i = 1; i < N - 1; ++i) {
89 double v = vecSignal(i);
90 if (v < dThreshold) continue;
91
92 // Local maximum check
93 if (v > vecSignal(i - 1) && v >= vecSignal(i + 1)) {
94 // Enforce minimum distance
95 if (i - lastPeak >= iMinDist) {
96 peaks.append(i);
97 lastPeak = i;
98 } else if (!peaks.isEmpty() && vecSignal(peaks.last()) < v) {
99 // Replace last peak if this one is higher and within the distance window
100 peaks.last() = i;
101 lastPeak = i;
102 }
103 }
104 }
105
106 return peaks;
107}
108
109//=============================================================================================================
110// PUBLIC DEFINITIONS
111//=============================================================================================================
112
113QVector<int> ArtifactDetect::detectEcg(const MatrixXd& matData,
114 const FiffInfo& fiffInfo,
115 double dSFreq,
116 const EcgParams& params)
117{
118 // ---- Find ECG channel ----
119 int ecgIdx = -1;
120 for (int i = 0; i < fiffInfo.nchan; ++i) {
121 if (fiffInfo.chs[i].kind == FIFFV_ECG_CH) {
122 ecgIdx = i;
123 break;
124 }
125 }
126
127 RowVectorXd ecgSignal;
128
129 if (ecgIdx >= 0) {
130 // Use dedicated ECG channel (already calibrated)
131 ecgSignal = matData.row(ecgIdx).cast<double>();
132 } else {
133 // ---- Synthetic ECG from MEG channels ----
134 // The cardiac artifact is visible on most MEG channels. A robust synthetic ECG is
135 // obtained by summing the absolute values of all magnetometer channels (gradiometer
136 // baseline artefact reduces them; magnetometers show the global field pattern).
137 qWarning() << "ArtifactDetect::detectEcg: No ECG channel found. "
138 "Synthesising ECG proxy from MEG magnetometers.";
139
140 QVector<int> magIdx;
141 for (int i = 0; i < fiffInfo.nchan; ++i) {
142 if (fiffInfo.chs[i].kind == FIFFV_MEG_CH) {
143 // Distinguish magnetometers from gradiometers by coil type:
144 // magnetometers have a single integration point — heuristically identified
145 // as FIFFV_COIL_MAG type; use channel unit as a proxy (T vs T/m).
146 // Use unit: FIFF_UNIT_T = 112, FIFF_UNIT_T_M = 201
147 if (fiffInfo.chs[i].unit == 112) { // Tesla — magnetometer
148 magIdx.append(i);
149 }
150 }
151 }
152
153 if (magIdx.isEmpty()) {
154 // Fall back: any MEG channel
155 for (int i = 0; i < fiffInfo.nchan; ++i) {
156 if (fiffInfo.chs[i].kind == FIFFV_MEG_CH) {
157 magIdx.append(i);
158 }
159 }
160 }
161
162 if (magIdx.isEmpty()) {
163 qWarning() << "ArtifactDetect::detectEcg: No MEG channels found. Cannot detect ECG.";
164 return {};
165 }
166
167 const int nSamp = static_cast<int>(matData.cols());
168 ecgSignal = RowVectorXd::Zero(nSamp);
169 for (int idx : magIdx) {
170 ecgSignal += matData.row(idx).cwiseAbs().cast<double>();
171 }
172 ecgSignal /= static_cast<double>(magIdx.size());
173 }
174
175 // ---- Band-pass filter to isolate QRS complex ----
176 RowVectorXd filtered = bandpassFilter(ecgSignal, dSFreq,
177 params.dFilterLow, params.dFilterHigh,
178 params.iFilterOrder);
179
180 // ---- Adaptive threshold: fraction of the peak-to-peak amplitude ----
181 double sigMin = filtered.minCoeff();
182 double sigMax = filtered.maxCoeff();
183 double threshold = params.dThreshFactor * (sigMax - sigMin) + sigMin;
184
185 // Minimum inter-peak distance in samples
186 int iMinDist = static_cast<int>(std::round(params.dMinRRSec * dSFreq));
187 iMinDist = std::max(iMinDist, 1);
188
189 return findPeaks(filtered, threshold, iMinDist);
190}
191
192//=============================================================================================================
193
194QVector<int> ArtifactDetect::detectEog(const MatrixXd& matData,
195 const FiffInfo& fiffInfo,
196 double dSFreq,
197 const EogParams& params)
198{
199 // ---- Find EOG channel(s) ----
200 QVector<int> eogIdx;
201 for (int i = 0; i < fiffInfo.nchan; ++i) {
202 if (fiffInfo.chs[i].kind == FIFFV_EOG_CH) {
203 eogIdx.append(i);
204 }
205 }
206
207 if (eogIdx.isEmpty()) {
208 qWarning() << "ArtifactDetect::detectEog: No EOG channel found.";
209 return {};
210 }
211
212 // Select the EOG channel with the largest peak-to-peak amplitude
213 int bestIdx = eogIdx[0];
214 double bestPtp = 0.0;
215 for (int idx : eogIdx) {
216 RowVectorXd row = matData.row(idx).cast<double>();
217 double ptp = row.maxCoeff() - row.minCoeff();
218 if (ptp > bestPtp) {
219 bestPtp = ptp;
220 bestIdx = idx;
221 }
222 }
223
224 RowVectorXd eogSignal = matData.row(bestIdx).cast<double>();
225
226 // ---- Low-pass filter ----
227 RowVectorXd filtered = bandpassFilter(eogSignal, dSFreq,
228 0.0, params.dFilterHigh,
229 params.iFilterOrder);
230
231 // ---- Detect excursions above ±threshold ----
232 // Work on the absolute value so both positive (downward blinks, depending on EOG polarity)
233 // and negative deflections are detected.
234 RowVectorXd absFiltered = filtered.cwiseAbs();
235
236 int iMinDist = static_cast<int>(std::round(params.dMinGapSec * dSFreq));
237 iMinDist = std::max(iMinDist, 1);
238
239 return findPeaks(absFiltered, params.dThresholdV, iMinDist);
240}
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
FIFF channel descriptor record (FIFF_CH_INFO): per-channel logical/scanner numbers,...
Butterworth IIR filter design and application via numerically stable second-order sections.
Declaration of ArtifactDetect — ECG and EOG physiological artifact event detection.
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:88
QList< FiffChInfo > chs