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

RF,ENH: Refactor Nifti class - it now pre-calculates affines between voxel,

fsl, and world coordinate systems. New getAffine method allows you to get an
affine between any of the coordinate systems
parent 46d1a79f
No related branches found
No related tags found
No related merge requests found
...@@ -135,13 +135,32 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -135,13 +135,32 @@ class Nifti(notifier.Notifier, meta.Meta):
methods. methods.
**The affine transformation** **Affine transformations**
The :meth:`voxToWorldMat` and :meth:`worldToVoxMat` attributes contain The ``Nifti`` class is aware of three coordinate systems:
transformation matrices for transforming between voxel and world
coordinates. The ``Nifti`` class follows the same process as ``nibabel`` - The ``voxel`` coordinate system, used to access image data
in selecting the affine (see
- The ``world`` coordinate system, where voxel coordinates are
transformed into a millimetre coordinate system, defined by the
``sform`` and/or ``qform`` elements of the NIFTI header.
- The ``fsl`` coordinate system, where voxel coordinated are scaled by
the ``pixdim`` values in the NIFTI header, and the X axis is inverted
if the voxel-to-world affine has a positive determinant.
The :meth:`getAffine` method is a simple means of acquiring an affine
which will transform between any of these coordinate systems.
See `here <http://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FLIRT/FAQ#What_is_the_format_of_the_matrix_used_by_FLIRT.2C_and_how_does_it_relate_to_the_transformation_parameters.3F>`_
for more details on the ``fsl`` coordinate system..
The ``Nifti`` class follows the same process as ``nibabel`` in determining
the ``voxel`` to ``world`` affine (see
http://nipy.org/nibabel/nifti_images.html#the-nifti-affines): http://nipy.org/nibabel/nifti_images.html#the-nifti-affines):
...@@ -227,20 +246,51 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -227,20 +246,51 @@ class Nifti(notifier.Notifier, meta.Meta):
header = header header = header
origShape, shape, pixdim = self.__determineShape(header) origShape, shape, pixdim = self.__determineShape(header)
voxToWorldMat = self.__determineTransform(header) self.header = header
worldToVoxMat = transform.invert(voxToWorldMat) self.__shape = shape
self.__origShape = origShape
self.__pixdim = pixdim
self.__intent = header.get('intent_code',
constants.NIFTI_INTENT_NONE)
voxToWorldMat = self.__determineAffine(header)
affines, isneuro = self.__generateAffines(voxToWorldMat)
self.header = header self.__affines = affines
self.__shape = shape self.__isNeurological = isneuro
self.__intent = header.get('intent_code',
constants.NIFTI_INTENT_NONE)
self.__origShape = origShape
self.__pixdim = pixdim
self.__voxToWorldMat = voxToWorldMat
self.__worldToVoxMat = worldToVoxMat
def __determineTransform(self, header): def __determineShape(self, header):
"""This method is called by :meth:`__init__`. It figures out the actual
shape of the image data, and the zooms/pixdims for each data axis. Any
empty trailing dimensions are squeezed, but the returned shape is
guaranteed to be at least 3 dimensions. Returns:
- A sequence/tuple containing the image shape, as reported in the
header.
- A sequence/tuple containing the effective image shape.
- A sequence/tuple containing the zooms/pixdims.
"""
# The canonicalShape function figures out
# the data shape that we should use.
origShape = list(header.get_data_shape())
shape = canonicalShape(origShape)
pixdims = list(header.get_zooms())
# if get_zooms() doesn't return at
# least len(shape) values, we'll
# fall back to the pixdim field in
# the header.
if len(pixdims) < len(shape):
pixdims = header['pixdim'][1:]
pixdims = pixdims[:len(shape)]
return origShape, shape, pixdims
def __determineAffine(self, header):
"""Called by :meth:`__init__`. Figures out the voxel-to-world """Called by :meth:`__init__`. Figures out the voxel-to-world
coordinate transformation matrix that is associated with this coordinate transformation matrix that is associated with this
``Nifti`` instance. ``Nifti`` instance.
...@@ -250,26 +300,9 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -250,26 +300,9 @@ class Nifti(notifier.Notifier, meta.Meta):
# specially, as FNIRT clobbers the # specially, as FNIRT clobbers the
# sform section of the NIFTI header # sform section of the NIFTI header
# to store other data. # to store other data.
# intent = header.get('intent_code', -1)
# TODO Nibabel <= 2.1.0 has a bug in header.get qform = header.get('qform_code', -1)
# for fields with a value of 0. When this sform = header.get('sform_code', -1)
# bug gets fixed, we can replace the if-else
# block below with this:
#
# intent = header.get('intent_code', -1)
# qform = header.get('qform_code', -1)
# sform = header.get('sform_code', -1)
#
# TODO Change this in fslpy 2.0.0
#
if isinstance(header, nib.nifti1.Nifti1Header):
intent = header['intent_code']
qform = header['qform_code']
sform = header['sform_code']
else:
intent = -1
qform = -1
sform = -1
if intent in (constants.FSL_FNIRT_DISPLACEMENT_FIELD, if intent in (constants.FSL_FNIRT_DISPLACEMENT_FIELD,
constants.FSL_CUBIC_SPLINE_COEFFICIENTS, constants.FSL_CUBIC_SPLINE_COEFFICIENTS,
...@@ -300,34 +333,47 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -300,34 +333,47 @@ class Nifti(notifier.Notifier, meta.Meta):
return voxToWorldMat return voxToWorldMat
def __determineShape(self, header): def __generateAffines(self, voxToWorldMat):
"""This method is called by :meth:`__init__`. It figures out the actual """Called by :meth:`__init__`, and the :meth:`voxToWorldMat` setter.
shape of the image data, and the zooms/pixdims for each data axis. Any Generates and returns a dictionary containing affine transformations
empty trailing dimensions are squeezed, but the returned shape is between the ``voxel``, ``fsl``, and ``world`` coordinate
guaranteed to be at least 3 dimensions. Returns: systems. These affines are accessible via the :meth:`getAffine`
method.
- A sequence/tuple containing the image shape, as reported in the :arg voxToWorldMat: The voxel-to-world affine transformation
header. :returns: A tuple containing:
- A sequence/tuple containing the effective image shape.
- A sequence/tuple containing the zooms/pixdims. - a dictionary of affine transformations between
each pair of coordinate systems
- ``True`` if the image is to be considered
"neurological", ``False`` otherwise - see the
:meth:`isNeurological` method.
""" """
# The canonicalShape function figures out import numpy.linalg as npla
# the data shape that we should use.
origShape = list(header.get_data_shape())
shape = canonicalShape(origShape)
pixdims = list(header.get_zooms())
# if get_zooms() doesn't return at affines = {}
# least len(shape) values, we'll shape = list(self.shape[ :3])
# fall back to the pixdim field in pixdim = list(self.pixdim[:3])
# the header. voxToScaledVoxMat = np.diag(pixdim + [1.0])
if len(pixdims) < len(shape): isneuro = npla.det(voxToWorldMat) > 0
pixdims = header['pixdim'][1:]
pixdims = pixdims[:len(shape)] if isneuro:
x = (shape[0] - 1) * pixdim[0]
flip = transform.scaleOffsetXform([-1, 1, 1],
[ x, 0, 0])
voxToScaledVoxMat = transform.concat(flip, voxToScaledVoxMat)
return origShape, shape, pixdims affines['voxel', 'world'] = voxToWorldMat
affines['world', 'voxel'] = transform.invert(voxToWorldMat)
affines['voxel', 'fsl'] = voxToScaledVoxMat
affines['fsl', 'voxel'] = transform.invert(voxToScaledVoxMat)
affines['fsl', 'world'] = transform.concat(affines['voxel', 'world'],
affines['fsl', 'voxel'])
affines['world', 'fsl'] = transform.concat(affines['fsl', 'voxel'],
affines['voxel', 'world'])
return affines, isneuro
def strval(self, key): def strval(self, key):
...@@ -382,6 +428,16 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -382,6 +428,16 @@ class Nifti(notifier.Notifier, meta.Meta):
return tuple(self.__shape) return tuple(self.__shape)
@property
def ndim(self):
"""Returns the number of dimensions in this image. This number may not
match the number of dimensions specified in the NIFTI header, as
trailing dimensions of length 1 are ignored. But it is guaranteed to be
at least 3.
"""
return len(self.__shape)
@property @property
def pixdim(self): def pixdim(self):
"""Returns a tuple containing the image pixdims (voxel sizes).""" """Returns a tuple containing the image pixdims (voxel sizes)."""
...@@ -426,12 +482,39 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -426,12 +482,39 @@ class Nifti(notifier.Notifier, meta.Meta):
return units return units
def getAffine(self, from_, to):
"""Return an affine transformation which can be used to transform
coordinates from ``from_`` to ``to``.
Valid values for the ``from_`` and ``to`` arguments are:
- ``'voxel'``: The voxel coordinate system
- ``'world'``: The world coordinate system, as defined by the image
sform/qform
- ``'fsl'``: The FSL coordinate system (scaled voxels, with a
left-right flip if the sform/qform has a positive determinant)
:arg from_: Source coordinate system
:arg to: Destination coordinate system
:returns: A ``numpy`` array of shape ``(4, 4)``
"""
from_ = from_.lower()
to = to .lower()
if from_ not in ('voxel', 'fsl', 'world') or \
to not in ('voxel', 'fsl', 'world'):
raise ValueError('Invalid source/reference spaces: '
'{} -> {}'.format(from_, to))
return np.copy(self.__affines[from_, to])
@property @property
def worldToVoxMat(self): def worldToVoxMat(self):
"""Returns a ``numpy`` array of shape ``(4, 4)`` containing an """Returns a ``numpy`` array of shape ``(4, 4)`` containing an
affine transformation from world coordinates to voxel coordinates. affine transformation from world coordinates to voxel coordinates.
""" """
return np.array(self.__worldToVoxMat) return self.getAffine('world', 'voxel')
@property @property
...@@ -439,7 +522,7 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -439,7 +522,7 @@ class Nifti(notifier.Notifier, meta.Meta):
"""Returns a ``numpy`` array of shape ``(4, 4)`` containing an """Returns a ``numpy`` array of shape ``(4, 4)`` containing an
affine transformation from voxel coordinates to world coordinates. affine transformation from voxel coordinates to world coordinates.
""" """
return np.array(self.__voxToWorldMat) return self.getAffine('voxel', 'world')
@voxToWorldMat.setter @voxToWorldMat.setter
...@@ -463,8 +546,10 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -463,8 +546,10 @@ class Nifti(notifier.Notifier, meta.Meta):
header.set_sform(xform, code=sformCode) header.set_sform(xform, code=sformCode)
self.__voxToWorldMat = self.__determineTransform(header) affines, isneuro = self.__generateAffines(xform)
self.__worldToVoxMat = transform.invert(self.__voxToWorldMat)
self.__affines = affines
self.__isNeurological = isneuro
log.debug('Affine changed:\npixdims: ' log.debug('Affine changed:\npixdims: '
'%s\nsform: %s\nqform: %s', '%s\nsform: %s\nqform: %s',
...@@ -475,6 +560,27 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -475,6 +560,27 @@ class Nifti(notifier.Notifier, meta.Meta):
self.notify(topic='transform') self.notify(topic='transform')
@property
def voxToScaledVoxMat(self):
"""Returns a transformation matrix which transforms from voxel
coordinates into scaled voxel coordinates, with a left-right flip
if the image appears to be stored in neurological order.
See http://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FLIRT/FAQ#What_is_the\
_format_of_the_matrix_used_by_FLIRT.2C_and_how_does_it_relate_to\
_the_transformation_parameters.3F
"""
return self.getAffine('voxel', 'fsl')
@property
def scaledVoxToVoxMat(self):
"""Returns a transformation matrix which transforms from scaled voxels
into voxels, the inverse of the :meth:`voxToScaledVoxMat` transform.
"""
return self.getAffine('fsl', 'voxel')
def mapIndices(self, sliceobj): def mapIndices(self, sliceobj):
"""Adjusts the given slice object so that it may be used to index the """Adjusts the given slice object so that it may be used to index the
underlying ``nibabel`` NIFTI image object. underlying ``nibabel`` NIFTI image object.
...@@ -490,16 +596,6 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -490,16 +596,6 @@ class Nifti(notifier.Notifier, meta.Meta):
return fileslice.canonical_slicers(sliceobj, self.__origShape) return fileslice.canonical_slicers(sliceobj, self.__origShape)
@property
def ndim(self):
"""Returns the number of dimensions in this image. This number may not
match the number of dimensions specified in the NIFTI header, as
trailing dimensions of length 1 are ignored. But it is guaranteed to be
at least 3.
"""
return len(self.__shape)
def getXFormCode(self, code=None): def getXFormCode(self, code=None):
"""This method returns the code contained in the NIFTI header, """This method returns the code contained in the NIFTI header,
indicating the space to which the (transformed) image is oriented. indicating the space to which the (transformed) image is oriented.
...@@ -546,6 +642,7 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -546,6 +642,7 @@ class Nifti(notifier.Notifier, meta.Meta):
return int(code) return int(code)
# TODO Check what has worse performance - hashing # TODO Check what has worse performance - hashing
# a 4x4 array (via memoizeMD5), or the call # a 4x4 array (via memoizeMD5), or the call
# to aff2axcodes. I'm guessing the latter, # to aff2axcodes. I'm guessing the latter,
...@@ -563,7 +660,6 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -563,7 +660,6 @@ class Nifti(notifier.Notifier, meta.Meta):
return nib.orientations.aff2axcodes(xform, inaxes) return nib.orientations.aff2axcodes(xform, inaxes)
@memoize.Instanceify(memoize.memoize)
def isNeurological(self): def isNeurological(self):
"""Returns ``True`` if it looks like this ``Nifti`` object has a """Returns ``True`` if it looks like this ``Nifti`` object has a
neurological voxel orientation, ``False`` otherwise. This test is neurological voxel orientation, ``False`` otherwise. This test is
...@@ -582,51 +678,7 @@ class Nifti(notifier.Notifier, meta.Meta): ...@@ -582,51 +678,7 @@ class Nifti(notifier.Notifier, meta.Meta):
_format_of_the_matrix_used_by_FLIRT.2C_and_how_does_it_relate_to\ _format_of_the_matrix_used_by_FLIRT.2C_and_how_does_it_relate_to\
_the_transformation_parameters.3F _the_transformation_parameters.3F
""" """
import numpy.linalg as npla return self.__isNeurological
return npla.det(self.__voxToWorldMat) > 0
@property
def voxToScaledVoxMat(self):
"""Returns a transformation matrix which transforms from voxel
coordinates into scaled voxel coordinates, with a left-right flip
if the image appears to be stored in neurological order.
See http://fsl.fmrib.ox.ac.uk/fsl/fslwiki/FLIRT/FAQ#What_is_the\
_format_of_the_matrix_used_by_FLIRT.2C_and_how_does_it_relate_to\
_the_transformation_parameters.3F
"""
return self.__voxToScaledVoxMat()
@memoize.Instanceify(memoize.memoize)
def __voxToScaledVoxMat(self):
"""See :meth:`voxToScaledVoxMat`. """
shape = list(self.shape[ :3])
pixdim = list(self.pixdim[:3])
voxToPixdimMat = np.diag(pixdim + [1.0])
if self.isNeurological():
x = (shape[0] - 1) * pixdim[0]
flip = transform.scaleOffsetXform([-1, 1, 1], [x, 0, 0])
voxToPixdimMat = transform.concat(flip, voxToPixdimMat)
return voxToPixdimMat
@property
def scaledVoxToVoxMat(self):
"""Returns a transformation matrix which transforms from scaled voxels
into voxels, the inverse of the :meth:`voxToScaledVoxMat` transform.
"""
return self.__scaledVoxToVoxMat()
@memoize.Instanceify(memoize.memoize)
def __scaledVoxToVoxMat(self):
"""See :meth:`scaledVoxToVoxMat`. """
return transform.invert(self.voxToScaledVoxMat)
def sameSpace(self, other): def sameSpace(self, other):
......
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