v2.0.0
Loading...
Searching...
No Matches
mri_mgh_io.cpp
Go to the documentation of this file.
1//=============================================================================================================
25
26//=============================================================================================================
27// INCLUDES
28//=============================================================================================================
29
30#include "mri_mgh_io.h"
31
33#include <fiff/fiff_constants.h>
34#include <fiff/fiff_file.h>
35
36//=============================================================================================================
37// QT INCLUDES
38//=============================================================================================================
39
40#include <QFile>
41#include <QFileInfo>
42#include <QDataStream>
43#include <QDebug>
44#include <QRegularExpression>
45#include <QDebug>
46
47#include <zlib.h>
48
49//=============================================================================================================
50// EIGEN INCLUDES
51//=============================================================================================================
52
53#include <Eigen/Core>
54
55//=============================================================================================================
56// USED NAMESPACES
57//=============================================================================================================
58
59using namespace MRILIB;
60using namespace FIFFLIB;
61using namespace Eigen;
62
63//=============================================================================================================
64// DEFINE MEMBER METHODS
65//=============================================================================================================
66
67bool MriMghIO::read(const QString& mgzFile,
68 MriVolData& volData,
69 QVector<FiffCoordTrans>& additionalTrans,
70 const QString& subjectMriDir,
71 bool verbose)
72{
73 volData.fileName = mgzFile;
74
75 // Step 1: Get raw (decompressed) bytes
76 bool isCompressed = mgzFile.endsWith(".mgz", Qt::CaseInsensitive);
77 QByteArray fileData;
78
79 if (isCompressed) {
80 if (!decompress(mgzFile, fileData)) {
81 return false;
82 }
83 } else {
84 QFile file(mgzFile);
85 if (!file.open(QIODevice::ReadOnly)) {
86 qCritical() << "MriMghIO::read - Could not open" << mgzFile;
87 return false;
88 }
89 fileData = file.readAll();
90 file.close();
91 }
92
93 if (fileData.size() < MRI_MGH_DATA_OFFSET) {
94 qCritical() << "MriMghIO::read - File" << mgzFile
95 << "is too small to be a valid MGH file ("
96 << fileData.size() << "bytes)";
97 return false;
98 }
99
100 // Step 2: Parse header
101 if (!parseHeader(fileData, volData, verbose)) {
102 return false;
103 }
104
105 // Step 3: Build the voxel -> surface RAS transform
106 Matrix4f vox2ras = volData.computeVox2Ras();
108 FIFFV_COORD_MRI_SLICE, FIFFV_COORD_MRI, vox2ras, true);
109
110 if (verbose) {
111 qInfo("Voxel -> FsSurface RAS transform:\n");
112 for (int r = 0; r < 4; ++r) {
113 qInfo(" %10.6f %10.6f %10.6f %10.6f\n",
114 vox2ras(r, 0), vox2ras(r, 1), vox2ras(r, 2), vox2ras(r, 3));
115 }
116 }
117
118 // Step 4: Read voxel data
119 if (!readVoxelData(fileData, volData)) {
120 return false;
121 }
122
123 // Step 5: Parse footer (optional)
124 parseFooter(fileData, volData, additionalTrans, subjectMriDir, verbose);
125
126 if (verbose) {
127 qInfo("Read %d slices from %s (%dx%d pixels)\n",
128 static_cast<int>(volData.slices.size()), qPrintable(mgzFile),
129 volData.width, volData.height);
130 }
131
132 return true;
133}
134
135//=============================================================================================================
136
137bool MriMghIO::decompress(const QString& mgzFile, QByteArray& rawData)
138{
139 QFile file(mgzFile);
140 if (!file.open(QIODevice::ReadOnly)) {
141 qCritical() << "MriMghIO::decompress - Could not open" << mgzFile;
142 return false;
143 }
144 QByteArray compressedData = file.readAll();
145 file.close();
146
147 if (compressedData.isEmpty()) {
148 qCritical() << "MriMghIO::decompress - File is empty:" << mgzFile;
149 return false;
150 }
151
152 // Use zlib to decompress gzip data in memory
153 z_stream strm = {};
154
155 // MAX_WBITS + 16 tells zlib to detect and handle gzip headers
156 int ret = inflateInit2(&strm, MAX_WBITS + 16);
157 if (ret != Z_OK) {
158 qCritical() << "MriMghIO::decompress - inflateInit2 failed";
159 return false;
160 }
161
162 strm.next_in = reinterpret_cast<Bytef*>(compressedData.data());
163 strm.avail_in = static_cast<uInt>(compressedData.size());
164
165 const int chunkSize = 256 * 1024; // 256 KB chunks
166 rawData.clear();
167
168 do {
169 rawData.resize(rawData.size() + chunkSize);
170 strm.next_out = reinterpret_cast<Bytef*>(rawData.data() + rawData.size() - chunkSize);
171 strm.avail_out = chunkSize;
172
173 ret = inflate(&strm, Z_NO_FLUSH);
174 if (ret == Z_STREAM_ERROR || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) {
175 qCritical() << "MriMghIO::decompress - inflate failed for" << mgzFile
176 << "- zlib error:" << ret;
177 inflateEnd(&strm);
178 return false;
179 }
180 } while (ret != Z_STREAM_END);
181
182 // Trim to actual decompressed size
183 rawData.resize(rawData.size() - static_cast<int>(strm.avail_out));
184 inflateEnd(&strm);
185
186 return true;
187}
188
189//=============================================================================================================
190
191bool MriMghIO::parseHeader(const QByteArray& data, MriVolData& volData, bool verbose)
192{
193 //
194 // MGH header layout (all big-endian):
195 // Bytes 0-3: version (int32)
196 // Bytes 4-7: width (int32)
197 // Bytes 8-11: height (int32)
198 // Bytes 12-15: depth (int32)
199 // Bytes 16-19: nframes (int32)
200 // Bytes 20-23: type (int32)
201 // Bytes 24-27: dof (int32)
202 // Bytes 28-29: goodRASflag (int16)
203 // Bytes 30-41: spacingX/Y/Z (3×float32) — only if goodRASflag > 0
204 // Bytes 42-77: Mdc (9×float32) — direction cosines, only if goodRASflag > 0
205 // Bytes 78-89: c_ras (3×float32) — center RAS, only if goodRASflag > 0
206 // Bytes 90-283: unused (padding)
207 //
208
209 QDataStream stream(data);
210 stream.setByteOrder(QDataStream::BigEndian);
211 stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
212
213 qint32 version, width, height, depth, nframes, type, dof;
214 stream >> version >> width >> height >> depth >> nframes >> type >> dof;
215
216 if (version != MRI_MGH_VERSION) {
217 qCritical() << "MriMghIO::parseHeader - Unknown MGH version:" << version;
218 return false;
219 }
220
221 volData.version = version;
222 volData.width = width;
223 volData.height = height;
224 volData.depth = depth;
225 volData.nframes = nframes;
226 volData.type = type;
227 volData.dof = dof;
228
229 if (verbose) {
230 qInfo("MGH file: %dx%dx%d, %d frame(s), type=%d\n",
231 width, height, depth, nframes, type);
232 }
233
234 // goodRASflag (2 bytes short)
235 qint16 goodRASflag;
236 stream >> goodRASflag;
237 volData.rasGood = (goodRASflag > 0);
238
239 if (goodRASflag > 0) {
240 // Voxel sizes
241 stream >> volData.xsize >> volData.ysize >> volData.zsize;
242
243 // Direction cosines (Mdc matrix):
244 // xr, xa, xs (x-direction cosines)
245 // yr, ya, ys (y-direction cosines)
246 // zr, za, zs (z-direction cosines)
247 stream >> volData.x_ras[0] >> volData.x_ras[1] >> volData.x_ras[2];
248 stream >> volData.y_ras[0] >> volData.y_ras[1] >> volData.y_ras[2];
249 stream >> volData.z_ras[0] >> volData.z_ras[1] >> volData.z_ras[2];
250
251 // Center RAS
252 stream >> volData.c_ras[0] >> volData.c_ras[1] >> volData.c_ras[2];
253 }
254 // Else: default values from MriVolData constructor are used
255
256 if (verbose) {
257 qInfo("Voxel sizes: %.4f x %.4f x %.4f mm\n",
258 volData.xsize, volData.ysize, volData.zsize);
259 qInfo("goodRAS: %d\n", goodRASflag);
260 qInfo("c_ras: %.4f %.4f %.4f\n",
261 volData.c_ras[0], volData.c_ras[1], volData.c_ras[2]);
262 }
263
264 return true;
265}
266
267//=============================================================================================================
268
269bool MriMghIO::readVoxelData(const QByteArray& data, MriVolData& volData)
270{
271 //
272 // Read voxel data starting at byte 284 (MRI_MGH_DATA_OFFSET).
273 // Data layout in MGH: [width][height][depth][frames] in Fortran order (x fastest).
274 // Only the first frame is read.
275 //
276
277 int bpv = bytesPerVoxel(volData.type);
278 if (bpv == 0) {
279 qCritical() << "MriMghIO::readVoxelData - Unsupported MGH data type:" << volData.type;
280 return false;
281 }
282
283 qint64 frameSize = static_cast<qint64>(volData.width) * volData.height * volData.depth * bpv;
284 if (data.size() < MRI_MGH_DATA_OFFSET + frameSize) {
285 qCritical() << "MriMghIO::readVoxelData - File too small for expected data size";
286 return false;
287 }
288
289 QDataStream stream(data);
290 stream.setByteOrder(QDataStream::BigEndian);
291 stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
292 stream.device()->seek(MRI_MGH_DATA_OFFSET);
293
294 int nslice = volData.depth;
295 int nPixels = volData.width * volData.height;
296 volData.slices.resize(nslice);
297
298 // Build the vox2ras transform for per-slice transforms
299 Matrix4f vox2ras = volData.computeVox2Ras();
300
301 for (int k = 0; k < nslice; ++k) {
302 MriSlice& slice = volData.slices[k];
303 slice.width = volData.width;
304 slice.height = volData.height;
305 slice.dimx = volData.xsize / 1000.0f; // mm -> meters
306 slice.dimy = volData.ysize / 1000.0f;
307
308 // Read pixel data for this slice
309 switch (volData.type) {
310 case MRI_UCHAR: {
312 slice.pixels.resize(nPixels);
313 for (int p = 0; p < nPixels; ++p) {
314 quint8 val;
315 stream >> val;
316 slice.pixels[p] = val;
317 }
318 slice.scale = 1.0f;
319 break;
320 }
321 case MRI_SHORT: {
323 slice.pixelsWord.resize(nPixels);
324 for (int p = 0; p < nPixels; ++p) {
325 qint16 val;
326 stream >> val;
327 slice.pixelsWord[p] = static_cast<unsigned short>(val < 0 ? 0 : val);
328 }
329 slice.scale = 1.0f;
330 break;
331 }
332 case MRI_INT: {
333 // Convert INT to FLOAT
335 slice.pixelsFloat.resize(nPixels);
336 for (int p = 0; p < nPixels; ++p) {
337 qint32 val;
338 stream >> val;
339 slice.pixelsFloat[p] = static_cast<float>(val);
340 }
341 slice.scale = 1.0f;
342 break;
343 }
344 case MRI_FLOAT: {
346 slice.pixelsFloat.resize(nPixels);
347 for (int p = 0; p < nPixels; ++p) {
348 float val;
349 stream >> val;
350 slice.pixelsFloat[p] = val;
351 }
352 slice.scale = 1.0f;
353 break;
354 }
355 }
356
357 //
358 // Build per-slice coordinate transform (slice -> MRI surface RAS).
359 // For each slice k:
360 // sliceOrigin = vox2ras * [0, 0, k, 1]^T
361 // sliceRot = vox2ras rotation columns (x, y, z pixel axes)
362 //
363 Vector3f sliceOrigin;
364 sliceOrigin(0) = vox2ras(0, 2) * k + vox2ras(0, 3);
365 sliceOrigin(1) = vox2ras(1, 2) * k + vox2ras(1, 3);
366 sliceOrigin(2) = vox2ras(2, 2) * k + vox2ras(2, 3);
367
368 Matrix3f sliceRot;
369 sliceRot.col(0) = vox2ras.block<3, 1>(0, 0); // x-pixel direction
370 sliceRot.col(1) = vox2ras.block<3, 1>(0, 1); // y-pixel direction
371 sliceRot.col(2) = vox2ras.block<3, 1>(0, 2); // z (normal) direction
372
373 Vector3f sliceMove;
374 sliceMove << sliceOrigin(0), sliceOrigin(1), sliceOrigin(2);
375
376 slice.trans = FiffCoordTrans(FIFFV_COORD_MRI_SLICE, FIFFV_COORD_MRI, sliceRot, sliceMove);
377 }
378
379 return true;
380}
381
382//=============================================================================================================
383
384bool MriMghIO::parseFooter(const QByteArray& data,
385 MriVolData& volData,
386 QVector<FiffCoordTrans>& additionalTrans,
387 const QString& subjectMriDir,
388 bool verbose)
389{
390 //
391 // The footer starts after the voxel data.
392 // It contains (in order):
393 // 1. Scan parameters: TR(f32), flipAngle(f32), TE(f32), TI(f32), FoV(f32)
394 // 2. Tags: tagType(i32) + tagLen(i32 or i64) + tagData
395 //
396
397 int bpv = bytesPerVoxel(volData.type);
398 if (bpv == 0) {
399 bpv = 1; // Fallback for footer calculation
400 }
401
402 qint64 frameSize = static_cast<qint64>(volData.width) * volData.height * volData.depth * bpv;
403 qint64 footerPos = MRI_MGH_DATA_OFFSET + frameSize;
404
405 if (data.size() <= footerPos) {
406 // No footer — that's fine
407 return true;
408 }
409
410 QDataStream stream(data);
411 stream.setByteOrder(QDataStream::BigEndian);
412 stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
413 stream.device()->seek(footerPos);
414
415 // Read scan parameters (5 × float32 = 20 bytes)
416 constexpr int kScanParamBytes = 5 * sizeof(float);
417 qint64 remainingBytes = data.size() - footerPos;
418 if (remainingBytes >= kScanParamBytes) {
419 stream >> volData.TR >> volData.flipAngle >> volData.TE >> volData.TI >> volData.FoV;
420 } else {
421 return true;
422 }
423
424 // Parse tags
425 while (!stream.atEnd()) {
426 qint32 tagType;
427 stream >> tagType;
428 if (stream.atEnd()) break;
429
430 qint64 tagLen;
431 // For TAG_OLD_SURF_GEOM (20) and TAG_OLD_MGH_XFORM (30), length is 4 bytes
432 // For newer tags, length is 8 bytes
433 if (tagType == MGH_TAG_OLD_SURF_GEOM || tagType == MGH_TAG_OLD_MGH_XFORM) {
434 qint32 len32;
435 stream >> len32;
436 tagLen = len32;
437 } else {
438 qint64 len64;
439 stream >> len64;
440 tagLen = len64;
441 }
442
443 if (tagLen <= 0 || tagLen > data.size()) break;
444
445 QByteArray tagData(tagLen, '\0');
446 if (stream.readRawData(tagData.data(), tagLen) != tagLen) break;
447
448 if (tagType == MGH_TAG_MGH_XFORM) {
449 // TAG_MGH_XFORM: contains path to talairach.xfm
450 QString xfmPath = QString::fromLatin1(tagData).trimmed();
451 volData.talairachXfmPath = xfmPath;
452
453 if (verbose) {
454 qInfo("Found Talairach transform reference: %s\n", qPrintable(xfmPath));
455 }
456
457 // Resolve relative paths using subject MRI directory
458 if (!QFileInfo(xfmPath).isAbsolute() && !subjectMriDir.isEmpty()) {
459 xfmPath = subjectMriDir + "/transforms/" + xfmPath;
460 }
461
462 if (QFileInfo::exists(xfmPath)) {
463 QFile xfmFile(xfmPath);
464 if (xfmFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
465 // Parse Linear_Transform from .xfm file
466 // Format:
467 // MNI Transform File
468 // ...
469 // Linear_Transform =
470 // mat[0][0] mat[0][1] mat[0][2] mat[0][3]
471 // mat[1][0] mat[1][1] mat[1][2] mat[1][3]
472 // mat[2][0] mat[2][1] mat[2][2] mat[2][3] ;
473 QString xfmContent = xfmFile.readAll();
474 xfmFile.close();
475
476 int ltIdx = xfmContent.indexOf("Linear_Transform");
477 if (ltIdx >= 0) {
478 int eqIdx = xfmContent.indexOf('=', ltIdx);
479 if (eqIdx >= 0) {
480 QString matStr = xfmContent.mid(eqIdx + 1).trimmed();
481 matStr.remove(';');
482
483 QStringList vals = matStr.split(QRegularExpression("\\s+"),
484 Qt::SkipEmptyParts);
485
486 if (vals.size() >= 12) {
487 // RAS -> MNI Talairach (3×4 matrix, in mm)
488 Matrix4f rasMniTal = Matrix4f::Identity();
489 for (int r = 0; r < 3; ++r) {
490 for (int c = 0; c < 4; ++c) {
491 rasMniTal(r, c) = vals[r * 4 + c].toFloat();
492 }
493 }
494
495 // Convert translation from mm to meters
496 rasMniTal(0, 3) /= 1000.0f;
497 rasMniTal(1, 3) /= 1000.0f;
498 rasMniTal(2, 3) /= 1000.0f;
499
500 // Create RAS -> MNI Talairach transform
501 FiffCoordTrans talTrans(
503 rasMniTal, true);
504
505 additionalTrans.append(talTrans);
506
507 if (verbose) {
508 qInfo("Read Talairach transform from %s\n", qPrintable(xfmPath));
509 }
510 }
511 }
512 }
513 }
514 } else {
515 if (verbose) {
516 qWarning("Talairach transform file not found: %s\n", qPrintable(xfmPath));
517 }
518 }
519 }
520 }
521
522 return true;
523}
524
525//=============================================================================================================
526
527int MriMghIO::bytesPerVoxel(int type)
528{
529 switch (type) {
530 case MRI_UCHAR: return 1;
531 case MRI_SHORT: return 2;
532 case MRI_INT: return 4;
533 case MRI_FLOAT: return 4;
534 default: return 0;
535 }
536}
FreeSurfer MGH / MGZ volume reader: byte-level decoder for the 284-byte fixed header,...
return FiffCoordTrans(from_frame, to_frame, R, moveVec)
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_COORD_MRI_SLICE
#define FIFFV_COORD_MRI_DISPLAY
#define FIFFV_COORD_MRI
FIFF tag-kind, block-kind and type-code numerical definitions, authoritative for FIFFLIB.
#define FIFFV_MRI_PIXEL_BYTE
Definition fiff_file.h:695
#define FIFFV_MRI_PIXEL_FLOAT
Definition fiff_file.h:698
#define FIFFV_MRI_PIXEL_WORD
Definition fiff_file.h:696
4x4 affine FIFF coordinate transform (FIFF_COORD_TRANS) annotated with source/destination coordinate-...
FIFF file I/O, in-memory data structures and high-level readers/writers.
Volume I/O, voxel geometry and slice resampling for structural MRI data inside mne-cpp.
constexpr int MRI_MGH_VERSION
Definition mri_types.h:50
constexpr int MGH_TAG_OLD_MGH_XFORM
Definition mri_types.h:118
constexpr int MRI_MGH_DATA_OFFSET
Definition mri_types.h:100
constexpr int MGH_TAG_OLD_SURF_GEOM
Definition mri_types.h:117
constexpr int MGH_TAG_MGH_XFORM
Definition mri_types.h:119
constexpr int MRI_SHORT
Definition mri_types.h:72
constexpr int MRI_UCHAR
Definition mri_types.h:68
constexpr int MRI_INT
Definition mri_types.h:69
constexpr int MRI_FLOAT
Definition mri_types.h:71
static bool read(const QString &mgzFile, MriVolData &volData, QVector< FIFFLIB::FiffCoordTrans > &additionalTrans, const QString &subjectMriDir=QString(), bool verbose=false)
QVector< unsigned char > pixels
FIFFLIB::FiffCoordTrans trans
QVector< unsigned short > pixelsWord
QVector< float > pixelsFloat
Format-agnostic 3D MRI volume: header geometry, voxel buffer (as a vector of MriSlice),...
FIFFLIB::FiffCoordTrans voxelSurfRasT
Eigen::Vector3f y_ras
Eigen::Vector3f x_ras
QVector< MriSlice > slices
Eigen::Matrix4f computeVox2Ras() const
Eigen::Vector3f z_ras
Eigen::Vector3f c_ras