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 "firfilter.h"
19#include "peak_finder.h"
20
21//=============================================================================================================
22// FIFF INCLUDES
23//=============================================================================================================
24
25#include <fiff/fiff_constants.h>
26#include <fiff/fiff_ch_info.h>
27#include <fiff/fiff_evoked.h>
28
29//=============================================================================================================
30// EIGEN INCLUDES
31//=============================================================================================================
32
33#include <Eigen/Core>
34
35//=============================================================================================================
36// QT INCLUDES
37//=============================================================================================================
38
39#include <QDebug>
40
41//=============================================================================================================
42// C++ INCLUDES
43//=============================================================================================================
44
45#include <cmath>
46#include <algorithm>
47#include <vector>
48
49//=============================================================================================================
50// USED NAMESPACES
51//=============================================================================================================
52
53using namespace UTILSLIB;
54using namespace FIFFLIB;
55using namespace Eigen;
56
57//=============================================================================================================
58// PRIVATE HELPERS
59//=============================================================================================================
60
61namespace
62{
63
64QVector<int> goodChannelsOfKind(const FiffInfo& info, int kind)
65{
66 QVector<int> idx;
67 for (int i = 0; i < info.nchan; ++i) {
68 if (info.chs[i].kind == kind && !info.bads.contains(info.ch_names.value(i, info.chs[i].ch_name)))
69 idx.append(i);
70 }
71 return idx;
72}
73
74// Adapted from mne.preprocessing.ssp._compute_exg_proj (MNE-Python, BSD-3-Clause).
75QList<FiffProj> exgProjectors(const MatrixXd& data, const FiffInfo& info, double sf, const QVector<int>& events,
76 const ArtifactDetectProjParams& p, const QString& mode)
77{
78 QList<FiffProj> projs = info.projs;
79 if (events.isEmpty()) {
80 qWarning() << "[ArtifactDetect] No" << mode << "events found";
81 return projs;
82 }
83
84 QVector<int> picks;
86 picks += goodChannelsOfKind(info, kind);
87 }
88 std::sort(picks.begin(), picks.end());
89 RowVectorXi sel(picks.size());
90 MatrixXd picked(picks.size(), data.cols());
91 for (int c = 0; c < picks.size(); ++c) {
92 sel(c) = picks[c];
93 picked.row(c) = data.row(picks[c]);
94 }
95 const FiffInfo pickedInfo = info.pick_info(sel);
97
98 // Epochs(proj=True) applies every info projector before rejection.
99 QList<FiffProj> active = info.projs;
100 for (FiffProj& proj : active) {
101 proj.active = true;
102 }
103 MatrixXd projector;
104 FiffProj::make_projector(active, pickedInfo.ch_names, projector, pickedInfo.bads);
105
106 QVector<double> limits(picks.size(), 0.0);
107 for (int c = 0; c < picks.size(); ++c) {
108 const FiffChInfo& ch = pickedInfo.chs[c];
109 if (ch.kind == FIFFV_MEG_CH) {
110 limits[c] = ch.unit == FIFF_UNIT_T ? p.dMagReject : p.dGradReject;
111 } else if (ch.kind == FIFFV_EEG_CH) {
112 limits[c] = p.dEegReject;
113 } else if (ch.kind == FIFFV_EOG_CH) {
114 limits[c] = p.dEogReject;
115 }
116 }
117
118 const int first = static_cast<int>(std::round(p.dTmin * sf));
119 const int last = static_cast<int>(std::round(p.dTmax * sf));
120 MatrixXd sum = MatrixXd::Zero(picks.size(), last - first + 1);
121 int nave = 0;
122 for (int ev : events) {
123 if (ev + first < 0 || ev + last >= filtered.cols()) {
124 continue;
125 }
126 const MatrixXd epoch = projector * filtered.middleCols(ev + first, last - first + 1);
127 bool good = true;
128 for (int c = 0; c < picks.size() && good; ++c) {
129 good = limits[c] <= 0.0 || epoch.row(c).maxCoeff() - epoch.row(c).minCoeff() <= limits[c];
130 }
131 if (good) {
132 sum += epoch;
133 ++nave;
134 }
135 }
136 if (nave == 0) {
137 qWarning() << "[ArtifactDetect] No good" << mode << "epochs found";
138 return projs;
139 }
140
141 FiffEvoked evoked;
142 evoked.info = pickedInfo;
143 evoked.nave = nave;
144 evoked.data = sum / nave;
145 evoked.times = RowVectorXf::LinSpaced(last - first + 1, static_cast<float>(first / sf), static_cast<float>(last / sf));
146 QList<FiffProj> exg = FiffProj::compute_from_evoked(evoked, p.nGrad, p.nMag, p.nEeg);
147 for (FiffProj& proj : exg) {
148 proj.desc = mode + "-" + proj.desc;
149 }
150 return projs + exg;
151}
152
153} // namespace
154
155//=============================================================================================================
156
157QVector<int> ArtifactDetect::qrsDetector(const RowVectorXd& filtered, double dSFreq, double dThreshold, double dTStart)
158{
159 // Adapted from mne.preprocessing.qrs_detector (MNE-Python, BSD-3-Clause), filtering done by the caller.
160 const int winSize = static_cast<int>(std::nearbyint(60.0 * dSFreq / 120.0));
161 const int init = static_cast<int>(dSFreq);
162 const int nStart = static_cast<int>(dSFreq * dTStart);
163 const RowVectorXd ecgAbs = filtered.tail(std::max<Index>(filtered.size() - nStart, 0)).cwiseAbs();
164 const int nPoints = static_cast<int>(ecgAbs.size());
165 if (nPoints < 3 * init || init < 1) {
166 qWarning() << "ArtifactDetect::detectEcg: need at least 3 s of data after tstart.";
167 return {};
168 }
169 const double initMax = (ecgAbs.segment(0, init).maxCoeff() + ecgAbs.segment(init, init).maxCoeff() + ecgAbs.segment(2 * init, init).maxCoeff()) / 3.0;
170
171 std::vector<double> threshRuns;
172 if (dThreshold < 0.0) {
173 for (int k = 0; k < 16; ++k)
174 threshRuns.push_back(0.3 + 0.05 * k);
175 } else {
176 threshRuns.push_back(dThreshold);
177 }
178
179 QVector<QVector<int>> cleanEvents;
180 for (double threshValue : threshRuns) {
181 const double thresh1 = initMax * threshValue;
182 QVector<int> times;
183 std::vector<double> rms;
184 for (int ii = 0; ii < nPoints - winSize;) {
185 const auto window = ecgAbs.segment(ii, winSize);
186 if (window(0) > thresh1) {
187 Index maxTime = 0;
188 window.maxCoeff(&maxTime);
189 times.append(ii + static_cast<int>(maxTime));
190 rms.push_back(std::sqrt(window.squaredNorm() / winSize));
191 ii += winSize;
192 } else {
193 ++ii;
194 }
195 }
196 // mne's crossing count per window is always < n_thresh (the window starts above threshold), so it is omitted
197 if (rms.empty())
198 continue;
199 double mean = 0.0;
200 for (double r : rms)
201 mean += r;
202 mean /= static_cast<double>(rms.size());
203 double var = 0.0;
204 for (double r : rms)
205 var += (r - mean) * (r - mean);
206 const double rmsThresh = mean + std::sqrt(var / static_cast<double>(rms.size())) * 2.5;
207 QVector<int> events;
208 for (int k = 0; k < times.size(); ++k) {
209 if (rms[static_cast<std::size_t>(k)] < rmsThresh)
210 events.append(times[k] + nStart);
211 }
212 if (!events.isEmpty())
213 cleanEvents.append(events);
214 }
215 if (cleanEvents.isEmpty())
216 return {};
217
218 // Pick the threshold whose heart rate is closest to the median plausible (40-160 bpm) one, else 80 bpm
219 std::vector<double> rates;
220 std::vector<double> plausible;
221 for (const QVector<int>& events : std::as_const(cleanEvents)) {
222 rates.push_back(60.0 * events.size() / (filtered.size() / dSFreq));
223 if (rates.back() >= 40.0 && rates.back() <= 160.0)
224 plausible.push_back(rates.back());
225 }
226 double ideal = 80.0;
227 if (!plausible.empty()) {
228 std::sort(plausible.begin(), plausible.end());
229 const std::size_t m = plausible.size() / 2;
230 ideal = plausible.size() % 2 ? plausible[m] : 0.5 * (plausible[m - 1] + plausible[m]);
231 }
232 std::size_t best = 0;
233 for (std::size_t k = 1; k < rates.size(); ++k) {
234 if (std::abs(rates[k] - ideal) < std::abs(rates[best] - ideal))
235 best = k;
236 }
237 return cleanEvents[static_cast<int>(best)];
238}
239
240//=============================================================================================================
241// PUBLIC DEFINITIONS
242//=============================================================================================================
243
244QVector<int> ArtifactDetect::detectEcg(const MatrixXd& matData,
245 const FiffInfo& fiffInfo,
246 double dSFreq,
247 const EcgParams& params)
248{
249 // Adapted from mne.preprocessing.find_ecg_events / _make_ecg (MNE-Python, BSD-3-Clause).
250 if (matData.cols() == 0)
251 return {};
252
253 RowVectorXd ecg;
254 const QVector<int> ecgIdx = goodChannelsOfKind(fiffInfo, FIFFV_ECG_CH);
255 if (!ecgIdx.isEmpty()) {
256 ecg = matData.row(ecgIdx.first());
257 } else {
258 QVector<int> mags, grads;
259 for (int i : goodChannelsOfKind(fiffInfo, FIFFV_MEG_CH))
260 (fiffInfo.chs[i].unit == FIFF_UNIT_T ? mags : grads).append(i);
261 const QVector<int>& meg = mags.isEmpty() ? grads : mags;
262 if (meg.isEmpty()) {
263 qWarning() << "ArtifactDetect::detectEcg: no ECG channel and no MEG channels to synthesise one from.";
264 return {};
265 }
266 ecg = RowVectorXd::Zero(matData.cols());
267 for (int i : meg)
268 ecg += matData.row(i);
269 ecg /= static_cast<double>(meg.size());
270 }
271
272 const RowVectorXd filtered = FirFilter::filterDataZeroDouble(ecg, dSFreq, params.dFilterLow, params.dFilterHigh);
273 return qrsDetector(filtered, dSFreq, params.dQrsThreshold, params.dTStart);
274}
275
276//=============================================================================================================
277
278QVector<int> ArtifactDetect::detectEog(const MatrixXd& matData,
279 const FiffInfo& fiffInfo,
280 double dSFreq,
281 const EogParams& params)
282{
283 // Adapted from mne.preprocessing._find_eog_events (MNE-Python, BSD-3-Clause).
284 const QVector<int> eogIdx = goodChannelsOfKind(fiffInfo, FIFFV_EOG_CH);
285 if (eogIdx.isEmpty() || matData.cols() == 0) {
286 qWarning() << "ArtifactDetect::detectEog: No EOG channel found.";
287 return {};
288 }
289
290 // The channel with the most 2-45 Hz energy
291 MatrixXd eog(eogIdx.size(), matData.cols());
292 for (int k = 0; k < eogIdx.size(); ++k)
293 eog.row(k) = matData.row(eogIdx[k]);
294 const MatrixXd broad = FirFilter::filterDataZeroDouble(eog, dSFreq, 2.0, std::min(45.0, dSFreq / 2.0 - 0.75));
295 Index best = 0;
296 broad.rowwise().squaredNorm().maxCoeff(&best);
297
298 const RowVectorXd filtered = FirFilter::filterDataZeroDouble(eog.row(best), dSFreq, params.dFilterLow, params.dFilterHigh);
299 const double mean = filtered.mean();
300 const int extrema = std::abs(filtered.maxCoeff() - mean) > std::abs(filtered.minCoeff() - mean) ? 1 : -1;
301 const int nStart = static_cast<int>(dSFreq * params.dTStart);
302
303 QVector<int> events;
304 const VectorXd tail = filtered.tail(std::max<Index>(filtered.size() - nStart, 0)).transpose();
305 for (const auto& peak : peakFinderMne(tail, params.dThreshold, extrema))
306 events.append(peak.first + nStart);
307 return events;
308}
309
310//=============================================================================================================
311
312QList<FiffProj> ArtifactDetect::computeProjEcg(const MatrixXd& matData, const FiffInfo& fiffInfo, double dSFreq,
313 const ProjParams& proj, const EcgParams& detect)
314{
315 return exgProjectors(matData, fiffInfo, dSFreq, detectEcg(matData, fiffInfo, dSFreq, detect), proj, "ECG");
316}
317
318//=============================================================================================================
319
320QList<FiffProj> ArtifactDetect::computeProjEog(const MatrixXd& matData, const FiffInfo& fiffInfo, double dSFreq,
321 const ProjParams& proj, const EogParams& detect)
322{
323 return exgProjectors(matData, fiffInfo, dSFreq, detectEog(matData, fiffInfo, dSFreq, detect), proj, "EOG");
324}
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.
constexpr auto FIFFV_ECG_CH
#define FIFFV_EEG_CH
#define FIFFV_MEG_CH
constexpr auto FIFF_UNIT_T
constexpr auto FIFFV_EOG_CH
Single averaged evoked response: time axis, samples, baseline, channel info and processing history.
Local-maxima peak detection in 1-D signals with prominence filtering.
Discoverable design / apply façade over the UTILSLIB::FilterKernel FIR engine.
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).
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.
SSP parameters of compute_proj_ecg / compute_proj_eog; ecg() and eog() give the MNE-Python defaults.
ArtifactDetectProjParams ProjParams
static QVector< int > detectEcg(const Eigen::MatrixXd &matData, const FIFFLIB::FiffInfo &fiffInfo, double dSFreq, const EcgParams &params=EcgParams())
static QList< FIFFLIB::FiffProj > computeProjEcg(const Eigen::MatrixXd &matData, const FIFFLIB::FiffInfo &fiffInfo, double dSFreq, const ProjParams &proj=ProjParams::ecg(), const EcgParams &detect=EcgParams())
ArtifactDetectEogParams EogParams
ArtifactDetectEcgParams EcgParams
static QVector< int > detectEog(const Eigen::MatrixXd &matData, const FIFFLIB::FiffInfo &fiffInfo, double dSFreq, const EogParams &params=EogParams())
static QList< FIFFLIB::FiffProj > computeProjEog(const Eigen::MatrixXd &matData, const FIFFLIB::FiffInfo &fiffInfo, double dSFreq, const ProjParams &proj=ProjParams::eog(), const EogParams &detect=EogParams())
static Eigen::MatrixXd filterDataZeroDouble(const Eigen::MatrixXd &matData, double dSFreq, double dLFreq, double dHFreq, FirWindow window=FirWindow::Hann)
Per-channel FIFF descriptor: identifiers, kind, calibration, coil type, channel-frame coil position a...
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Definition fiff_evoked.h:77
Eigen::RowVectorXf times
Eigen::MatrixXd data
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:90
FiffInfo pick_info(const Eigen::RowVectorXi &sel=defaultVectorXi) const
QList< FiffProj > projs
Definition fiff_info.h:292
QList< FiffChInfo > chs
Single SSP projection item: kind, active flag, desired flag and the named projection vector matrix.
Definition fiff_proj.h:82
static fiff_int_t make_projector(const QList< FiffProj > &projs, const QStringList &ch_names, Eigen::MatrixXd &proj, const QStringList &bads=defaultQStringList, Eigen::MatrixXd &U=defaultMatrixXd)
static QList< FiffProj > compute_from_evoked(const FiffEvoked &evoked, int nGrad=2, int nMag=2, int nEeg=2)