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

Re-did axis label orientation calculation; hopefully it is better

defined (and correct!).
parent 25fa4934
No related branches found
No related tags found
No related merge requests found
......@@ -26,22 +26,23 @@ import fsl.data.imagefile as imagefile
log = logging.getLogger(__name__)
L2R = 0
R2L = 1
P2A = 2
A2P = 3
I2S = 4
S2I = 5
orientationLabels = {
L2R : ('L', 'R'),
R2L : ('R', 'L'),
P2A : ('P', 'A'),
A2P : ('A', 'P'),
S2I : ('S', 'I'),
I2S : ('I', 'S')
}
# Constants which represent the orientation of an axis,
# in either voxel or world space.
ORIENT_UNKNOWN = -1
ORIENT_L2R = 0
ORIENT_R2L = 1
ORIENT_P2A = 2
ORIENT_A2P = 3
ORIENT_I2S = 4
ORIENT_S2I = 5
# Constants from the NIFTI1 specification that define
# the 'space' in which an image is assumed to be.
NIFTI_XFORM_UNKNOWN = 0
NIFTI_XFORM_SCANNER_ANAT = 1
NIFTI_XFORM_ALIGNED_ANAT = 2
NIFTI_XFORM_TALAIRACH = 3
NIFTI_XFORM_MNI_152 = 4
def _loadImageFile(filename):
"""Given the name of an image file, loads it using nibabel.
......@@ -384,15 +385,78 @@ class Image(props.HasProperties):
else: return worldp
def getOrientation(self, axis):
def getXFormCode(self):
"""This method returns the code contained in the NIFTI1 header,
indicating the space to which the (transformed) image is oriented.
"""
sform_code = self.nibImage.get_header()['sform_code']
qform_code = self.nibImage.get_header()['qform_code']
# if the qform and sform codes don't
# match, I don't know what to do
if sform_code != qform_code: return NIFTI_XFORM_UNKNOWN
# Invalid values
elif sform_code > 4: return NIFTI_XFORM_UNKNOWN
elif sform_code < 0: return NIFTI_XFORM_UNKNOWN
# All is well
else: return sform_code
def getWorldOrientation(self, axis):
"""Returns a code representing the orientation of the specified axis
in world space.
This method returns one of the following values, indicating the
direction in which coordinates along the specified axis increase:
- :attr:`~fsl.data.image.ORIENT_L2R`: Left to right
- :attr:`~fsl.data.image.ORIENT_R2L`: Right to left
- :attr:`~fsl.data.image.ORIENT_A2P`: Anterior to posterior
- :attr:`~fsl.data.image.ORIENT_P2A`: Posterior to anterior
- :attr:`~fsl.data.image.ORIENT_I2S`: Inferior to superior
- :attr:`~fsl.data.image.ORIENT_S2I`: Superior to inferior
- :attr:`~fsl.data.image.ORIENT_UNKNOWN`: Orientation is unknown
The returned value is dictated by the XForm code contained in the
image file header (see the :meth:`getXFormCode` method). Basically,
if the XForm code is 'unknown', this method will return -1 for all
axes. Otherwise, it is assumed that the image is in RAS orientation
(i.e. the X axis increases from left to right, the Y axis increases
from posterior to anterior, and the Z axis increases from inferior
to superior).
"""
if self.getXFormCode() == NIFTI_XFORM_UNKNOWN:
return -1
if axis == 0: return ORIENT_L2R
elif axis == 1: return ORIENT_P2A
elif axis == 2: return ORIENT_I2S
else: return -1
def getVoxelOrientation(self, axis):
"""Returns a code representing the (estimated) orientation of the
specified voxelwise axis.
See the :meth:`getWorldOrientation` method for a description
of the return value.
"""
if self.getXFormCode() == NIFTI_XFORM_UNKNOWN:
return -1
# the aff2axcodes returns one code for each
# axis in the image array (i.e. in voxel space),
# which denotes the real world direction
code = nib.orientations.aff2axcodes(self.nibImage.get_affine(),
((R2L, L2R),
(A2P, P2A),
(S2I, I2S)))[axis]
return orientationLabels[code]
((ORIENT_R2L, ORIENT_L2R),
(ORIENT_A2P, ORIENT_P2A),
(ORIENT_S2I, ORIENT_I2S)))[axis]
return code
def _transform(self, p, a, axes):
......
......@@ -5,6 +5,8 @@
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
import fsl.data.image as fslimage
from views .orthopanel import OrthoPanel
from views .lightboxpanel import LightBoxPanel
from views .timeseriespanel import TimeSeriesPanel
......@@ -49,3 +51,48 @@ orthoConfigMenu = '{} display'
lightBoxConfigMenu = '{} display'
locationPanelOutOfBounds = 'Out of bounds'
imageAxisLowLongLabels = {
fslimage.ORIENT_A2P : 'Anterior',
fslimage.ORIENT_P2A : 'Posterior',
fslimage.ORIENT_L2R : 'Left',
fslimage.ORIENT_R2L : 'Right',
fslimage.ORIENT_I2S : 'Inferior',
fslimage.ORIENT_S2I : 'Superior',
fslimage.ORIENT_UNKNOWN : 'Unknown'}
imageAxisHighLongLabels = {
fslimage.ORIENT_A2P : 'Posterior',
fslimage.ORIENT_P2A : 'Anterior',
fslimage.ORIENT_L2R : 'Right',
fslimage.ORIENT_R2L : 'Left',
fslimage.ORIENT_I2S : 'Superior',
fslimage.ORIENT_S2I : 'Inferior',
fslimage.ORIENT_UNKNOWN : 'Unknown'}
imageAxisLowShortLabels = {
fslimage.ORIENT_A2P : 'A',
fslimage.ORIENT_P2A : 'P',
fslimage.ORIENT_L2R : 'L',
fslimage.ORIENT_R2L : 'R',
fslimage.ORIENT_I2S : 'I',
fslimage.ORIENT_S2I : 'S',
fslimage.ORIENT_UNKNOWN : '?'}
imageAxisHighShortLabels = {
fslimage.ORIENT_A2P : 'P',
fslimage.ORIENT_P2A : 'A',
fslimage.ORIENT_L2R : 'R',
fslimage.ORIENT_R2L : 'L',
fslimage.ORIENT_I2S : 'S',
fslimage.ORIENT_S2I : 'I',
fslimage.ORIENT_UNKNOWN : '?'}
imageSpaceLabels = {
fslimage.NIFTI_XFORM_UNKNOWN : 'Unknown',
fslimage.NIFTI_XFORM_SCANNER_ANAT : 'Scanner anatomical',
fslimage.NIFTI_XFORM_ALIGNED_ANAT : 'Aligned anatomical',
fslimage.NIFTI_XFORM_TALAIRACH : 'Talairach',
fslimage.NIFTI_XFORM_MNI_152 : 'MNI152'}
......@@ -19,6 +19,8 @@ log = logging.getLogger(__name__)
import wx
import props
import fsl.data.image as fslimage
import fsl.fslview.gl as fslgl
import fsl.fslview.gl.wxglslicecanvas as slicecanvas
import canvaspanel
......@@ -221,16 +223,25 @@ class OrthoPanel(canvaspanel.CanvasPanel):
self.bindProps('invertZ_X', self._zcanvas, 'invertX')
self.bindProps('invertZ_Y', self._zcanvas, 'invertY')
llName = '{}_layout'.format(self._name)
self.addListener('layout', llName, self._layoutChanged)
self.addListener('showColourBar', llName, self._layoutChanged)
self.addListener('colourBarLocation', llName, self._layoutChanged)
self.addListener('showLabels', llName, self._toggleLabels)
# Callbacks for ortho panel layout options
self.addListener('layout', self._name, self._layoutChanged)
self.addListener('showColourBar', self._name, self._layoutChanged)
self.addListener('colourBarLocation', self._name, self._layoutChanged)
self.addListener('showLabels', self._name, self._toggleLabels)
# Callbacks for image list/selected image changes
self._imageList.addListener( 'images',
self._name,
self._imageListChanged)
self._displayCtx.addListener('selectedImage',
self._name,
self._imageListChanged)
self._imageListChanged()
self._layoutChanged()
self._toggleLabels()
# Callbacks for mouse events on the three xcanvases
self._xcanvas.Bind(wx.EVT_LEFT_DOWN, self._onMouseEvent)
self._ycanvas.Bind(wx.EVT_LEFT_DOWN, self._onMouseEvent)
self._zcanvas.Bind(wx.EVT_LEFT_DOWN, self._onMouseEvent)
......@@ -238,20 +249,16 @@ class OrthoPanel(canvaspanel.CanvasPanel):
self._ycanvas.Bind(wx.EVT_MOTION, self._onMouseEvent)
self._zcanvas.Bind(wx.EVT_MOTION, self._onMouseEvent)
# Callback for the display context location - when it
# changes, update the displayed canvas locations
def move(*a):
if self.posSync:
self.setPosition(*self._displayCtx.location)
self.setPosition(*self._displayCtx.location)
self._displayCtx.addListener('location', self._name, move)
def onDestroy(ev):
self._displayCtx.removeListener('location', self._name)
ev.Skip()
self.Bind(wx.EVT_WINDOW_DESTROY, onDestroy)
self.Bind(wx.EVT_SIZE, self._resize)
# Callbacks for toggling x/y/z canvas display
def toggle(canvas, toggle):
self._canvasSizer.Show(canvas, toggle)
if self.layout.lower() == 'grid':
......@@ -268,6 +275,55 @@ class OrthoPanel(canvaspanel.CanvasPanel):
lambda *a: toggle(self._zCanvasPanel,
self.showZCanvas))
# Do some cleaning up if/when this panel is destroyed
self.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroy)
# And finally, call the _resize method to
# re-layout things when this panel is resized
self.Bind(wx.EVT_SIZE, self._resize)
def _imageListChanged(self, *a):
"""Called when the image list or selected image is changed.
Adds a listener to the currently selected image, to listen
for changes on its affine transformation matrix.
"""
if len(self._imageList) == 0: return
for i, img in enumerate(self._imageList):
# Update anatomy labels when the image
# transformation matrix changes
if i == self._displayCtx.selectedImage:
img.addListener('transform', self._name, self._toggleLabels)
else:
img.removeListener('transform', self._name)
def _onDestroy(self, ev):
"""Called when this panel is destroyed.
The display context and image list will probably live longer than
this OrthoPanel. So when this panel is destroyed, all those
registered listeners are removed.
"""
ev.Skip()
# Do nothing if the destroyed window is not
# this panel (i.e. it is a child of this panel)
if ev.GetEventObject() != self: return
self._displayCtx.removeListener('location', self._name)
self._displayCtx.removeListener('selectedImage', self._name)
self._imageList .removeListener('images', self._name)
# The _imageListChanged method adds
# listeners to individual images,
# so we have to remove them too
for img in self._imageList:
img.removeListener('transform', self._name)
def _resize(self, ev):
"""
......@@ -284,34 +340,79 @@ class OrthoPanel(canvaspanel.CanvasPanel):
def _toggleLabels(self, *a):
"""Shows/hides labels depicting anatomical orientation on each canvas.
"""
allLabels = [self._xLeftLabel, self._xRightLabel,
self._xTopLabel, self._xBottomLabel,
self._yLeftLabel, self._yRightLabel,
self._yTopLabel, self._yBottomLabel,
self._zLeftLabel, self._zRightLabel,
self._zTopLabel, self._zBottomLabel]
# Are we showing or hiding the labels?
if self.showLabels: show = True
else: show = False
self._xLeftLabel .Show(show)
self._xRightLabel .Show(show)
self._xTopLabel .Show(show)
self._xBottomLabel.Show(show)
self._yLeftLabel .Show(show)
self._yRightLabel .Show(show)
self._yTopLabel .Show(show)
self._yBottomLabel.Show(show)
self._zLeftLabel .Show(show)
self._zRightLabel .Show(show)
self._zTopLabel .Show(show)
self._zBottomLabel.Show(show)
self._xLeftLabel .SetLabel('?')
self._xRightLabel .SetLabel('?')
self._xTopLabel .SetLabel('?')
self._xBottomLabel.SetLabel('?')
self._yLeftLabel .SetLabel('?')
self._yRightLabel .SetLabel('?')
self._yTopLabel .SetLabel('?')
self._yBottomLabel.SetLabel('?')
self._zLeftLabel .SetLabel('?')
self._zRightLabel .SetLabel('?')
self._zTopLabel .SetLabel('?')
self._zBottomLabel.SetLabel('?')
for lbl in allLabels:
lbl.Show(show)
# If we're hiding the labels, do no more
if not show:
return
# Default colour is white - if the orientation labels
# cannot be determined, the background colour will be
# changed to red
colour = 'white'
if len(self._imageList) > 0:
image = self._imageList[self._displayCtx.selectedImage]
# The image is being displayed as it is stored on
# disk - the image.getOrientation method calculates
# and returns labels for each voxelwise axis.
if image.transform in ('pixdim', 'id'):
xorient = image.getVoxelOrientation(0)
yorient = image.getVoxelOrientation(1)
zorient = image.getVoxelOrientation(2)
# The image is being displayed in 'real world' space -
# the definition of this space may be present in the
# image meta data
else:
xorient = image.getWorldOrientation(0)
yorient = image.getWorldOrientation(1)
zorient = image.getWorldOrientation(2)
if fslimage.ORIENT_UNKNOWN in (xorient, yorient, zorient):
colour = 'red'
# Imported here to avoid circular import issues
import fsl.fslview.strings as strings
xlo = strings.imageAxisLowShortLabels[ xorient]
ylo = strings.imageAxisLowShortLabels[ yorient]
zlo = strings.imageAxisLowShortLabels[ zorient]
xhi = strings.imageAxisHighShortLabels[xorient]
yhi = strings.imageAxisHighShortLabels[yorient]
zhi = strings.imageAxisHighShortLabels[zorient]
for lbl in allLabels:
lbl.SetForegroundColour(colour)
self._xLeftLabel .SetLabel(ylo)
self._xRightLabel .SetLabel(yhi)
self._xBottomLabel.SetLabel(zlo)
self._xTopLabel .SetLabel(zhi)
self._yLeftLabel .SetLabel(xlo)
self._yRightLabel .SetLabel(xhi)
self._yBottomLabel.SetLabel(zlo)
self._yTopLabel .SetLabel(zhi)
self._zLeftLabel .SetLabel(xlo)
self._zRightLabel .SetLabel(xhi)
self._zBottomLabel.SetLabel(ylo)
self._zTopLabel .SetLabel(yhi)
self._xCanvasPanel.Layout()
self._yCanvasPanel.Layout()
......
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