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