#
# niftiopts.py - The NiftiOpts class
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module defines the :class:`NiftiOpts` class.
.. _volumeopts-coordinate-systems:
---------------------------------------
An important note on coordinate systems
---------------------------------------
*FSLeyes* displays all overlays in a single coordinate system, referred
throughout as the *display coordinate system*. However, :class:`.Nifti`
overlays can potentially be transformed into the display coordinate system
in one of several ways:
============================== ===============================================
**voxels** (a.k.a. ``id``) The image data voxel
coordinates map to the display coordinates.
**scaled voxels** (a.k.a. ``pixdim``) The image data voxel
coordinates are scaled by the ``pixdim`` values
stored in the NIFTI header. The origin is
fixed at the centre of voxel ``(0, 0, 0)``.
**radioloigcal scaled voxels** (a.k.a. ``pixdim-flip``) The image data voxel
coordinates are scaled by the ``pixdim``
values stored in the NIFTI header and, if the
image appears to be stored in neurological
order, the X (left-right) axis is
inverted. The origin is fixed at the centre of
voxel ``(0, 0, 0)`` (or ``(X-1, 0, 0)`` for
inverted images).
**world** (a.k.a. ``affine``) The image data voxel
coordinates are transformed by the
transformation matrix stored in the NIFTI
header - see the :class:`.Nifti` class for more
details.
**reference** (a.k.a. ``reference``) The image data voxel
coordinates are transformed into the
``pixdim-flip`` coordinate system of another,
*reference*, NIFTI image. The reference overlay
is specified by the
:attr:`.DisplayContext.displaySpace` attribute.
============================== ===============================================
The :attr:`NiftiOpts.transform` property controls how the image data is
transformed into the display coordinate system. It allows any of the above
spaces to be specified (as ``id``, ``pixdim``, ``pixdim-flip``, ``affine```,
or ``reference`` respectively).
Pixdim flip
^^^^^^^^^^^
The ``pixdim-flip`` transform is the coordinate system used internally by many
of the FSL tools. For instance, this is the coordinate system used by
FSLView, by ``flirt``, and in the VTK sub-cortical segmentation model files
output by ``first``.
Furthermore, the vectors in eigenvector images images output by ``dtifit`` are
oriented according to this space, so if the input data is in neurological
orientation, these vectors need to be inverted along the x axis.
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
What is a voxel?
^^^^^^^^^^^^^^^^
Regardless of the space in which the ``Nifti`` is displayed , the
voxel-to-display space transformation (and in general, all of FSLeyes) assumes
that integer voxel coordinates correspond to the centre of the voxel in the
display coordinate system. In other words, a voxel at location::
[x, y, z]
is assumed to occupy the space that corresponds to::
[x-0.5 - x+0.5, y-0.5 - y+0.5, z-0.5 - z+0.5]
For example, if the :attr:`NiftiOpts.transform` property is set to ``id``, the
voxel::
[2, 3, 4]
is drawn such that it occupies the space::
[1.5 - 2.5, 2.5 - 3.5, 3.5 - 4.5]
This convention is in line with the convention defined by the ``NIFTI``
specification: it assumes that the voxel coordinates ``[x, y, z]`` correspond
to the centre of a voxel.
"""
import logging
import numpy as np
import fsl.data.image as fslimage
import fsl.transform.affine as affine
import fsleyes_props as props
from . import display as fsldisplay
log = logging.getLogger(__name__)
[docs]class NiftiOpts(fsldisplay.DisplayOpts):
"""The ``NiftiOpts`` class describes how a :class:`.Nifti` overlay
should be displayed.
``NiftiOpts`` is the base class for a number of :class:`.DisplayOpts`
sub-classes - it contains display options which are common to all overlay
types that represent a NIFTI image.
"""
volume = props.Int(minval=0, maxval=0, default=0, clamped=True)
"""If the ``Image`` has more than 3 dimensions, the current volume to
display. The volume dimension is controlled by the :attr:`volumeDim`
property.
"""
volumeDim = props.Int(minval=0, maxval=5, default=0, clamped=True)
"""For images with more than three dimensions, this property controls
the dimension that the :attr:`volume` property indexes into. When the
``volumeDim`` changes, the ``volume`` for the previous ``volumeDim``
is fixed at its last value, and used for subsequent lookups.
"""
transform = props.Choice(
('affine', 'pixdim', 'pixdim-flip', 'id', 'reference'),
default='pixdim-flip')
"""This property defines how the overlay should be transformd into
the display coordinate system. See the
:ref:`note on coordinate systems <volumeopts-coordinate-systems>`
for important information regarding this property.
"""
displayXform = props.Array(
dtype=np.float64,
shape=(4, 4),
resizable=False,
default=[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])
"""A custom transformation matrix which is concatenated on to the voxel ->
world transformation of the :class:`.Nifti` overlay.
This transform is intended for temporary changes to the overlay display
(when :attr:`.DisplayContext.displaySpace` ``== 'world'``) - changes to it
will *not* result in the ::attr:`.DisplayContext.bounds` being updated.
If you change the ``displayXform``, make sure to change it back to an
identity matrix when you are done.
"""
enableOverrideDataRange = props.Boolean(default=False)
"""By default, the :attr:`.Image.dataRange` property is used to set
display and clipping ranges. However, if this property is ``True``,
the :attr:`overrideDataRange` is used instead.
..note:: The point of this property is to make it easier to display images
with a very large data range driven by outliers. On platforms
which do not support floating point textures, these images are
impossible to display unless they are normalised according to
a smaller data range. See the
:meth:`.Texture3D.__determineTextureType` method for some more
details.
"""
overrideDataRange = props.Bounds(ndims=1, clamped=False)
"""Data range used in place of the :attr:`.Image.dataRange` if the
:attr:`enableOverrideDataRange` property is ``True``.
"""
[docs] def __init__(self, *args, **kwargs):
"""Create a ``NiftiOpts`` instance.
All arguments are passed through to the :class:`.DisplayOpts`
constructor.
"""
nounbind = kwargs.get('nounbind', [])
nobind = kwargs.get('nobind', [])
nounbind.append('overrideDataRange')
nounbind.append('enableOverrideDataRange')
nobind .append('displayXform')
kwargs['nounbind'] = nounbind
kwargs['nobind'] = nobind
fsldisplay.DisplayOpts.__init__(self, *args, **kwargs)
self.__child = self.getParent() is not None
if self.__child:
# is this a >3D volume?
ndims = self.overlay.ndim
# We store indices for every dimension
# past the XYZ dims. Whenever the volumeDim
# changes, we cache the index for the old
# dimensions, and restore the index for the
# new dimension.
self.setAttribute('volumeDim', 'maxval', max(0, ndims - 4))
self.setAttribute('volume', 'cache', [0] * (ndims - 3))
if ndims <= 3:
self.setAttribute('volume', 'maxval', 0)
self.overlay .register( self.name,
self.__overlayTransformChanged,
topic='transform')
self .addListener('volumeDim',
self.name,
self.__volumeDimChanged,
immediate=True)
self .addListener('transform',
self.name,
self.__transformChanged,
immediate=True)
self .addListener('displayXform',
self.name,
self.__displayXformChanged,
immediate=True)
self.displayCtx.addListener('displaySpace',
self.name,
self.__displaySpaceChanged,
immediate=True)
# The display<->* transformation matrices
# are created in the _setupTransforms method.
# The __displaySpaceChanged method registers
# a listener with the current display space
# (if it is an overlay)
self.__xforms = {}
self.__dsOverlay = None
self.__setupTransforms()
self.__transformChanged()
self.__volumeDimChanged()
[docs] def destroy(self):
"""Calls the :meth:`.DisplayOpts.destroy` method. """
if self.__child:
self.overlay .deregister( self.name, topic='transform')
self.displayCtx.removeListener('displaySpace', self.name)
self .removeListener('volumeDim', self.name)
self .removeListener('transform', self.name)
self .removeListener('displayXform', self.name)
if self.__dsOverlay is not None:
self.__dsOverlay.deregister(self.name, topic='transform')
self.__dsOverlay = None
fsldisplay.DisplayOpts.destroy(self)
def __toggleSiblingListeners(self, enable=True):
"""Enables/disables the ``volumeDim`` listeners of sibling
``NiftiOpts`` instances. This is used by the :meth:`__volumeDimChanged`
method to avoid nastiness.
"""
for s in self.getParent().getChildren():
if s is not self:
if enable: s.enableListener( 'volumeDim', s.name)
else: s.disableListener('volumeDim', s.name)
def __volumeDimChanged(self, *a):
"""Called when the :attr:`volumeDim` changes. Saves the value of
``volume`` for the last ``volumeDim``, and restores the previous
value of ``volume`` for the new ``volumeDim``.
"""
if self.overlay.ndim <= 3:
return
# Here we disable volumeDim listeners on all
# sibling instances, then save/restore the
# volume value and properties asynchronously,
# then re-enable the slblings. This is a
# horrible means of ensuring that only the
# first VolumeOpts instance (out of a set of
# synchronised instances) updates the volume
# value and properties. The other instances
# will be updated through synchronisation.
# This is necessary because subsequent
# instances would corrupt the update made by
# the first instance.
#
# A nicer way to do things like this would be
# nice.
def update():
oldVolume = self.volume
oldVolumeDim = self.getLastValue('volumeDim')
if oldVolumeDim is None:
oldVolumeDim = 0
cache = list(self.getAttribute('volume', 'cache'))
cache[oldVolumeDim] = oldVolume
newVolume = cache[self.volumeDim]
newVolumeLim = self.overlay.shape[self.volumeDim + 3] - 1
self.setAttribute('volume', 'maxval', newVolumeLim)
self.setAttribute('volume', 'cache', cache)
self.volume = newVolume
self.__toggleSiblingListeners(False)
props.safeCall(update)
props.safeCall(self.__toggleSiblingListeners, True)
def __overlayTransformChanged(self, *a):
"""Called when the :class:`.Nifti` overlay sends a notification
on the ``'transform'`` topic, indicating that its voxel->world
transformation matrix has been updated.
"""
self.__setupTransforms()
self.__transformChanged()
def __displaySpaceTransformChanged(self, *a):
"""Called when the :attr:`.DisplayContext.displaySpace` is a
:class:`.Nifti` overlay, and its :attr:`.Nifti.voxToWorldMat`
changes. Updates the transformation matrices for this image.
"""
self.__setupTransforms()
self.__transformChanged()
def __transformChanged(self, *a):
"""Called when the :attr:`transform` property changes.
Calculates the min/max values of a 3D bounding box, in the display
coordinate system, which is big enough to contain the image. Sets the
:attr:`.DisplayOpts.bounds` property accordingly.
"""
lo, hi = affine.axisBounds(
self.overlay.shape[:3],
self.getTransform('voxel', 'display'))
self.bounds[:] = [lo[0], hi[0], lo[1], hi[1], lo[2], hi[2]]
def __displaySpaceChanged(self, *a):
"""Called when the :attr:`.DisplayContext.displaySpace` property
changes. Re-generates transformation matrices, and re-calculates
the display :attr:`bounds` (via calls to :meth:`__setupTransforms` and
:meth:`__transformChanged`).
"""
displaySpace = self.displayCtx.displaySpace
if self.__dsOverlay is not None:
self.__dsOverlay.deregister(self.name, topic='transform')
self.__dsOverlay = None
# Register a listener on the display space reference
# image, because when its voxToWorldMat changes, we
# need to update our *toref and refto* transforms.
if isinstance(displaySpace, fslimage.Nifti) and \
displaySpace is not self.overlay:
self.__dsOverlay = displaySpace
self.__dsOverlay.register(self.name,
self.__displaySpaceTransformChanged,
topic='transform')
self.__setupTransforms()
if self.transform == 'reference':
self.__transformChanged()
def __displayXformChanged(self, *a):
"""Called when the :attr:`displayXform` property changes. Updates
the transformation matrices and :attr:`bounds` accordingly.
Critically, when the :attr:`displayXform` property changes, the
:class:`.DisplayContext` is *not* notified. This is because
the ``displayXform`` is intended for temporary changes.
"""
# The displayXform is intended as a temporary
# transformation for display purposes - the
# DisplayOpts.bounds property gets updated when
# it changes, but we don't want the
# DisplayContext.bounds property to be updated.
# So we suppress all notification while
# updating the transformation matrices.
with self.displayCtx.freeze(self.overlay):
self.__setupTransforms()
self.__transformChanged()
def __setupTransforms(self):
"""Calculates transformation matrices between all of the possible
spaces in which the overlay may be displayed.
These matrices are accessible via the :meth:`getTransform` method.
"""
image = self.overlay
shape = np.array(image.shape[:3])
voxToIdMat = np.eye(4)
voxToPixdimMat = np.diag(list(image.pixdim[:3]) + [1.0])
voxToPixFlipMat = image.voxToScaledVoxMat
voxToWorldMat = image.voxToWorldMat
voxToWorldMat = affine.concat(self.displayXform, voxToWorldMat)
ds = self.displayCtx.displaySpace
# The reference transforms depend
# on the value of displaySpace
if ds == 'world':
voxToRefMat = voxToWorldMat
elif ds == 'scaledVoxel':
voxToRefMat = voxToPixdimMat
elif ds is self.overlay:
voxToRefMat = voxToPixFlipMat
else:
voxToRefMat = affine.concat(ds.voxToScaledVoxMat,
ds.worldToVoxMat,
voxToWorldMat)
# When going from voxels to textures,
# we add 0.5 to centre the voxel (see
# the note on coordinate systems at
# the top of this file).
voxToTexMat = affine.scaleOffsetXform(tuple(1.0 / shape),
tuple(0.5 / shape))
idToVoxMat = affine.invert(voxToIdMat)
idToPixdimMat = affine.concat(voxToPixdimMat, idToVoxMat)
idToPixFlipMat = affine.concat(voxToPixFlipMat, idToVoxMat)
idToWorldMat = affine.concat(voxToWorldMat, idToVoxMat)
idToRefMat = affine.concat(voxToRefMat, idToVoxMat)
idToTexMat = affine.concat(voxToTexMat, idToVoxMat)
pixdimToVoxMat = affine.invert(voxToPixdimMat)
pixdimToIdMat = affine.concat(voxToIdMat, pixdimToVoxMat)
pixdimToPixFlipMat = affine.concat(voxToPixFlipMat, pixdimToVoxMat)
pixdimToWorldMat = affine.concat(voxToWorldMat, pixdimToVoxMat)
pixdimToRefMat = affine.concat(voxToRefMat, pixdimToVoxMat)
pixdimToTexMat = affine.concat(voxToTexMat, pixdimToVoxMat)
pixFlipToVoxMat = affine.invert(voxToPixFlipMat)
pixFlipToIdMat = affine.concat(voxToIdMat, pixFlipToVoxMat)
pixFlipToPixdimMat = affine.concat(voxToPixdimMat, pixFlipToVoxMat)
pixFlipToWorldMat = affine.concat(voxToWorldMat, pixFlipToVoxMat)
pixFlipToRefMat = affine.concat(voxToRefMat, pixFlipToVoxMat)
pixFlipToTexMat = affine.concat(voxToTexMat, pixFlipToVoxMat)
worldToVoxMat = affine.invert(voxToWorldMat)
worldToIdMat = affine.concat(voxToIdMat, worldToVoxMat)
worldToPixdimMat = affine.concat(voxToPixdimMat, worldToVoxMat)
worldToPixFlipMat = affine.concat(voxToPixFlipMat, worldToVoxMat)
worldToRefMat = affine.concat(voxToRefMat, worldToVoxMat)
worldToTexMat = affine.concat(voxToTexMat, worldToVoxMat)
refToVoxMat = affine.invert(voxToRefMat)
refToIdMat = affine.concat(voxToIdMat, refToVoxMat)
refToPixdimMat = affine.concat(voxToPixdimMat, refToVoxMat)
refToPixFlipMat = affine.concat(voxToPixFlipMat, refToVoxMat)
refToWorldMat = affine.concat(voxToWorldMat, refToVoxMat)
refToTexMat = affine.concat(voxToTexMat, refToVoxMat)
texToVoxMat = affine.invert(voxToTexMat)
texToIdMat = affine.concat(voxToIdMat, texToVoxMat)
texToPixdimMat = affine.concat(voxToPixdimMat, texToVoxMat)
texToPixFlipMat = affine.concat(voxToPixFlipMat, texToVoxMat)
texToWorldMat = affine.concat(voxToWorldMat, texToVoxMat)
texToRefMat = affine.concat(voxToRefMat, texToVoxMat)
self.__xforms['id', 'id'] = np.eye(4)
self.__xforms['id', 'pixdim'] = idToPixdimMat
self.__xforms['id', 'pixdim-flip'] = idToPixFlipMat
self.__xforms['id', 'affine'] = idToWorldMat
self.__xforms['id', 'reference'] = idToRefMat
self.__xforms['id', 'texture'] = idToTexMat
self.__xforms['pixdim', 'pixdim'] = np.eye(4)
self.__xforms['pixdim', 'id'] = pixdimToIdMat
self.__xforms['pixdim', 'pixdim-flip'] = pixdimToPixFlipMat
self.__xforms['pixdim', 'affine'] = pixdimToWorldMat
self.__xforms['pixdim', 'reference'] = pixdimToRefMat
self.__xforms['pixdim', 'texture'] = pixdimToTexMat
self.__xforms['pixdim-flip', 'pixdim-flip'] = np.eye(4)
self.__xforms['pixdim-flip', 'id'] = pixFlipToIdMat
self.__xforms['pixdim-flip', 'pixdim'] = pixFlipToPixdimMat
self.__xforms['pixdim-flip', 'affine'] = pixFlipToWorldMat
self.__xforms['pixdim-flip', 'reference'] = pixFlipToRefMat
self.__xforms['pixdim-flip', 'texture'] = pixFlipToTexMat
self.__xforms['affine', 'affine'] = np.eye(4)
self.__xforms['affine', 'id'] = worldToIdMat
self.__xforms['affine', 'pixdim'] = worldToPixdimMat
self.__xforms['affine', 'pixdim-flip'] = worldToPixFlipMat
self.__xforms['affine', 'reference'] = worldToRefMat
self.__xforms['affine', 'texture'] = worldToTexMat
self.__xforms['reference', 'reference'] = np.eye(4)
self.__xforms['reference', 'id'] = refToIdMat
self.__xforms['reference', 'pixdim'] = refToPixdimMat
self.__xforms['reference', 'pixdim-flip'] = refToPixFlipMat
self.__xforms['reference', 'affine'] = refToWorldMat
self.__xforms['reference', 'texture'] = refToTexMat
self.__xforms['texture', 'texture'] = np.eye(4)
self.__xforms['texture', 'id'] = texToIdMat
self.__xforms['texture', 'pixdim'] = texToPixdimMat
self.__xforms['texture', 'pixdim-flip'] = texToPixFlipMat
self.__xforms['texture', 'affine'] = texToWorldMat
self.__xforms['texture', 'reference'] = texToRefMat
[docs] @classmethod
def getVolumeProps(cls):
"""Overrides :meth:`DisplayOpts.getVolumeProps`. Returns a list
of property names which control the displayed volume/timepoint.
"""
return ['volume', 'volumeDim']
[docs] def roundVoxels(self, voxels, daxes=None, roundOther=False):
"""Round the given voxel coordinates to integers. This is a
surprisingly complicated operation.
FSLeyes and the NIFTI standard map integer voxel coordinates to the
voxel centre. For example, 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. But there are a
few problems with breaking ties when rounding...
The numpy.round function breaks ties (e.g. 7.5) by rounding to the
nearest *even* integer, which can cause funky behaviour. So instead
of using numpy.round, we take floor(x+0.5), to force consistent
behaviour (i.e. always rounding central values up).
The next problem is that we have to round the voxel coordaintes
carefully, depending on the orientation of the voxel axis w.r.t. the
display axis. We want to round in the same direction in the display
coordinate system, regardless of the voxel orientation. So we need to
check the orientation of the voxel axis, and round down or up
accordingly.
This is to handle scenarios where we have two anatomically aligned
images, but with opposing storage orders (e.g. one stored
neurologically, and one stored radiologically). If we have such
images, and the display location is on a voxel boundary, we want the
voxel coordinates for one image to be rounded in the same anatomical
direction (i.e. the same direction in the display coordinate
system). Otherwise the same display location will map to mis-aligned
voxels in the two images, because the voxel coordinate rounding will
move in anatomically opposing directions.
This method also prevents coordinates that are close to 0 from being
set to -1, and coordinates that are close to the axis size from being
set to (size + 1). In other words, voxel coordinates which are on the
low or high boundaries will be rounded so as to be valid voxel
coordinates.
:arg voxels: A ``(N, 3)`` ``numpy`` array containing the voxel
coordinates to be rounded.
:arg daxes: Display coordinate system axes along which to round
the coordinates (defaults to all axes).
:arg roundOther: If ``True``, any voxel axes which are not in
``daxes`` will still be rounded, but not with an
orientation-specific rounding convention.
:returns: The ``voxels``, rounded appropriately.
"""
if not self.__child:
raise RuntimeError('roundVoxels cannot be called on '
'a parent NiftiOpts instance')
if daxes is None:
daxes = list(range(3))
shape = self.overlay.shape[:3]
ornts = self.overlay.axisMapping(self.getTransform('display', 'voxel'))
# We start by truncating the precision
# of the coordinates, so that values
# which are very close to voxel midpoints
# (e.g. 0.49999), get rounded to 0.5.
voxels = np.round(voxels, decimals=3)
# Keep track of the voxel axes that
# have had the rounding treatment
roundedAxes = []
for dax in daxes:
ornt = ornts[dax]
vax = abs(ornt) - 1
vals = voxels[:, vax]
roundedAxes.append(vax)
# Identify values which are close
# to the low or high bounds - we
# will clamp them after rounding.
#
# This is a third rounding problem
# which is not documented above -
# we clamp low/high values to avoid
# them under/overflowing in the
# floor/ceil operations below
closeLow = np.isclose(vals, -0.5)
closeHigh = np.isclose(vals, shape[vax] - 0.5)
# Round in a direction which is
# dictated by the image orientation
if ornt < 0: vals = np.floor(vals + 0.5)
else: vals = np.ceil( vals - 0.5)
# Clamp low/high voxel coordinates
vals[closeLow] = 0
vals[closeHigh] = shape[vax] - 1
voxels[:, vax] = vals
# If the roundOther flag is true,
# we round all other voxel axes
# in a more conventional manner
# (but still using floor(v + 0.5)
# rather than round to avoid
# annoying numpy even/odd behaviour).
if roundOther:
for vax in range(3):
if vax not in roundedAxes:
voxels[:, vax] = np.floor(voxels[:, vax] + 0.5)
return voxels
[docs] def getVoxel(self, xyz=None, clip=True, vround=True):
"""Calculates and returns the voxel coordinates corresponding to the
given location (assumed to be in the display coordinate system) for
the :class:`.Nifti` associated with this ``NiftiOpts`` 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``.
: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 not self.__child:
raise RuntimeError('getVoxel cannot be called on '
'a parent NiftiOpts instance')
if xyz is not None: x, y, z = xyz
else: x, y, z = self.displayCtx.location.xyz
overlay = self.overlay
vox = self.transformCoords([[x, y, z]],
'display',
'voxel',
vround=vround)[0]
if vround:
vox = [int(v) for v in vox]
if not clip:
return vox
for ax in (0, 1, 2):
if vox[ax] < 0 or vox[ax] >= overlay.shape[ax]:
return None
return vox
[docs] def index(self, slc=None, atVolume=True):
"""Given a slice object ``slc``, which indexes into the X, Y, and Z
dimensions, fills it to slice every dimension of the image, using
the current :attr:`volume` and :attr:`volumeDim`, and saved values
for the other volume dimensions.
:arg slc: Something which can slice the first three dimensions
of the image. If ``None``, defaults to ``[:, :, :]``.
:arg atVolume: If ``True``, the returned slice will index the current
:attr:`volume` of the current :attr:`volumeDim`.
Otherwise the returned slice will index across the whole
:attr:`volumeDim`.
"""
if slc is None:
slc = [slice(None), slice(None), slice(None)]
if self.overlay.ndim <= 3:
return tuple(slc)
newSlc = [None] * self.overlay.ndim
newSlc[:3] = slc
newSlc[ 3:] = self.getAttribute('volume', 'cache')
vdim = self.volumeDim + 3
if atVolume: newSlc[vdim] = self.volume
else: newSlc[vdim] = slice(None)
return tuple(newSlc)