v2.0.0
Loading...
Searching...
No Matches
rtsensorinterpolationmatworker.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
19#include <fwd/fwd_field_map.h>
20
21#include <fiff/fiff_ch_info.h>
22#include <fiff/fiff_constants.h>
24#include <fwd/fwd_coil_set.h>
25#include <fs/fs_surface.h>
26
27#include <QCoreApplication>
28#include <QDebug>
29#include <QVector3D>
30#include <QMatrix4x4>
31#include <cmath>
32
33using namespace FIFFLIB;
34using namespace DISP3DLIB;
35
36//=============================================================================================================
37// ANONYMOUS HELPERS
38//=============================================================================================================
39
40namespace
41{
42
46Eigen::Vector3f applyTransform(const Eigen::Vector3f& point,
47 const FiffCoordTrans& trans)
48{
49 if (trans.isEmpty())
50 return point;
51 float r[3] = {point.x(), point.y(), point.z()};
53 return Eigen::Vector3f(r[0], r[1], r[2]);
54}
55
59QMatrix4x4 toQMatrix4x4(const Eigen::Matrix4f& m)
60{
61 QMatrix4x4 q;
62 for (int r = 0; r < 4; ++r)
63 for (int c = 0; c < 4; ++c)
64 q(r, c) = m(r, c);
65 return q;
66}
67
68} // anonymous namespace
69
70//=============================================================================================================
71// MEMBER METHODS
72//=============================================================================================================
73
75: QObject(parent)
76{
77}
78
79//=============================================================================================================
80
82{
83 QMutexLocker locker(&m_mutex);
84 m_evoked = evoked;
85 m_hasEvoked = (evoked.data.rows() > 0 && evoked.data.cols() > 0);
86}
87
88//=============================================================================================================
89
91 bool applySensorTrans)
92{
93 QMutexLocker locker(&m_mutex);
94 m_headToMriTrans = trans;
95 m_applySensorTrans = applySensorTrans;
96}
97
98//=============================================================================================================
99
101{
102 QMutexLocker locker(&m_mutex);
103 m_megOnHead = onHead;
104}
105
106//=============================================================================================================
107
109 const Eigen::MatrixX3f& vertices,
110 const Eigen::MatrixX3f& normals,
111 const Eigen::MatrixX3i& triangles)
112{
113 QMutexLocker locker(&m_mutex);
114 m_megSurfaceKey = surfaceKey;
115 m_megVertices = vertices;
116 m_megNormals = normals;
117 m_megTriangles = triangles;
118 m_hasMegSurface = (vertices.rows() > 0);
119}
120
121//=============================================================================================================
122
124 const Eigen::MatrixX3f& vertices)
125{
126 QMutexLocker locker(&m_mutex);
127 m_eegSurfaceKey = surfaceKey;
128 m_eegVertices = vertices;
129 m_hasEegSurface = (vertices.rows() > 0);
130}
131
132//=============================================================================================================
133
135{
136 QMutexLocker locker(&m_mutex);
137 m_bads = bads;
138}
139
140//=============================================================================================================
141
143{
144 // ── Snapshot parameters under lock ─────────────────────────────────
145 FiffEvoked evoked;
146 FiffCoordTrans headToMriTrans;
147 bool applySensorTrans;
148 bool megOnHead;
149 QString megSurfaceKey, eegSurfaceKey;
150 Eigen::MatrixX3f megVerts, megNorms, eegVerts;
151 Eigen::MatrixX3i megTris;
152 bool hasMegSurface, hasEegSurface, hasEvoked;
153 QStringList bads;
154
155 {
156 QMutexLocker locker(&m_mutex);
157 evoked = m_evoked;
158 hasEvoked = m_hasEvoked;
159 headToMriTrans = m_headToMriTrans;
160 applySensorTrans = m_applySensorTrans;
161 megOnHead = m_megOnHead;
162 megSurfaceKey = m_megSurfaceKey;
163 eegSurfaceKey = m_eegSurfaceKey;
164 megVerts = m_megVertices;
165 megNorms = m_megNormals;
166 megTris = m_megTriangles;
167 hasMegSurface = m_hasMegSurface;
168 eegVerts = m_eegVertices;
169 hasEegSurface = m_hasEegSurface;
170 bads = m_bads;
171 }
172
173 if (!hasEvoked) {
174 qDebug() << "RtSensorInterpolationMatWorker: No evoked data set, skipping.";
175 return;
176 }
177
178 qDebug() << "RtSensorInterpolationMatWorker: Computing mapping matrices...";
179
180 // ── Build coordinate transforms ────────────────────────────────────
181 bool hasDevHead = false;
182 QMatrix4x4 devHeadQt;
183 if (!evoked.info.dev_head_t.isEmpty() &&
186 !evoked.info.dev_head_t.trans.isIdentity()) {
187 hasDevHead = true;
188 devHeadQt = toQMatrix4x4(evoked.info.dev_head_t.trans);
189 }
190
191 QMatrix4x4 headToMri;
192 if (applySensorTrans && !headToMriTrans.isEmpty()) {
193 headToMri = toQMatrix4x4(headToMriTrans.trans);
194 }
195
196 // ── Classify channels ──────────────────────────────────────────────
197 QList<FiffChInfo> megChs, eegChs;
198 QVector<int> megPick, eegPick;
199
200 for (int k = 0; k < evoked.info.chs.size(); ++k) {
201 const auto& ch = evoked.info.chs[k];
202 if (bads.contains(ch.ch_name) || evoked.info.bads.contains(ch.ch_name))
203 continue;
204
205 QVector3D pos(ch.chpos.r0(0), ch.chpos.r0(1), ch.chpos.r0(2));
206
207 if (ch.kind == FIFFV_MEG_CH) {
208 if (hasDevHead)
209 pos = devHeadQt.map(pos);
210 if (applySensorTrans && !headToMriTrans.isEmpty())
211 pos = headToMri.map(pos);
212 megPick.append(k);
213 megChs.append(ch);
214 } else if (ch.kind == FIFFV_EEG_CH) {
215 if (applySensorTrans && !headToMriTrans.isEmpty())
216 pos = headToMri.map(pos);
217 eegPick.append(k);
218 eegChs.append(ch);
219 }
220 }
221
222 // ── Constants (matching MNE-Python) ────────────────────────────────
223 constexpr float kIntrad = 0.06f;
224 constexpr float kMegMiss = 1e-4f;
225 constexpr float kEegMiss = 1e-3f;
226 // origin="auto" of MNE-Python's make_field_map, as in SensorFieldMapper::buildMapping()
227 const Eigen::Vector3f fittedOrigin = SensorFieldMapper::fitSphereOrigin(evoked.info);
228
229 FiffCoordTrans headMri = (applySensorTrans && !headToMriTrans.isEmpty())
230 ? headToMriTrans
231 : FiffCoordTrans();
232 FiffCoordTrans devHead = (!evoked.info.dev_head_t.isEmpty() &&
235 ? evoked.info.dev_head_t
236 : FiffCoordTrans();
237
238 // ── MEG mapping ────────────────────────────────────────────────────
239 if (hasMegSurface && !megChs.isEmpty()) {
240 Eigen::MatrixX3f norms = megNorms;
241
242 // Recompute normals if missing
243 if (norms.rows() != megVerts.rows()) {
244 if (megTris.rows() > 0) {
245 norms = FSLIB::FsSurface::compute_normals(megVerts, megTris);
246 }
247 }
248
249 if (megVerts.rows() > 0 && norms.rows() == megVerts.rows()) {
250 const QString coilPath = QCoreApplication::applicationDirPath() + "/../resources/general/coilDefinitions/coil_def.dat";
251 auto templates =
253
254 if (templates) {
255 FiffCoordTrans devToTarget;
256 if (megOnHead && !headMri.isEmpty()) {
257 if (!devHead.isEmpty()) {
258 devToTarget = FiffCoordTrans::combine(
260 devHead, headMri);
261 }
262 } else if (!devHead.isEmpty()) {
263 devToTarget = devHead;
264 }
265
266 Eigen::Vector3f origin = fittedOrigin;
267 if (megOnHead && !headMri.isEmpty()) {
268 origin = applyTransform(origin, headMri);
269 }
270
271 auto coils = templates->create_meg_coils(
272 megChs, megChs.size(), FWDLIB::FWD_COIL_ACCURACY_NORMAL, devToTarget);
273
274 if (coils && coils->ncoil() > 0) {
276 *coils, megVerts, norms, origin, kIntrad, kMegMiss);
277
278 if (mat && mat->rows() > 0) {
279 qDebug() << "RtSensorInterpolationMatWorker: MEG mapping computed:"
280 << mat->rows() << "x" << mat->cols();
281 emit newMegMappingAvailable(megSurfaceKey,
282 std::shared_ptr<Eigen::MatrixXf>(std::move(mat)), megPick);
283 }
284 }
285 }
286 }
287 }
288
289 // ── EEG mapping ────────────────────────────────────────────────────
290 if (hasEegSurface && !eegChs.isEmpty()) {
291 if (eegVerts.rows() > 0) {
292 Eigen::Vector3f origin = fittedOrigin;
293 if (!headMri.isEmpty())
294 origin = applyTransform(origin, headMri);
295
296 auto eegCoils =
298 eegChs, eegChs.size(), headMri);
299
300 if (eegCoils && eegCoils->ncoil() > 0) {
302 *eegCoils, eegVerts, origin, kIntrad, kEegMiss);
303
304 if (mat && mat->rows() > 0) {
305 qDebug() << "RtSensorInterpolationMatWorker: EEG mapping computed:"
306 << mat->rows() << "x" << mat->cols();
307 emit newEegMappingAvailable(eegSurfaceKey,
308 std::shared_ptr<Eigen::MatrixXf>(std::move(mat)), eegPick);
309 }
310 }
311 }
312 }
313
314 qDebug() << "RtSensorInterpolationMatWorker: Mapping computation complete.";
315}
FIFF channel descriptor record (FIFF_CH_INFO): per-channel logical/scanner numbers,...
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_EEG_CH
#define FIFFV_COORD_DEVICE
#define FIFFV_MEG_CH
#define FIFFV_COORD_HEAD
#define FIFFV_COORD_MRI
#define FIFFV_MOVE
4x4 affine FIFF coordinate transform (FIFF_COORD_TRANS) annotated with source/destination coordinate-...
return FiffCoordTrans(from_frame, to_frame, R, moveVec)
Background worker that recomputes the dense MEG / EEG sensor-to-surface mapping matrix off the GUI th...
Builds the dense sensor-to-surface mapping matrix and the iso-contour overlay for MEG / EEG evoked da...
Container of FwdCoil instances representing either a sensor-type template database or a concrete per-...
Sphere-model field interpolator that maps measured MEG/EEG values onto a dense scalp or cortical surf...
Reader and in-memory representation of a single FreeSurfer triangular surface (e.g....
FIFF file I/O, in-memory data structures and high-level readers/writers.
3-D brain visualisation using the Qt RHI rendering backend.
QMatrix4x4 toQMatrix4x4(const Eigen::Matrix4f &m)
constexpr int FWD_COIL_ACCURACY_NORMAL
Definition fwd_coil.h:76
static Eigen::Vector3f fitSphereOrigin(const FIFFLIB::FiffInfo &info, float *radius=nullptr)
void setEvoked(const FIFFLIB::FiffEvoked &evoked)
void newEegMappingAvailable(const QString &surfaceKey, std::shared_ptr< Eigen::MatrixXf > mappingMat, const QVector< int > &pick)
void setMegSurface(const QString &surfaceKey, const Eigen::MatrixX3f &vertices, const Eigen::MatrixX3f &normals, const Eigen::MatrixX3i &triangles)
void setTransform(const FIFFLIB::FiffCoordTrans &trans, bool applySensorTrans)
void newMegMappingAvailable(const QString &surfaceKey, std::shared_ptr< Eigen::MatrixXf > mappingMat, const QVector< int > &pick)
void setEegSurface(const QString &surfaceKey, const Eigen::MatrixX3f &vertices)
Labelled 4x4 FIFF affine: source frame, destination frame, rotation, translation and cached inverse.
static FiffCoordTrans combine(int from, int to, const FiffCoordTrans &t1, const FiffCoordTrans &t2)
Eigen::MatrixX3f apply_trans(const Eigen::MatrixX3f &rr, bool do_move=true) const
Eigen::Matrix< float, 4, 4, Eigen::DontAlign > trans
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Definition fiff_evoked.h:77
Eigen::MatrixXd data
QList< FiffChInfo > chs
FiffCoordTrans dev_head_t
static Eigen::MatrixX3f compute_normals(const Eigen::MatrixX3f &rr, const Eigen::MatrixX3i &tris)
static FwdCoilSet::UPtr read_coil_defs(const QString &name)
static FwdCoilSet::UPtr create_eeg_els(const QList< FIFFLIB::FiffChInfo > &chs, int nch, const FIFFLIB::FiffCoordTrans &t=FIFFLIB::FiffCoordTrans())
static std::unique_ptr< Eigen::MatrixXf > computeEegMapping(const FwdCoilSet &coils, const Eigen::MatrixX3f &vertices, const Eigen::Vector3f &origin, float intrad=0.06f, float miss=1e-3f)
static std::unique_ptr< Eigen::MatrixXf > computeMegMapping(const FwdCoilSet &coils, const Eigen::MatrixX3f &vertices, const Eigen::MatrixX3f &normals, const Eigen::Vector3f &origin, float intrad=0.06f, float miss=1e-4f)