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) {
63 case IcaComponentLabel::Brain: return QStringLiteral("brain");
64 case IcaComponentLabel::Eog: return QStringLiteral("eog");
65 case IcaComponentLabel::Ecg: return QStringLiteral("ecg");
66 case IcaComponentLabel::Muscle: return QStringLiteral("muscle");
67 case IcaComponentLabel::Other: return QStringLiteral("other");
68 }
69 return QStringLiteral("unknown");
70}
71
72//=============================================================================================================
73
74QList<IcaLabelResult> MlIcaLabel::classify(const MatrixXd& matSources,
75 const MatrixXd& matEog,
76 const MatrixXd& matEcg,
77 double dSFreq,
78 double dEogThresh,
79 double dEcgThresh)
80{
81 QList<IcaLabelResult> results;
82
83 const int nComp = static_cast<int>(matSources.rows());
84 if (nComp == 0)
85 return results;
86
87 const double dMuscleThresh = 0.7;
88
89 for (int k = 0; k < nComp; ++k) {
91 res.componentIndex = k;
93 res.confidence = 0.0;
94
95 VectorXd source = matSources.row(k).transpose();
96
97 // Check EOG correlation
98 double eogCorr = 0.0;
99 if (matEog.rows() > 0 && matEog.cols() == matSources.cols())
100 eogCorr = maxAbsCorrelation(source, matEog);
101
102 // Check ECG correlation
103 double ecgCorr = 0.0;
104 if (matEcg.rows() > 0 && matEcg.cols() == matSources.cols())
105 ecgCorr = maxAbsCorrelation(source, matEcg);
106
107 // Check muscle score
108 double muscle = muscleScore(source, dSFreq);
109
110 // Classification logic: highest evidence wins
111 if (eogCorr >= dEogThresh && eogCorr >= ecgCorr && eogCorr >= muscle) {
113 res.confidence = eogCorr;
114 } else if (ecgCorr >= dEcgThresh && ecgCorr >= eogCorr && ecgCorr >= muscle) {
116 res.confidence = ecgCorr;
117 } else if (muscle >= dMuscleThresh) {
119 res.confidence = muscle;
120 } else {
121 // Default to brain if no artifact criteria met
123 res.confidence = 1.0 - std::max({eogCorr, ecgCorr, muscle});
124 }
125
126 results.append(res);
127 }
128
129 return results;
130}
131
132//=============================================================================================================
133
134QVector<int> MlIcaLabel::findArtifactComponents(const QList<IcaLabelResult>& labels)
135{
136 QVector<int> artifacts;
137 for (const auto& res : labels) {
138 if (res.label == IcaComponentLabel::Eog ||
139 res.label == IcaComponentLabel::Ecg ||
140 res.label == IcaComponentLabel::Muscle) {
141 artifacts.append(res.componentIndex);
142 }
143 }
144 return artifacts;
145}
146
147//=============================================================================================================
148
149double MlIcaLabel::maxAbsCorrelation(const VectorXd& source, const MatrixXd& matRef)
150{
151 const int n = static_cast<int>(source.size());
152 if (n < 2)
153 return 0.0;
154
155 // Demean source
156 double srcMean = source.mean();
157 VectorXd srcCentered = source.array() - srcMean;
158 double srcStd = std::sqrt(srcCentered.squaredNorm() / static_cast<double>(n - 1));
159
160 if (srcStd < 1e-15)
161 return 0.0;
162
163 double maxCorr = 0.0;
164 for (int r = 0; r < matRef.rows(); ++r) {
165 VectorXd ref = matRef.row(r).transpose();
166 double refMean = ref.mean();
167 VectorXd refCentered = ref.array() - refMean;
168 double refStd = std::sqrt(refCentered.squaredNorm() / static_cast<double>(n - 1));
169
170 if (refStd < 1e-15)
171 continue;
172
173 double corr = std::abs(srcCentered.dot(refCentered) / (static_cast<double>(n - 1) * srcStd * refStd));
174 if (corr > maxCorr)
175 maxCorr = corr;
176 }
177
178 return maxCorr;
179}
180
181//=============================================================================================================
182
183double MlIcaLabel::muscleScore(const VectorXd& source, double dSFreq)
184{
185 // Simple time-domain approximation of HF power ratio:
186 // Compute variance of first-differenced signal vs original.
187 // First-difference acts as a high-pass filter (accentuates high freq).
188 const int n = static_cast<int>(source.size());
189 if (n < 3 || dSFreq <= 0.0)
190 return 0.0;
191
192 double totalVar = 0.0;
193 double srcMean = source.mean();
194 for (int i = 0; i < n; ++i) {
195 double d = source[i] - srcMean;
196 totalVar += d * d;
197 }
198 totalVar /= static_cast<double>(n - 1);
199
200 if (totalVar < 1e-30)
201 return 0.0;
202
203 // Compute variance of second difference (approximates d²/dt²)
204 double hfVar = 0.0;
205 for (int i = 1; i < n - 1; ++i) {
206 double d2 = source[i + 1] - 2.0 * source[i] + source[i - 1];
207 hfVar += d2 * d2;
208 }
209 hfVar /= static_cast<double>(n - 2);
210
211 // Normalize: for white noise, hfVar/totalVar → 6.0 (analytical result for second diff of white noise)
212 // So ratio = hfVar / (6 * totalVar) gives ~1 for white noise, <1 for smooth signals
213 double ratio = hfVar / (6.0 * totalVar);
214
215 return std::min(ratio, 1.0);
216}
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)