v2.0.0
Loading...
Searching...
No Matches
mri_nifti_io.cpp
Go to the documentation of this file.
1//=============================================================================================================
27
28//=============================================================================================================
29// INCLUDES
30//=============================================================================================================
31
32#include "mri_nifti_io.h"
33
34#include "mri_types.h"
35#include "mri_vol_data.h"
36
37#include <fiff/fiff_file.h>
38
39//=============================================================================================================
40// QT INCLUDES
41//=============================================================================================================
42
43#include <QByteArray>
44#include <QDataStream>
45#include <QDebug>
46#include <QFile>
47#include <QtEndian>
48
49//=============================================================================================================
50// EIGEN INCLUDES
51//=============================================================================================================
52
53#include <Eigen/Core>
54
55//=============================================================================================================
56// SYSTEM INCLUDES
57//=============================================================================================================
58
59#include <zlib.h>
60
61#include <cmath>
62#include <cstring>
63
64//=============================================================================================================
65// USED NAMESPACES
66//=============================================================================================================
67
68using namespace MRILIB;
69using namespace FIFFLIB;
70using namespace Eigen;
71
72namespace
73{
74
75// NIfTI-1 datatype codes (subset that we map onto MRI_* types).
76constexpr qint16 DT_UINT8 = 2;
77constexpr qint16 DT_INT16 = 4;
78constexpr qint16 DT_INT32 = 8;
79constexpr qint16 DT_FLOAT32 = 16;
80constexpr qint16 DT_INT8 = 256;
81constexpr qint16 DT_UINT16 = 512;
82
83constexpr int NIFTI_HDR_SIZE = 348;
84
85template<typename T>
86T readLE(const char* p)
87{
88 T v;
89 std::memcpy(&v, p, sizeof(T));
90 return qFromLittleEndian(v);
91}
92
93template<typename T>
94T readBE(const char* p)
95{
96 T v;
97 std::memcpy(&v, p, sizeof(T));
98 return qFromBigEndian(v);
99}
100
101} // namespace
102
103//=============================================================================================================
104
105bool MriNiftiIO::decompress(const QString& gzFile, QByteArray& rawData)
106{
107 QFile file(gzFile);
108 if (!file.open(QIODevice::ReadOnly)) {
109 qCritical() << "MriNiftiIO::decompress - Could not open" << gzFile;
110 return false;
111 }
112 const QByteArray compressed = file.readAll();
113 file.close();
114
115 if (compressed.isEmpty()) {
116 qCritical() << "MriNiftiIO::decompress - File is empty:" << gzFile;
117 return false;
118 }
119
120 z_stream strm = {};
121 if (inflateInit2(&strm, MAX_WBITS + 16) != Z_OK) {
122 qCritical() << "MriNiftiIO::decompress - inflateInit2 failed";
123 return false;
124 }
125
126 strm.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(compressed.data()));
127 strm.avail_in = static_cast<uInt>(compressed.size());
128
129 const int chunkSize = 256 * 1024;
130 rawData.clear();
131
132 int ret = Z_OK;
133 do {
134 rawData.resize(rawData.size() + chunkSize);
135 strm.next_out = reinterpret_cast<Bytef*>(rawData.data() + rawData.size() - chunkSize);
136 strm.avail_out = chunkSize;
137
138 ret = inflate(&strm, Z_NO_FLUSH);
139 if (ret == Z_STREAM_ERROR || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) {
140 qCritical() << "MriNiftiIO::decompress - inflate failed for" << gzFile
141 << "- zlib error:" << ret;
142 inflateEnd(&strm);
143 return false;
144 }
145 } while (ret != Z_STREAM_END);
146
147 rawData.resize(rawData.size() - static_cast<int>(strm.avail_out));
148 inflateEnd(&strm);
149 return true;
150}
151
152//=============================================================================================================
153
154bool MriNiftiIO::read(const QString& niiFile, MriVolData& volData, bool verbose)
155{
156 volData.fileName = niiFile;
157
158 QByteArray bytes;
159 const bool compressed = niiFile.endsWith(QStringLiteral(".gz"), Qt::CaseInsensitive);
160 if (compressed) {
161 if (!decompress(niiFile, bytes)) {
162 return false;
163 }
164 } else {
165 QFile f(niiFile);
166 if (!f.open(QIODevice::ReadOnly)) {
167 qCritical() << "MriNiftiIO::read - Could not open" << niiFile;
168 return false;
169 }
170 bytes = f.readAll();
171 }
172
173 if (bytes.size() < NIFTI_HDR_SIZE) {
174 qCritical() << "MriNiftiIO::read - file too small (" << bytes.size() << "bytes) :" << niiFile;
175 return false;
176 }
177
178 const char* hdr = bytes.constData();
179
180 // Endianness detection via sizeof_hdr (must be 348).
181 qint32 sizeofHdr = readLE<qint32>(hdr);
182 bool bigEndian = false;
183 if (sizeofHdr != NIFTI_HDR_SIZE) {
184 sizeofHdr = readBE<qint32>(hdr);
185 if (sizeofHdr != NIFTI_HDR_SIZE) {
186 qCritical() << "MriNiftiIO::read - not a NIfTI-1 file (sizeof_hdr ="
187 << sizeofHdr << "):" << niiFile;
188 return false;
189 }
190 bigEndian = true;
191 }
192
193 auto i16 = [&](int off) {
194 return bigEndian ? readBE<qint16>(hdr + off) : readLE<qint16>(hdr + off);
195 };
196 auto f32 = [&](int off) {
197 if (bigEndian) {
198 quint32 raw = readBE<quint32>(hdr + off);
199 float v;
200 std::memcpy(&v, &raw, sizeof(float));
201 return v;
202 }
203 quint32 raw = readLE<quint32>(hdr + off);
204 float v;
205 std::memcpy(&v, &raw, sizeof(float));
206 return v;
207 };
208
209 // dim[0..7] starts at offset 40; dim[0] = ndim, dim[1..3] = nx,ny,nz, dim[4] = nt.
210 qint16 ndim = i16(40);
211 qint16 nx = i16(42);
212 qint16 ny = i16(44);
213 qint16 nz = i16(46);
214 qint16 nt = i16(48);
215 if (ndim < 3 || nx <= 0 || ny <= 0 || nz <= 0) {
216 qCritical() << "MriNiftiIO::read - degenerate dims" << ndim << nx << ny << nz;
217 return false;
218 }
219
220 qint16 datatype = i16(70);
221 qint16 bitpix = i16(72);
222 Q_UNUSED(bitpix);
223
224 // pixdim[0..7] starts at offset 76. pixdim[1..3] = spacing in mm; pixdim[0] is the qfac sign.
225 float qfac = f32(76);
226 if (qfac == 0.0f) {
227 qfac = 1.0f;
228 }
229 const float dx = std::fabs(f32(80));
230 const float dy = std::fabs(f32(84));
231 const float dz = std::fabs(f32(88));
232
233 const float voxOffset = f32(108);
234 const float sclSlope = f32(112);
235 const float sclInter = f32(116);
236
237 const qint16 qformCode = i16(252);
238 const qint16 sformCode = i16(254);
239
240 // Map NIfTI datatype to MRI_* + bytes-per-voxel.
241 int mriType = MRI_UCHAR;
242 int bpv = 1;
243 switch (datatype) {
244 case DT_UINT8:
245 mriType = MRI_UCHAR;
246 bpv = 1;
247 break;
248 case DT_INT8:
249 mriType = MRI_UCHAR;
250 bpv = 1;
251 break; // promoted to unsigned, negatives clamped
252 case DT_INT16:
253 mriType = MRI_SHORT;
254 bpv = 2;
255 break;
256 case DT_UINT16:
257 mriType = MRI_SHORT;
258 bpv = 2;
259 break;
260 case DT_INT32:
261 mriType = MRI_INT;
262 bpv = 4;
263 break;
264 case DT_FLOAT32:
265 mriType = MRI_FLOAT;
266 bpv = 4;
267 break;
268 default:
269 qCritical() << "MriNiftiIO::read - unsupported datatype" << datatype;
270 return false;
271 }
272
273 // Build the 4×4 voxel→RAS transform (mm) using sform → qform → pixdim fallback.
274 Matrix4f vox2ras = Matrix4f::Identity();
275 if (sformCode > 0) {
276 for (int c = 0; c < 4; ++c) {
277 vox2ras(0, c) = f32(280 + c * 4);
278 vox2ras(1, c) = f32(296 + c * 4);
279 vox2ras(2, c) = f32(312 + c * 4);
280 }
281 } else if (qformCode > 0) {
282 const float b = f32(256);
283 const float c = f32(260);
284 const float d = f32(264);
285 const float aSq = 1.0f - (b * b + c * c + d * d);
286 const float a = aSq > 0.0f ? std::sqrt(aSq) : 0.0f;
287 Matrix3f R;
288 R << a * a + b * b - c * c - d * d, 2.0f * (b * c - a * d), 2.0f * (b * d + a * c),
289 2.0f * (b * c + a * d), a * a + c * c - b * b - d * d, 2.0f * (c * d - a * b),
290 2.0f * (b * d - a * c), 2.0f * (c * d + a * b), a * a + d * d - b * b - c * c;
291 Matrix3f S = Matrix3f::Zero();
292 S(0, 0) = dx;
293 S(1, 1) = dy;
294 S(2, 2) = dz * qfac;
295 Matrix3f M = R * S;
296 vox2ras.block<3, 3>(0, 0) = M;
297 vox2ras(0, 3) = f32(268);
298 vox2ras(1, 3) = f32(272);
299 vox2ras(2, 3) = f32(276);
300 } else {
301 // Method 1: pixdim diagonal, origin at volume centre.
302 vox2ras(0, 0) = dx;
303 vox2ras(1, 1) = dy;
304 vox2ras(2, 2) = dz;
305 vox2ras(0, 3) = -0.5f * dx * nx;
306 vox2ras(1, 3) = -0.5f * dy * ny;
307 vox2ras(2, 3) = -0.5f * dz * nz;
308 }
309
310 // Decompose vox2ras → (Mdc, spacing, c_ras) so MriVolData::computeVox2Ras round-trips.
311 Vector3f col0 = vox2ras.block<3, 1>(0, 0);
312 Vector3f col1 = vox2ras.block<3, 1>(0, 1);
313 Vector3f col2 = vox2ras.block<3, 1>(0, 2);
314 float sx = col0.norm();
315 float sy = col1.norm();
316 float sz = col2.norm();
317 if (sx <= 0.0f)
318 sx = dx > 0.0f ? dx : 1.0f;
319 if (sy <= 0.0f)
320 sy = dy > 0.0f ? dy : 1.0f;
321 if (sz <= 0.0f)
322 sz = dz > 0.0f ? dz : 1.0f;
323
324 Vector3f xRas = col0 / sx;
325 Vector3f yRas = col1 / sy;
326 Vector3f zRas = col2 / sz;
327
328 Vector4f centreVox(nx * 0.5f, ny * 0.5f, nz * 0.5f, 1.0f);
329 Vector4f cRas4 = vox2ras * centreVox;
330
331 volData.version = MRI_MGH_VERSION; // mark as a valid volume for isValid()
332 volData.width = nx;
333 volData.height = ny;
334 volData.depth = nz;
335 volData.nframes = nt > 0 ? nt : 1;
336 volData.type = mriType;
337 volData.dof = 0;
338 volData.rasGood = true;
339 volData.xsize = sx;
340 volData.ysize = sy;
341 volData.zsize = sz;
342 volData.x_ras = xRas;
343 volData.y_ras = yRas;
344 volData.z_ras = zRas;
345 volData.c_ras = cRas4.head<3>();
348
349 // Read voxel data starting at vox_offset (typically 352 for single-file).
350 const qint64 dataOff = static_cast<qint64>(voxOffset > 0.0f ? voxOffset : NIFTI_HDR_SIZE + 4);
351 const qint64 frameSize = static_cast<qint64>(nx) * ny * nz * bpv;
352 if (bytes.size() < dataOff + frameSize) {
353 qCritical() << "MriNiftiIO::read - data section truncated (need"
354 << (dataOff + frameSize) << "bytes, got" << bytes.size() << ")";
355 return false;
356 }
357
358 QDataStream stream(bytes);
359 stream.setByteOrder(bigEndian ? QDataStream::BigEndian : QDataStream::LittleEndian);
360 stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
361 stream.device()->seek(dataOff);
362
363 const int nslice = nz;
364 const int nPixels = nx * ny;
365 volData.slices.resize(nslice);
366
367 const Matrix4f vox2rasFinal = volData.computeVox2Ras();
368 const bool useScale = (sclSlope != 0.0f) && !(sclSlope == 1.0f && sclInter == 0.0f);
369
370 for (int k = 0; k < nslice; ++k) {
371 MriSlice& slice = volData.slices[k];
372 slice.width = nx;
373 slice.height = ny;
374 slice.dimx = sx / 1000.0f;
375 slice.dimy = sy / 1000.0f;
376 slice.scale = 1.0f;
377
378 switch (mriType) {
379 case MRI_UCHAR: {
381 slice.pixels.resize(nPixels);
382 if (datatype == DT_INT8) {
383 for (int p = 0; p < nPixels; ++p) {
384 qint8 v;
385 stream >> v;
386 slice.pixels[p] = static_cast<unsigned char>(v < 0 ? 0 : v);
387 }
388 } else {
389 for (int p = 0; p < nPixels; ++p) {
390 quint8 v;
391 stream >> v;
392 slice.pixels[p] = v;
393 }
394 }
395 break;
396 }
397 case MRI_SHORT: {
399 slice.pixelsWord.resize(nPixels);
400 if (datatype == DT_UINT16) {
401 for (int p = 0; p < nPixels; ++p) {
402 quint16 v;
403 stream >> v;
404 slice.pixelsWord[p] = v;
405 }
406 } else {
407 for (int p = 0; p < nPixels; ++p) {
408 qint16 v;
409 stream >> v;
410 slice.pixelsWord[p] = static_cast<unsigned short>(v < 0 ? 0 : v);
411 }
412 }
413 break;
414 }
415 case MRI_INT: {
417 slice.pixelsFloat.resize(nPixels);
418 for (int p = 0; p < nPixels; ++p) {
419 qint32 v;
420 stream >> v;
421 slice.pixelsFloat[p] = static_cast<float>(v);
422 }
423 break;
424 }
425 case MRI_FLOAT: {
427 slice.pixelsFloat.resize(nPixels);
428 for (int p = 0; p < nPixels; ++p) {
429 float v;
430 stream >> v;
431 slice.pixelsFloat[p] = v;
432 }
433 break;
434 }
435 }
436
437 // Apply scl_slope / scl_inter for floating-point output (the NIfTI spec
438 // states the transform produces the "physical" value; we only honour it
439 // for floats — integer outputs stay as-is to preserve label maps).
440 if (useScale && slice.pixelFormat == FIFFV_MRI_PIXEL_FLOAT) {
441 for (int p = 0; p < slice.pixelsFloat.size(); ++p) {
442 slice.pixelsFloat[p] = slice.pixelsFloat[p] * sclSlope + sclInter;
443 }
444 }
445
446 Vector3f sliceOrigin;
447 sliceOrigin(0) = vox2rasFinal(0, 2) * k + vox2rasFinal(0, 3);
448 sliceOrigin(1) = vox2rasFinal(1, 2) * k + vox2rasFinal(1, 3);
449 sliceOrigin(2) = vox2rasFinal(2, 2) * k + vox2rasFinal(2, 3);
450
451 Matrix3f sliceRot;
452 sliceRot.col(0) = vox2rasFinal.block<3, 1>(0, 0);
453 sliceRot.col(1) = vox2rasFinal.block<3, 1>(0, 1);
454 sliceRot.col(2) = vox2rasFinal.block<3, 1>(0, 2);
455
456 slice.trans = FiffCoordTrans(FIFFV_COORD_MRI_SLICE, FIFFV_COORD_MRI, sliceRot, sliceOrigin);
457 }
458
459 if (verbose) {
460 qInfo("NIfTI file: %dx%dx%d, datatype=%d, voxel %.3fx%.3fx%.3f mm, c_ras=(%.2f, %.2f, %.2f)",
461 nx, ny, nz, datatype, sx, sy, sz,
462 volData.c_ras[0], volData.c_ras[1], volData.c_ras[2]);
463 }
464
465 return true;
466}
#define FIFFV_COORD_MRI_SLICE
#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
Eigen::Matrix3f R
return FiffCoordTrans(from_frame, to_frame, R, moveVec)
Eigen::Matrix3f S
Format-agnostic in-memory representation of a 3D MRI volume plus its slice decomposition.
Numeric type codes, layout constants, and sentinel values shared by every MRI reader.
NIfTI-1 single-file (.nii / .nii.gz) volume reader producing the same per-slice layout as the MGH rea...
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:51
constexpr int MRI_SHORT
Definition mri_types.h:73
constexpr int MRI_UCHAR
Definition mri_types.h:69
constexpr int MRI_INT
Definition mri_types.h:70
constexpr int MRI_FLOAT
Definition mri_types.h:72
static bool decompress(const QString &gzFile, QByteArray &rawData)
static bool read(const QString &niiFile, MriVolData &volData, bool verbose=false)
Single 2D MRI slice (pixels + slice→RAS transform) used as the volume's storage unit.
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