v2.0.0
Loading...
Searching...
No Matches
decoding_ica_label.cpp
Go to the documentation of this file.
1//=============================================================================================================
24
25//=============================================================================================================
26// INCLUDES
27//=============================================================================================================
28
29#include "decoding_ica_label.h"
30
31//=============================================================================================================
32// EIGEN INCLUDES
33//=============================================================================================================
34
35#include <Eigen/Core>
36
37//=============================================================================================================
38// QT INCLUDES
39//=============================================================================================================
40
41#include <QDebug>
42
43//=============================================================================================================
44// STD INCLUDES
45//=============================================================================================================
46
47#include <cmath>
48
49//=============================================================================================================
50// USED NAMESPACES
51//=============================================================================================================
52
53using namespace DECODINGLIB;
54using namespace Eigen;
55
56//=============================================================================================================
57// DEFINE MEMBER METHODS
58//=============================================================================================================
59
61{
62 switch (label) {
64 return QStringLiteral("brain");
66 return QStringLiteral("eog");
68 return QStringLiteral("ecg");
70 return QStringLiteral("muscle");
72 return QStringLiteral("other");
73 }
74 return QStringLiteral("unknown");
75}
76
77//=============================================================================================================
78
79QList<IcaLabelResult> MlIcaLabel::classify(const MatrixXd& matSources,
80 const MatrixXd& matEog,
81 const MatrixXd& matEcg,
82 double dSFreq,
83 double dEogThresh,
84 double dEcgThresh)
85{
86 QList<IcaLabelResult> results;
87
88 const int nComp = static_cast<int>(matSources.rows());
89 if (nComp == 0)
90 return results;
91
92 const double dMuscleThresh = 0.7;
93
94 for (int k = 0; k < nComp; ++k) {
96 res.componentIndex = k;
98 res.confidence = 0.0;
99
100 VectorXd source = matSources.row(k).transpose();
101
102 // Check EOG correlation
103 double eogCorr = 0.0;
104 if (matEog.rows() > 0 && matEog.cols() == matSources.cols())
105 eogCorr = maxAbsCorrelation(source, matEog);
106
107 // Check ECG correlation
108 double ecgCorr = 0.0;
109 if (matEcg.rows() > 0 && matEcg.cols() == matSources.cols())
110 ecgCorr = maxAbsCorrelation(source, matEcg);
111
112 // Check muscle score
113 double muscle = muscleScore(source, dSFreq);
114
115 // Classification logic: highest evidence wins
116 if (eogCorr >= dEogThresh && eogCorr >= ecgCorr && eogCorr >= muscle) {
118 res.confidence = eogCorr;
119 } else if (ecgCorr >= dEcgThresh && ecgCorr >= eogCorr && ecgCorr >= muscle) {
121 res.confidence = ecgCorr;
122 } else if (muscle >= dMuscleThresh) {
124 res.confidence = muscle;
125 } else {
126 // Default to brain if no artifact criteria met
128 res.confidence = 1.0 - std::max({eogCorr, ecgCorr, muscle});
129 }
130
131 results.append(res);
132 }
133
134 return results;
135}
136
137//=============================================================================================================
138
139QVector<int> MlIcaLabel::findArtifactComponents(const QList<IcaLabelResult>& labels)
140{
141 QVector<int> artifacts;
142 for (const auto& res : labels) {
143 if (res.label == IcaComponentLabel::Eog ||
144 res.label == IcaComponentLabel::Ecg ||
145 res.label == IcaComponentLabel::Muscle) {
146 artifacts.append(res.componentIndex);
147 }
148 }
149 return artifacts;
150}
151
152//=============================================================================================================
153
154double MlIcaLabel::maxAbsCorrelation(const VectorXd& source, const MatrixXd& matRef)
155{
156 const int n = static_cast<int>(source.size());
157 if (n < 2)
158 return 0.0;
159
160 // Demean source
161 double srcMean = source.mean();
162 VectorXd srcCentered = source.array() - srcMean;
163 double srcStd = std::sqrt(srcCentered.squaredNorm() / static_cast<double>(n - 1));
164
165 if (srcStd < 1e-15)
166 return 0.0;
167
168 double maxCorr = 0.0;
169 for (int r = 0; r < matRef.rows(); ++r) {
170 VectorXd ref = matRef.row(r).transpose();
171 double refMean = ref.mean();
172 VectorXd refCentered = ref.array() - refMean;
173 double refStd = std::sqrt(refCentered.squaredNorm() / static_cast<double>(n - 1));
174
175 if (refStd < 1e-15)
176 continue;
177
178 double corr = std::abs(srcCentered.dot(refCentered) / (static_cast<double>(n - 1) * srcStd * refStd));
179 if (corr > maxCorr)
180 maxCorr = corr;
181 }
182
183 return maxCorr;
184}
185
186//=============================================================================================================
187
188double MlIcaLabel::muscleScore(const VectorXd& source, double dSFreq)
189{
190 // Simple time-domain approximation of HF power ratio:
191 // Compute variance of first-differenced signal vs original.
192 // First-difference acts as a high-pass filter (accentuates high freq).
193 const int n = static_cast<int>(source.size());
194 if (n < 3 || dSFreq <= 0.0)
195 return 0.0;
196
197 double totalVar = 0.0;
198 double srcMean = source.mean();
199 for (int i = 0; i < n; ++i) {
200 double d = source[i] - srcMean;
201 totalVar += d * d;
202 }
203 totalVar /= static_cast<double>(n - 1);
204
205 if (totalVar < 1e-30)
206 return 0.0;
207
208 // Compute variance of second difference (approximates d²/dt²)
209 double hfVar = 0.0;
210 for (int i = 1; i < n - 1; ++i) {
211 double d2 = source[i + 1] - 2.0 * source[i] + source[i - 1];
212 hfVar += d2 * d2;
213 }
214 hfVar /= static_cast<double>(n - 2);
215
216 // Normalize: for white noise, hfVar/totalVar → 6.0 (analytical result for second diff of white noise)
217 // So ratio = hfVar / (6 * totalVar) gives ~1 for white noise, <1 for smooth signals
218 double ratio = hfVar / (6.0 * totalVar);
219
220 return std::min(ratio, 1.0);
221}
Automatic ICA component labelling for artefact identification on M/EEG.
Supervised and unsupervised spatial-filter decompositions for M/EEG decoding.
IcaComponentLabel
Categorical label assigned to a single ICA component.
Outcome of labelling a single ICA component.
static QString labelToString(IcaComponentLabel label)
static double muscleScore(const Eigen::VectorXd &source, double dSFreq)
static QList< IcaLabelResult > classify(const Eigen::MatrixXd &matSources, const Eigen::MatrixXd &matEog, const Eigen::MatrixXd &matEcg, double dSFreq, double dEogThresh=0.3, double dEcgThresh=0.3)
static double maxAbsCorrelation(const Eigen::VectorXd &source, const Eigen::MatrixXd &matRef)
static QVector< int > findArtifactComponents(const QList< IcaLabelResult > &labels)