-
Notifications
You must be signed in to change notification settings - Fork 262
image_model
A nibabel image is the association of 4.5 things:
- 'data' - something that can make an ndarray with the data in it
- affine - a 4x4 affine giving the relationship between voxel coordinates and coordinates in RAS+ space
- header - the metadata associated with this image, in the image's own format.
For example, a nifti image has a header of type
Nifti1Header
. - filenames / fileobjects to read or write the file from / to
- extra - a possibly unused dictionary for other stuff you might want to carry with the image
>>> import numpy as np
>>> import nibabel as nib
>>> arr = np.arange(24).reshape((2,3,4))
>>> aff = np.diag([2,3,4,1])
>>> img = nib.Nifti1Pair(arr, aff)
>>> type(img._affine)
<type 'numpy.ndarray'>
>>> type(img._header)
<class 'nibabel.nifti1.Nifti1PairHeader'>
>>> img.extra
{}
In this case, the data for the image is the original array
>>> type(img._data)
<type 'numpy.ndarray'>
>>> img._data is arr
True
The filenames (all null in this case) are in the file_map attribute, which is a dict, one entry per file needed to load / save the image.
>>> sorted(img.file_map.keys())
['header', 'image']
The image above looks at an array in memory. Let's call that an array image. There can also be proxy images.
We can save the image like this:
>>> nib.save(img, 'example.img')
and load it again:
>>> loaded_img = nib.load('example.img')
Now note that the data is not an array but an ArrayProxy instance:
>>> type(loaded_img._data)
<class 'nibabel.analyze.ImageArrayProxy'>
A proxy is an object that has a shape, and can be made into an array:
>>> loaded_img._data.shape
(2, 3, 4)
>>> type(np.array(loaded_img._data))
<type 'numpy.ndarray'>
Notice that the attributes are all private - img._data, img._affine,
img._header
. Access is via:
>>> data = img.get_data()
>>> data is img._data
True
>>> hdr = img.get_header()
>>> hdr is img._header
True
>>> aff = img.get_affine()
>>> aff is img._affine
True
Why this java-like interface?
For the data - because _data
could be a proxy, and I wanted to keep that to
myself for now.
For the header - because I wanted to forbid setting of the header attribute, on the basis that the header type must be correct for it to be possible to save the image, or predict what fields etc would be available in a given image.
For the affine - because it should always be None or a 4x4 affine. Also, setting affine could conflict with affine information in the header.
Last, I had wanted a path to images that, if they are not immutable, at least we may be able to know the image has not been modified after load. Protecting access with these accessor methods makes this easier. See below.
Say 10 minutes or so?
At the moment, I've hidden _data
. So, there's no public way of telling
whether the image is a proxy image or an array image. Options are:
-
No change
-
Expose the
_data
attribute asimg.dataobj
directly. This makes the machinery more explicit but perhaps confusing. In this case, your proxy test would be:loaded_img.dataobj.is_proxy
or something like that.
-
Put
is_proxy
onto the image as a property or method:img.is_proxy
Immutable images would be nice. One place we often need something like that is when we are passing images to external programs as files. For example, we might do this:
loaded_img = nib.load('example.img') data = loaded_img.get_data() if np.any(data) < 0: run_something_on(img) filename = img.get_filename() new_img = some_spm_processing(filename)
The problem is that, at the moment, when we get to the new_img =
line, we
can't be sure that loaded_img
still corresponds to the data in the original
file example.img
, without going through all the code in
run_something_on
. In practice that means that, each time we load an image,
and we then need to pass it on as a filename, we'll have to save it as a new
file.
There's no way with the current design to make the images immutable in general, because an array image contains a reference to an array:
>>> arr = np.arange(24).reshape((2,3,4))
>>> an_img = nib.Nifti1Image(arr, None)
>>> an_img.get_data()[0,0,0]
0
>>> arr[0,0,0] = 99
>>> an_img.get_data()[0,0,0]
99
But we might be able to get somewhere by setting a maybe_modified flag when we
know there's a risk that the image has been modified since load. For example,
an array image would always have img.maybe_modified == True
because of the
issue above.
Options:
- Stay the same. Force save when passing out filenames. At least it's safe.
-
- Conservative. Set
maybe_modified
for any image that has had a call to any of -
img.get_data(), img.get_header(), img.get_affine()
- and for any array image. Disadvantage - you can't do anything much to an image - even look at it - without setting the maybe modified flag
- Conservative. Set
-
- Ugly. Make default calls to ``img.get_header(), img.get_affine(),
- img.get_data()`` return copies, with argument like
img.get_affine(copy=False)
as an altenative. Forcopy=False
, set the modified flag, otherwise, unless this is an array image, we can be sure that the image object has not been modified relative to its loaded state.