nipy / nibabel

Python package to access a cacophony of neuro-imaging file formats

Home Page:http://nipy.org/nibabel/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Loss of precision in NIFTI header affine

mailys-hau opened this issue · comments

Hi everyone,

I am trying to store an affine matrix in a Nifti1Header using set_sform but encounter a problem when accessing the stored matrix later:

>>> affine
array([[ 7.e-01,  0.e+00,  0.e+00,  0.e+00],
       [ 0.e+00,  0.e+00, -7.e-01,  0.e+00],
       [ 0.e+00,  7.e-01,  0.e+00,  0.e+00],
       [ 0.e+00,  0.e+00,  0.e+00,  1.e+03]])
>>> hdr = nib.Nifti1Header()
>>> hdr.set_sform(affine)
>>> hdr.get_sform()
array([[ 0.69999999,  0.        ,  0.        ,  0.        ],
       [ 0.        ,  0.        , -0.69999999,  0.        ],
       [ 0.        ,  0.69999999,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  1.        ]])
>>> hdr.get_sform().dtype
dtype('float64')
>>> hdr["srow_x"]
array([0.7, 0. , 0. , 0. ], dtype=float32)

I think the problem occurs here, as it seems np.eye default returned dtype is float64, however, affine rows are stored as float32 in the header. When the values are copied in out the cast from float32 to float64 can lead to some error.

I believe the same error happens in set_qform as the quaternion is also stored in float32.

Converting from float32 to float64 is lossless, but not the other way around:

>>> np.float64(np.float32(0.7)) == np.float32(0.7)
True
>>> np.float32(np.float64(0.7)) == np.float64(0.7)
False
>>> np.float32(0.7)
0.7
>>> np.float64(np.float32(0.7))
0.699999988079071

But neither of these is exactly equal to 0.7, it's just a trick of formatting. When you determine the number of decimal digits you will print to, the number is rounded to that many digits, and trailing zeros are truncated. It turns out at 17 decimal places, we can see the difference between the float64 approximation of 0.7 and 0.7:

>>> f'{np.float32(0.7):.17f}'
'0.69999998807907104'
>>> f'{np.float64(0.7):.17f}'
'0.69999999999999996'

So the precision loss happens in set_sform(), but only becomes visible by numpy's printing rules when converting back to float64.

If you need 64-bit affines, you can use Nifti2Image.

Hi,

Thanks for the fast answer, it was enlightening.

I have one more question then: is the fact the get_sform returns a float64 machine dependent? Even when passing a float32 affine the imprecision shows up when calling get_sform.

get_sform() (and all other affine generating/retrieval functions/methods) will always return float64.

You can call get_sform().astype('float32') to , if it's important, but if you are going to do any arithmetic with it, I would not cast it just to make printing nicer. You could do something simple like:

def format_affine(aff):
    """Cast affine to float32 and print for consistent view"""
    return str(aff.astype('f4'))

print(format_affine(img.get_sform()))