diff --git a/fsl/data/image.py b/fsl/data/image.py index 68ca3a39e241589aa481e960e119be89e458d1c4..0a776b8d1cc49bec7eafe17ba510c715a0bde7a4 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -136,7 +136,9 @@ class Nifti(notifier.Notifier, meta.Meta): object. ``shape`` A list/tuple containing the number of voxels along - each image dimension. + each image dimension - see notes below. + + ``realShape`` A list/tuple containing the actual image data shape. ``pixdim`` A list/tuple containing the length of one voxel along each image dimension. @@ -163,8 +165,11 @@ class Nifti(notifier.Notifier, meta.Meta): The ``shape`` attribute may not precisely match the image shape as reported in the NIFTI header, because trailing dimensions of size 1 are - squeezed out. See the :meth:`__determineShape` and :meth:`mapIndices` - methods. + squeezed out. See the :meth:`__determineShape` method and the + :func:`canonicalSliceObj` function. The actual image data shape can be + queried via the :meth:`realShape` property. Note also that the + :class:`Image` class expects data access to be with respect to the adjusted + shape, not the real shape. **Affine transformations** @@ -569,6 +574,12 @@ class Nifti(notifier.Notifier, meta.Meta): return tuple(self.__shape) + @property + def realShape(self): + """Returns a tuple containing the image data shape. """ + return tuple(self.__origShape) + + @property def ndim(self): """Returns the number of dimensions in this image. This number may not @@ -738,18 +749,9 @@ class Nifti(notifier.Notifier, meta.Meta): return self.getAffine('fsl', 'voxel') + @deprecated.deprecated('3.9.0', '4.0.0', 'Use canonicalSliceObj instead') def mapIndices(self, sliceobj): - """Adjusts the given slice object so that it may be used to index the - underlying ``nibabel`` NIFTI image object. - - See the :meth:`__determineShape` method. - - :arg sliceobj: Something that can be used to slice a - multi-dimensional array, e.g. ``arr[sliceobj]``. - """ - - # How convenient - nibabel has a function - # that does the dirty work for us. + """Deprecated - use :func:`canonicalSliceObj` instead. """ return fileslice.canonical_slicers(sliceobj, self.__origShape) @@ -1554,6 +1556,102 @@ def canonicalShape(shape): return shape +def isValidFancySliceObj(sliceobj, shape): + """Returns ``True`` if the given ``sliceobj`` is a valid and fancy slice + object. + + ``nibabel`` refers to slice objects as "fancy" if they comprise anything + but tuples of integers and simple ``slice`` objects. The ``Image`` class + supports an additional type of "fancy" slicing, where the ``sliceobj`` is + a boolean ``numpy`` array of the same shape as the image. + + This function returns ``True`` if the given ``sliceobj`` adheres to these + requirements, ``False`` otherwise. + """ + + # We only support boolean numpy arrays + # which have the same shape as the image + return (isinstance(sliceobj, np.ndarray) and + sliceobj.dtype == bool and + np.prod(sliceobj.shape) == np.prod(shape)) + + +def canonicalSliceObj(sliceobj, shape): + """Returns a canonical version of the given ``sliceobj``. See the + ``nibabel.fileslice.canonical_slicers`` function. + """ + + # Fancy slice objects must have + # the same shape as the data + if isValidFancySliceObj(sliceobj, shape): + return sliceobj.reshape(shape) + + else: + + if not isinstance(sliceobj, tuple): + sliceobj = (sliceobj,) + + if len(sliceobj) > len(shape): + sliceobj = sliceobj[:len(shape)] + + return nib.fileslice.canonical_slicers(sliceobj, shape) + + +def expectedShape(sliceobj, shape): + """Given a slice object, and the shape of an array to which + that slice object is going to be applied, returns the expected + shape of the result. + + .. note:: It is assumed that the ``sliceobj`` has been passed through + the :func:`canonicalSliceObj` function. + + :arg sliceobj: Something which can be used to slice an array + of shape ``shape``. + + :arg shape: Shape of the array being sliced. + + :returns: A tuple containing: + + - Expected number of dimensions of the result + + - Expected shape of the result (or ``None`` if + ``sliceobj`` is fancy). + """ + + if isValidFancySliceObj(sliceobj, shape): + return 1, None + + # Truncate some dimensions from the + # slice object if it has too many + # (e.g. trailing dims of length 1). + elif len(sliceobj) > len(shape): + sliceobj = sliceobj[:len(shape)] + + # Figure out the number of dimensions + # that the result should have, given + # this slice object. + expShape = [] + + for i in range(len(sliceobj)): + + # Each dimension which has an + # int slice will be collapsed + if isinstance(sliceobj[i], int): + continue + + start = sliceobj[i].start + stop = sliceobj[i].stop + + if start is None: start = 0 + if stop is None: stop = shape[i] + + stop = min(stop, shape[i]) + + expShape.append(stop - start) + + return len(expShape), expShape + + def loadMetadata(image): """Searches for and loads any sidecar JSON files associated with the given :class:`.Image`. diff --git a/fsl/data/imagewrapper.py b/fsl/data/imagewrapper.py index 196110fcf52de6044452d628b968460d14f85fac..4e6710db90e898c34374d19c4312cf8895b3fb7f 100644 --- a/fsl/data/imagewrapper.py +++ b/fsl/data/imagewrapper.py @@ -45,10 +45,9 @@ import collections import collections.abc as abc import itertools as it -import numpy as np -import nibabel as nib - +import numpy as np +import fsl.data.image as fslimage import fsl.utils.deprecated as deprecated import fsl.utils.notifier as notifier import fsl.utils.naninfrange as nir @@ -732,112 +731,29 @@ class ImageWrapper(notifier.Notifier): self.__updateDataRangeOnWrite(slices, values) -@deprecated.deprecated('3.9.0', '4.0.0', - 'The imagewrapper module been migrated to FSLeyes') +@deprecated.deprecated('3.9.0', '4.0.0', 'Moved to fsl.data.image') def isValidFancySliceObj(sliceobj, shape): - """Returns ``True`` if the given ``sliceobj`` is a valid and fancy slice - object. - - ``nibabel`` refers to slice objects as "fancy" if they comprise anything - but tuples of integers and simple ``slice`` objects. The ``ImageWrapper`` - class supports one type of "fancy" slicing, where the ``sliceobj`` is a - boolean ``numpy`` array of the same shape as the image. - - This function returns ``True`` if the given ``sliceobj`` adheres to these - requirements, ``False`` otherwise. - """ - - # We only support boolean numpy arrays - # which have the same shape as the image - return (isinstance(sliceobj, np.ndarray) and - sliceobj.dtype == bool and - np.prod(sliceobj.shape) == np.prod(shape)) + """Deprecated - moved to :mod:`fsl.data.image`.""" + return fslimage.isValidFancySliceObj(sliceobj, shape) -@deprecated.deprecated('3.9.0', '4.0.0', - 'The imagewrapper module been migrated to FSLeyes') +@deprecated.deprecated('3.9.0', '4.0.0', 'Moved to fsl.data.image') def canonicalSliceObj(sliceobj, shape): - """Returns a canonical version of the given ``sliceobj``. See the - ``nibabel.fileslice.canonical_slicers`` function. - """ - - # Fancy slice objects must have - # the same shape as the data - if isValidFancySliceObj(sliceobj, shape): - return sliceobj.reshape(shape) - - else: - - if not isinstance(sliceobj, tuple): - sliceobj = (sliceobj,) + """Deprecated - moved to :mod:`fsl.data.image`.""" + return fslimage.canonicalSliceObj(sliceobj, shape) - if len(sliceobj) > len(shape): - sliceobj = sliceobj[:len(shape)] - return nib.fileslice.canonical_slicers(sliceobj, shape) - - -@deprecated.deprecated('3.9.0', '4.0.0', - 'The imagewrapper module been migrated to FSLeyes') +@deprecated.deprecated('3.9.0', '4.0.0', 'Moved to fsl.data.image') def expectedShape(sliceobj, shape): - """Given a slice object, and the shape of an array to which - that slice object is going to be applied, returns the expected - shape of the result. - - .. note:: It is assumed that the ``sliceobj`` has been passed through - the :func:`canonicalSliceObj` function. - - :arg sliceobj: Something which can be used to slice an array - of shape ``shape``. - - :arg shape: Shape of the array being sliced. - - :returns: A tuple containing: + """Deprecated - moved to :mod:`fsl.data.image`.""" + return fslimage.expectedShape(sliceobj, shape) - - Expected number of dimensions of the result - - Expected shape of the result (or ``None`` if - ``sliceobj`` is fancy). - """ - - if isValidFancySliceObj(sliceobj, shape): - return 1, None - - # Truncate some dimensions from the - # slice object if it has too many - # (e.g. trailing dims of length 1). - elif len(sliceobj) > len(shape): - sliceobj = sliceobj[:len(shape)] - - # Figure out the number of dimensions - # that the result should have, given - # this slice object. - expShape = [] - - for i in range(len(sliceobj)): - - # Each dimension which has an - # int slice will be collapsed - if isinstance(sliceobj[i], int): - continue - - start = sliceobj[i].start - stop = sliceobj[i].stop - - if start is None: start = 0 - if stop is None: stop = shape[i] - - stop = min(stop, shape[i]) - - expShape.append(stop - start) - - return len(expShape), expShape - - -@deprecated.deprecated('3.9.0', '4.0.0', - 'The imagewrapper module been migrated to FSLeyes') +@deprecated.deprecated('3.9.0', '4.0.0', 'Moved to FSLeyes') def sliceObjToSliceTuple(sliceobj, shape): - """Turns an array slice object into a tuple of (low, high) index + """Deprecated - the imagewrapper has been moved to FSLeyes. + + Turns an array slice object into a tuple of (low, high) index pairs, one pair for each dimension in the given shape :arg sliceobj: Something which can be used to slice an array of shape @@ -876,10 +792,11 @@ def sliceObjToSliceTuple(sliceobj, shape): return tuple(indices) -@deprecated.deprecated('3.9.0', '4.0.0', - 'The imagewrapper module been migrated to FSLeyes') +@deprecated.deprecated('3.9.0', '4.0.0', 'Moved to FSLeyes') def sliceTupleToSliceObj(slices): - """Turns a sequence of (low, high) index pairs into a tuple of array + """Deprecated - the imagewrapper has been moved to FSLeyes. + + Turns a sequence of (low, high) index pairs into a tuple of array ``slice`` objects. :arg slices: A sequence of (low, high) index pairs. @@ -893,10 +810,11 @@ def sliceTupleToSliceObj(slices): return tuple(sliceobj) -@deprecated.deprecated('3.9.0', '4.0.0', - 'The imagewrapper module been migrated to FSLeyes') +@deprecated.deprecated('3.9.0', '4.0.0', 'Moved to FSLeyes') def adjustCoverage(oldCoverage, slices): - """Adjusts/expands the given ``oldCoverage`` so that it covers the + """Deprecated - the imagewrapper has been moved to FSLeyes. + + Adjusts/expands the given ``oldCoverage`` so that it covers the given set of ``slices``. :arg oldCoverage: A ``numpy`` array of shape ``(2, n)`` containing @@ -943,10 +861,11 @@ return code for the :func:`sliceOverlap` function. """ -@deprecated.deprecated('3.9.0', '4.0.0', - 'The imagewrapper module been migrated to FSLeyes') +@deprecated.deprecated('3.9.0', '4.0.0', 'Moved to FSLeyes') def sliceOverlap(slices, coverage): - """Determines whether the given ``slices`` overlap with the given + """Deprecated - the imagewrapper has been moved to FSLeyes. + + Determines whether the given ``slices`` overlap with the given ``coverage``. :arg slices: A sequence of (low, high) index pairs, assumed to cover @@ -1012,10 +931,11 @@ def sliceOverlap(slices, coverage): elif np.all(overlapStates == OVERLAP_ALL): return OVERLAP_ALL -@deprecated.deprecated('3.9.0', '4.0.0', - 'The imagewrapper module been migrated to FSLeyes') +@deprecated.deprecated('3.9.0', '4.0.0', 'Moved to FSLeyes') def sliceCovered(slices, coverage): - """Returns ``True`` if the portion of the image data calculated by + """Deprecated - the imagewrapper has been moved to FSLeyes. + + Returns ``True`` if the portion of the image data calculated by the given ``slices` has already been calculated, ``False`` otherwise. :arg slices: A sequence of (low, high) index pairs, assumed to cover @@ -1046,10 +966,11 @@ def sliceCovered(slices, coverage): return True -@deprecated.deprecated('3.9.0', '4.0.0', - 'The imagewrapper module been migrated to FSLeyes') +@deprecated.deprecated('3.9.0', '4.0.0', 'Moved to FSLeyes') def calcExpansion(slices, coverage): - """Calculates a series of *expansion* slices, which can be used to expand + """Deprecated - the imagewrapper has been moved to FSLeyes. + + Calculates a series of *expansion* slices, which can be used to expand the given ``coverage`` so that it includes the given ``slices``. :arg slices: Slices that the coverage needs to be expanded to cover. @@ -1218,10 +1139,11 @@ def calcExpansion(slices, coverage): return volumes, expansions -@deprecated.deprecated('3.9.0', '4.0.0', - 'The imagewrapper module been migrated to FSLeyes') +@deprecated.deprecated('3.9.0', '4.0.0', 'Moved to FSLeyes') def collapseExpansions(expansions, numDims): - """Scans through the given list of expansions (each assumed to pertain + """Deprecated - the imagewrapper has been moved to FSLeyes. + + Scans through the given list of expansions (each assumed to pertain to a single 3D image), and combines any which cover the same image area, and cover adjacent volumes.