nipy/nibabel — pull request #220 by matthew-brett (2013-12-12 00:36:53+00:00)
Title: WIP: extend / minor break of API allows explicit scaling

We've long wanted to do something like this:

img = Nifti1Image(arr, affine)
img.set_slope_inter(2, 8)
nib.save(img, 'test.nii')

so that:

img_back = nib.load('test.nii')
assert np.all(img_back.get_data() == arr * 2. + 8)

In other words, set the slope and intercept in a Nifti explicitly so that they
are saved with the image, and the loaded data applies this slope and intercept.

Until now, that wasn't possible because:

nib.save(img, 'test.nii')

would always rewrite the slope, intercept in the header to best save the array
data in whatever data type the header specified (``img.get_data_dtype()``). So,
whatever slope, intercept you set would get reset by the save and therefore
lost.

This PR changes the image API slightly, so that when you create an image from an
input header, it always resets the header slope, intercept to be NaN.

hdr = Nifti1Header()
hdr.set_slope_inter(2, 8)
img = Nifti1Image(arr, affine, hdr)
assert np.isnan(img.header['scl_slope'])
assert np.isnan(img.header['scl_inter'])

This includes loading the image from disk or file objects:

img_back = nib.load('test.nii')
assert np.isnan(img_back.header['scl_slope'])
assert np.isnan(img_back.header['scl_inter'])

The NaN values signal to the image save routines that they should recalculate
the slope and intercept - corresponding to the previous behavior - and the
default behavior unless you intervene. Specifically:

nib.save(img, 'test.nii')

will automatically set the slope and intercept to best store the image data with
the given datatype (``img.get_data_dtype()``)

If the slope and intercept are not both NaN then we assume you have set the
slope and intercept explicitly, and we do not recalculate the slope, intercept,
instead we write them directly to the header. We then do not alter the data
array other than to safely cast it to the correct dtype
(``img.get_data_dtype()``).

This means that you can now make the data you get from ``img.get_data()`` be
different from the data you get from saving the image and loading it again:

img = Nifti1Image(arr, affine, hdr)
assert np.all(img.get_data() == arr)
img.header.set_slope_inter(2, 8)
# No change the the data returned for this image
assert np.all(img.get_data() == arr)
# But slope, inter applied after save and load
nib.save('test2.nii')
img_scaled = nib.load('test2.nii')
assert np.all(img.get_data() == arr * 2. + 8.)

I did my best to keep the API as similar as possible, and I don't think you will
see changes to common nibabel behavior as a result of this PR, but I'd very much
like to hear if anyone does see something surprising.

One side-effect of the changes is that we no longer check and fix odd scaling
when we load a header from file, because we are always going to reset scaling
to NaN when creating images, and the log checks also made it harder to reason
about what was happening with the scaling.