diff --git a/fsl/data/image.py b/fsl/data/image.py index 3dda62827dec53ca10ec928cf5cbaea68875a5f0..7f235b8cb291acb14bb6b5cdf0d8e58b3f2e6001 100644 --- a/fsl/data/image.py +++ b/fsl/data/image.py @@ -37,6 +37,9 @@ import os import os.path as op import subprocess as sp +import numpy as np +import nibabel as nib + import props import fsl.utils.transform as transform @@ -47,58 +50,38 @@ import fsl.data.constants as constants log = logging.getLogger(__name__) -class Image(props.HasProperties): - """Class which represents a 3D/4D image. Internally, the image is - loaded/stored using :mod:`nibabel`. +class Nifti1(object): + """The ``Nifti1`` class is intended to be used as a base class for + things which either are, or are associated with, a NIFTI1 image. - - In addition to the :attr:`name`, :attr:`data`, and :attr:`saved` - properties, the following attributes are present on an ``Image`` instance: + When a ``Nifti1`` instance is created, it adds the following attributes + to itself: + ================= ==================================================== ``nibImage`` The :mod:`nibabel` image object. - ``dataSource`` The name of the file that the image was loaded from. + ``dataSource`` The name of the file that the image was loaded from. ``tempFile`` The name of the temporary file which was created (in the event that the image was large and was gzipped - see :func:`loadImage`). ``shape`` A list/tuple containing the number of voxels along - each image dimension. - ``pixdim`` A list/tuple containing the size of one voxel along - each image dimension. + each image dimension. ``voxToWorldMat`` A 4*4 array specifying the affine transformation for transforming voxel coordinates into real world coordinates. ``worldToVoxMat`` A 4*4 array specifying the affine transformation for transforming real world coordinates into voxel coordinates. - ================= ==================================================== - """ - - - name = props.String() - """The name of this image.""" - - - data = props.Object() - """The image data. This is a read-only :mod:`numpy` array - all changes - to the image data must be via the :meth:`applyChange` method. - """ - - - saved = props.Boolean(default=False) - """A read-only property (not enforced) which is ``True`` if the image, - as stored in memory, is saved to disk, ``False`` otherwise. + ================= ==================================================== """ - def __init__(self, image, xform=None, - name=None, header=None, loadData=True): - """Create an ``Image`` object with the given image data or file name. + """Create a ``Nifti1`` object. :arg image: A string containing the name of an image file to load, or a :mod:`numpy` array, or a :mod:`nibabel` image @@ -108,8 +91,6 @@ class Image(props.HasProperties): which transforms voxel coordinates into real world coordinates. - :arg name: A name for the image. - :arg header: If not ``None``, assumed to be a :class:`nibabel.nifti1.Nifti1Header` to be used as the image header. Not applied to images loaded from file, @@ -119,9 +100,9 @@ class Image(props.HasProperties): not loaded - this is useful if you're only interested in the header data, as the file will be loaded much more quickly. The image data may subsequently be loaded - via the :meth:`loadData` method. + via the :meth:`loadData` method. """ - + self.nibImage = None self.dataSource = None self.tempFile = None @@ -133,54 +114,35 @@ class Image(props.HasProperties): if header is not None: header = header.copy() - import numpy as np - import nibabel as nib - # The image parameter may be the name of an image file if isinstance(image, basestring): nibImage, filename = loadImage(addExt(image)) self.nibImage = nibImage - self.dataSource = op.abspath(image) + self.dataSource = op.abspath(image) # if the returned file name is not the same as # the provided file name, that means that the # image was opened from a temporary file if filename != image: - filepref = removeExt(op.basename(self.dataSource)) self.tempFile = nibImage.get_filename() - else: - filepref = removeExt(op.basename(self.dataSource)) - - if name is None: - name = filepref - - self.name = name - self.saved = True # Or a numpy array - we wrap it in a nibabel image, # with an identity transformation (each voxel maps # to 1mm^3 in real world space) elif isinstance(image, np.ndarray): - if xform is None: if header is None: xform = np.identity(4) else: xform = header.get_best_affine() - if name is None: name = 'Numpy array' self.nibImage = nib.nifti1.Nifti1Image(image, xform, header=header) - self.name = name # otherwise, we assume that it is a nibabel image else: - if name is None: - name = 'Nibabel image' - self.nibImage = image - self.name = name self.shape = self.nibImage.get_shape() self.pixdim = self.nibImage.get_header().get_zooms() @@ -194,34 +156,8 @@ class Image(props.HasProperties): if len(self.shape) < 3 or len(self.shape) > 4: raise RuntimeError('Only 3D or 4D images are supported') - - log.memory('{}.init ({})'.format(type(self).__name__, id(self))) - - - def __del__(self): - """Prints a log message. """ - log.memory('{}.del ({})'.format(type(self).__name__, id(self))) - - - def __hash__(self): - """Returns a number which uniquely idenfities this ``Image`` instance - (the result of ``id(self)``). - """ - return id(self) - - - def __str__(self): - """Return a string representation of this ``Image`` instance.""" - return '{}({}, {})'.format(self.__class__.__name__, - self.name, - self.dataSource) - - - def __repr__(self): - """See the :meth:`__str__` method.""" - return self.__str__() - - + + def loadData(self): """Loads the image data from the file. This method only needs to be called if the ``loadData`` parameter passed to :meth:`__init__` @@ -243,7 +179,7 @@ class Image(props.HasProperties): log.debug('Loaded image data ({}) - original ' 'shape {}, squeezed shape {}'.format( - self.name, + self.dataSource, shape, data.shape)) @@ -252,58 +188,12 @@ class Image(props.HasProperties): self.pixdim = self.pixdim[:len(data.shape)] - def applyChange(self, offset, newVals, vol=None): - """Changes the image data according to the given new values. - Any listeners registered on the :attr:`data` property will be - notified of the change. - - :arg offset: A tuple of three values, containing the xyz - offset of the image region to be changed. - - :arg newVals: A 3D numpy array containing the new image values. - - :arg vol: If this is a 4D image, the volume index. - """ - - if self.is4DImage() and vol is None: - raise ValueError('Volume must be specified for 4D images') - - data = self.data - xlo, ylo, zlo = offset - xhi = xlo + newVals.shape[0] - yhi = ylo + newVals.shape[1] - zhi = zlo + newVals.shape[2] - - try: - data.flags.writeable = True - if self.is4DImage(): data[xlo:xhi, ylo:yhi, zlo:zhi, vol] = newVals - else: data[xlo:xhi, ylo:yhi, zlo:zhi] = newVals - data.flags.writeable = False - - except: - data.flags.writeable = False - raise - - # Force a notification on the 'data' property - # by assigning its value back to itself - self.data = data - self.saved = False - - - def save(self): - """Convenience method to save any changes made to the :attr:`data` of - this :class:`Image` instance. - - See the :func:`saveImage` function. - """ - return saveImage(self) - - + # TODO: Remove this method, and use the shape attribute directly def is4DImage(self): """Returns ``True`` if this image is 4D, ``False`` otherwise. """ - return len(self.shape) > 3 and self.shape[3] > 1 - + return len(self.shape) > 3 and self.shape[3] > 1 + def getXFormCode(self, code=None): """This method returns the code contained in the NIFTI1 header, indicating the space to which the (transformed) image is oriented. @@ -372,7 +262,143 @@ class Image(props.HasProperties): (constants.ORIENT_A2P, constants.ORIENT_P2A), (constants.ORIENT_S2I, constants.ORIENT_I2S)))[axis] - return code + return code + + +class Image(Nifti1, props.HasProperties): + """Class which represents a 3D/4D NIFTI1 image. Internally, the image + is loaded/stored using :mod:`nibabel`. + + + In addition to the :attr:`data`, and :attr:`saved` properties, and + the attributes added by the :meth:`Nifti1.__init__` method, the + following attributes are present on an ``Image`` instance: + + + ================= ==================================================== + ``name`` the name of this ``Image`` - defaults to the image + file name, sans-suffix. + ================= ==================================================== + """ + + + data = props.Object() + """The image data. This is a read-only :mod:`numpy` array - all changes + to the image data must be via the :meth:`applyChange` method. + """ + + + saved = props.Boolean(default=False) + """A read-only property (not enforced) which is ``True`` if the image, + as stored in memory, is saved to disk, ``False`` otherwise. + """ + + + def __init__(self, image, name=None, **kwargs): + """Create an ``Image`` object with the given image data or file name. + + :arg image: A string containing the name of an image file to load, + or a :mod:`numpy` array, or a :mod:`nibabel` image + object. + + :arg name: A name for the image. + + All other arguments are passed through to :meth:`Nifti1.__init__`. + """ + + Nifti1.__init__(self, image, **kwargs) + + # Figure out the name of this image. + # It might have been explicitly passed in + if name is not None: + self.name = name + + # Or, if this image was loaded + # from disk, use the file name + elif isinstance(image, basestring): + self.name = removeExt(op.basename(self.dataSource)) + self.saved = True + + # Or the image was created from a numpy array + elif isinstance(image, np.ndarray): + self.name = 'Numpy array' + + # Or image from a nibabel image + else: + self.name = 'Nibabel image' + + log.memory('{}.init ({})'.format(type(self).__name__, id(self))) + + + def __del__(self): + """Prints a log message. """ + log.memory('{}.del ({})'.format(type(self).__name__, id(self))) + + + def __hash__(self): + """Returns a number which uniquely idenfities this ``Image`` instance + (the result of ``id(self)``). + """ + return id(self) + + + def __str__(self): + """Return a string representation of this ``Image`` instance.""" + return '{}({}, {})'.format(self.__class__.__name__, + self.name, + self.dataSource) + + + def __repr__(self): + """See the :meth:`__str__` method.""" + return self.__str__() + + + def applyChange(self, offset, newVals, vol=None): + """Changes the image data according to the given new values. + Any listeners registered on the :attr:`data` property will be + notified of the change. + + :arg offset: A tuple of three values, containing the xyz + offset of the image region to be changed. + + :arg newVals: A 3D numpy array containing the new image values. + + :arg vol: If this is a 4D image, the volume index. + """ + + if self.is4DImage() and vol is None: + raise ValueError('Volume must be specified for 4D images') + + data = self.data + xlo, ylo, zlo = offset + xhi = xlo + newVals.shape[0] + yhi = ylo + newVals.shape[1] + zhi = zlo + newVals.shape[2] + + try: + data.flags.writeable = True + if self.is4DImage(): data[xlo:xhi, ylo:yhi, zlo:zhi, vol] = newVals + else: data[xlo:xhi, ylo:yhi, zlo:zhi] = newVals + data.flags.writeable = False + + except: + data.flags.writeable = False + raise + + # Force a notification on the 'data' property + # by assigning its value back to itself + self.data = data + self.saved = False + + + def save(self): + """Convenience method to save any changes made to the :attr:`data` of + this :class:`Image` instance. + + See the :func:`saveImage` function. + """ + return saveImage(self) # TODO The wx.FileDialog does not