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