Skip to content
Snippets Groups Projects
Commit 71843960 authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Nifti instances now allow voxToWorldMat to be edited, and notify listeners

when it is. New functions in transform module.
parent 4a5f3dcc
No related branches found
No related tags found
No related merge requests found
...@@ -51,7 +51,7 @@ import fsl.data.imagewrapper as imagewrapper ...@@ -51,7 +51,7 @@ import fsl.data.imagewrapper as imagewrapper
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class Nifti(object): class Nifti(notifier.Notifier):
"""The ``Nifti`` class is intended to be used as a base class for """The ``Nifti`` class is intended to be used as a base class for
things which either are, or are associated with, a NIFTI image. things which either are, or are associated with, a NIFTI image.
The ``Nifti`` class is intended to represent information stored in The ``Nifti`` class is intended to represent information stored in
...@@ -104,6 +104,15 @@ class Nifti(object): ...@@ -104,6 +104,15 @@ class Nifti(object):
:attr:`.constants.NIFTI_XFORM_ANALYZE`. :attr:`.constants.NIFTI_XFORM_ANALYZE`.
The ``Nifti`` class implements the :class:`.Notifier` interface -
listeners may register to be notified on the following topics:
=============== ========================================================
``'transform'`` The affine transformation matrix has changed. This topic
will occur when the ``voxToWorldMat`` is changed.
=============== ========================================================
.. warning:: The ``header`` field may either be a ``nifti1``, ``nifti2``, .. warning:: The ``header`` field may either be a ``nifti1``, ``nifti2``,
or ``analyze`` header object. Make sure to take this into or ``analyze`` header object. Make sure to take this into
account if you are writing code that should work with all account if you are writing code that should work with all
...@@ -144,37 +153,16 @@ class Nifti(object): ...@@ -144,37 +153,16 @@ class Nifti(object):
voxToWorldMat = self.__determineTransform(header) voxToWorldMat = self.__determineTransform(header)
worldToVoxMat = transform.invert(voxToWorldMat) worldToVoxMat = transform.invert(voxToWorldMat)
self.header = header self.header = header
self.shape = shape self.__shape = shape
self.intent = header.get('intent_code', self.__intent = header.get('intent_code',
constants.NIFTI_INTENT_NONE) constants.NIFTI_INTENT_NONE)
self.__origShape = origShape self.__origShape = origShape
self.pixdim = pixdim self.__pixdim = pixdim
self.voxToWorldMat = voxToWorldMat self.__voxToWorldMat = voxToWorldMat
self.worldToVoxMat = worldToVoxMat self.__worldToVoxMat = worldToVoxMat
@property
def niftiVersion(self):
"""Returns the NIFTI file version:
- ``0`` for ANALYZE
- ``1`` for NIFTI1
- ``2`` for NIFTI2
"""
import nibabel as nib
# nib.Nifti2 is a subclass of Nifti1,
# and Nifti1 a subclass of Analyze,
# so we have to check in order
if isinstance(self.header, nib.nifti2.Nifti2Header): return 2
elif isinstance(self.header, nib.nifti1.Nifti1Header): return 1
elif isinstance(self.header, nib.analyze.AnalyzeHeader): return 0
else: raise RuntimeError('Unrecognised header: {}'.format(self.header))
def __determineTransform(self, header): def __determineTransform(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
...@@ -248,6 +236,87 @@ class Nifti(object): ...@@ -248,6 +236,87 @@ class Nifti(object):
return origShape, shape, pixdims return origShape, shape, pixdims
@property
def niftiVersion(self):
"""Returns the NIFTI file version:
- ``0`` for ANALYZE
- ``1`` for NIFTI1
- ``2`` for NIFTI2
"""
import nibabel as nib
# nib.Nifti2 is a subclass of Nifti1,
# and Nifti1 a subclass of Analyze,
# so we have to check in order
if isinstance(self.header, nib.nifti2.Nifti2Header): return 2
elif isinstance(self.header, nib.nifti1.Nifti1Header): return 1
elif isinstance(self.header, nib.analyze.AnalyzeHeader): return 0
else: raise RuntimeError('Unrecognised header: {}'.format(self.header))
@property
def shape(self):
"""Returns a tuple containing the image data shape. """
return tuple(self.__shape)
@property
def pixdim(self):
"""Returns a tuple containing the image pixdims (voxel sizes)."""
return tuple(self.__pixdim)
@property
def intent(self):
"""Returns the NIFTI intent code of this image.
"""
return self.__intent
@property
def worldToVoxMat(self):
"""Returns a ``numpy`` array of shape ``(4, 4)`` containing an
affine transformation from world coordinates to voxel coordinates.
"""
return np.array(self.__worldToVoxMat)
@property
def voxToWorldMat(self):
"""Returns a ``numpy`` array of shape ``(4, 4)`` containing an
affine transformation from voxel coordinates to world coordinates.
"""
return np.array(self.__voxToWorldMat)
@voxToWorldMat.setter
def voxToWorldMat(self, xform):
"""Update the ``voxToWorldMat``. The ``worldToVoxMat`` and ``pixdim``
values are also updated. This will result in notification on the
``'transform'`` topic.
"""
header = self.header
header.set_qform(xform)
header.set_sform(xform)
self.__voxToWorldMat = self.__determineTransform(header)
self.__worldToVoxMat = transform.invert(self.__voxToWorldMat)
self.__pixdim = header.get_zooms()
log.debug('{} affine changed:\npixdims: '
'{}\nsform: {}\nqform: {}'.format(
header.get_zooms(),
header.get_sform(),
header.get_qform()))
self.notify(topic='transform')
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.
...@@ -267,7 +336,7 @@ class Nifti(object): ...@@ -267,7 +336,7 @@ class Nifti(object):
# TODO: Remove this method, and use the shape attribute directly # TODO: Remove this method, and use the shape attribute directly
def is4DImage(self): def is4DImage(self):
"""Returns ``True`` if this image is 4D, ``False`` otherwise. """ """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): def getXFormCode(self, code=None):
...@@ -350,7 +419,7 @@ class Nifti(object): ...@@ -350,7 +419,7 @@ class Nifti(object):
_the_transformation_parameters.3F _the_transformation_parameters.3F
""" """
import numpy.linalg as npla import numpy.linalg as npla
return npla.det(self.voxToWorldMat) > 0 return npla.det(self.__voxToWorldMat) > 0
def sameSpace(self, other): def sameSpace(self, other):
...@@ -358,9 +427,12 @@ class Nifti(object): ...@@ -358,9 +427,12 @@ class Nifti(object):
:class:`Nifti` instance) has the same dimensions and is in the :class:`Nifti` instance) has the same dimensions and is in the
same space as this image. same space as this image.
""" """
return np.all(np.isclose(self.shape[:3], other.shape[:3])) and \ return np.all(np.isclose(self .__shape[:3],
np.all(np.isclose(self.pixdim, other.pixdim)) and \ other.__shape[:3])) and \
np.all(np.isclose(self.voxToWorldMat, other.voxToWorldMat)) np.all(np.isclose(self .__pixdim,
other.__pixdim)) and \
np.all(np.isclose(self .__voxToWorldMat,
other.__voxToWorldMat))
def getOrientation(self, axis, xform): def getOrientation(self, axis, xform):
...@@ -405,7 +477,7 @@ class Nifti(object): ...@@ -405,7 +477,7 @@ class Nifti(object):
return code return code
class Image(Nifti, notifier.Notifier): class Image(Nifti):
"""Class which represents a 3D/4D NIFTI image. Internally, the image is """Class which represents a 3D/4D NIFTI image. Internally, the image is
loaded/stored using a :mod:`nibabel.nifti1.Nifti1Image` or loaded/stored using a :mod:`nibabel.nifti1.Nifti1Image` or
:mod:`nibabel.nifti2.Nifti2Image`, and data access managed by a :mod:`nibabel.nifti2.Nifti2Image`, and data access managed by a
...@@ -439,10 +511,10 @@ class Image(Nifti, notifier.Notifier): ...@@ -439,10 +511,10 @@ class Image(Nifti, notifier.Notifier):
============== =========================================================== ============== ===========================================================
The ``Image`` class implements the :class:`.Notifier` interface - The ``Image`` class adds some :class:`.Notifier` topics to those which are
listeners may register to be notified of changes to the above properties, already provided by the :class:`Nifti` class - listeners may register to
by registering on the following _topic_ names (see the :class:`.Notifier` be notified of changes to the above properties, by registering on the
class documentation): following _topic_ names (see the :class:`.Notifier` class documentation):
=============== ====================================================== =============== ======================================================
...@@ -453,7 +525,8 @@ class Image(Nifti, notifier.Notifier): ...@@ -453,7 +525,8 @@ class Image(Nifti, notifier.Notifier):
value (see :meth:`.Notifier.notify`). value (see :meth:`.Notifier.notify`).
``'saveState'`` This topic is notified whenever the saved state of the ``'saveState'`` This topic is notified whenever the saved state of the
image changes (i.e. it is edited, or saved to disk). image changes (i.e. data or ``voxToWorldMat`` is
edited, or the image saved to disk).
``'dataRange'`` This topic is notified whenever the image data range ``'dataRange'`` This topic is notified whenever the image data range
is changed/adjusted. is changed/adjusted.
...@@ -596,6 +669,11 @@ class Image(Nifti, notifier.Notifier): ...@@ -596,6 +669,11 @@ class Image(Nifti, notifier.Notifier):
loadData=loadData, loadData=loadData,
threaded=threaded) threaded=threaded)
# Listen to ourself for changes
# to the voxToWorldMat, so we
# can update the saveState.
self.register(self.name, self.__transformChanged, topic='transform')
if calcRange: if calcRange:
self.calcRange() self.calcRange()
...@@ -688,6 +766,15 @@ class Image(Nifti, notifier.Notifier): ...@@ -688,6 +766,15 @@ class Image(Nifti, notifier.Notifier):
return self.__nibImage.dataobj[tuple(coords)].dtype return self.__nibImage.dataobj[tuple(coords)].dtype
def __transformChanged(self, *args, **kwargs):
"""Called when the ``voxToWorldMat`` of this :class:`Nifti` instance
changes. Updates the :attr:`saveState` accordinbgly.
"""
if self.__saveState:
self.__saveState = False
self.notify(topic='saveState')
def __dataRangeChanged(self, *args, **kwargs): def __dataRangeChanged(self, *args, **kwargs):
"""Called when the :class:`.ImageWrapper` data range changes. """Called when the :class:`.ImageWrapper` data range changes.
Notifies any listeners of this ``Image`` (registered through the Notifies any listeners of this ``Image`` (registered through the
......
...@@ -14,6 +14,10 @@ spaces. The following functions are provided: ...@@ -14,6 +14,10 @@ spaces. The following functions are provided:
scaleOffsetXform scaleOffsetXform
invert invert
concat concat
compse
decompose
rotMatToAxisAngles
axisAnglesToRotMat
axisBounds axisBounds
""" """
...@@ -76,6 +80,106 @@ def scaleOffsetXform(scales, offsets): ...@@ -76,6 +80,106 @@ def scaleOffsetXform(scales, offsets):
return xform return xform
def compose(scales, offsets, rotations, origin=None):
"""Compose a transformation matrix out of the given scales, offsets
and axis rotations.
:arg scales: Sequence of three scale values.
:arg offsets: Sequence of three offset values.
:arg rotations: Sequence of three rotation values, in radians.
:arg origin: Origin of rotation - must be scaled by the ``scales``.
If not provided, the rotation origin is ``(0, 0, 0)``.
"""
preRotate = np.eye(4)
postRotate = np.eye(4)
if origin is not None:
preRotate[ 0, 3] = -origin[0]
preRotate[ 1, 3] = -origin[1]
preRotate[ 2, 3] = -origin[2]
postRotate[0, 3] = origin[0]
postRotate[1, 3] = origin[1]
postRotate[2, 3] = origin[2]
scale = np.eye(4, dtype=np.float64)
offset = np.eye(4, dtype=np.float64)
rotate = np.eye(4, dtype=np.float64)
scale[ 0, 0] = scales[ 0]
scale[ 1, 1] = scales[ 1]
scale[ 2, 2] = scales[ 2]
offset[ 0, 3] = offsets[0]
offset[ 1, 3] = offsets[1]
offset[ 2, 3] = offsets[2]
rotate[:3, :3] = axisAnglesToRotMat(*rotations)
return concat(offset, postRotate, rotate, preRotate, scale)
def decompose(xform):
"""Decomposes the given transformation matrix into separate offsets,
scales, and rotations.
.. note:: Shears are not yet supported, and may never be supported.
"""
offsets = xform[:3, 3]
scales = [np.sqrt(np.sum(xform[:3, 0] ** 2)),
np.sqrt(np.sum(xform[:3, 1] ** 2)),
np.sqrt(np.sum(xform[:3, 2] ** 2))]
rotmat = np.copy(xform[:3, :3])
rotmat[:, 0] /= scales[0]
rotmat[:, 1] /= scales[1]
rotmat[:, 2] /= scales[2]
rots = rotMatToAxisAngles(rotmat)
return scales, offsets, rots
def rotMatToAxisAngles(rotmat):
"""Given a ``(3, 3)`` rotation matrix, decomposes the rotations into
an angle in radians about each axis.
"""
xrot = np.arctan2(rotmat[2, 1], rotmat[2, 2])
yrot = np.sqrt( rotmat[2, 1] ** 2 + rotmat[2, 2] ** 2)
yrot = np.arctan2(rotmat[2, 0], yrot)
zrot = np.arctan2(rotmat[1, 0], rotmat[0, 0])
return [xrot, yrot, zrot]
def axisAnglesToRotMat(xrot, yrot, zrot):
"""Constructs a ``(3, 3)`` rotation matrix from the given angles, which
must be specified in radians.
"""
xmat = np.eye(3)
ymat = np.eye(3)
zmat = np.eye(3)
xmat[1, 1] = np.cos(xrot)
xmat[1, 2] = -np.sin(xrot)
xmat[2, 1] = np.sin(xrot)
xmat[2, 2] = np.cos(xrot)
ymat[0, 0] = np.cos(yrot)
ymat[0, 2] = np.sin(yrot)
ymat[2, 0] = -np.sin(yrot)
ymat[2, 2] = np.cos(yrot)
zmat[0, 0] = np.cos(zrot)
zmat[0, 1] = -np.sin(zrot)
zmat[1, 0] = np.sin(zrot)
zmat[1, 1] = np.cos(zrot)
return concat(zmat, ymat, xmat)
def axisBounds(shape, def axisBounds(shape,
xform, xform,
axes=None, axes=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