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 // With all input consumed and output room left, a stream that has not ended is cut short
140 const bool truncated = ret != Z_STREAM_END && strm.avail_in == 0 && strm.avail_out > 0;
141 if (ret == Z_STREAM_ERROR || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR || truncated) {
142 qCritical() << "MriNiftiIO::decompress - inflate failed for" << gzFile
143 << "- zlib error:" << (truncated ? Z_BUF_ERROR : ret);
144 inflateEnd(&strm);
145 return false;
146 }
147 } while (ret != Z_STREAM_END);
148
149 rawData.resize(rawData.size() - static_cast<int>(strm.avail_out));
150 inflateEnd(&strm);
151 return true;
152}
153
154//=============================================================================================================
155
156bool MriNiftiIO::read(const QString& niiFile, MriVolData& volData, bool verbose)
157{
158 volData.fileName = niiFile;
159
160 QByteArray bytes;
161 const bool compressed = niiFile.endsWith(QStringLiteral(".gz"), Qt::CaseInsensitive);
162 if (compressed) {
163 if (!decompress(niiFile, bytes)) {
164 return false;
165 }
166 } else {
167 QFile f(niiFile);
168 if (!f.open(QIODevice::ReadOnly)) {
169 qCritical() << "MriNiftiIO::read - Could not open" << niiFile;
170 return false;
171 }
172 bytes = f.readAll();
173 }
174
175 if (bytes.size() < NIFTI_HDR_SIZE) {
176 qCritical() << "MriNiftiIO::read - file too small (" << bytes.size() << "bytes) :" << niiFile;
177 return false;
178 }
179
180 const char* hdr = bytes.constData();
181
182 // Endianness detection via sizeof_hdr (must be 348).
183 qint32 sizeofHdr = readLE<qint32>(hdr);
184 bool bigEndian = false;
185 if (sizeofHdr != NIFTI_HDR_SIZE) {
186 sizeofHdr = readBE<qint32>(hdr);
187 if (sizeofHdr != NIFTI_HDR_SIZE) {
188 qCritical() << "MriNiftiIO::read - not a NIfTI-1 file (sizeof_hdr ="
189 << sizeofHdr << "):" << niiFile;
190 return false;
191 }
192 bigEndian = true;
193 }
194
195 auto i16 = [&](int off) {
196 return bigEndian ? readBE<qint16>(hdr + off) : readLE<qint16>(hdr + off);
197 };
198 auto f32 = [&](int off) {
199 if (bigEndian) {
200 quint32 raw = readBE<quint32>(hdr + off);
201 float v;
202 std::memcpy(&v, &raw, sizeof(float));
203 return v;
204 }
205 quint32 raw = readLE<quint32>(hdr + off);
206 float v;
207 std::memcpy(&v, &raw, sizeof(float));
208 return v;
209 };
210
211 // dim[0..7] starts at offset 40; dim[0] = ndim, dim[1..3] = nx,ny,nz, dim[4] = nt.
212 qint16 ndim = i16(40);
213 qint16 nx = i16(42);
214 qint16 ny = i16(44);
215 qint16 nz = i16(46);
216 qint16 nt = i16(48);
217 if (ndim < 3 || nx <= 0 || ny <= 0 || nz <= 0) {
218 qCritical() << "MriNiftiIO::read - degenerate dims" << ndim << nx << ny << nz;
219 return false;
220 }
221
222 qint16 datatype = i16(70);
223 qint16 bitpix = i16(72);
224 Q_UNUSED(bitpix);
225
226 // pixdim[0..7] starts at offset 76. pixdim[1..3] = spacing in mm; pixdim[0] is the qfac sign.
227 float qfac = f32(76);
228 if (qfac == 0.0f) {
229 qfac = 1.0f;
230 }
231 const float dx = std::fabs(f32(80));
232 const float dy = std::fabs(f32(84));
233 const float dz = std::fabs(f32(88));
234
235 const float voxOffset = f32(108);
236 const float sclSlope = f32(112);
237 const float sclInter = f32(116);
238
239 const qint16 qformCode = i16(252);
240 const qint16 sformCode = i16(254);
241
242 // Map NIfTI datatype to MRI_* + bytes-per-voxel.
243 int mriType = MRI_UCHAR;
244 int bpv = 1;
245 switch (datatype) {
246 case DT_UINT8:
247 mriType = MRI_UCHAR;
248 bpv = 1;
249 break;
250 case DT_INT8:
251 mriType = MRI_UCHAR;
252 bpv = 1;
253 break; // promoted to unsigned, negatives clamped
254 case DT_INT16:
255 mriType = MRI_SHORT;
256 bpv = 2;
257 break;
258 case DT_UINT16:
259 mriType = MRI_SHORT;
260 bpv = 2;
261 break;
262 case DT_INT32:
263 mriType = MRI_INT;
264 bpv = 4;
265 break;
266 case DT_FLOAT32:
267 mriType = MRI_FLOAT;
268 bpv = 4;
269 break;
270 default:
271 qCritical() << "MriNiftiIO::read - unsupported datatype" << datatype;
272 return false;
273 }
274
275 // Build the 4×4 voxel→RAS transform (mm) using sform → qform → pixdim fallback.
276 Matrix4f vox2ras = Matrix4f::Identity();
277 if (sformCode > 0) {
278 for (int c = 0; c < 4; ++c) {
279 vox2ras(0, c) = f32(280 + c * 4);
280 vox2ras(1, c) = f32(296 + c * 4);
281 vox2ras(2, c) = f32(312 + c * 4);
282 }
283 } else if (qformCode > 0) {
284 const float b = f32(256);
285 const float c = f32(260);
286 const float d = f32(264);
287 const float aSq = 1.0f - (b * b + c * c + d * d);
288 const float a = aSq > 0.0f ? std::sqrt(aSq) : 0.0f;
289 Matrix3f R;
290 R << a * a + b * b - c * c - d * d, 2.0f * (b * c - a * d), 2.0f * (b * d + a * c),
291 2.0f * (b * c + a * d), a * a + c * c - b * b - d * d, 2.0f * (c * d - a * b),
292 2.0f * (b * d - a * c), 2.0f * (c * d + a * b), a * a + d * d - b * b - c * c;
293 Matrix3f S = Matrix3f::Zero();
294 S(0, 0) = dx;
295 S(1, 1) = dy;
296 S(2, 2) = dz * qfac;
297 Matrix3f M = R * S;
298 vox2ras.block<3, 3>(0, 0) = M;
299 vox2ras(0, 3) = f32(268);
300 vox2ras(1, 3) = f32(272);
301 vox2ras(2, 3) = f32(276);
302 } else {
303 // Method 1: pixdim diagonal, origin at volume centre.
304 vox2ras(0, 0) = dx;
305 vox2ras(1, 1) = dy;
306 vox2ras(2, 2) = dz;
307 vox2ras(0, 3) = -0.5f * dx * nx;
308 vox2ras(1, 3) = -0.5f * dy * ny;
309 vox2ras(2, 3) = -0.5f * dz * nz;
310 }
311
312 // Decompose vox2ras → (Mdc, spacing, c_ras) so MriVolData::computeVox2Ras round-trips.
313 Vector3f col0 = vox2ras.block<3, 1>(0, 0);
314 Vector3f col1 = vox2ras.block<3, 1>(0, 1);
315 Vector3f col2 = vox2ras.block<3, 1>(0, 2);
316 float sx = col0.norm();
317 float sy = col1.norm();
318 float sz = col2.norm();
319 if (sx <= 0.0f)
320 sx = dx > 0.0f ? dx : 1.0f;
321 if (sy <= 0.0f)
322 sy = dy > 0.0f ? dy : 1.0f;
323 if (sz <= 0.0f)
324 sz = dz > 0.0f ? dz : 1.0f;
325
326 Vector3f xRas = col0 / sx;
327 Vector3f yRas = col1 / sy;
328 Vector3f zRas = col2 / sz;
329
330 Vector4f centreVox(nx * 0.5f, ny * 0.5f, nz * 0.5f, 1.0f);
331 Vector4f cRas4 = vox2ras * centreVox;
332
333 volData.version = MRI_MGH_VERSION; // mark as a valid volume for isValid()
334 volData.width = nx;
335 volData.height = ny;
336 volData.depth = nz;
337 volData.nframes = nt > 0 ? nt : 1;
338 volData.type = mriType;
339 volData.dof = 0;
340 volData.rasGood = true;
341 volData.xsize = sx;
342 volData.ysize = sy;
343 volData.zsize = sz;
344 volData.x_ras = xRas;
345 volData.y_ras = yRas;
346 volData.z_ras = zRas;
347 volData.c_ras = cRas4.head<3>();
350
351 // Read voxel data starting at vox_offset (typically 352 for single-file).
352 const qint64 dataOff = static_cast<qint64>(voxOffset > 0.0f ? voxOffset : NIFTI_HDR_SIZE + 4);
353 const qint64 frameSize = static_cast<qint64>(nx) * ny * nz * bpv;
354 if (bytes.size() < dataOff + frameSize) {
355 qCritical() << "MriNiftiIO::read - data section truncated (need"
356 << (dataOff + frameSize) << "bytes, got" << bytes.size() << ")";
357 return false;
358 }
359
360 QDataStream stream(bytes);
361 stream.setByteOrder(bigEndian ? QDataStream::BigEndian : QDataStream::LittleEndian);
362 stream.setFloatingPointPrecision(QDataStream::SinglePrecision);
363 stream.device()->seek(dataOff);
364
365 const int nslice = nz;
366 const int nPixels = nx * ny;
367 volData.slices.resize(nslice);
368
369 const Matrix4f vox2rasFinal = volData.computeVox2Ras();
370 const bool useScale = (sclSlope != 0.0f) && !(sclSlope == 1.0f && sclInter == 0.0f);
371
372 for (int k = 0; k < nslice; ++k) {
373 MriSlice& slice = volData.slices[k];
374 slice.width = nx;
375 slice.height = ny;
376 slice.dimx = sx / 1000.0f;
377 slice.dimy = sy / 1000.0f;
378 slice.scale = 1.0f;
379
380 switch (mriType) {
381 case MRI_UCHAR: {
383 slice.pixels.resize(nPixels);
384 if (datatype == DT_INT8) {
385 for (int p = 0; p < nPixels; ++p) {
386 qint8 v;
387 stream >> v;
388 slice.pixels[p] = static_cast<unsigned char>(v < 0 ? 0 : v);
389 }
390 } else {
391 for (int p = 0; p < nPixels; ++p) {
392 quint8 v;
393 stream >> v;
394 slice.pixels[p] = v;
395 }
396 }
397 break;
398 }
399 case MRI_SHORT: {
401 slice.pixelsWord.resize(nPixels);
402 if (datatype == DT_UINT16) {
403 for (int p = 0; p < nPixels; ++p) {
404 quint16 v;
405 stream >> v;
406 slice.pixelsWord[p] = v;
407 }
408 } else {
409 for (int p = 0; p < nPixels; ++p) {
410 qint16 v;
411 stream >> v;
412 slice.pixelsWord[p] = static_cast<unsigned short>(v < 0 ? 0 : v);
413 }
414 }
415 break;
416 }
417 case MRI_INT: {
419 slice.pixelsFloat.resize(nPixels);
420 for (int p = 0; p < nPixels; ++p) {
421 qint32 v;
422 stream >> v;
423 slice.pixelsFloat[p] = static_cast<float>(v);
424 }
425 break;
426 }
427 case MRI_FLOAT: {
429 slice.pixelsFloat.resize(nPixels);
430 for (int p = 0; p < nPixels; ++p) {
431 float v;
432 stream >> v;
433 slice.pixelsFloat[p] = v;
434 }
435 break;
436 }
437 }
438
439 // Apply scl_slope / scl_inter for floating-point output (the NIfTI spec
440 // states the transform produces the "physical" value; we only honour it
441 // for floats — integer outputs stay as-is to preserve label maps).
442 if (useScale && slice.pixelFormat == FIFFV_MRI_PIXEL_FLOAT) {
443 for (int p = 0; p < slice.pixelsFloat.size(); ++p) {
444 slice.pixelsFloat[p] = slice.pixelsFloat[p] * sclSlope + sclInter;
445 }
446 }
447
448 Vector3f sliceOrigin;
449 sliceOrigin(0) = vox2rasFinal(0, 2) * k + vox2rasFinal(0, 3);
450 sliceOrigin(1) = vox2rasFinal(1, 2) * k + vox2rasFinal(1, 3);
451 sliceOrigin(2) = vox2rasFinal(2, 2) * k + vox2rasFinal(2, 3);
452
453 Matrix3f sliceRot;
454 sliceRot.col(0) = vox2rasFinal.block<3, 1>(0, 0);
455 sliceRot.col(1) = vox2rasFinal.block<3, 1>(0, 1);
456 sliceRot.col(2) = vox2rasFinal.block<3, 1>(0, 2);
457
458 slice.trans = FiffCoordTrans(FIFFV_COORD_MRI_SLICE, FIFFV_COORD_MRI, sliceRot, sliceOrigin);
459 }
460
461 if (verbose) {
462 qInfo("NIfTI file: %dx%dx%d, datatype=%d, voxel %.3fx%.3fx%.3f mm, c_ras=(%.2f, %.2f, %.2f)",
463 nx, ny, nz, datatype, sx, sy, sz,
464 volData.c_ras[0], volData.c_ras[1], volData.c_ras[2]);
465 }
466
467 return true;
468}
constexpr auto FIFFV_COORD_MRI_SLICE
constexpr auto FIFFV_COORD_MRI
FIFF tag-kind, block-kind and type-code numerical definitions, authoritative for FIFFLIB.
constexpr auto FIFFV_MRI_PIXEL_FLOAT
Definition fiff_file.h:696
constexpr auto FIFFV_MRI_PIXEL_BYTE
Definition fiff_file.h:693
constexpr auto FIFFV_MRI_PIXEL_WORD
Definition fiff_file.h:694
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