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
39namespace DISP3DLIB
40{
41
42//=============================================================================================================
43// STATIC METHODS
44//=============================================================================================================
45
47 const QString& megHelmetOverridePath)
48{
49 SensorLoadResult result;
50
51 QFile file(fifPath);
52 if (!file.exists())
53 return result;
54
55 FiffInfo info;
56 FiffDigPointSet digSet;
57 FiffStream::SPtr stream(new FiffStream(&file));
58
59 if (stream->open()) {
60 FiffDirNode::SPtr tree = stream->dirtree();
61 FiffDirNode::SPtr nodeInfo;
62 if (stream->read_meas_info(tree, info, nodeInfo)) {
63 result.hasInfo = !info.isEmpty();
64 }
65 }
66
67 if (result.hasInfo) {
68 result.info = info;
69
70 // Prepare Device->Head transformation
71 QMatrix4x4 devHeadQTrans;
72 bool hasDevHead = false;
73 if (!info.dev_head_t.isEmpty() &&
76 !info.dev_head_t.trans.isIdentity()) {
77 hasDevHead = true;
78 devHeadQTrans = SURFACEKEYS::toQMatrix4x4(info.dev_head_t.trans);
79 }
80
81 result.devHeadTrans = devHeadQTrans;
82 result.hasDevHead = hasDevHead;
83
84 for (const auto& ch : info.chs) {
85 if (ch.kind == FIFFV_MEG_CH) {
86 QVector3D pos(ch.chpos.r0(0), ch.chpos.r0(1), ch.chpos.r0(2));
87
88 if (hasDevHead) {
89 pos = devHeadQTrans.map(pos);
90 }
91
92 auto* item = new SensorTreeItem(ch.ch_name, pos, QColor(100, 100, 100), 0.01f);
93
94 // Store coil orientation
95 QMatrix4x4 orient;
96 for (int r = 0; r < 3; ++r)
97 for (int c = 0; c < 3; ++c)
98 orient(r, c) = ch.coil_trans(r, c);
99 if (hasDevHead) {
100 QMatrix4x4 devHeadRot;
101 for (int r = 0; r < 3; ++r)
102 for (int c = 0; c < 3; ++c)
103 devHeadRot(r, c) = devHeadQTrans(r, c);
104 orient = devHeadRot * orient;
105 }
106 item->setOrientation(orient);
107
108 if (ch.unit == FIFF_UNIT_T_M) {
109 result.megGradItems.append(item);
110 } else {
111 result.megMagItems.append(item);
112 }
113 } else if (ch.kind == FIFFV_EEG_CH) {
114 QVector3D pos(ch.chpos.r0(0), ch.chpos.r0(1), ch.chpos.r0(2));
115 result.eegItems.append(new SensorTreeItem(ch.ch_name, pos, QColor(0, 200, 220), 0.002f));
116 }
117 }
118
119 // ── MEG helmet surface ──────────────────────────────────────
120 if (!result.megGradItems.isEmpty() || !result.megMagItems.isEmpty()) {
121 auto pickHelmetFile = [&info]() -> QString {
122 int coilType = -1;
123 int nMeg = 0;
124 for (const auto& ch : info.chs) {
125 if (ch.kind == FIFFV_MEG_CH) {
126 coilType = ch.chpos.coil_type & 0xFFFF;
127 ++nMeg;
128 }
129 }
130
131 QString fileName = "306m.fif";
132 if (coilType == FIFFV_COIL_BABY_GRAD) {
133 fileName = "BabySQUID.fif";
134 } else if (coilType == FIFFV_COIL_NM_122) {
135 fileName = "122m.fif";
136 } else if (coilType == FIFFV_COIL_CTF_GRAD) {
137 fileName = "CTF_275.fif";
138 } else if (coilType == FIFFV_COIL_KIT_GRAD) {
139 fileName = "KIT.fif";
140 } else if (coilType == FIFFV_COIL_MAGNES_MAG || coilType == FIFFV_COIL_MAGNES_GRAD) {
141 fileName = (nMeg > 150) ? "Magnes_3600wh.fif" : "Magnes_2500wh.fif";
142 } else if (coilType / 1000 == 3) {
143 fileName = "306m.fif";
144 }
145
146 return QCoreApplication::applicationDirPath() + "/../resources/general/sensorSurfaces/" + fileName;
147 };
148
149 QString helmetPath;
150 if (!megHelmetOverridePath.isEmpty()) {
151 helmetPath = megHelmetOverridePath;
152 if (!QFile::exists(helmetPath)) {
153 qWarning() << "MEG helmet override file not found:" << helmetPath
154 << "- falling back to auto selection.";
155 helmetPath.clear();
156 }
157 }
158
159 if (helmetPath.isEmpty()) {
160 helmetPath = pickHelmetFile();
161#ifdef __EMSCRIPTEN__
162 // WASM: applicationDirPath()/../ may not resolve correctly.
163 // Try the preloaded virtual FS path directly.
164 if (!QFile::exists(helmetPath)) {
165 QString fn = QFileInfo(helmetPath).fileName();
166 if (fn.isEmpty())
167 fn = QStringLiteral("306m.fif");
168 helmetPath = QStringLiteral("/resources/general/sensorSurfaces/") + fn;
169 }
170#endif
171 }
172
173 if (!QFile::exists(helmetPath)) {
174 QString fallback = QCoreApplication::applicationDirPath() + "/../resources/general/sensorSurfaces/306m.fif";
175 if (QFile::exists(fallback)) {
176 helmetPath = fallback;
177 }
178 }
179
180#ifdef __EMSCRIPTEN__
181 // WASM fallback: sensor surfaces are preloaded into the
182 // Emscripten virtual FS at /resources/general/sensorSurfaces/.
183 // applicationDirPath() may not resolve the /../ correctly.
184 if (!QFile::exists(helmetPath)) {
185 QString wasmFallback = QStringLiteral("/resources/general/sensorSurfaces/") + QFileInfo(helmetPath).fileName();
186 if (wasmFallback.endsWith('/'))
187 wasmFallback += "306m.fif";
188 if (QFile::exists(wasmFallback)) {
189 helmetPath = wasmFallback;
190 }
191 }
192#endif
193
194 if (!QFile::exists(helmetPath)) {
195 qWarning() << "MEG helmet surface file not found. Checked:" << helmetPath;
196 }
197
198 if (QFile::exists(helmetPath)) {
199 QFile helmetFile(helmetPath);
200 MNEBem helmetBem(helmetFile);
201 if (helmetBem.size() > 0) {
202 MNEBemSurface helmetSurf = helmetBem[0];
203 if (helmetSurf.nn.rows() != helmetSurf.rr.rows()) {
204 helmetSurf.nn = FSLIB::FsSurface::compute_normals(Eigen::MatrixX3f(helmetSurf.rr), Eigen::MatrixX3i(helmetSurf.itris));
205 }
206
207 if (hasDevHead) {
208 QMatrix3x3 normalMat = devHeadQTrans.normalMatrix();
209 for (int i = 0; i < helmetSurf.rr.rows(); ++i) {
210 QVector3D pos(helmetSurf.rr(i, 0), helmetSurf.rr(i, 1), helmetSurf.rr(i, 2));
211 pos = devHeadQTrans.map(pos);
212 helmetSurf.rr(i, 0) = pos.x();
213 helmetSurf.rr(i, 1) = pos.y();
214 helmetSurf.rr(i, 2) = pos.z();
215
216 QVector3D nn(helmetSurf.nn(i, 0), helmetSurf.nn(i, 1), helmetSurf.nn(i, 2));
217 const float* d = normalMat.constData();
218 float nx = d[0] * nn.x() + d[3] * nn.y() + d[6] * nn.z();
219 float ny = d[1] * nn.x() + d[4] * nn.y() + d[7] * nn.z();
220 float nz = d[2] * nn.x() + d[5] * nn.y() + d[8] * nn.z();
221 QVector3D n = QVector3D(nx, ny, nz).normalized();
222 helmetSurf.nn(i, 0) = n.x();
223 helmetSurf.nn(i, 1) = n.y();
224 helmetSurf.nn(i, 2) = n.z();
225 }
226 }
227
228 auto helmetSurface = std::make_shared<BrainSurface>();
229 helmetSurface->fromBemSurface(helmetSurf, QColor(0, 0, 77, 200));
230 helmetSurface->setVisible(true);
231 result.helmetSurface = helmetSurface;
232 } else {
233 qWarning() << "DataLoader::loadSensors: helmetBem[0] has 0 verts/tris!";
234 }
235 }
236 }
237 }
238
239 // ── Digitizer points ────────────────────────────────────────────
240 if (result.hasInfo && info.dig.size() > 0) {
241 result.digitizerPoints = info.dig;
242 result.hasDigitizer = true;
243 } else if (!result.hasInfo) {
244 file.reset();
245 digSet = FiffDigPointSet(file);
246 if (digSet.size() > 0) {
247 for (int i = 0; i < digSet.size(); ++i) {
248 result.digitizerPoints.append(digSet[i]);
249 }
250 result.hasDigitizer = true;
251 }
252 }
253
254 return result;
255}
256
257//=============================================================================================================
258
259std::shared_ptr<BrainSurface> DataLoader::loadHelmetSurface(
260 const QString& helmetFilePath,
261 const QMatrix4x4& devHeadTrans,
262 bool applyTrans)
263{
264 if (!QFile::exists(helmetFilePath)) {
265 qWarning() << "DataLoader::loadHelmetSurface: file not found:" << helmetFilePath;
266 return nullptr;
267 }
268
269 QFile helmetFile(helmetFilePath);
270 MNEBem helmetBem(helmetFile);
271 if (helmetBem.size() == 0) {
272 qWarning() << "DataLoader::loadHelmetSurface: no BEM surfaces in" << helmetFilePath;
273 return nullptr;
274 }
275
276 MNEBemSurface helmetSurf = helmetBem[0];
277
278 if (helmetSurf.nn.rows() != helmetSurf.rr.rows()) {
279 helmetSurf.nn = FSLIB::FsSurface::compute_normals(Eigen::MatrixX3f(helmetSurf.rr), Eigen::MatrixX3i(helmetSurf.itris));
280 }
281
282 if (applyTrans) {
283 QMatrix3x3 normalMat = devHeadTrans.normalMatrix();
284 for (int i = 0; i < helmetSurf.rr.rows(); ++i) {
285 QVector3D pos(helmetSurf.rr(i, 0), helmetSurf.rr(i, 1), helmetSurf.rr(i, 2));
286 pos = devHeadTrans.map(pos);
287 helmetSurf.rr(i, 0) = pos.x();
288 helmetSurf.rr(i, 1) = pos.y();
289 helmetSurf.rr(i, 2) = pos.z();
290
291 QVector3D nn(helmetSurf.nn(i, 0), helmetSurf.nn(i, 1), helmetSurf.nn(i, 2));
292 const float* d = normalMat.constData();
293 float nx = d[0] * nn.x() + d[3] * nn.y() + d[6] * nn.z();
294 float ny = d[1] * nn.x() + d[4] * nn.y() + d[7] * nn.z();
295 float nz = d[2] * nn.x() + d[5] * nn.y() + d[8] * nn.z();
296 QVector3D n = QVector3D(nx, ny, nz).normalized();
297 helmetSurf.nn(i, 0) = n.x();
298 helmetSurf.nn(i, 1) = n.y();
299 helmetSurf.nn(i, 2) = n.z();
300 }
301 }
302
303 auto surface = std::make_shared<BrainSurface>();
304 surface->fromBemSurface(helmetSurf, QColor(0, 0, 77, 200));
305 surface->setVisible(true);
306
307 return surface;
308}
309
310//=============================================================================================================
311
312InvEcdSet DataLoader::loadDipoles(const QString& dipPath)
313{
314 InvEcdSet ecdSet = InvEcdSet::read_dipoles_dip(dipPath);
315 if (ecdSet.size() == 0) {
316 qWarning() << "DataLoader: Failed to load dipoles from" << dipPath;
317 }
318 return ecdSet;
319}
320
321//=============================================================================================================
322
324{
325 QFile file(fwdPath);
326 if (!file.exists()) {
327 qWarning() << "DataLoader: Source space file not found:" << fwdPath;
328 return {};
329 }
330
331 MNESourceSpaces srcSpace;
332 FiffStream::SPtr stream(new FiffStream(&file));
333 if (!stream->open()) {
334 qWarning() << "DataLoader: Failed to open FIF stream for source space";
335 return {};
336 }
337
338 if (!MNESourceSpaces::readFromStream(stream, true, srcSpace)) {
339 qWarning() << "DataLoader: Failed to read source space from" << fwdPath;
340 return {};
341 }
342
343 if (srcSpace.isEmpty()) {
344 qWarning() << "DataLoader: Source space is empty";
345 return {};
346 }
347
348 return srcSpace;
349}
350
351//=============================================================================================================
352
353bool DataLoader::loadHeadToMriTransform(const QString& transPath,
354 FiffCoordTrans& trans)
355{
356 // A file may hold several transforms (e.g. all-trans.fif: device -> head and MRI -> head)
358 return !trans.isEmpty();
359}
360
361//=============================================================================================================
362
363FiffEvoked DataLoader::loadEvoked(const QString& evokedPath, int aveIndex)
364{
365 QFile file(evokedPath);
366 if (!file.exists()) {
367 qWarning() << "DataLoader: Sensor evoked file not found:" << evokedPath;
368 return {};
369 }
370
371 FiffEvoked evoked(file, aveIndex);
372 if (evoked.isEmpty()) {
373 qWarning() << "DataLoader: Failed to read evoked data from" << evokedPath;
374 }
375 return evoked;
376}
377
378//=============================================================================================================
379
380QStringList DataLoader::probeEvokedSets(const QString& evokedPath)
381{
382 QStringList result;
383 QFile file(evokedPath);
384 if (!file.exists()) {
385 return result;
386 }
387
388 FiffEvokedSet evokedSet(file);
389 for (int i = 0; i < evokedSet.evoked.size(); ++i) {
390 const auto& ev = evokedSet.evoked.at(i);
391 QString label = QString("%1: %2 (%3, nave=%4)")
392 .arg(i)
393 .arg(ev.comment.isEmpty() ? QStringLiteral("Set %1").arg(i) : ev.comment)
394 .arg(ev.aspectKindToString())
395 .arg(ev.nave);
396 result.append(label);
397 }
398 return result;
399}
400
401} // namespace DISP3DLIB
Static MATLAB-style FIFF facade: thin wrapper functions kept for parity with the historical mne-matla...
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
FIFF binary tag-stream layer: wraps a QIODevice to read and write FIFF tags, directories,...
Container for the FIFF_DIG_POINT records of a measurement (a parsed FIFFB_ISOTRAK block).
Tree item for a single MEG / EEG sensor with position, optional coil orientation and rendered scale.
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.
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....
Core MNE data structures (source spaces, source estimates, hemispheres).
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)
Inverse source estimation (MNE, dSPM, sLORETA, dipole fitting).
static std::shared_ptr< BrainSurface > loadHelmetSurface(const QString &helmetFilePath, const QMatrix4x4 &devHeadTrans=QMatrix4x4(), bool applyTrans=false)
static FIFFLIB::FiffEvoked loadEvoked(const QString &evokedPath, int aveIndex=0)
static MNELIB::MNESourceSpaces loadSourceSpace(const QString &fwdPath)
static INVLIB::InvEcdSet loadDipoles(const QString &dipPath)
static QStringList probeEvokedSets(const QString &evokedPath)
static SensorLoadResult loadSensors(const QString &fifPath, const QString &megHelmetOverridePath={})
static bool loadHeadToMriTransform(const QString &transPath, FIFFLIB::FiffCoordTrans &trans)
Return value bundling loaded sensor geometry, labels, and channel-to-sensor mapping.
Definition dataloader.h:81
FIFFLIB::FiffInfo info
Channel / dig info.
Definition dataloader.h:85
QMatrix4x4 devHeadTrans
Device→Head transform (identity if absent).
Definition dataloader.h:93
QList< QStandardItem * > megGradItems
Ownership passes to caller.
Definition dataloader.h:86
QList< FIFFLIB::FiffDigPoint > digitizerPoints
Definition dataloader.h:89
QList< QStandardItem * > megMagItems
Definition dataloader.h:87
QList< QStandardItem * > eegItems
Definition dataloader.h:88
bool hasDevHead
Whether a valid dev→head transform was found.
Definition dataloader.h:94
std::shared_ptr< BrainSurface > helmetSurface
May be null.
Definition dataloader.h:91
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.
Eigen::Matrix< float, 4, 4, Eigen::DontAlign > trans
static FiffCoordTrans readTransform(const QString &name, int from, int to)
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:77
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:90
QList< FiffDigPoint > dig
Definition fiff_info.h:290
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:67
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:80
qint32 size() const
Definition mne_bem.h:265
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)