Commit 1299e7e6 authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

ENH: DisplayContext.displaySpace can now be set to "scaledVoxel". Update

comments about origin being centre, not corner, of voxel (0,0,0).
parent 92541b8a
......@@ -165,7 +165,7 @@ class DisplayContext(props.SyncableHasProperties):
"""
displaySpace = props.Choice(('world', ))
displaySpace = props.Choice(('world', 'scaledVoxel'))
"""The *space* in which overlays are displayed. This property defines the
display coordinate system for this ``DisplayContext``. When it is changed,
the :attr:`.NiftiOpts.transform` property of all :class:`.Nifti` overlays
......@@ -179,7 +179,15 @@ class DisplayContext(props.SyncableHasProperties):
their affine transformation matrix - the :attr:`.NiftiOpts.transform`
property for every ``Nifti`` overlay is set to ``affine``.
2. **Reference image** space
2. **Scaled-voxel** space (a.k.a. ``'scaledVoxel'``)
All :class:`.Nifti` overlays are displayed in a scaled-voxel coordinate
system, with origin set to the centre of voxel ``(0, 0, 0)``, and
voxels scaled by image pixdims. This is accomplished by setting the
:attr:`.NiftiOpts.transform` property for every ``Nifti`` overlay to
``pixdim-flip``.
3. **Reference image** space
A single :class:`.Nifti` overlay is selected as a *reference* image,
and is displayed in scaled voxel space (with a potential L/R flip for
......@@ -508,6 +516,7 @@ class DisplayContext(props.SyncableHasProperties):
if len(self.__overlayList) == 0:
return True
opts = None
space = self.displaySpace
# Display space is either 'world', or an image.
......@@ -518,11 +527,26 @@ class DisplayContext(props.SyncableHasProperties):
# right).
if space == 'world':
return False
else:
opts = self.getOpts(space)
xform = opts.getTransform('pixdim-flip', 'display')
return npla.det(xform) > 0
# If space == scaledVoxel, we decide based on
# the currently selected overlay. Otherwise
# (reference image), we decide based on the ref
# image.
elif space == 'scaledVoxel':
space = self.getSelectedOverlay()
# Use the FSL / FLIRT convention - if the affine
# determinant is negative, assume neurological
# storage order.
if space is not None:
ref = self.getOpts(space).referenceImage
if ref is not None:
opts = self.getOpts(space)
xform = opts.getTransform('pixdim-flip', 'display')
return npla.det(xform) > 0
# no nifti overlays loaded
return False
def selectOverlay(self, overlay):
......@@ -647,9 +671,9 @@ class DisplayContext(props.SyncableHasProperties):
def defaultDisplaySpace(self, ds):
"""Sets the :meth:`defaultDisplaySpace`.
:arg ds: Either ``'ref'`` or ``'world'``.
:arg ds: Either ``'ref'``, ``'scaledVoxel'``, or ``'world'``.
"""
if ds not in ('world', 'ref'):
if ds not in ('world', 'scaledVoxel', 'ref'):
raise ValueError('Invalid default display space: {}'.format(ds))
self.__defaultDisplaySpace = ds
......@@ -849,7 +873,7 @@ class DisplayContext(props.SyncableHasProperties):
if isinstance(overlay, fslimage.Nifti):
choices.append(overlay)
choices.append('world')
choices.extend(('world', 'scaledVoxel'))
choiceProp.setChoices(choices, instance=self)
......@@ -876,9 +900,10 @@ class DisplayContext(props.SyncableHasProperties):
# get called before we have registered a
# listener on the bounds property.
with props.skip(opts, 'bounds', self.__name, ignoreInvalid=True):
if space == 'world': opts.transform = 'affine'
elif image is space: opts.transform = 'pixdim-flip'
else: opts.transform = 'reference'
if space == 'world': opts.transform = 'affine'
elif space == 'scaledVoxel': opts.transform = 'pixdim-flip'
elif image is space: opts.transform = 'pixdim-flip'
else: opts.transform = 'reference'
def __displaySpaceChanged(self, *a):
......@@ -1141,14 +1166,23 @@ class DisplayContext(props.SyncableHasProperties):
self.location = self.worldLocation
return
ref = self.displaySpace
if self.displaySpace == 'scaledVoxel':
ref = self.getSelectedOverlay()
srcSpace = 'pixdim-flip'
else:
ref = self.displaySpace
srcSpace = 'display'
if ref is None:
return
opts = self.getOpts(ref)
if dest == 'world':
with props.skip(self, 'location', self.__name):
self.worldLocation = opts.transformCoords(
self.location, 'display', 'world')
self.location, srcSpace, 'world')
else:
with props.skip(self, 'worldLocation', self.__name):
self.location = opts.transformCoords(
self.worldLocation, 'world', 'display')
self.worldLocation, 'world', srcSpace)
......@@ -27,15 +27,16 @@ in one of several ways:
**scaled voxels** (a.k.a. ``pixdim``) The image data voxel
coordinates are scaled by the ``pixdim`` values
stored in the NIFTI header.
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.
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)``.
**world** (a.k.a. ``affine``) The image data voxel
coordinates are transformed by the
......@@ -443,6 +444,8 @@ class NiftiOpts(fsldisplay.DisplayOpts):
# on the value of displaySpace
if ds == 'world':
voxToRefMat = voxToWorldMat
elif ds == 'scaledVoxel':
voxToRefMat = voxToPixFlipMat
elif ds is self.overlay:
voxToRefMat = voxToPixFlipMat
else:
......@@ -455,7 +458,7 @@ class NiftiOpts(fsldisplay.DisplayOpts):
# the note on coordinate systems at
# the top of this file).
voxToTexMat = affine.scaleOffsetXform(tuple(1.0 / shape),
tuple(0.5 / shape))
tuple(0.5 / shape))
idToVoxMat = affine.invert(voxToIdMat)
idToPixdimMat = affine.concat(voxToPixdimMat, idToVoxMat)
......
......@@ -2607,6 +2607,8 @@ def applySceneArgs(args, overlayList, displayCtx, sceneOpts):
if args.displaySpace == 'world':
displaySpace = 'world'
elif args.displaySpace == 'scaledVoxel':
displaySpace = 'scaledVoxel'
elif args.displaySpace is not None:
try:
......
......@@ -345,11 +345,11 @@ class OverlayInfoPanel(ctrlpanel.ControlPanel):
dsDisplay = self.displayCtx.getDisplay(dsImg)
displaySpace = displaySpace.format(dsDisplay.name)
else:
log.warn('{} transform ({}) seems to be out '
'of date (display space: {})'.format(
overlay,
opts.transform,
self.displayCtx.displaySpace))
log.warning('{} transform ({}) seems to be out '
'of date (display space: {})'.format(
overlay,
opts.transform,
self.displayCtx.displaySpace))
dataType = strings.nifti.get(('datatype', int(hdr['datatype'])),
'Unknown')
......
......@@ -1274,7 +1274,9 @@ properties = TypeDict({
choices = TypeDict({
'DisplayContext.displaySpace' : {'world' : 'World coordinates'},
'DisplayContext.displaySpace' : {'world' : 'World coordinates',
'scaledVoxel' : 'Scaled voxel coordinates',
},
'SceneOpts.colourBarLocation' : {'top' : 'Top',
'bottom' : 'Bottom',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment