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

VolumeOpts.transformCoords will optionally round coordinates (if the

target space is 'voxel'), and will optionally not clip the
result. VolumeOpts.getVoxel allows coordinates to be passed
in. Transform.axisBounds allows an offset to be applied to voxel
coordinates. New method, Image.axisMapping, which basically wraps
nib.aff2axcodes.
parent 06713f6d
No related branches found
No related tags found
No related merge requests found
...@@ -225,6 +225,20 @@ class Nifti1(object): ...@@ -225,6 +225,20 @@ class Nifti1(object):
return int(code) return int(code)
def axisMapping(self, xform):
"""Returns the (approximate) correspondence of each axis in the source
coordinate system to the axes in the destination coordinate system,
where the source and destinations are defined by the given affine
transformation matrix.
"""
import nibabel as nib
inaxes = [[-1, 1], [-2, 2], [-3, 3]]
return nib.orientations.aff2axcodes(xform, inaxes)
def getOrientation(self, axis, xform): def getOrientation(self, axis, xform):
"""Returns a code representing the orientation of the specified data """Returns a code representing the orientation of the specified data
axis in the coordinate system defined by the given transformation axis in the coordinate system defined by the given transformation
......
...@@ -369,7 +369,7 @@ class Nifti1Opts(fsldisplay.DisplayOpts): ...@@ -369,7 +369,7 @@ class Nifti1Opts(fsldisplay.DisplayOpts):
return (0, 0, 0), (0, 0, 0) return (0, 0, 0), (0, 0, 0)
def transformCoords(self, coords, from_, to_): def transformCoords(self, coords, from_, to_, vround=False):
"""Transforms the given coordinates from ``from_`` to ``to_``. """Transforms the given coordinates from ``from_`` to ``to_``.
The ``from_`` and ``to_`` parameters must both be one of: The ``from_`` and ``to_`` parameters must both be one of:
...@@ -379,41 +379,85 @@ class Nifti1Opts(fsldisplay.DisplayOpts): ...@@ -379,41 +379,85 @@ class Nifti1Opts(fsldisplay.DisplayOpts):
- ``world``: The image world coordinate system - ``world``: The image world coordinate system
- ``custom``: The coordinate system defined by the custom - ``custom``: The coordinate system defined by the custom
transformation matrix (see :attr:`customXform`) transformation matrix (see :attr:`customXform`)
:arg coords: Coordinates to transform
:arg from_: Space to transform from
:arg to_: Space to transform to
:arg vround: If ``True``, and ``to_ == 'voxel'``, the transformed
coordinates are rounded to the nearest integer.
""" """
xform = self.getTransform( from_, to_) xform = self.getTransform( from_, to_)
pre, post = self.getTransformOffsets(from_, to_) pre, post = self.getTransformOffsets(from_, to_)
coords = np.array(coords) + pre coords = np.array(coords) + pre
coords = transform.transform(coords, xform) coords = transform.transform(coords, xform) + post
return coords + post # Round to integer voxel coordinates?
if to_ == 'voxel' and vround:
# The transformation matrices treat integer
# voxel coordinates as the voxel centre (e.g.
# a voxel [3, 4, 5] fills the space:
#
# [2.5-3.5, 3.5-4.5, 4.5-5.5].
#
# So all we need to do is round to the
# nearest integer.
#
# Note that the numpy.round function breaks
# ties (e.g. 7.5) by rounding to the nearest
# *even* integer, which can cause funky
# behaviour. So we take (floor(x)+0.5) instead
# of rounding, to force consistent behaviour
# (i.e. always rounding central values up).
coords = np.floor(coords + 0.5)
return coords
def getVoxel(self): def getVoxel(self, xyz=None, clip=True, vround=True):
"""Calculates and returns the voxel coordinates corresponding to the """Calculates and returns the voxel coordinates corresponding to the
current :attr:`.DisplayContext.location` for the :class:`.Nifti1` given location (assumed to be in the display coordinate system) for
associated with this ``Nifti1Opts`` instance. the :class:`.Nifti1` associated with this ``Nifti1Opts`` instance..
:arg xyz: Display space location to convert to voxels. If not
provided, the current :attr:`.DisplayContext.location`
is used.
:arg clip: If ``False``, and the transformed coordinates are out of
the voxel coordinate bounds, the coordinates returned
anyway. Defaults to ``True``.
Returns ``None`` if the current location is outside of the image
bounds. :arg vround: If ``True``, the returned voxel coordinates are rounded
to the nearest integer. Otherwise they may be fractional.
:returns: ``None`` if the location is outside of the image bounds,
unless ``clip=False``.
""" """
if xyz is not None: x, y, z = xyz
else: x, y, z = self.displayCtx.location.xyz
overlay = self.overlay overlay = self.overlay
x, y, z = self.displayCtx.location.xyz vox = self.transformCoords([[x, y, z]],
'display',
'voxel',
vround=vround)[0]
if vround:
vox = map(int, vox)
vox = self.transformCoords([[x, y, z]], 'display', 'voxel')[0] if not clip:
vox = map(int, np.round(vox)) return vox
if vox[0] < 0 or \ for ax in (0, 1, 2):
vox[1] < 0 or \ if vox[ax] < 0 or vox[ax] >= overlay.shape[ax]:
vox[2] < 0 or \ return None
vox[0] >= overlay.shape[0] or \
vox[1] >= overlay.shape[1] or \
vox[2] >= overlay.shape[2]:
return None
return vox return vox
def displayToStandardCoordinates(self, coords): def displayToStandardCoordinates(self, coords):
......
...@@ -489,8 +489,17 @@ def slice2D(dataShape, ...@@ -489,8 +489,17 @@ def slice2D(dataShape,
# coordinates to map to the effective range [0, 1] # coordinates to map to the effective range [0, 1]
if origin == 'centre': if origin == 'centre':
voxCoords = voxCoords + 0.5 voxCoords = voxCoords + 0.5
texCoords = voxCoords / dataShape
# If the origin is the voxel corner, we still need
# to offset the texture coordinates, as the voxel
# coordinate range [-0.5, shape-0.5] must be scaled
# to the texture coordinate range [0.0, 1.0].
elif origin == 'corner':
texCoords = (voxCoords + 0.5) / dataShape
texCoords = voxCoords / dataShape else:
raise ValueError('Unrecognised origin: {}'.format(origin))
return vertices, voxCoords, texCoords return vertices, voxCoords, texCoords
......
...@@ -70,9 +70,14 @@ def scaleOffsetXform(scales, offsets): ...@@ -70,9 +70,14 @@ def scaleOffsetXform(scales, offsets):
return xform return xform
def axisBounds(shape, xform, axes=None, origin='centre'): def axisBounds(shape,
"""Returns the ``(lo, hi)`` bounds of the specified axis/axes in the world xform,
coordinate system defined by ``xform``. axes=None,
origin='centre',
boundary='high',
offset=1e-4):
"""Returns the ``(lo, hi)`` bounds of the specified axis/axes in the
world coordinate system defined by ``xform``.
If the ``origin`` parameter is set to ``centre`` (the default), If the ``origin`` parameter is set to ``centre`` (the default),
this function assumes that voxel indices correspond to the voxel this function assumes that voxel indices correspond to the voxel
...@@ -100,21 +105,35 @@ def axisBounds(shape, xform, axes=None, origin='centre'): ...@@ -100,21 +105,35 @@ def axisBounds(shape, xform, axes=None, origin='centre'):
to the corner at to the corner at
``(shape[0], shape[1]5, shape[1])``. ``(shape[0], shape[1], shape[1])``.
If the ``boundary`` parameter is set to ``high``, the high voxel bounds
are reduced by a small amount (specified by the ``offset`` parameter)
before they are transformed to the world coordinate system. If
``boundary`` is set to ``low``, the low bounds are increased by a small
amount. The ``boundary`` parameter can also be set to ``'both'``, or
``None``. This option is provided so that you can ensure that the
resulting bounds will always be contained within the image space.
:arg shape: The ``(x, y, z)`` shape of the data. :arg shape: The ``(x, y, z)`` shape of the data.
:arg xform: Transformation matrix which transforms voxel coordinates
to the world coordinate system.
:arg axes: The world coordinate system axis bounds to calculate.
:arg xform: Transformation matrix which transforms voxel coordinates :arg origin: Either ``'centre'`` (the default) or ``'origin'``.
to the world coordinate system.
:arg axes: The world coordinate system axis bounds to calculate. :arg boundary: Either ``'high'`` (the default), ``'low'``, ''`both'``,
or ``None``.
:arg origin: Either ``'centre'`` or ``'origin'`` :arg offset: Amount by which the boundary voxel coordinates should be
offset. Defaults to ``1e-4``.
:returns: A list of tuples, one for each axis specified in the ``axes`` :returns: A list of tuples, one for each axis specified in the
argument. Each tuple contains the ``(lo, hi)`` bounds of the ``axes`` argument. Each tuple contains the ``(lo, hi)``
corresponding world coordinate system axis. bounds of the corresponding world coordinate system axis.
""" """
origin = origin.lower() origin = origin.lower()
...@@ -151,6 +170,16 @@ def axisBounds(shape, xform, axes=None, origin='centre'): ...@@ -151,6 +170,16 @@ def axisBounds(shape, xform, axes=None, origin='centre'):
y0 = 0 y0 = 0
z0 = 0 z0 = 0
if boundary in ('low', 'both'):
x0 += offset
y0 += offset
z0 += offset
if boundary in ('high', 'both'):
x -= offset
y -= offset
z -= offset
points[0, :] = [x0, y0, z0] points[0, :] = [x0, y0, z0]
points[1, :] = [x0, y0, z] points[1, :] = [x0, y0, z]
points[2, :] = [x0, y, z0] points[2, :] = [x0, y, z0]
...@@ -168,7 +197,6 @@ def axisBounds(shape, xform, axes=None, origin='centre'): ...@@ -168,7 +197,6 @@ def axisBounds(shape, xform, axes=None, origin='centre'):
if scalar: return (lo[0], hi[0]) if scalar: return (lo[0], hi[0])
else: return (lo, hi) else: return (lo, hi)
def transform(p, xform, axes=None): def transform(p, xform, axes=None):
"""Transforms the given set of points ``p`` according to the given affine """Transforms the given set of points ``p`` according to the given affine
......
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