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 QStringList* bads)
179{
180 FiffAnnotations annot;
181 if (bads)
182 bads->clear();
183 const Eigen::Index nTimes = data.cols();
184 if (data.rows() == 0 || nTimes < 2 || (!params.peak && !params.flat))
185 return annot;
186
187 const int minSamples = static_cast<int>(std::round(params.dMinDuration * sfreq));
188 VectorXi anyFlat = VectorXi::Zero(nTimes - 1);
189 VectorXi anyPeak = VectorXi::Zero(nTimes - 1);
190
191 for (const int ch : info.pick_data_channels(info.bads)) {
192 if (ch >= data.rows())
193 break;
194 const ArrayXd diff = (data.row(ch).tail(nTimes - 1) - data.row(ch).head(nTimes - 1)).array().abs().transpose();
195 bool bad = false;
196 for (const auto& [threshold, isPeak] : {std::pair{params.flat, false}, std::pair{params.peak, true}}) {
197 if (!threshold)
198 continue;
199 const VectorXi mask = isPeak ? (diff >= *threshold).cast<int>().matrix().eval() : (diff <= *threshold).cast<int>().matrix().eval();
200 // Runs shorter than the minimum duration do not count
201 QVector<QPair<int, int>> segs = findContiguousSegments(mask);
202 removeShortSegments(segs, minSamples);
203 int count = 0;
204 for (const auto& seg : segs)
205 count += seg.second - seg.first + 1;
206 // A run of n differences spans n + 1 samples
207 const double percent = (count > 0 ? count + 1 : 0) * 100.0 / static_cast<double>(nTimes);
208 if (percent >= params.dBadPercent) {
209 bad = true;
210 } else if (percent > 0.0) {
211 VectorXi& any = isPeak ? anyPeak : anyFlat;
212 for (const auto& seg : segs)
213 any.segment(seg.first, seg.second - seg.first + 1).setOnes();
214 }
215 }
216 if (bad && bads)
217 bads->append(info.ch_names[ch]);
218 }
219
220 for (const auto& [any, description] : {std::pair{&anyFlat, "BAD_flat"}, std::pair{&anyPeak, "BAD_peak"}}) {
221 for (const auto& seg : findContiguousSegments(*any))
222 annot.append(seg.first / sfreq, (seg.second - seg.first + 1) / sfreq, QString::fromLatin1(description));
223 }
224 return annot;
225}
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, QStringList *bads=nullptr)
Annotate spans where consecutive samples jump or stay flat, as mne.preprocessing.annotate_amplitude.
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
Eigen::RowVectorXi pick_data_channels(const QStringList &exclude=defaultQStringList) const