v2.0.0
Loading...
Searching...
No Matches
annotate_artifact.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "annotate_artifact.h"
18#include "connectivity_aec.h"
19#include "firfilter.h"
20
21#include <fiff/fiff_info.h>
22#include <fiff/fiff_constants.h>
23
24//=============================================================================================================
25// QT INCLUDES
26//=============================================================================================================
27
28#include <QDebug>
29
30//=============================================================================================================
31// STL INCLUDES
32//=============================================================================================================
33
34#include <cmath>
35#include <algorithm>
36
37//=============================================================================================================
38// USED NAMESPACES
39//=============================================================================================================
40
41using namespace UTILSLIB;
42using namespace FIFFLIB;
43using namespace Eigen;
44
45//=============================================================================================================
46// STATIC HELPERS
47//=============================================================================================================
48
49namespace
50{
51
52//=============================================================================================================
56QVector<QPair<int, int>> findContiguousSegments(const VectorXi& mask)
57{
58 QVector<QPair<int, int>> segs;
59 const int n = static_cast<int>(mask.size());
60 int i = 0;
61 while (i < n) {
62 if (mask(i)) {
63 int start = i;
64 while (i < n && mask(i))
65 ++i;
66 segs.append({start, i - 1});
67 } else {
68 ++i;
69 }
70 }
71 return segs;
72}
73
74//=============================================================================================================
78void removeShortSegments(QVector<QPair<int, int>>& segs, int minSamples)
79{
80 if (minSamples <= 1)
81 return;
82 QVector<QPair<int, int>> filtered;
83 for (const auto& seg : segs) {
84 if (seg.second - seg.first + 1 >= minSamples)
85 filtered.append(seg);
86 }
87 segs = filtered;
88}
89
90//=============================================================================================================
94int nextFastLen(int n)
95{
96 int best = std::numeric_limits<int>::max();
97 for (long long p5 = 1; p5 < 2LL * n + 1; p5 *= 5) {
98 for (long long p35 = p5; p35 < 2LL * n + 1; p35 *= 3) {
99 long long v = p35;
100 while (v < n)
101 v *= 2;
102 best = static_cast<int>(std::min<long long>(best, v));
103 }
104 }
105 return best;
106}
107
108} // anonymous namespace
109
110//=============================================================================================================
111// DEFINE MEMBER METHODS
112//=============================================================================================================
113
115 const MatrixXd& data,
116 const FiffInfo& info,
117 double sfreq,
118 const AnnotateMusclParams& params,
119 RowVectorXd* scores)
120{
121 // Adapted from mne.preprocessing.annotate_muscle_zscore (MNE-Python, BSD-3-Clause).
122 FiffAnnotations annot;
123 const Index nTimes = data.cols();
124 if (data.rows() == 0 || nTimes == 0)
125 return annot;
126
127 RowVectorXi picks = info.pick_types(QStringLiteral("mag"), false, false);
128 if (picks.size() == 0)
129 picks = info.pick_types(QStringLiteral("grad"), false, false);
130 if (picks.size() == 0)
131 picks = info.pick_types(false, true, false);
132 if (picks.size() == 0) {
133 qWarning("annotateMusclZscore: no MEG or EEG channels found");
134 return annot;
135 }
136 MatrixXd sub(picks.size(), nTimes);
137 for (Index i = 0; i < picks.size(); ++i)
138 sub.row(i) = data.row(picks(i));
139
140 const MatrixXd band = FirFilter::filterData(sub, sfreq, params.dFilterLow, params.dFilterHigh);
141 const int nFft = nextFastLen(static_cast<int>(nTimes));
142 RowVectorXd score = RowVectorXd::Zero(nTimes);
143 for (Index i = 0; i < band.rows(); ++i) {
144 const RowVectorXd env = ConnectivityAec::hilbertEnvelope(band.row(i).transpose(), nFft).transpose();
145 const double mean = env.mean();
146 const double sd = std::sqrt((env.array() - mean).square().mean());
147 if (sd > 0.0)
148 score += (env.array() - mean).matrix() / sd;
149 }
150 score /= std::sqrt(static_cast<double>(band.rows()));
151 score = FirFilter::filterData(score, sfreq, -1.0, 4.0);
152 if (scores)
153 *scores = score;
154
155 VectorXi mask(nTimes);
156 for (Index i = 0; i < nTimes; ++i)
157 mask(i) = score(i) > params.dThreshold ? 1 : 0;
158 // Good stretches shorter than min_length_good, including those at the edges, become bad.
159 const double minGood = params.dMinLengthGood * sfreq;
160 for (const auto& seg : findContiguousSegments((1 - mask.array()).matrix())) {
161 if (seg.second - seg.first + 1 < minGood)
162 mask.segment(seg.first, seg.second - seg.first + 1).setOnes();
163 }
164 for (const auto& seg : findContiguousSegments(mask)) {
165 const int last = std::min(seg.second + 1, static_cast<int>(nTimes) - 1);
166 annot.append(seg.first / sfreq, (last - seg.first) / sfreq, QStringLiteral("BAD_muscle"));
167 }
168 return annot;
169}
170
171//=============================================================================================================
172
174 const MatrixXd& data,
175 const FiffInfo& info,
176 double sfreq,
177 const AnnotateAmplitudeParams& params)
178{
179 FiffAnnotations annot;
180 const Eigen::Index nCh = data.rows();
181 const Eigen::Index nTimes = data.cols();
182
183 if (nCh == 0 || nTimes == 0)
184 return annot;
185
186 const bool checkPeakMax = std::isfinite(params.dPeakMax);
187 const bool checkPeakMin = std::isfinite(params.dPeakMin);
188 const bool checkFlat = params.dFlatMin > 0.0;
189 const int minSamples = static_cast<int>(std::round(params.dMinDuration * sfreq));
190
191 //--- Peak amplitude check per channel ---
192 if (checkPeakMax || checkPeakMin) {
193 for (Eigen::Index ch = 0; ch < nCh; ++ch) {
194 const QString chName = (ch < info.ch_names.size()) ? info.ch_names[static_cast<int>(ch)] : QString("CH%1").arg(ch);
195
196 VectorXi mask(nTimes);
197 for (Eigen::Index s = 0; s < nTimes; ++s) {
198 const double val = data(ch, s);
199 mask(static_cast<int>(s)) = ((checkPeakMax && val > params.dPeakMax) ||
200 (checkPeakMin && val < params.dPeakMin))
201 ? 1
202 : 0;
203 }
204
205 auto segs = findContiguousSegments(mask);
206 removeShortSegments(segs, minSamples);
207
208 for (const auto& seg : segs) {
209 const double onset = static_cast<double>(seg.first) / sfreq;
210 const double duration = static_cast<double>(seg.second - seg.first + 1) / sfreq;
211 annot.append(onset, duration, params.badDescription, QStringList{chName});
212 }
213 }
214 }
215
216 //--- Flatness check per channel ---
217 if (checkFlat) {
218 const int winSamples = std::max(1, static_cast<int>(std::round(params.dWindowSec * sfreq)));
219
220 for (Eigen::Index ch = 0; ch < nCh; ++ch) {
221 const QString chName = (ch < info.ch_names.size()) ? info.ch_names[static_cast<int>(ch)] : QString("CH%1").arg(ch);
222
223 VectorXi mask = VectorXi::Zero(static_cast<int>(nTimes));
224
225 for (Eigen::Index s = 0; s <= nTimes - winSamples; ++s) {
226 const auto seg = data.block(ch, s, 1, winSamples);
227 const double p2p = seg.maxCoeff() - seg.minCoeff();
228 if (p2p < params.dFlatMin) {
229 for (int j = static_cast<int>(s); j < static_cast<int>(s) + winSamples; ++j)
230 mask(j) = 1;
231 }
232 }
233
234 auto segs = findContiguousSegments(mask);
235 removeShortSegments(segs, minSamples);
236
237 for (const auto& seg : segs) {
238 const double onset = static_cast<double>(seg.first) / sfreq;
239 const double duration = static_cast<double>(seg.second - seg.first + 1) / sfreq;
240 annot.append(onset, duration, QStringLiteral("BAD_flat"), QStringList{chName});
241 }
242 }
243 }
244
245 return annot;
246}
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
Full FIFF measurement metadata: everything from FIFFB_MEAS / FIFFB_MEAS_INFO needed to interpret a re...
Continuous-data annotation of muscle and amplitude artefacts.
Discoverable design / apply façade over the UTILSLIB::FilterKernel FIR engine.
ConnectivityAec — Amplitude Envelope Correlation connectivity metric.
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 FIFFLIB::FiffAnnotations annotateMusclZscore(const Eigen::MatrixXd &data, const FIFFLIB::FiffInfo &info, double sfreq, const AnnotateMusclParams &params=AnnotateMusclParams(), Eigen::RowVectorXd *scores=nullptr)
Detect muscle artifacts via high-frequency z-score and annotate bad segments.
DSPSHARED_EXPORT FIFFLIB::FiffAnnotations annotateAmplitude(const Eigen::MatrixXd &data, const FIFFLIB::FiffInfo &info, double sfreq, const AnnotateAmplitudeParams &params=AnnotateAmplitudeParams())
Annotate segments where amplitude exceeds thresholds or is too flat.
Parameters for muscle artifact annotation.
Parameters for amplitude-based annotation.
static Eigen::VectorXd hilbertEnvelope(const Eigen::VectorXd &signal, int nFft=0)
static Eigen::MatrixXd filterData(const Eigen::MatrixXd &matData, double dSFreq, double dLFreq, double dHFreq)
Container for FiffAnnotation entries with FIFF, JSON and CSV serializers.
void append(const FiffAnnotation &annotation)
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:90
Eigen::RowVectorXi pick_types(const QString meg, bool eeg=false, bool stim=false, const QStringList &include=defaultQStringList, const QStringList &exclude=defaultQStringList) const