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 "iirfilter.h"
19
20#include <fiff/fiff_info.h>
21#include <fiff/fiff_constants.h>
22
23//=============================================================================================================
24// QT INCLUDES
25//=============================================================================================================
26
27#include <QDebug>
28
29//=============================================================================================================
30// STL INCLUDES
31//=============================================================================================================
32
33#include <cmath>
34#include <algorithm>
35
36//=============================================================================================================
37// USED NAMESPACES
38//=============================================================================================================
39
40using namespace UTILSLIB;
41using namespace FIFFLIB;
42using namespace Eigen;
43
44//=============================================================================================================
45// STATIC HELPERS
46//=============================================================================================================
47
48namespace {
49
50//=============================================================================================================
54QVector<QPair<int,int>> findContiguousSegments(const VectorXi& mask)
55{
56 QVector<QPair<int,int>> segs;
57 const int n = static_cast<int>(mask.size());
58 int i = 0;
59 while (i < n) {
60 if (mask(i)) {
61 int start = i;
62 while (i < n && mask(i))
63 ++i;
64 segs.append({start, i - 1});
65 } else {
66 ++i;
67 }
68 }
69 return segs;
70}
71
72//=============================================================================================================
76void mergeCloseSegments(QVector<QPair<int,int>>& segs, int gapSamples)
77{
78 if (segs.size() < 2)
79 return;
80 QVector<QPair<int,int>> merged;
81 merged.append(segs.first());
82 for (int i = 1; i < segs.size(); ++i) {
83 if (segs[i].first - merged.last().second <= gapSamples) {
84 merged.last().second = segs[i].second;
85 } else {
86 merged.append(segs[i]);
87 }
88 }
89 segs = merged;
90}
91
92//=============================================================================================================
96void removeShortSegments(QVector<QPair<int,int>>& segs, int minSamples)
97{
98 if (minSamples <= 1)
99 return;
100 QVector<QPair<int,int>> filtered;
101 for (const auto& seg : segs) {
102 if (seg.second - seg.first + 1 >= minSamples)
103 filtered.append(seg);
104 }
105 segs = filtered;
106}
107
108//=============================================================================================================
112QVector<int> findChannelsByKind(const FiffInfo& info, int kind)
113{
114 QVector<int> indices;
115 for (int i = 0; i < info.chs.size(); ++i) {
116 if (info.chs[i].kind == kind)
117 indices.append(i);
118 }
119 return indices;
120}
121
122} // anonymous namespace
123
124//=============================================================================================================
125// DEFINE MEMBER METHODS
126//=============================================================================================================
127
129 const MatrixXd& data,
130 const FiffInfo& info,
131 double sfreq,
132 const AnnotateMusclParams& params)
133{
134 FiffAnnotations annot;
135 const Eigen::Index nTimes = data.cols();
136
137 if (data.rows() == 0 || nTimes == 0)
138 return annot;
139
140 //--- 1. Find MEG channels; fall back to EEG if none ---
141 QVector<int> chIdx = findChannelsByKind(info, FIFFV_MEG_CH);
142 if (chIdx.isEmpty())
143 chIdx = findChannelsByKind(info, FIFFV_EEG_CH);
144 if (chIdx.isEmpty()) {
145 qWarning("annotateMusclZscore: no MEG or EEG channels found");
146 return annot;
147 }
148
149 //--- 2. Extract submatrix ---
150 const int nCh = chIdx.size();
151 MatrixXd sub(nCh, nTimes);
152 for (int i = 0; i < nCh; ++i)
153 sub.row(i) = data.row(chIdx[i]);
154
155 //--- 3. Bandpass filter ---
158 params.dFilterLow,
159 params.dFilterHigh,
160 sfreq);
161 MatrixXd filtered = IirFilter::applyZeroPhaseMatrix(sub, sos);
162
163 //--- 4. Envelope approximation (absolute value) ---
164 filtered = filtered.cwiseAbs();
165
166 //--- 5. Z-score per channel ---
167 MatrixXd zscores(nCh, nTimes);
168 int validChannels = 0;
169 for (int ch = 0; ch < nCh; ++ch) {
170 const double mean = filtered.row(ch).mean();
171 const double variance = (filtered.row(ch).array() - mean).square().mean();
172 const double stddev = std::sqrt(variance);
173 if (stddev < 1e-30) {
174 zscores.row(ch).setZero();
175 continue;
176 }
177 zscores.row(ch) = (filtered.row(ch).array() - mean) / stddev;
178 ++validChannels;
179 }
180
181 if (validChannels == 0)
182 return annot;
183
184 //--- 6. Average z-score across channels ---
185 RowVectorXd avgZ = zscores.colwise().mean();
186
187 //--- 7. Threshold ---
188 VectorXi mask(nTimes);
189 for (Eigen::Index i = 0; i < nTimes; ++i)
190 mask(static_cast<int>(i)) = (avgZ(i) > params.dThreshold) ? 1 : 0;
191
192 //--- 8. Find contiguous segments ---
193 auto segs = findContiguousSegments(mask);
194
195 //--- 9. Merge close segments ---
196 const int gapSamples = static_cast<int>(std::round(params.dMinGapSec * sfreq));
197 mergeCloseSegments(segs, gapSamples);
198
199 //--- 10. Remove short segments ---
200 const int minSamples = static_cast<int>(std::round(params.dMinDuration * sfreq));
201 removeShortSegments(segs, minSamples);
202
203 //--- Convert to annotations ---
204 for (const auto& seg : segs) {
205 const double onset = static_cast<double>(seg.first) / sfreq;
206 const double duration = static_cast<double>(seg.second - seg.first + 1) / sfreq;
207 annot.append(onset, duration, QStringLiteral("BAD_muscle"));
208 }
209
210 return annot;
211}
212
213//=============================================================================================================
214
216 const MatrixXd& data,
217 const FiffInfo& info,
218 double sfreq,
219 const AnnotateAmplitudeParams& params)
220{
221 FiffAnnotations annot;
222 const Eigen::Index nCh = data.rows();
223 const Eigen::Index nTimes = data.cols();
224
225 if (nCh == 0 || nTimes == 0)
226 return annot;
227
228 const bool checkPeakMax = std::isfinite(params.dPeakMax);
229 const bool checkPeakMin = std::isfinite(params.dPeakMin);
230 const bool checkFlat = params.dFlatMin > 0.0;
231 const int minSamples = static_cast<int>(std::round(params.dMinDuration * sfreq));
232
233 //--- Peak amplitude check per channel ---
234 if (checkPeakMax || checkPeakMin) {
235 for (Eigen::Index ch = 0; ch < nCh; ++ch) {
236 const QString chName = (ch < info.ch_names.size()) ? info.ch_names[static_cast<int>(ch)] : QString("CH%1").arg(ch);
237
238 VectorXi mask(nTimes);
239 for (Eigen::Index s = 0; s < nTimes; ++s) {
240 const double val = data(ch, s);
241 mask(static_cast<int>(s)) = ((checkPeakMax && val > params.dPeakMax) ||
242 (checkPeakMin && val < params.dPeakMin)) ? 1 : 0;
243 }
244
245 auto segs = findContiguousSegments(mask);
246 removeShortSegments(segs, minSamples);
247
248 for (const auto& seg : segs) {
249 const double onset = static_cast<double>(seg.first) / sfreq;
250 const double duration = static_cast<double>(seg.second - seg.first + 1) / sfreq;
251 annot.append(onset, duration, params.badDescription, QStringList{chName});
252 }
253 }
254 }
255
256 //--- Flatness check per channel ---
257 if (checkFlat) {
258 const int winSamples = std::max(1, static_cast<int>(std::round(params.dWindowSec * sfreq)));
259
260 for (Eigen::Index ch = 0; ch < nCh; ++ch) {
261 const QString chName = (ch < info.ch_names.size()) ? info.ch_names[static_cast<int>(ch)] : QString("CH%1").arg(ch);
262
263 VectorXi mask = VectorXi::Zero(static_cast<int>(nTimes));
264
265 for (Eigen::Index s = 0; s <= nTimes - winSamples; ++s) {
266 const auto seg = data.block(ch, s, 1, winSamples);
267 const double p2p = seg.maxCoeff() - seg.minCoeff();
268 if (p2p < params.dFlatMin) {
269 for (int j = static_cast<int>(s); j < static_cast<int>(s) + winSamples; ++j)
270 mask(j) = 1;
271 }
272 }
273
274 auto segs = findContiguousSegments(mask);
275 removeShortSegments(segs, minSamples);
276
277 for (const auto& seg : segs) {
278 const double onset = static_cast<double>(seg.first) / sfreq;
279 const double duration = static_cast<double>(seg.second - seg.first + 1) / sfreq;
280 annot.append(onset, duration, QStringLiteral("BAD_flat"), QStringList{chName});
281 }
282 }
283 }
284
285 return annot;
286}
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_EEG_CH
#define FIFFV_MEG_CH
Full FIFF measurement metadata: everything from FIFFB_MEAS / FIFFB_MEAS_INFO needed to interpret a re...
Butterworth IIR filter design and application via numerically stable second-order sections.
Continuous-data annotation of muscle and amplitude artefacts.
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())
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 QVector< IirBiquad > designButterworth(int iOrder, FilterType type, double dCutoffLow, double dCutoffHigh, double dSFreq)
static Eigen::MatrixXd applyZeroPhaseMatrix(const Eigen::MatrixXd &matData, const QVector< IirBiquad > &sos)
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:88
QList< FiffChInfo > chs