Skip to content
Snippets Groups Projects
Commit 3d6f94be authored by Paul McCarthy's avatar Paul McCarthy :mountain_bicyclist:
Browse files

MNT: Update Image data access/assignment to use normalised data

shape  (minimum 3 dimensions, trailing dimensions of length 1 stripped)
parent bf578610
No related branches found
No related tags found
No related merge requests found
...@@ -32,13 +32,14 @@ and file names: ...@@ -32,13 +32,14 @@ and file names:
""" """
import os import os
import os.path as op import os.path as op
import itertools as it import itertools as it
import json import collections.abc as abc
import string import json
import logging import string
import tempfile import logging
import tempfile
from pathlib import Path from pathlib import Path
from typing import Union from typing import Union
...@@ -995,6 +996,9 @@ class Image(Nifti): ...@@ -995,6 +996,9 @@ class Image(Nifti):
============== =========================================================== ============== ===========================================================
*Data access*
The ``Image`` class supports access to and assignment of the image data The ``Image`` class supports access to and assignment of the image data
via the ``[]`` slice operator, e.g.:: via the ``[]`` slice operator, e.g.::
...@@ -1033,6 +1037,22 @@ class Image(Nifti): ...@@ -1033,6 +1037,22 @@ class Image(Nifti):
- have undefined semantics when a custom :class:`DataManager` is in use - 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 The ``Image`` class adds some :class:`.Notifier` topics to those which are
already provided by the :class:`Nifti` class - listeners may register to already provided by the :class:`Nifti` class - listeners may register to
be notified of changes to the above properties, by registering on the be notified of changes to the above properties, by registering on the
...@@ -1485,16 +1505,50 @@ class Image(Nifti): ...@@ -1485,16 +1505,50 @@ class Image(Nifti):
def __getitem__(self, slc): 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. :arg slc: Something which can slice the image data.
""" """
log.debug('%s: __getitem__ [%s]', self.name, slc) log.debug('%s: __getitem__ [%s]', self.name, slc)
if self.__dataMgr is not None: return self.__dataMgr[slc] # Make the slice object compatible
elif self.__data is not None: return self.__data[slc] # with the actual image shape - e.g.
else: return self.__nibImage.dataobj[slc] # 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): def __setitem__(self, slc, values):
...@@ -1513,6 +1567,36 @@ class Image(Nifti): ...@@ -1513,6 +1567,36 @@ class Image(Nifti):
log.debug('%s: __setitem__ [%s = %s]', self.name, slc, values.shape) 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 # Use DataManager to manage data
# access if one has been specified # access if one has been specified
if self.__dataMgr is not None: if self.__dataMgr is not None:
......
...@@ -181,8 +181,6 @@ class ImageWrapper(notifier.Notifier): ...@@ -181,8 +181,6 @@ class ImageWrapper(notifier.Notifier):
data range is updated directly on reads/writes. data range is updated directly on reads/writes.
""" """
import fsl.data.image as fslimage
self.__image = image self.__image = image
self.__name = name self.__name = name
self.__taskThread = None self.__taskThread = None
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment