nipy / nibabel

Python package to access a cacophony of neuro-imaging file formats
http://nipy.org/nibabel/
Other
649 stars 258 forks source link

Nibabel converts nan to zero when saving a floating point image #1248

Closed robbwh closed 1 year ago

robbwh commented 1 year ago

Here is a reproducible example using nibable.testing. Is this behavior expected? Personally, I feel like at least a warning should be generated.

import os
from nibabel.testing import data_path
import numpy as np
import nibabel as nib

example_ni1 = os.path.join(data_path, 'example4d.nii.gz')

example_ni1_img = nib.load(example_ni1)

example_ni1_img_data = example_ni1_img.get_fdata()

print(example_ni1_img_data)
new_data = np.random.rand(example_ni1_img_data.shape[0],example_ni1_img_data.shape[1],example_ni1_img_data.shape[2],example_ni1_img_data.shape[3])
new_data[0,0,0,0] = np.nan

img_out = nib.Nifti1Image(new_data, affine=example_ni1_img.affine,header=example_ni1_img.header)
img_out_data = img_out.get_fdata()
print(img_out_data[0,0,0,0])
#prints out nan

img_out.to_filename("tst.nii")
img_in = nib.load("tst.nii")
img_in_data = img_in.get_fdata()
print(img_in_data[0,0,0,0])
#prints out zero

nilearn correctly handles this situation

#continuing from above working with nilearn
from nilearn.image import load_img,math_img

img_in_nilearn = load_img("tst.nii")
img_in_nilearn_data = img_in_nilearn.get_fdata()
#prints out zero, indicating zero was written to image
print(img_in_nilearn_data[0,0,0,0])

#recreate the nan using math_img and save

img_in_nilearn_nan = math_img("np.where(img==0,np.nan,img)",img=img_in_nilearn)
img_in_nilearn_nan.to_filename("tst2.nii")

test2img = nib.load("tst2.nii")
test2img_data = test2img.get_fdata()

#correctly prints nan
print(test2img_data[0,0,0,0])
effigies commented 1 year ago

Try running nib-ls on each of these files. The on-disk dtype of example4d.nii.gz is int16. I suspect that tst2.nii will be float32 or float64.

When you create an image with a pre-existing header, the header will tell nibabel how the data should be written on-disk. Because you passed a header with an int16 data type, nibabel will find scale factors that allow you to cover the range of the data with 16 bits. Because there's no way to encode nan as an integer, it gets converted to zero.

I would probably recommend explicitly setting your desired data type with img.set_data_dtype(). If you're using a recent version of nibabel (>=4), you can even set it at creation time with img = nb.Nifti1Image(data, affine, header, dtype='float32').

robbwh commented 1 year ago

Ah okay makes sense. I thought it would be determined by the dtype of the array passed in Nifti1Image. But it makes sense that it is determined by the header. Yes setting dtype on save would be a good idea.

and yes, tst2.nii does get saved as float64.

Thank you