Exporting file to .nii and .nii.gz changes the transform in the header

I have an .mrb with multiple files, from which I export one to .nii.gz

When I load the file again, it doesn’t match the original file because it’s affine is changed (I loaded both files in Python with SimpleITK and saw that the voxel values were identical, only the origin and direction have changed)

This does not happen when I export this file to .nrrd.

I tried this with multiple versions of Slicer and it always kept happening.

Unfortunately I’m not allowed to share the file.

There are also no errors or warnings when exporting this file. Is there a way to debug this?

We’ve noticed this with other data too. The nifti1 headers that are used by default use 32bit floats for orientation and there can be rounding error. Avoid nifti if you can.

Here’s a related discussion:

1 Like

ok. I have three follow-up questions

  1. Is there a way of determining when the mismatch might occur?
  2. Would it make sense to export to nifti2?
  3. Could you elaborate on ‘Avoid nifti if you can?’

PR 6454 should fix this. Give it a try and report whether the problem still occurs.

PR 6454 is now part of preview release.

The problem persists.

I also wrote a simple Python script that uses the newest SimpleITK version (2.2.0) to export the .nrrds in question to .nii and the same change happens.

From what I understood from discussions with @pieper, the problem is rather with the NIFTI standard that doesn’t support shear in the affine matrix - and the files that I have problems have a non-zero shear component.

@koeglfryderyk would you be able to make an example file using the header information and synthetic pixel data? It would simplify the discussion if you provided a shareable example. Also provide a simple example script that demonstrates the exact issue (you can be inspired by this advice.

As I understand it the nifti reference is open to interpretation here. There are both a quaternion (no shear) and an affine matrix (could have shear) but ITK’s logic has many special cases to work with such data (e.g. see this discussion. This logic may or may not match the logic used by other programs, which is why I suggest avoiding the format. If you read the nifti reference above you can see how it is very neuroimaging specific and has many special cases that have proven problematic in practice.

1 Like

Here are a few lines that will create an .nrrd with noise as data and with the header copied from one of the problematic files (you need to have numpy and pynrrd installed to run this) - the noise is big enough to visually see how the file changes after it was exported to .nii

from collections import OrderedDict
import numpy as np
import nrrd

# path to where the .nrrd file will be saved
path = "dummy.nrrd"

# the header is copied from one of the problematic .nrrd files
header = OrderedDict([('type', 'unsigned short'),
             ('dimension', 3),
             ('space', 'left-posterior-superior'),
             ('sizes', np.array([256, 256,  80])),
             ('space directions',
              np.array([[-0.82093026, -0.03815682, -0.03294253],
                     [-0.03818959,  0.81657389,  0.06732129],
                     [ 0.09523261,  0.14437391, -1.9763117 ]])),
             ('kinds', ['domain', 'domain', 'domain']),
             ('endian', 'little'),
             ('encoding', 'gzip'),
             ('space origin',
              np.array([ 110.19170784, -173.7526922 ,  101.14669097]))])

# the data array has the same size as the the file from which the header was copied, but it's just noise
data = np.random.rand(256, 256, 80)

nrrd.write(path, data, header)

Please spread the word: do not use Nifti in non-neuroimaging applications. The much simper NRRD file format can be used instead.

By using Nifti file format, people maintain a continuous drain of our resources via requiring lots of user support (keep explaining issues with Nifti and what to do about it) and trying to implement and maintain complex mechanisms (to deal with redundancies and ambiguities in the Nifti format), and in general by distracting our attention from more impactful work.

Thanks for providing the concrete example :+1: This does help further the investigation.

@koeglfryderyk this matrix does have a non-trivial shear component. Is that intended?

array([[-0.82093026, -0.03815682, -0.03294253],
       [-0.03818959,  0.81657389,  0.06732129],
       [ 0.09523261,  0.14437391, -1.9763117 ]])
>>> numpy.dot(a[0], a[1])
-0.0020246065049000987
>>> numpy.dot(a[0], a[2])
-0.018583473117743804
>>> numpy.dot(a[1], a[2])
-0.01879278211341301

As noted in some of the links above ITK may not be consistent in handling this case (e.g. shear may be silently dropped, and that may be what’s happening in the nifti case, so the issue might not even be related to the 32 bit precision issue).

Shear should be handled well for viewing in Slicer but some of the ITK or VTK based filters may ignore the shear.

Seeing this data makes me think we should remove the ambiguity at the Slicer level. An option could be to give users the option to factor out shear into a separate transform when loading so that the vtkMRMLVolume*Node classes can always expect orthogonal direction vectors. Users could resample through the transform at the desired resolution if needed to incorporate the shear.

As a general rule, I suggest that people doing any kind of non-rigid registration save the transform somewhere other than the image header whenever possible.

1 Like

I’m not sure I would use the word ‘intended’ - that’s the data that we received from Brainlab and we only sometimes applied rigid transformations to it.

But I’ll keep saving non-rigid transformations elsewhere in mind