diff --git a/fsl/data/image.py b/fsl/data/image.py index 0a776b8d1cc49bec7eafe17ba510c715a0bde7a4..44e4ad51c9558678f98c1c355fcfd1edc45db636 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -32,13 +32,14 @@ and file names: """ -import os -import os.path as op -import itertools as it -import json -import string -import logging -import tempfile +import os +import os.path as op +import itertools as it +import collections.abc as abc +import json +import string +import logging +import tempfile from pathlib import Path from typing import Union @@ -995,6 +996,9 @@ class Image(Nifti): ============== =========================================================== + *Data access* + + The ``Image`` class supports access to and assignment of the image data via the ``[]`` slice operator, e.g.:: @@ -1033,6 +1037,22 @@ class Image(Nifti): - have undefined semantics when a custom :class:`DataManager` is in use + + *Image dimensionality* + + + The ``Image`` class abstracts away trailing image dimensions of length 1. + This means that if the header for a NIFTI image specifies that the image + has four dimensions, but the fourth dimension is of length 1, you do not + need to worry about indexing that fourth dimension. However, all NIFTI + images will be presented as having at least three dimensions, so if your + image header specifies a third dimension of length 1, you will still + need provide an index of 0 for that dimensions, for all data accesses. + + + *Notification of changes to an Image* + + The ``Image`` class adds some :class:`.Notifier` topics to those which are already provided by the :class:`Nifti` class - listeners may register to be notified of changes to the above properties, by registering on the @@ -1485,16 +1505,50 @@ class Image(Nifti): def __getitem__(self, slc): - """Access the image data with the specified ``sliceobj``. + """Access the image data with the specified ``slc``. :arg slc: Something which can slice the image data. """ log.debug('%s: __getitem__ [%s]', self.name, slc) - if self.__dataMgr is not None: return self.__dataMgr[slc] - elif self.__data is not None: return self.__data[slc] - else: return self.__nibImage.dataobj[slc] + # Make the slice object compatible + # with the actual image shape - e.g. + # an underlying 2D image is presented + # as having 3 dimensions. + shape = self.shape + realShape = self.realShape + slc = canonicalSliceObj(slc, shape) + fancy = isValidFancySliceObj(sliceobj, shape) + expNdims, expShape = expectedShape( slc, shape) + slc = canonicalSliceObj(slc, realShape) + + if self.__dataMgr is not None: data = self.__dataMgr[slc] + elif self.__data is not None: data = self.__data[slc] + else: data = self.__nibImage.dataobj[slc] + + # Make sure that the result has the + # shape that the caller is expecting. + if fancy: data = data.reshape((data.size, )) + else: data = data.reshape(expShape) + + # If expNdims == 0, we should + # return a scalar. If expNdims + # == 0, but data.size != 1, + # something is wrong somewhere + # (and is not being handled + # here). + if expNdims == 0 and data.size == 1: + + # Funny behaviour with numpy scalar arrays. + # data[()] returns a numpy scalar (which is + # what we want). But data.item() returns a + # python scalar. And if the data is a + # ndarray with 0 dims, data[0] will raise + # an error! + data = data[()] + + return data def __setitem__(self, slc, values): @@ -1513,6 +1567,36 @@ class Image(Nifti): log.debug('%s: __setitem__ [%s = %s]', self.name, slc, values.shape) + realShape = self.realShape + slc = canonicalSliceObj(slc, realShape) + + # If the image shape does not match its + # 'display' shape (either less three + # dims, or has trailing dims of length + # 1), we might need to re-shape the + # values to prevent numpy from raising + # an error in the assignment below. + if realShape != self.shape: + + expNdims, expShape = expectedShape(slc, realShape) + + # If we are slicing a scalar, the + # assigned value has to be scalar. + if expNdims == 0 and isinstance(values, abc.Sequence): + + if len(values) > 1: + raise IndexError('Invalid assignment: [{}] = {}'.format( + slc, len(values))) + + values = np.array(values).flatten()[0] + + # Make sure that the values + # have a compatible shape. + else: + values = np.array(values) + if values.shape != expShape: + values = values.reshape(expShape) + # Use DataManager to manage data # access if one has been specified if self.__dataMgr is not None: diff --git a/fsl/data/imagewrapper.py b/fsl/data/imagewrapper.py index 4e6710db90e898c34374d19c4312cf8895b3fb7f..bd20d6e5629c4b22e8af88d214491a1518dcbbf6 100644 --- a/fsl/data/imagewrapper.py +++ b/fsl/data/imagewrapper.py @@ -181,8 +181,6 @@ class ImageWrapper(notifier.Notifier): data range is updated directly on reads/writes. """ - import fsl.data.image as fslimage - self.__image = image self.__name = name self.__taskThread = None