v2.0.0
Loading...
Searching...
No Matches
dataloader.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "dataloader.h"
18#include "core/surfacekeys.h"
21
22#include <QFile>
23#include <QFileInfo>
24#include <QDebug>
25#include <QCoreApplication>
26
27#include <Eigen/Dense>
28#include <fiff/fiff.h>
29#include <fiff/fiff_constants.h>
30#include <fiff/fiff_stream.h>
32#include <mne/mne_bem.h>
33#include <fs/fs_surface.h>
34
35using namespace FIFFLIB;
36using namespace MNELIB;
37using namespace INVLIB;
38
39//=============================================================================================================
40// STATIC METHODS
41//=============================================================================================================
42
44 const QString &megHelmetOverridePath)
45{
46 SensorLoadResult result;
47
48 QFile file(fifPath);
49 if (!file.exists()) return result;
50
51 FiffInfo info;
52 FiffDigPointSet digSet;
53 FiffStream::SPtr stream(new FiffStream(&file));
54
55 if (stream->open()) {
56 FiffDirNode::SPtr tree = stream->dirtree();
57 FiffDirNode::SPtr nodeInfo;
58 if (stream->read_meas_info(tree, info, nodeInfo)) {
59 result.hasInfo = !info.isEmpty();
60 }
61 }
62
63 if (result.hasInfo) {
64 result.info = info;
65
66 // Prepare Device->Head transformation
67 QMatrix4x4 devHeadQTrans;
68 bool hasDevHead = false;
69 if (!info.dev_head_t.isEmpty() &&
72 !info.dev_head_t.trans.isIdentity()) {
73 hasDevHead = true;
74 devHeadQTrans = SURFACEKEYS::toQMatrix4x4(info.dev_head_t.trans);
75 }
76
77 result.devHeadTrans = devHeadQTrans;
78 result.hasDevHead = hasDevHead;
79
80 for (const auto &ch : info.chs) {
81 if (ch.kind == FIFFV_MEG_CH) {
82 QVector3D pos(ch.chpos.r0(0), ch.chpos.r0(1), ch.chpos.r0(2));
83
84 if (hasDevHead) {
85 pos = devHeadQTrans.map(pos);
86 }
87
88 auto *item = new SensorTreeItem(ch.ch_name, pos, QColor(100, 100, 100), 0.01f);
89
90 // Store coil orientation
91 QMatrix4x4 orient;
92 for (int r = 0; r < 3; ++r)
93 for (int c = 0; c < 3; ++c)
94 orient(r, c) = ch.coil_trans(r, c);
95 if (hasDevHead) {
96 QMatrix4x4 devHeadRot;
97 for (int r = 0; r < 3; ++r)
98 for (int c = 0; c < 3; ++c)
99 devHeadRot(r, c) = devHeadQTrans(r, c);
100 orient = devHeadRot * orient;
101 }
102 item->setOrientation(orient);
103
104 if (ch.unit == FIFF_UNIT_T_M) {
105 result.megGradItems.append(item);
106 } else {
107 result.megMagItems.append(item);
108 }
109 } else if (ch.kind == FIFFV_EEG_CH) {
110 QVector3D pos(ch.chpos.r0(0), ch.chpos.r0(1), ch.chpos.r0(2));
111 result.eegItems.append(new SensorTreeItem(ch.ch_name, pos, QColor(0, 200, 220), 0.002f));
112 }
113 }
114
115 // ── MEG helmet surface ──────────────────────────────────────
116 if (!result.megGradItems.isEmpty() || !result.megMagItems.isEmpty()) {
117 auto pickHelmetFile = [&info]() -> QString {
118 int coilType = -1;
119 int nMeg = 0;
120 for (const auto &ch : info.chs) {
121 if (ch.kind == FIFFV_MEG_CH) {
122 coilType = ch.chpos.coil_type & 0xFFFF;
123 ++nMeg;
124 }
125 }
126
127 QString fileName = "306m.fif";
128 if (coilType == FIFFV_COIL_BABY_GRAD) {
129 fileName = "BabySQUID.fif";
130 } else if (coilType == FIFFV_COIL_NM_122) {
131 fileName = "122m.fif";
132 } else if (coilType == FIFFV_COIL_CTF_GRAD) {
133 fileName = "CTF_275.fif";
134 } else if (coilType == FIFFV_COIL_KIT_GRAD) {
135 fileName = "KIT.fif";
136 } else if (coilType == FIFFV_COIL_MAGNES_MAG || coilType == FIFFV_COIL_MAGNES_GRAD) {
137 fileName = (nMeg > 150) ? "Magnes_3600wh.fif" : "Magnes_2500wh.fif";
138 } else if (coilType / 1000 == 3) {
139 fileName = "306m.fif";
140 }
141
142 return QCoreApplication::applicationDirPath()
143 + "/../resources/general/sensorSurfaces/" + fileName;
144 };
145
146 QString helmetPath;
147 if (!megHelmetOverridePath.isEmpty()) {
148 helmetPath = megHelmetOverridePath;
149 if (!QFile::exists(helmetPath)) {
150 qWarning() << "MEG helmet override file not found:" << helmetPath
151 << "- falling back to auto selection.";
152 helmetPath.clear();
153 }
154 }
155
156 if (helmetPath.isEmpty()) {
157 helmetPath = pickHelmetFile();
158#ifdef __EMSCRIPTEN__
159 // WASM: applicationDirPath()/../ may not resolve correctly.
160 // Try the preloaded virtual FS path directly.
161 if (!QFile::exists(helmetPath)) {
162 QString fn = QFileInfo(helmetPath).fileName();
163 if (fn.isEmpty()) fn = QStringLiteral("306m.fif");
164 helmetPath = QStringLiteral("/resources/general/sensorSurfaces/") + fn;
165 }
166#endif
167 }
168
169 if (!QFile::exists(helmetPath)) {
170 QString fallback = QCoreApplication::applicationDirPath()
171 + "/../resources/general/sensorSurfaces/306m.fif";
172 if (QFile::exists(fallback)) {
173 helmetPath = fallback;
174 }
175 }
176
177#ifdef __EMSCRIPTEN__
178 // WASM fallback: sensor surfaces are preloaded into the
179 // Emscripten virtual FS at /resources/general/sensorSurfaces/.
180 // applicationDirPath() may not resolve the /../ correctly.
181 if (!QFile::exists(helmetPath)) {
182 QString wasmFallback = QStringLiteral("/resources/general/sensorSurfaces/")
183 + QFileInfo(helmetPath).fileName();
184 if (wasmFallback.endsWith('/')) wasmFallback += "306m.fif";
185 if (QFile::exists(wasmFallback)) {
186 helmetPath = wasmFallback;
187 }
188 }
189#endif
190
191 if (!QFile::exists(helmetPath)) {
192 qWarning() << "MEG helmet surface file not found. Checked:" << helmetPath;
193 }
194
195 if (QFile::exists(helmetPath)) {
196 QFile helmetFile(helmetPath);
197 MNEBem helmetBem(helmetFile);
198 if (helmetBem.size() > 0) {
199 MNEBemSurface helmetSurf = helmetBem[0];
200 if (helmetSurf.nn.rows() != helmetSurf.rr.rows()) {
201 helmetSurf.nn = FSLIB::FsSurface::compute_normals(Eigen::MatrixX3f(helmetSurf.rr), Eigen::MatrixX3i(helmetSurf.itris));
202 }
203
204 if (hasDevHead) {
205 QMatrix3x3 normalMat = devHeadQTrans.normalMatrix();
206 for (int i = 0; i < helmetSurf.rr.rows(); ++i) {
207 QVector3D pos(helmetSurf.rr(i, 0), helmetSurf.rr(i, 1), helmetSurf.rr(i, 2));
208 pos = devHeadQTrans.map(pos);
209 helmetSurf.rr(i, 0) = pos.x();
210 helmetSurf.rr(i, 1) = pos.y();
211 helmetSurf.rr(i, 2) = pos.z();
212
213 QVector3D nn(helmetSurf.nn(i, 0), helmetSurf.nn(i, 1), helmetSurf.nn(i, 2));
214 const float *d = normalMat.constData();
215 float nx = d[0] * nn.x() + d[3] * nn.y() + d[6] * nn.z();
216 float ny = d[1] * nn.x() + d[4] * nn.y() + d[7] * nn.z();
217 float nz = d[2] * nn.x() + d[5] * nn.y() + d[8] * nn.z();
218 QVector3D n = QVector3D(nx, ny, nz).normalized();
219 helmetSurf.nn(i, 0) = n.x();
220 helmetSurf.nn(i, 1) = n.y();
221 helmetSurf.nn(i, 2) = n.z();
222 }
223 }
224
225 auto helmetSurface = std::make_shared<BrainSurface>();
226 helmetSurface->fromBemSurface(helmetSurf, QColor(0, 0, 77, 200));
227 helmetSurface->setVisible(true);
228 result.helmetSurface = helmetSurface;
229 } else {
230 qWarning() << "DataLoader::loadSensors: helmetBem[0] has 0 verts/tris!";
231 }
232 }
233 }
234 }
235
236 // ── Digitizer points ────────────────────────────────────────────
237 if (result.hasInfo && info.dig.size() > 0) {
238 result.digitizerPoints = info.dig;
239 result.hasDigitizer = true;
240 } else if (!result.hasInfo) {
241 file.reset();
242 digSet = FiffDigPointSet(file);
243 if (digSet.size() > 0) {
244 for (int i = 0; i < digSet.size(); ++i) {
245 result.digitizerPoints.append(digSet[i]);
246 }
247 result.hasDigitizer = true;
248 }
249 }
250
251 return result;
252}
253
254//=============================================================================================================
255
256std::shared_ptr<BrainSurface> DataLoader::loadHelmetSurface(
257 const QString &helmetFilePath,
258 const QMatrix4x4 &devHeadTrans,
259 bool applyTrans)
260{
261 if (!QFile::exists(helmetFilePath)) {
262 qWarning() << "DataLoader::loadHelmetSurface: file not found:" << helmetFilePath;
263 return nullptr;
264 }
265
266 QFile helmetFile(helmetFilePath);
267 MNEBem helmetBem(helmetFile);
268 if (helmetBem.size() == 0) {
269 qWarning() << "DataLoader::loadHelmetSurface: no BEM surfaces in" << helmetFilePath;
270 return nullptr;
271 }
272
273 MNEBemSurface helmetSurf = helmetBem[0];
274
275 if (helmetSurf.nn.rows() != helmetSurf.rr.rows()) {
276 helmetSurf.nn = FSLIB::FsSurface::compute_normals(Eigen::MatrixX3f(helmetSurf.rr), Eigen::MatrixX3i(helmetSurf.itris));
277 }
278
279 if (applyTrans) {
280 QMatrix3x3 normalMat = devHeadTrans.normalMatrix();
281 for (int i = 0; i < helmetSurf.rr.rows(); ++i) {
282 QVector3D pos(helmetSurf.rr(i, 0), helmetSurf.rr(i, 1), helmetSurf.rr(i, 2));
283 pos = devHeadTrans.map(pos);
284 helmetSurf.rr(i, 0) = pos.x();
285 helmetSurf.rr(i, 1) = pos.y();
286 helmetSurf.rr(i, 2) = pos.z();
287
288 QVector3D nn(helmetSurf.nn(i, 0), helmetSurf.nn(i, 1), helmetSurf.nn(i, 2));
289 const float *d = normalMat.constData();
290 float nx = d[0] * nn.x() + d[3] * nn.y() + d[6] * nn.z();
291 float ny = d[1] * nn.x() + d[4] * nn.y() + d[7] * nn.z();
292 float nz = d[2] * nn.x() + d[5] * nn.y() + d[8] * nn.z();
293 QVector3D n = QVector3D(nx, ny, nz).normalized();
294 helmetSurf.nn(i, 0) = n.x();
295 helmetSurf.nn(i, 1) = n.y();
296 helmetSurf.nn(i, 2) = n.z();
297 }
298 }
299
300 auto surface = std::make_shared<BrainSurface>();
301 surface->fromBemSurface(helmetSurf, QColor(0, 0, 77, 200));
302 surface->setVisible(true);
303
304 return surface;
305}
306
307//=============================================================================================================
308
309InvEcdSet DataLoader::loadDipoles(const QString &dipPath)
310{
311 InvEcdSet ecdSet = InvEcdSet::read_dipoles_dip(dipPath);
312 if (ecdSet.size() == 0) {
313 qWarning() << "DataLoader: Failed to load dipoles from" << dipPath;
314 }
315 return ecdSet;
316}
317
318//=============================================================================================================
319
321{
322 QFile file(fwdPath);
323 if (!file.exists()) {
324 qWarning() << "DataLoader: Source space file not found:" << fwdPath;
325 return {};
326 }
327
328 MNESourceSpaces srcSpace;
329 FiffStream::SPtr stream(new FiffStream(&file));
330 if (!stream->open()) {
331 qWarning() << "DataLoader: Failed to open FIF stream for source space";
332 return {};
333 }
334
335 if (!MNESourceSpaces::readFromStream(stream, true, srcSpace)) {
336 qWarning() << "DataLoader: Failed to read source space from" << fwdPath;
337 return {};
338 }
339
340 if (srcSpace.isEmpty()) {
341 qWarning() << "DataLoader: Source space is empty";
342 return {};
343 }
344
345 return srcSpace;
346}
347
348//=============================================================================================================
349
350bool DataLoader::loadHeadToMriTransform(const QString &transPath,
351 FiffCoordTrans &trans)
352{
353 QFile file(transPath);
354
355 FiffCoordTrans raw;
356 if (!FiffCoordTrans::read(file, raw)) {
357 qWarning() << "DataLoader: Failed to load transformation from" << transPath;
358 return false;
359 }
360 file.close();
361
362 if (raw.from == FIFFV_COORD_HEAD && raw.to == FIFFV_COORD_MRI) {
363 trans = raw;
364 } else if (raw.from == FIFFV_COORD_MRI && raw.to == FIFFV_COORD_HEAD) {
365 // Invert: MRI->Head becomes Head->MRI
366 trans.from = raw.to;
367 trans.to = raw.from;
368 trans.trans = raw.trans.inverse();
369 trans.invtrans = raw.trans;
370 } else {
371 qWarning() << "DataLoader: Loaded transformation is not Head<->MRI (from"
372 << raw.from << "to" << raw.to << "). Using as is.";
373 trans = raw;
374 }
375
376 return true;
377}
378
379//=============================================================================================================
380
381FiffEvoked DataLoader::loadEvoked(const QString &evokedPath, int aveIndex)
382{
383 QFile file(evokedPath);
384 if (!file.exists()) {
385 qWarning() << "DataLoader: Sensor evoked file not found:" << evokedPath;
386 return {};
387 }
388
389 FiffEvoked evoked(file, aveIndex);
390 if (evoked.isEmpty()) {
391 qWarning() << "DataLoader: Failed to read evoked data from" << evokedPath;
392 }
393 return evoked;
394}
395
396//=============================================================================================================
397
398QStringList DataLoader::probeEvokedSets(const QString &evokedPath)
399{
400 QStringList result;
401 QFile file(evokedPath);
402 if (!file.exists()) {
403 return result;
404 }
405
406 FiffEvokedSet evokedSet(file);
407 for (int i = 0; i < evokedSet.evoked.size(); ++i) {
408 const auto &ev = evokedSet.evoked.at(i);
409 QString label = QString("%1: %2 (%3, nave=%4)")
410 .arg(i)
411 .arg(ev.comment.isEmpty() ? QStringLiteral("Set %1").arg(i) : ev.comment)
412 .arg(ev.aspectKindToString())
413 .arg(ev.nave);
414 result.append(label);
415 }
416 return result;
417}
Renderable cortical / BEM mesh with interleaved vertex attributes and Qt-RHI buffer management.
String key constants for surfaces in the DISP3DLIB scene map.
Static helpers for loading FreeSurfer / MNE source-space and BEM data into the disp3D model tree.
Tree item for a single MEG / EEG sensor with position, optional coil orientation and rendered scale.
Reader and in-memory representation of a single FreeSurfer triangular surface (e.g....
Boundary element model bundle (inner skull, outer skull, outer skin) loaded from -bem....
Container for the FIFF_DIG_POINT records of a measurement (a parsed FIFFB_ISOTRAK block).
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_COIL_CTF_GRAD
#define FIFFV_EEG_CH
#define FIFFV_COORD_DEVICE
#define FIFFV_COIL_NM_122
#define FIFFV_COIL_MAGNES_MAG
#define FIFFV_MEG_CH
#define FIFFV_COIL_BABY_GRAD
#define FIFFV_COORD_HEAD
#define FIFFV_COORD_MRI
#define FIFFV_COIL_MAGNES_GRAD
#define FIFFV_COIL_KIT_GRAD
#define FIFF_UNIT_T_M
Static MATLAB-style FIFF facade: thin wrapper functions kept for parity with the historical mne-matla...
FIFF binary tag-stream layer: wraps a QIODevice to read and write FIFF tags, directories,...
Core MNE data structures (source spaces, source estimates, hemispheres).
FIFF file I/O, in-memory data structures and high-level readers/writers.
QMatrix4x4 toQMatrix4x4(const Eigen::Matrix4f &m)
Inverse source estimation (MNE, dSPM, sLORETA, dipole fitting).
static SensorLoadResult loadSensors(const QString &fifPath, const QString &megHelmetOverridePath={})
static QStringList probeEvokedSets(const QString &evokedPath)
static MNELIB::MNESourceSpaces loadSourceSpace(const QString &fwdPath)
static bool loadHeadToMriTransform(const QString &transPath, FIFFLIB::FiffCoordTrans &trans)
static std::shared_ptr< BrainSurface > loadHelmetSurface(const QString &helmetFilePath, const QMatrix4x4 &devHeadTrans=QMatrix4x4(), bool applyTrans=false)
static INVLIB::InvEcdSet loadDipoles(const QString &dipPath)
static FIFFLIB::FiffEvoked loadEvoked(const QString &evokedPath, int aveIndex=0)
Return value bundling loaded sensor geometry, labels, and channel-to-sensor mapping.
Definition dataloader.h:78
std::shared_ptr< BrainSurface > helmetSurface
May be null.
Definition dataloader.h:88
QList< QStandardItem * > megGradItems
Ownership passes to caller.
Definition dataloader.h:83
FIFFLIB::FiffInfo info
Channel / dig info.
Definition dataloader.h:82
QMatrix4x4 devHeadTrans
Device→Head transform (identity if absent).
Definition dataloader.h:90
QList< QStandardItem * > eegItems
Definition dataloader.h:85
bool hasDevHead
Whether a valid dev→head transform was found.
Definition dataloader.h:91
QList< FIFFLIB::FiffDigPoint > digitizerPoints
Definition dataloader.h:86
QList< QStandardItem * > megMagItems
Definition dataloader.h:84
Tree item representing MEG or EEG sensor positions in the 3-D scene hierarchy.
Labelled 4x4 FIFF affine: source frame, destination frame, rotation, translation and cached inverse.
static bool read(QIODevice &p_IODevice, FiffCoordTrans &p_Trans)
Eigen::Matrix< float, 4, 4, Eigen::DontAlign > trans
Eigen::Matrix< float, 4, 4, Eigen::DontAlign > invtrans
Collection of FiffDigPoint records as parsed from a FIFFB_ISOTRAK block.
QSharedPointer< FiffDirNode > SPtr
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Definition fiff_evoked.h:75
bool isEmpty() const
Set of FiffEvoked instances sharing one FiffInfo, plus channel-picking and compensation helpers.
QList< FiffEvoked > evoked
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:88
QList< FiffDigPoint > dig
Definition fiff_info.h:275
QList< FiffChInfo > chs
FiffCoordTrans dev_head_t
FIFF tag-stream reader/writer: wraps a QIODevice and exposes typed read_* / write_* methods for every...
QSharedPointer< FiffStream > SPtr
static Eigen::MatrixX3f compute_normals(const Eigen::MatrixX3f &rr, const Eigen::MatrixX3i &tris)
Holds a set of Electric Current Dipoles.
Definition inv_ecd_set.h:64
qint32 size() const
static InvEcdSet read_dipoles_dip(const QString &fileName)
Aggregated boundary element model loaded from a -bem.fif file.
Definition mne_bem.h:78
qint32 size() const
Definition mne_bem.h:259
BEM surface provides geometry information.
List of MNESourceSpace objects forming a subject source space.
static bool readFromStream(FIFFLIB::FiffStream::SPtr &p_pStream, bool add_geom, MNESourceSpaces &p_SourceSpace)