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

Merge branch 'display_world_location_stuff'. Haven't thoroughly tested

the merge yet ..
parents 8ed6bbf2 81f49290
No related branches found
No related tags found
No related merge requests found
Showing
with 613 additions and 674 deletions
TODO LIST -*- mode: org; -*-
A mixture of things to do, things which might be nice to do, and things which
......@@ -6,7 +7,16 @@ https://internal.fmrib.ox.ac.uk/i-wiki/Analysis/Todos/FSLView regularly, too.
* Bugs to fix
** 'World location' should always be defined by the NIFTI affine transformation
** Canvases are not displayed properly when labels are shown.
** World location is clamped to voxel centre when voxel location changes. Not sure if this really needs to be fixed ...
** Linux/Jalapeno Bad window crash
** No worky in VNC session.
** WX Bugs
*** Floating panels get moved about when opening a new floating panel.
*** Make range widget limit buttons expand properly. Ugh. Works fine under linux. This looks like an OSX Mavericks bug.
*** Wild cards in image file load (WX/OSX issue)
** OSMesa render doesn't like circle voxels
** DONE 'World location' should always be defined by the NIFTI affine transformation
Currently your 'world coordinate system is equivalent to the display
coordinate system. You need to ensure that all displayed 'world coordinates'
are the ones generated by the image affine transformation matrix.
......@@ -15,13 +25,6 @@ You should remove the 'transform' property from the Image class. This should
only be a property on the ImageDisplay class. This means that it might be
worth moving the transformation matrix stuff out of the Image class, and
perhaps into a standalone module...
** Linux/Jalapeno Bad window crash
** No worky in VNC session.
** WX Bugs
*** Floating panels get moved about when opening a new floating panel.
*** Make range widget limit buttons expand properly. Ugh. Works fine under linux. This looks like an OSX Mavericks bug.
*** Wild cards in image file load (WX/OSX issue)
** OSMesa render doesn't like circle voxels
** DONE Ortho anatomical labels not being refreshed
** DONE Cursor location in lightbox can be out of the slices
** DONE LightBox screwy on image transformation change
......@@ -59,15 +62,19 @@ perhaps into a standalone module...
** DONE Graceful handling of bad input filenames
** DONE Aspect ratio on slicecanvas zoom, and panning is broken.
* Little things
** Allow display resolution greater than image resolution
** CLI options
*** Show/hide labels
*** Ortho layout (horiz/vert/grid)
*** Check that show/hide cursor is being applied correctly
*** A switch to automatically choose colour maps/clipping thresholds for a provided set of images (like Eugene's fast fslview startup script)
** Scale values in colour map according to largest value?
** ColourBarPanel should only do stuff if it is being displayed
** ImageDisplay objects could be managed by DisplayContetx, rather than being tacked on as an image attribute. The same could be said of GLObjects.
** Arrow keys on image list change selected image
** 'Centre view' option on slice canvases
** Props package - add a setting on HasProperties instances to allow validation of all properties to be disabled for instances which don't need it (see comments at properties.py:264).
** Props package - floating point property equality - if difference is < some small value, the property should be considered unchanged, and listeners should not be notified.
** Optimisations - tune image._transform, and glimage.genVertexData functions. Go back to using triangle strips?
** Get rid of imagefile module, put functionality in image module.
** View/view config panels persist across shutdown/restart
......@@ -93,6 +100,7 @@ Display either the cbar for the current image, or cbars for all images.
** IN PROGRESS 'Actions'? - Make 'Add File' and 'Add Std' menu items actions.
Need to be able to specify the order that actions appear in the menu - perhaps
just hard code in fsl/fslview/actions/__init__.py
** DONE Preserve image world location when transformation changed.
** DONE Orientation labels on slice canvas.
** DONE Display image space code in cursor panel
** DONE Turn your test_gl14_coords.py script into a fslview ViewPanel; it might come in handy.
......
......@@ -8,26 +8,24 @@
"""Classes for representing 3D/4D images and collections of said images."""
import os
import sys
import logging
import tempfile
import collections
import subprocess as sp
import os.path as op
import subprocess as sp
import os.path as op
import numpy as np
import numpy.linalg as linalg
import nibabel as nib
import numpy as np
import nibabel as nib
import props
import fsl.data.imagefile as imagefile
import fsl.data.imagefile as imagefile
import fsl.utils.transform as transform
log = logging.getLogger(__name__)
# Constants which represent the orientation of an axis,
# in either voxel or world space.
# Constants which represent the orientation
# of an axis, in either voxel or world space.
ORIENT_UNKNOWN = -1
ORIENT_L2R = 0
ORIENT_R2L = 1
......@@ -44,9 +42,6 @@ NIFTI_XFORM_ALIGNED_ANAT = 2
NIFTI_XFORM_TALAIRACH = 3
NIFTI_XFORM_MNI_152 = 4
# My own code, used to indicate that the
# image is being displayed in voxel space
NIFTI_XFORM_VOXEL = 5
def _loadImageFile(filename):
"""Given the name of an image file, loads it using nibabel.
......@@ -120,30 +115,30 @@ class Image(props.HasProperties):
The following attributes are present on an :class:`Image` object:
:ivar nibImage: The :mod:`nibabel` image object.
:ivar data: A reference to the image data, stored as a
:mod`numpy` array.
:ivar shape: A list/tuple containing the number of voxels
along each image dimension.
:ivar nibImage: The :mod:`nibabel` image object.
:ivar pixdim: A list/tuple containing the size of one voxel
along each image dimension.
:ivar data: A reference to the image data, stored as a
:mod`numpy` array.
:ivar voxToWorldMat: A 4*4 array specifying the affine transformation
for transforming voxel coordinates into real world
coordinates.
:ivar shape: A list/tuple containing the number of voxels
along each image dimension.
:ivar worldToVoxMat: A 4*4 array specifying the affine transformation
for transforming real world coordinates into voxel
coordinates.
:ivar pixdim: A list/tuple containing the size of one voxel
along each image dimension.
:ivar imageFile: The name of the file that the image was loaded from.
:ivar voxToWorldMat: A 4*4 array specifying the affine transformation
for transforming voxel coordinates into real world
coordinates.
:ivar worldToVoxMat: A 4*4 array specifying the affine transformation
for transforming real world coordinates into voxel
coordinates.
:ivar imageFile: The name of the file that the image was loaded from.
:ivar tempFile: The name of the temporary file which was created (in
the event that the image was large and was gzipped -
see :func:`_loadImageFile`).
:ivar tempFile: The name of the temporary file which was created (in
the event that the image was large and was gzipped -
see :func:`_loadImageFile`).
"""
......@@ -156,27 +151,6 @@ class Image(props.HasProperties):
"""This property defines the type of image data."""
transform = props.Choice(
collections.OrderedDict([
('affine', 'Use qform/sform transformation matrix'),
('pixdim', 'Use pixdims only'),
('id', 'Do not use qform/sform or pixdims')]),
default='pixdim')
"""This property defines how the image should be transformd into real world
space.
- ``affine``: Use the affine transformation matrix stored in the image
(the ``qform``/``sform`` fields in NIFTI1 headers).
- ``pixdim``: Scale voxel sizes by the ``pixdim`` fields in the image
header.
- ``id``: Perform no scaling or transformation - voxels will be
interpreted as :math:`1mm^3` isotropic, with the origin at voxel
(0,0,0).
"""
name = props.String()
"""The name of this image."""
......@@ -221,16 +195,11 @@ class Image(props.HasProperties):
self.tempFile = None
self.imageFile = None
self.data = self.nibImage.get_data()
self.shape = self.nibImage.get_shape()
self.pixdim = self.nibImage.get_header().get_zooms()
self.addListener(
'transform',
'{}_{}'.format(self.__class__.__name__, self.name),
self._transformChanged)
self._transformChanged()
self.data = self.nibImage.get_data()
self.shape = self.nibImage.get_shape()
self.pixdim = self.nibImage.get_header().get_zooms()
self.voxToWorldMat = np.array(self.nibImage.get_affine())
self.worldToVoxMat = transform.invert(self.voxToWorldMat)
if len(self.shape) < 3 or len(self.shape) > 4:
raise RuntimeError('Only 3D or 4D images are supported')
......@@ -253,140 +222,7 @@ class Image(props.HasProperties):
def is4DImage(self):
"""Returns ``True`` if this image is 4D, ``False`` otherwise.
"""
return len(self.shape) > 3 and self.shape[3] > 1
def _transformChanged(self, *a):
"""This method is called when the :attr:`transform` property value
changes. It updates the :attr:`voxToWorldMat`, :attr:`worldToVoxMat`,
and :attr:`pixdim` attributes to reflect the new transformation
type.
"""
if self.transform == 'affine':
voxToWorldMat = self.nibImage.get_affine()
elif self.transform == 'pixdim':
pixdim = self.nibImage.get_header().get_zooms()
voxToWorldMat = np.diag([pixdim[0], pixdim[1], pixdim[2], 1.0])
elif self.transform == 'id':
voxToWorldMat = np.identity(4)
self.voxToWorldMat = np.array(voxToWorldMat, dtype=np.float32)
self.worldToVoxMat = linalg.inv(self.voxToWorldMat)
self.voxToWorldMat = self.voxToWorldMat.transpose()
self.worldToVoxMat = self.worldToVoxMat.transpose()
if self.transform == 'affine':
pixdim = [self.axisLength(ax) / self.shape[ax] for ax in range(3)]
elif self.transform == 'pixdim':
pixdim = self.nibImage.get_header().get_zooms()
elif self.transform == 'id':
pixdim = [1.0, 1.0, 1.0]
self.pixdim = pixdim
# for pixdim/identity transformations, we want the world
# location (0, 0, 0) to map to voxel location (0, 0, 0)
if self.transform in ['pixdim', 'id']:
for i in range(3):
self.voxToWorldMat[3, i] = self.pixdim[i] * 0.5
self.worldToVoxMat[3, i] = -0.5
log.debug('Image {} transformation matrix changed: {}'.format(
self.name, self.voxToWorldMat))
log.debug('Inverted matrix: {}'.format(self.worldToVoxMat))
def imageBounds(self, axis):
"""Return the bounds (min, max) of the image, in real world
The returned bounds give the coordinates, along the specified axis, of
a bounding box which contains the entire image.
"""
x, y, z = self.shape[:3]
x -= 0.5
y -= 0.5
z -= 0.5
points = np.zeros((8, 3), dtype=np.float32)
points[0, :] = [-0.5, -0.5, -0.5]
points[1, :] = [-0.5, -0.5, z]
points[2, :] = [-0.5, y, -0.5]
points[3, :] = [-0.5, y, z]
points[4, :] = [x, -0.5, -0.5]
points[5, :] = [x, -0.5, z]
points[6, :] = [x, y, -0.5]
points[7, :] = [x, y, z]
tx = self.voxToWorld(points)
lo = tx[:, axis].min()
hi = tx[:, axis].max()
return (lo, hi)
def axisLength(self, axis):
"""Return the length, in real world units, of the specified axis.
"""
points = np.zeros((2, 3), dtype=np.float32)
points[:] = [-0.5, -0.5, -0.5]
points[1, axis] = self.shape[axis] - 0.5
tx = self.voxToWorld(points)
# euclidean distance between each boundary point
return sum((tx[0, :] - tx[1, :]) ** 2) ** 0.5
def worldToVox(self, p, axes=None):
"""Transforms the given set of points in voxel coordinates to points
in world coordinates, according to the current :attr:`transform`.
The returned array is either a :class:`numpy.float64` array, or a
single ``float`` value, depending on the input. There is no guarantee
that the returned array of voxel coordinates is within the bounds of
the image shape. Parameters:
:arg p: N*A array, where N is the number of points, and A
is the number of axes to consider (default: 3).
:arg axes: If ``None``, it is assumed that the input p is a N*3
array, with each point being specified by x,y,z
coordinates. If a single value in the range (0-2),
it is assumed that p is a 1D array. Or, if a
sequence of 2 or 3 values, p must be an array of
N*2 or N*3, respectively.
"""
voxp = self._transform(p, self.worldToVoxMat, axes)
voxp = np.array(voxp, dtype=np.float64)
if voxp.size == 1: return voxp[0]
else: return voxp
def voxToWorld(self, p, axes=None):
"""Transforms the given set of points in world coordinates to
points in voxel coordinates, according to the current
:attr:`transform`.
The returned array is either a :class:`numpy.float64` array,
or a single ``float`` value, depending on the input. See the
:meth:`worldToVox` method for more details.
"""
worldp = self._transform(p, self.voxToWorldMat, axes)
if worldp.size == 1: return float(worldp)
else: return worldp
return len(self.shape) > 3 and self.shape[3] > 1
def getXFormCode(self):
......@@ -459,64 +295,7 @@ class Image(props.HasProperties):
(ORIENT_S2I, ORIENT_I2S)))[axis]
return code
def _transform(self, p, a, axes):
"""Used by the :meth:`worldToVox` and :meth:`voxToWorld` methods.
Transforms the given set of points ``p`` according to the given affine
transformation ``a``. The transformed points are returned as a
:class:``numpy.float64`` array.
"""
p = self._fillPoints(p, axes)
t = np.zeros((len(p), 3), dtype=np.float64)
x = p[:, 0]
y = p[:, 1]
z = p[:, 2]
t[:, 0] = x * a[0, 0] + y * a[1, 0] + z * a[2, 0] + a[3, 0]
t[:, 1] = x * a[0, 1] + y * a[1, 1] + z * a[2, 1] + a[3, 1]
t[:, 2] = x * a[0, 2] + y * a[1, 2] + z * a[2, 2] + a[3, 2]
if axes is None: axes = [0, 1, 2]
return t[:, axes]
def _fillPoints(self, p, axes):
"""Used by the :meth:`_transform` method. Turns the given array p into
a N*3 array of x,y,z coordinates. The array p may be a 1D array, or an
N*2 or N*3 array.
"""
if not isinstance(p, collections.Iterable): p = [p]
p = np.array(p)
if axes is None: return p
if not isinstance(axes, collections.Iterable): axes = [axes]
if p.ndim == 1:
p = p.reshape((len(p), 1))
if p.ndim != 2:
raise ValueError('Points array must be either one or two '
'dimensions')
if len(axes) != p.shape[1]:
raise ValueError('Points array shape does not match specified '
'number of axes')
newp = np.zeros((len(p), 3), dtype=p.dtype)
for i, ax in enumerate(axes):
newp[:, ax] = p[:, i]
return newp
def getAttribute(self, name):
"""Retrieve the attribute with the given name.
......@@ -524,6 +303,7 @@ class Image(props.HasProperties):
"""
return self._attributes[name]
def delAttribute(self, name):
"""Delete and return the value of the attribute with the given name.
......@@ -544,9 +324,7 @@ class ImageList(props.HasProperties):
"""Class representing a collection of images to be displayed together.
Contains a :class:`props.properties_types.List` property containing
:class:`Image` objects, and some other properties on which listeners may
register themselves to be notified when the properties of the image
collection changes (e.g. image bounds).
:class:`Image` objects.
An :class:`ImageList` object has a few wrapper methods around the
:attr:`images` property, allowing the :class:`ImageList` to be used
......@@ -564,16 +342,6 @@ class ImageList(props.HasProperties):
"""A list of :class:`Image` objects. to be displayed"""
bounds = props.Bounds(ndims=3)
"""This property contains the min/max values of
a bounding box (in real world coordinates) which
is big enough to contain all of the images in the
:attr:`images` list. This property shouid be
read-only, but I don't have a way to enforce it
(yet).
"""
def __init__(self, images=None):
"""Create an ImageList object from the given sequence of
:class:`Image` objects."""
......@@ -582,14 +350,6 @@ class ImageList(props.HasProperties):
self.images = images
self.addListener(
'images',
self.__class__.__name__,
self._imageListChanged)
# initialise image bounds
self._imageListChanged()
# set the _lastDir attribute,
# used by the addImages method
if len(images) == 0: self._lastDir = os.getcwd()
......@@ -655,55 +415,6 @@ class ImageList(props.HasProperties):
return True
def _imageListChanged(self, *a):
"""Called whenever an item is added or removed from the :attr:`images`
list. Registers listeners with the properties of each image, and
calls the :meth:`_updateImageBounds` method.
"""
for img in self.images:
# This may be called multiple times on each image,
# but it doesn't matter, as any listener which has
# previously been registered with an image will
# just be replaced by the new one here.
img.addListener(
'transform',
self.__class__.__name__,
self._updateImageBounds,
overwrite=True)
self._updateImageBounds()
def _updateImageBounds(self, *a):
"""Called whenever an item is added or removed from the
:attr:`images` list, or an image property changes. Updates
the :attr:`bounds` property.
"""
if len(self.images) == 0:
minBounds = [0.0, 0.0, 0.0]
maxBounds = [0.0, 0.0, 0.0]
else:
minBounds = 3 * [ sys.float_info.max]
maxBounds = 3 * [-sys.float_info.max]
for img in self.images:
for ax in range(3):
lo, hi = img.imageBounds(ax)
if lo < minBounds[ax]: minBounds[ax] = lo
if hi > maxBounds[ax]: maxBounds[ax] = hi
self.bounds[:] = [minBounds[0], maxBounds[0],
minBounds[1], maxBounds[1],
minBounds[2], maxBounds[2]]
# Wrappers around the images list property, allowing this
# ImageList object to be used as if it is actually a list.
def __len__( self): return self.images.__len__()
......
#!/usr/bin/env python
#
# locationpanel.py - Panel which displays controls allowing the user to change
# the currently displayed location in both real world and voxel coordinates
# (with the latter in terms of the currently selected image)
# the currently displayed location in both real world and voxel coordinates,
# both in the space of the currently selected image. These changes are
# propagated to the current display coordinate system location, managed by the
# display context (and external changes to the display context location are
# propagated back to the voxel/world location properties managed by a
# Location Panel).
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
import logging
import wx
import numpy as np
import props
import fsl.data.image as fslimage
import fsl.utils.transform as transform
import fsl.fslview.controlpanel as controlpanel
import imageselectpanel as imageselect
log = logging.getLogger(__name__)
class LocationPanel(controlpanel.ControlPanel, props.HasProperties):
"""
A wx.Panel which contains widgets for changing the currently displayed
......@@ -26,8 +35,12 @@ class LocationPanel(controlpanel.ControlPanel, props.HasProperties):
currently selected voxel.
"""
voxelLocation = props.Point(ndims=3, real=False, labels=('X', 'Y', 'Z'))
worldLocation = props.Point(ndims=3, real=True, labels=('X', 'Y', 'Z'))
def _adjustFont(self, label, by, weight):
"""
......@@ -50,46 +63,46 @@ class LocationPanel(controlpanel.ControlPanel, props.HasProperties):
controlpanel.ControlPanel.__init__(self, parent, imageList, displayCtx)
self._voxelPanel = wx.Panel(self)
self._locationPanel = wx.Panel(self)
self._voxelPanel = wx.Panel(self)
self._worldPanel = wx.Panel(self)
self._imageSelect = imageselect.ImageSelectPanel(
self, imageList, displayCtx)
self._locationLabel = wx.StaticText( self._locationPanel,
style=wx.ALIGN_LEFT)
self._spaceLabel = wx.StaticText( self._locationPanel,
style=wx.ALIGN_LEFT)
self._locationWidget = props.makeWidget(self, displayCtx, 'location')
self._worldLabel = wx.StaticText( self._worldPanel,
style=wx.ALIGN_LEFT)
self._spaceLabel = wx.StaticText( self._worldPanel,
style=wx.ALIGN_LEFT)
self._worldWidget = props.makeWidget(self, self, 'worldLocation')
self._dividerLine1 = wx.StaticLine( self, style=wx.LI_HORIZONTAL)
self._voxelWidget = props.makeWidget(self, self, 'voxelLocation')
self._dividerLine1 = wx.StaticLine( self, style=wx.LI_HORIZONTAL)
self._voxelWidget = props.makeWidget(self, self, 'voxelLocation')
self._dividerLine2 = wx.StaticLine( self, style=wx.LI_HORIZONTAL)
self._volumeLabel = wx.StaticText( self, style=wx.ALIGN_LEFT)
self._volumeWidget = props.makeWidget(self, displayCtx, 'volume')
self._dividerLine2 = wx.StaticLine( self, style=wx.LI_HORIZONTAL)
self._volumeLabel = wx.StaticText( self, style=wx.ALIGN_LEFT)
self._volumeWidget = props.makeWidget(self, displayCtx, 'volume')
self._voxelLabel = wx.StaticText(self._voxelPanel,
style=wx.ALIGN_LEFT)
self._valueLabel = wx.StaticText(self._voxelPanel,
style=wx.ALIGN_RIGHT)
self._adjustFont(self._locationLabel, -2, wx.FONTWEIGHT_LIGHT)
self._adjustFont(self._spaceLabel, -2, wx.FONTWEIGHT_LIGHT)
self._adjustFont(self._volumeLabel, -2, wx.FONTWEIGHT_LIGHT)
self._adjustFont(self._voxelLabel, -2, wx.FONTWEIGHT_LIGHT)
self._adjustFont(self._valueLabel, -2, wx.FONTWEIGHT_LIGHT)
self._adjustFont(self._worldLabel, -2, wx.FONTWEIGHT_LIGHT)
self._adjustFont(self._spaceLabel, -2, wx.FONTWEIGHT_LIGHT)
self._adjustFont(self._volumeLabel, -2, wx.FONTWEIGHT_LIGHT)
self._adjustFont(self._voxelLabel, -2, wx.FONTWEIGHT_LIGHT)
self._adjustFont(self._valueLabel, -2, wx.FONTWEIGHT_LIGHT)
self._locationLabel.SetLabel(strings.locationPanelLocationLabel)
self._voxelLabel .SetLabel(strings.locationPanelVoxelLabel)
self._volumeLabel .SetLabel(strings.locationPanelVolumeLabel)
self._worldLabel .SetLabel(strings.locationPanelWorldLabel)
self._voxelLabel .SetLabel(strings.locationPanelVoxelLabel)
self._volumeLabel.SetLabel(strings.locationPanelVolumeLabel)
self._locationSizer = wx.BoxSizer(wx.HORIZONTAL)
self._locationPanel.SetSizer(self._locationSizer)
self._worldSizer = wx.BoxSizer(wx.HORIZONTAL)
self._worldPanel.SetSizer(self._worldSizer)
self._locationSizer.Add(self._locationLabel, flag=wx.EXPAND)
self._locationSizer.Add((1, 1), flag=wx.EXPAND, proportion=1)
self._locationSizer.Add(self._spaceLabel, flag=wx.EXPAND)
self._worldSizer.Add(self._worldLabel, flag=wx.EXPAND)
self._worldSizer.Add((1, 1), flag=wx.EXPAND, proportion=1)
self._worldSizer.Add(self._spaceLabel, flag=wx.EXPAND)
self._voxelSizer = wx.BoxSizer(wx.HORIZONTAL)
self._voxelPanel.SetSizer(self._voxelSizer)
......@@ -101,15 +114,15 @@ class LocationPanel(controlpanel.ControlPanel, props.HasProperties):
self._sizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(self._sizer)
self._sizer.Add(self._imageSelect, flag=wx.EXPAND)
self._sizer.Add(self._locationPanel, flag=wx.EXPAND)
self._sizer.Add(self._locationWidget, flag=wx.EXPAND)
self._sizer.Add(self._dividerLine1, flag=wx.EXPAND)
self._sizer.Add(self._voxelPanel, flag=wx.EXPAND)
self._sizer.Add(self._voxelWidget, flag=wx.EXPAND)
self._sizer.Add(self._dividerLine2, flag=wx.EXPAND)
self._sizer.Add(self._volumeLabel, flag=wx.EXPAND)
self._sizer.Add(self._volumeWidget, flag=wx.EXPAND)
self._sizer.Add(self._imageSelect, flag=wx.EXPAND)
self._sizer.Add(self._worldPanel, flag=wx.EXPAND)
self._sizer.Add(self._worldWidget, flag=wx.EXPAND)
self._sizer.Add(self._dividerLine1, flag=wx.EXPAND)
self._sizer.Add(self._voxelPanel, flag=wx.EXPAND)
self._sizer.Add(self._voxelWidget, flag=wx.EXPAND)
self._sizer.Add(self._dividerLine2, flag=wx.EXPAND)
self._sizer.Add(self._volumeLabel, flag=wx.EXPAND)
self._sizer.Add(self._volumeWidget, flag=wx.EXPAND)
self._voxelPanel.Layout()
self.Layout()
......@@ -124,25 +137,28 @@ class LocationPanel(controlpanel.ControlPanel, props.HasProperties):
self._name,
self._volumeChanged)
self._displayCtx.addListener('location',
'{}_worldToVox'.format(self._name),
self._worldLocationChanged)
self._name,
self._displayLocationChanged)
self.addListener( 'voxelLocation',
'{}_voxToWorld'.format(self._name),
self._name,
self._voxelLocationChanged)
self.addListener( 'worldLocation',
self._name,
self._worldLocationChanged)
def onDestroy(ev):
ev.Skip()
self._imageList.removeListener( 'images', self._name)
self._displayCtx.removeListener('selectedImage', self._name)
self._displayCtx.removeListener('volume', self._name)
self._displayCtx.removeListener('location',
'{}_worldToVox'.format(self._name))
self._displayCtx.removeListener('location', self._name)
self.Bind(wx.EVT_WINDOW_DESTROY, onDestroy)
self._internalLocationChange = False
self._selectedImageChanged()
self._displayLocationChanged()
self._volumeChanged()
self._worldLocationChanged()
def _updateVoxelValue(self, voxVal=None):
......@@ -153,34 +169,47 @@ class LocationPanel(controlpanel.ControlPanel, props.HasProperties):
the value at the current voxel location is displayed.
"""
import fsl.fslview.strings as strings
if voxVal is not None:
self._valueLabel.SetLabel('{}'.format(voxVal))
self._voxelPanel.Layout()
return
image = self._imageList[self._displayCtx.selectedImage]
image = self._imageList[self._displayCtx.selectedImage]
display = image.getAttribute('display')
dloc = self._displayCtx.location.xyz
vloc = transform.transform([dloc], display.displayToVoxMat)[0]
vloc = np.round(vloc)
volume = self._displayCtx.volume
voxLoc = self.voxelLocation.xyz
if voxVal is None:
# There's a chance that the voxel location will temporarily
# be out of bounds when the selected image is changed.
# So we'll be safe and check them.
for i in range(3):
if voxLoc[i] < 0 or voxLoc[i] >= image.shape[i]:
return
# Test to see if the voxel
# location/volume is out of bounds
inBounds = True
for i in range(3):
if vloc[i] < 0 or vloc[i] >= image.shape[i]:
inBounds = False
if image.is4DImage():
if volume >= image.shape[3]:
inBounds = False
# If the value is out of the voxel bounds,
# display some appropriate text
if not inBounds:
voxVal = strings.locationPanelOutOfBounds
else:
# 3D image
if len(image.shape) == 3:
voxVal = image.data[voxLoc[0], voxLoc[1], voxLoc[2]]
voxVal = image.data[vloc[0], vloc[1], vloc[2]]
# 4D image. This will crash on non-4D images,
# which is intentional for the time being.
# No support for images of more
# than 4 dimensions at the moment
else:
if volume >= image.shape[3]:
return
voxVal = image.data[voxLoc[0], voxLoc[1], voxLoc[2], volume]
voxVal = image.data[vloc[0], vloc[1], vloc[2], volume]
if np.isnan(voxVal): voxVal = 'NaN'
elif np.isinf(voxVal): voxVal = 'Inf'
......@@ -194,24 +223,26 @@ class LocationPanel(controlpanel.ControlPanel, props.HasProperties):
:attr:`fsl.fslview.displaycontext.DisplayContext.volume`
property changes. Updates the voxel value label.
"""
import fsl.fslview.strings as strings
self._updateVoxelValue()
volume = self._displayCtx.volume
def _displayLocationChanged(self, *a):
if len(self._imageList) == 0:
return
if len(self._imageList) == 0: return
if self._internalLocationChange: return
image = self._imageList[self._displayCtx.selectedImage]
voxVal = None
if image.is4DImage():
if volume >= image.shape[3]:
voxVal = strings.locationPanelOutOfBounds
elif volume > 0:
voxVal = strings.locationPanelOutOfBounds
image = self._imageList[self._displayCtx.selectedImage]
display = image.getAttribute('display')
self._updateVoxelValue(voxVal)
dloc = self._displayCtx.location.xyz
vloc = transform.transform([dloc], display.displayToVoxMat)[ 0]
wloc = transform.transform([dloc], display.displayToWorldMat)[0]
self._internalLocationChange = True
self.voxelLocation.xyz = np.round(vloc)
self.worldLocation.xyz = wloc
self._internalLocationChange = False
self._updateVoxelValue()
def _voxelLocationChanged(self, *a):
......@@ -220,26 +251,22 @@ class LocationPanel(controlpanel.ControlPanel, props.HasProperties):
change on to the display context world location.
"""
image = self._imageList[self._displayCtx.selectedImage]
voxLoc = self.voxelLocation.xyz
worldLoc = image.voxToWorld([voxLoc])[0]
worldVoxLoc = image.worldToVox([self._displayCtx.location.xyz])[0]
worldVoxLoc = np.round(worldVoxLoc)
if len(self._imageList) == 0: return
if self._internalLocationChange: return
image = self._imageList[self._displayCtx.selectedImage]
display = image.getAttribute('display')
vloc = self.voxelLocation.xyz
dloc = transform.transform([vloc], display.voxToDisplayMat)[0]
wloc = transform.transform([vloc], display.voxToWorldMat)[ 0]
self._internalLocationChange = True
self._displayCtx.location.xyz = dloc
self.worldLocation .xyz = wloc
self._internalLocationChange = False
self._updateVoxelValue()
# if the current image list location is already equal to the
# new voxel location, don't change it. The voxel location,
# transformed to world coordinates, will be in the centre of
# voxel. But the world location can be anywhere within a
# voxel. So if the world location is already in the correct
# voxel, we don't want it to be shifted to the voxel centre.
diffs = map(lambda vl, wvl: vl - wvl, voxLoc, worldVoxLoc)
if not any(map(lambda d: d > 0.001, diffs)): return
self._displayCtx.location.xyz = worldLoc
def _worldLocationChanged(self, *a):
"""
......@@ -247,33 +274,22 @@ class LocationPanel(controlpanel.ControlPanel, props.HasProperties):
Propagates the change on to the voxel location in the currently
selected image.
"""
import fsl.fslview.strings as strings
if len(self._imageList) == 0: return
image = self._imageList[self._displayCtx.selectedImage]
loc = self._displayCtx.location.xyz
voxLoc = np.round(image.worldToVox([loc]))[0]
inBounds = True
# If the selected world location is not within the selected
# image, we're going to temporarily disable notification on
# the voxel location property, because this would otherwise
# cause some infinite-property-listener-callback-recursion
# nastiness.
for i in range(3):
# allow the voxel location values to be equal to the image
if voxLoc[i] < 0 or voxLoc[i] > image.shape[i]:
inBounds = False
if not inBounds:
self._updateVoxelValue(voxVal=strings.locationPanelOutOfBounds)
self.voxelLocation.disableNotification()
if len(self._imageList) == 0: return
if self._internalLocationChange: return
self.voxelLocation.xyz = voxLoc
self.voxelLocation.enableNotification()
image = self._imageList[self._displayCtx.selectedImage]
display = image.getAttribute('display')
wloc = self.worldLocation.xyz
dloc = transform.transform([wloc], display.worldToDisplayMat)[0]
vloc = transform.transform([wloc], display.worldToVoxMat)[ 0]
self._internalLocationChange = True
self._displayCtx.location.xyz = dloc
self.voxelLocation .xyz = np.round(vloc)
self._internalLocationChange = False
self._updateVoxelValue()
def _selectedImageChanged(self, *a):
......@@ -282,59 +298,35 @@ class LocationPanel(controlpanel.ControlPanel, props.HasProperties):
(which contains the image name), and sets the voxel location limits.
"""
# Make sure that a listener is registered on the
# selected image, so that the space label can be
# updated when its transformation matrix is changed
for i, img in enumerate(self._imageList):
img.removeListener('transform', self._name)
if i == self._displayCtx.selectedImage:
img.addListener('transform', self._name, self._spaceChanged)
self._spaceChanged()
if len(self._imageList) == 0:
self._updateVoxelValue('')
self._voxelPanel.Layout()
return
image = self._imageList[self._displayCtx.selectedImage]
oldLoc = self._displayCtx.location.xyz
voxLoc = np.round(image.worldToVox([oldLoc]))[0]
for i in range(3):
self.voxelLocation.setLimits(i, 0, image.shape[i] - 1)
self.voxelLocation.xyz = voxLoc
# The voxel coordinates may have inadvertently been
# changed due to a change in their limits. So we'll
# restore the old location from the real world
# coordinates.
self._displayCtx.location.xyz = oldLoc
def _spaceChanged(self, *a):
"""Called when the transformation matrix of the currently selected
image changes. Updates the 'space' label to reflect the change.
"""
import fsl.fslview.strings as strings
if len(self._imageList) == 0:
self._updateVoxelValue( '')
self._spaceLabel.SetLabel('')
self._locationPanel.Layout()
self._worldPanel.Layout()
return
image = self._imageList[self._displayCtx.selectedImage]
image = self._imageList[self._displayCtx.selectedImage]
display = image.getAttribute('display')
if image.transform == 'affine':
spaceLabel = strings.imageSpaceLabels[image.getXFormCode()]
else:
spaceLabel = strings.imageSpaceLabels[fslimage.NIFTI_XFORM_VOXEL]
# Update the label which
# displays the image space
spaceLabel = strings.imageSpaceLabels[image.getXFormCode()]
spaceLabel = strings.locationPanelSpaceLabel.format(spaceLabel)
self._spaceLabel.SetLabel(spaceLabel)
self._worldPanel.Layout()
self._locationPanel.Layout()
# Update the voxel and world location limits,
# but don't trigger a listener callback, as
# this would change the display location
self._internalLocationChange = True
for i in range(3):
vlo, vhi = 0, image.shape[i] - 1
wlo, whi = transform.axisBounds(image.shape,
display.voxToWorldMat,
i)
self.voxelLocation.setLimits(i, vlo, vhi)
self.worldLocation.setLimits(i, wlo, whi)
self._internalLocationChange = False
self._updateVoxelValue()
......@@ -5,13 +5,16 @@
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
from collections import OrderedDict
import sys
import collections
from collections import OrderedDict
import numpy as np
import props
import fsl.data.image as fslimage
import fsl.data.image as fslimage
import fsl.utils.transform as transform
import fsl.fslview.colourmaps as fslcm
class ImageDisplay(props.HasProperties):
......@@ -76,11 +79,25 @@ class ImageDisplay(props.HasProperties):
volume = props.Int(minval=0, maxval=0, default=0, clamped=True)
"""If a 4D image, the current volume to display."""
transform = fslimage.Image.transform
"""How the image is transformed from voxel to real world coordinates.
This property is bound to the :attr:`~fsl.data.image.Image.transform`
property of the image associated with this :class:`ImageDisplay`.
transform = props.Choice(
collections.OrderedDict([
('affine', 'Use qform/sform transformation matrix'),
('pixdim', 'Use pixdims only'),
('id', 'Do not use qform/sform or pixdims')]),
default='pixdim')
"""This property defines how the image should be transformd into the display
coordinate system.
- ``affine``: Use the affine transformation matrix stored in the image
(the ``qform``/``sform`` fields in NIFTI1 headers).
- ``pixdim``: Scale voxel sizes by the ``pixdim`` fields in the image
header.
- ``id``: Perform no scaling or transformation - voxels will be
interpreted as :math:`1mm^3` isotropic, with the origin at voxel
(0,0,0).
"""
......@@ -241,11 +258,9 @@ class ImageDisplay(props.HasProperties):
self.image = image
# bind self.transform and self.name to
# image.transform/image.name, so changes
# bind self.name to # image.name, so changes
# in one are propagated to the other
self.bindProps('transform', image)
self.bindProps('name', image)
self.bindProps('name', image)
# Attributes controlling image display. Only
# determine the real min/max for small images -
......@@ -271,19 +286,95 @@ class ImageDisplay(props.HasProperties):
self.setConstraint('worldResolution', 'minval', min(image.pixdim[:3]))
self.worldResolution = min(image.pixdim[:3])
# The display<->* transformation matrices
# are created in the _transformChanged method
self.voxToWorldMat = image.voxToWorldMat.transpose()
self.worldToVoxMat = image.worldToVoxMat.transpose()
self.voxToDisplayMat = None
self.displayToVoxMat = None
self.worldToDisplayMat = None
self.displayToWorldMat = None
# is this a 4D volume?
if image.is4DImage():
self.setConstraint('volume', 'maxval', image.shape[3] - 1)
# Update transformation matrices when
# the transform property changes
self.addListener(
'transform',
'ImageDisplay_{}'.format(id(self)),
self._transformChanged)
self._transformChanged()
# When the transform property changes,
# the display<->* transformation matrices
# are recalculated. References to the
# previous matrices are stored here, just
# in case anything (hint the DisplayContext
# object) needs them for any particular
# reason (hint: so the DisplayContext can
# preserve the current display location,
# in terms of image world space, when the
# transform of the selected changes)
self._oldVoxToDisplayMat = self.voxToDisplayMat
self._oldDisplayToVoxMat = self.displayToVoxMat
self._oldWorldToDisplayMat = self.worldToDisplayMat
self._oldDisplayToWorldMat = self.displayToWorldMat
def _transformChanged(self, *a):
"""Called when the :attr:`transform` property is changed.
Generates transformation matrices for transforming between voxel and
display coordinate space.
If :attr:`transform` is set to ``affine``, the :attr:`interpolation`
property is changed to ``spline. Otherwise, it is set to ``none``.
"""
# Store references to the previous display related
# transformation matrices (see comments in __init__)
self._oldVoxToDisplayMat = self.voxToDisplayMat
self._oldDisplayToVoxMat = self.displayToVoxMat
self._oldWorldToDisplayMat = self.worldToDisplayMat
self._oldDisplayToWorldMat = self.displayToWorldMat
# The transform property defines the way
# in which image voxel coordinates map
# to the display coordinate system
if self.transform == 'id':
pixdim = [1.0, 1.0, 1.0]
voxToDisplayMat = np.eye(4)
elif self.transform == 'pixdim':
pixdim = self.image.pixdim
voxToDisplayMat = np.diag([pixdim[0], pixdim[1], pixdim[2], 1.0])
elif self.transform == 'affine':
voxToDisplayMat = self.voxToWorldMat
# for pixdim/identity transformations, we want the world
# location (0, 0, 0) to map to voxel location (0, 0, 0)
if self.transform in ('id', 'pixdim'):
for i in range(3):
voxToDisplayMat[3, i] = pixdim[i] * 0.5
# Transformation matrices for moving between the voxel
# coordinate system and the display coordinate system
self.voxToDisplayMat = np.array(voxToDisplayMat, dtype=np.float32)
self.displayToVoxMat = transform.invert(self.voxToDisplayMat)
# Matrices for moving between the display coordinate
# system, and the image world coordinate system
self.displayToWorldMat = transform.concat(self.displayToVoxMat,
self.voxToWorldMat)
self.worldToDisplayMat = transform.invert(self.displayToWorldMat)
# When transform is changed to 'affine', enable interpolation
# and, when changed to 'pixdim' or 'id', disable interpolation
def changeInterp(*a):
if self.transform == 'affine': self.interpolation = 'spline'
else: self.interpolation = 'none'
self.addListener('transform',
'ImageDisplay_{}'.format(id(self)),
changeInterp)
if self.transform == 'affine': self.interpolation = 'spline'
else: self.interpolation = 'none'
class DisplayContext(props.HasProperties):
......@@ -307,6 +398,14 @@ class DisplayContext(props.HasProperties):
3D location (xyz) in the image list space.
"""
bounds = props.Bounds(ndims=3)
"""This property contains the min/max values of a bounding box (in display
coordinates) which is big enough to contain all of the images in the
:attr:`images` list. This property shouid be read-only, but I don't have a
way to enforce it (yet).
"""
volume = props.Int(minval=0, maxval=0, default=0, clamped=True)
"""The volume property contains the currently selected volume
......@@ -325,12 +424,14 @@ class DisplayContext(props.HasProperties):
self._imageList = imageList
self._name = '{}_{}'.format(self.__class__.__name__, id(self))
# Ensure that an ImageDisplay object exists for
# every image, and that the display bounds
# property is initialised
self._imageListChanged()
self._imageListBoundsChanged()
# initialise the location to be
# the centre of the image world
b = imageList.bounds
b = self.bounds
self.location.xyz = [
b.xlo + b.xlen / 2.0,
b.ylo + b.ylen / 2.0,
......@@ -339,9 +440,9 @@ class DisplayContext(props.HasProperties):
imageList.addListener('images',
self._name,
self._imageListChanged)
imageList.addListener('bounds',
self.addListener( 'bounds',
self._name,
self._imageListBoundsChanged)
self._boundsChanged)
self.addListener( 'volume',
self._name,
self._volumeChanged)
......@@ -361,9 +462,32 @@ class DisplayContext(props.HasProperties):
# Ensure that an ImageDisplay
# object exists for every image
for image in self._imageList:
try: image.getAttribute('display')
except KeyError: image.setAttribute('display', ImageDisplay(image))
try:
display = image.getAttribute('display')
except KeyError:
display = ImageDisplay(image)
image.setAttribute('display', display)
# Register a listener with the transform property
# of every image display so that when they change,
# we can update the display bounds, and preserve
# the current display location so that it is in
# terms of the world location of the currently
# selected image
#
# This may be called multiple times on each image,
# but it doesn't matter, as any listener which has
# previously been registered with an image will
# just be replaced by the new one here.
display.addListener(
'transform',
self.__class__.__name__,
self._transformChanged,
overwrite=True)
# Ensure that the bounds property is accurate
self._updateBounds()
# Limit the selectedImage property
# so it cannot take a value greater
# than len(imageList)-1
......@@ -391,6 +515,66 @@ class DisplayContext(props.HasProperties):
self.setConstraint('volume', 'maxval', 0)
def _transformChanged(self, xform, valid, display):
"""Called when the
:attr:`~fsl.fslview.displaycontext.ImageDisplay.transform property
changes on any image in the :attr:`images` list. Sets the
:attr:`location` property, so that the selected image world location
is preserved, in the new display coordinate system.
"""
if display.image != self._imageList[self.selectedImage]:
self._updateBounds()
return
# Calculate the image world location using
# the old display<-> world transform, then
# transform it back to the new world->display
# transform
imgWorldLoc = transform.transform([self.location.xyz],
display._oldDisplayToWorldMat)[0]
newDispLoc = transform.transform([imgWorldLoc],
display.worldToDisplayMat)[0]
# Update the display coordinate system bounds -
# this will also update the constraints on the
# location property, so we have to do this first
# before setting said location property.
self._updateBounds()
self.location.xyz = newDispLoc
def _updateBounds(self, *a):
"""Called when the image list changes, or when any image display
transform is changed. Updates the :attr:`bounds` property.
"""
if len(self._imageList) == 0:
minBounds = [0.0, 0.0, 0.0]
maxBounds = [0.0, 0.0, 0.0]
else:
minBounds = 3 * [ sys.float_info.max]
maxBounds = 3 * [-sys.float_info.max]
for img in self._imageList.images:
display = img.getAttribute('display')
xform = display.voxToDisplayMat
for ax in range(3):
lo, hi = transform.axisBounds(img.shape, xform, ax)
if lo < minBounds[ax]: minBounds[ax] = lo
if hi > maxBounds[ax]: maxBounds[ax] = hi
self.bounds[:] = [minBounds[0], maxBounds[0],
minBounds[1], maxBounds[1],
minBounds[2], maxBounds[2]]
def _volumeChanged(self, *a):
"""Called when the :attr:`volume` property changes.
......@@ -406,14 +590,12 @@ class DisplayContext(props.HasProperties):
image.getAttribute('display').volume = self.volume
def _imageListBoundsChanged(self, *a):
"""Called when the :attr:`fsl.data.image.ImageList.bounds` property
changes.
def _boundsChanged(self, *a):
"""Called when the :attr:`bounds` property changes.
Updates the constraints on the :attr:`location` property.
"""
bounds = self._imageList.bounds
self.location.setLimits(0, bounds.xlo, bounds.xhi)
self.location.setLimits(1, bounds.ylo, bounds.yhi)
self.location.setLimits(2, bounds.zlo, bounds.zhi)
self.location.setLimits(0, self.bounds.xlo, self.bounds.xhi)
self.location.setLimits(1, self.bounds.ylo, self.bounds.yhi)
self.location.setLimits(2, self.bounds.zlo, self.bounds.zhi)
......@@ -8,15 +8,16 @@
import logging
log = logging.getLogger(__name__)
import scipy.ndimage as ndi
import OpenGL.GL as gl
import scipy.ndimage as ndi
import OpenGL.GL as gl
import fsl.utils.transform as transform
def draw(glimg, zpos, xform=None):
"""Draws a slice of the image at the given Z location using immediate
mode rendering.
"""
image = glimg.image
display = glimg.display
# Don't draw the slice if this
......@@ -31,7 +32,7 @@ def draw(glimg, zpos, xform=None):
# Transform world texture coordinates
# to (floating point) voxel coordinates
voxCoords = image.worldToVox(texCoords)
voxCoords = transform.transform(texCoords, display.displayToVoxMat)
imageData = glimg.imageData
texCoordXform = glimg.texCoordXform
colourTexture = glimg.colourTexture
......
......@@ -34,9 +34,11 @@ This module provides the following functions:
import logging
log = logging.getLogger(__name__)
import numpy as np
import scipy.ndimage as ndi
import OpenGL.GL as gl
import numpy as np
import scipy.ndimage as ndi
import OpenGL.GL as gl
import fsl.utils.transform as transform
def init(glimg, xax, yax):
......@@ -86,7 +88,6 @@ def draw(glimg, zpos, xform=None):
mode rendering.
"""
image = glimg.image
display = glimg.display
# Don't draw the slice if this
......@@ -101,7 +102,7 @@ def draw(glimg, zpos, xform=None):
# Transform world texture coordinates
# to (floating point) voxel coordinates
voxCoords = image.worldToVox(texCoords)
voxCoords = transform.transform(texCoords, display.displayToVoxMat)
imageData = glimg.imageData
texCoordXform = glimg.texCoordXform
colourTexture = glimg.colourTexture
......
......@@ -39,9 +39,9 @@ def draw(glimg, zpos, xform=None):
# to the shader variable
if xform is None: xform = np.identity(4)
w2w = np.array(xform, dtype=np.float32).ravel('C')
w2v = np.array(image.worldToVoxMat, dtype=np.float32).ravel('C')
tcx = np.array(glimg.texCoordXform, dtype=np.float32).ravel('C')
w2w = np.array(xform, dtype=np.float32).ravel('C')
w2v = np.array(display.displayToVoxMat, dtype=np.float32).ravel('C')
tcx = np.array(glimg.texCoordXform, dtype=np.float32).ravel('C')
gl.glUniformMatrix4fv(glimg.worldToVoxMatPos, 1, False, w2v)
gl.glUniformMatrix4fv(glimg.worldToWorldMatPos, 1, False, w2w)
......
......@@ -47,8 +47,6 @@ This module provides the following functions:
import logging
log = logging.getLogger(__name__)
import os.path as op
import numpy as np
import OpenGL.GL as gl
import OpenGL.arrays.vbo as vbo
......@@ -397,9 +395,9 @@ def draw(glimg, zpos, xform=None):
# to the shader variable
if xform is None: xform = np.identity(4)
w2w = np.array(xform, dtype=np.float32).ravel('C')
w2v = np.array(image.worldToVoxMat, dtype=np.float32).ravel('C')
tcx = np.array(glimg.texCoordXform, dtype=np.float32).ravel('C')
w2w = np.array(xform, dtype=np.float32).ravel('C')
w2v = np.array(display.displayToVoxMat, dtype=np.float32).ravel('C')
tcx = np.array(glimg.texCoordXform, dtype=np.float32).ravel('C')
gl.glUniformMatrix4fv(glimg.worldToVoxMatPos, 1, False, w2v)
gl.glUniformMatrix4fv(glimg.worldToWorldMatPos, 1, False, w2w)
......
......@@ -10,6 +10,8 @@ import logging
log = logging.getLogger(__name__)
import numpy as np
import fsl.utils.transform as transform
import fsl.fslview.gl as fslgl
import fsl.fslview.gl.glimage as glimage
......@@ -27,21 +29,22 @@ class GLCircle(glimage.GLImage):
def genVertexData(image, display, xax, yax):
zax = 3 - xax - yax
worldRes = display.worldResolution
voxelRes = display.voxelResolution
transform = display.transform
zax = 3 - xax - yax
worldRes = display.worldResolution
voxelRes = display.voxelResolution
transformCode = display.transform
transformMat = display.voxToDisplayMat
# These values give the min/max x/y values
# of a bounding box which encapsulates
# the entire image
xmin, xmax = image.imageBounds(xax)
ymin, ymax = image.imageBounds(yax)
xmin, xmax = transform.axisBounds(image.shape, transformMat, xax)
ymin, ymax = transform.axisBounds(image.shape, transformMat, yax)
# The width/height of a displayed voxel.
# If we are displaying in real world space,
# we use the world display resolution
if transform in ('affine'):
if transformCode in ('affine'):
xpixdim = worldRes
ypixdim = worldRes
......@@ -64,7 +67,7 @@ def genVertexData(image, display, xax, yax):
log.debug('Generating coordinate buffers for {} '
'({} resolution {}/{}, num samples {})'.format(
image.name, transform, worldRes, voxelRes,
image.name, transformCode, worldRes, voxelRes,
xNumSamples * yNumSamples))
# The location of every displayed
......
......@@ -18,7 +18,8 @@ GL Objects must have the following methods:
import logging
log = logging.getLogger(__name__)
import numpy as np
import numpy as np
import fsl.utils.transform as transform
def createGLObject(image, display):
......@@ -68,20 +69,21 @@ def calculateSamplePoints(image, display, xax, yax):
"""
worldRes = display.worldResolution
voxelRes = display.voxelResolution
transform = display.transform
worldRes = display.worldResolution
voxelRes = display.voxelResolution
transformCode = display.transform
transformMat = display.voxToDisplayMat
# These values give the min/max x/y values
# of a bounding box which encapsulates
# the entire image
xmin, xmax = image.imageBounds(xax)
ymin, ymax = image.imageBounds(yax)
xmin, xmax = transform.axisBounds(image.shape, transformMat, xax)
ymin, ymax = transform.axisBounds(image.shape, transformMat, yax)
# The width/height of a displayed voxel.
# If we are displaying in real world space,
# we use the world display resolution
if transform in ('affine'):
if transformCode == 'affine':
xpixdim = worldRes
ypixdim = worldRes
......@@ -89,9 +91,13 @@ def calculateSamplePoints(image, display, xax, yax):
# But if we're just displaying the data (the
# transform is 'id' or 'pixdim'), we display
# it in the resolution of said data.
else:
elif transformCode == 'pixdim':
xpixdim = image.pixdim[xax] * voxelRes
ypixdim = image.pixdim[yax] * voxelRes
elif transformCode == 'id':
xpixdim = 1.0 * voxelRes
ypixdim = 1.0 * voxelRes
# Number of samples across each dimension,
# given the current sample rate
......@@ -104,7 +110,7 @@ def calculateSamplePoints(image, display, xax, yax):
log.debug('Generating coordinate buffers for {} '
'({} resolution {}/{}, num samples {})'.format(
image.name, transform, worldRes, voxelRes,
image.name, transformCode, worldRes, voxelRes,
xNumSamples * yNumSamples))
# The location of every displayed
......
......@@ -18,6 +18,8 @@ import scipy.ndimage as ndi
import globject
import fsl.utils.transform as transform
class GLTensorLine(object):
"""The :class:`GLTensorLine` class encapsulates the data and logic required to
render 2D slices of a X*Y*Z*3 image as tensor lines.
......@@ -104,7 +106,9 @@ class GLTensorLine(object):
# Transform the world coordinates to
# floating point voxel coordinates
voxCoords = image.worldToVox(worldCoords).transpose()
dToVMat = display.displayToVoxMat
voxCoords = transform.transform(worldCoords, dToVMat).transpose()
imageData = image.data
nVoxels = worldCoords.shape[0]
......
......@@ -101,8 +101,8 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
"""
sliceno = int(np.floor((zpos - self.zrange.xlo) / self.sliceSpacing))
xlen = self.imageList.bounds.getLen(self.xax)
ylen = self.imageList.bounds.getLen(self.yax)
xlen = self.displayCtx.bounds.getLen(self.xax)
ylen = self.displayCtx.bounds.getLen(self.yax)
row = self._totalRows - int(np.floor(sliceno / self.ncols)) - 1
col = int(np.floor(sliceno % self.ncols))
......@@ -130,10 +130,10 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
screenx, screeny = slicecanvas.SliceCanvas.canvasToWorld(
self, xpos, ypos)
xmin = self.imageList.bounds.getLo( self.xax)
ymin = self.imageList.bounds.getLo( self.yax)
xlen = self.imageList.bounds.getLen(self.xax)
ylen = self.imageList.bounds.getLen(self.yax)
xmin = self.displayCtx.bounds.getLo( self.xax)
ymin = self.displayCtx.bounds.getLo( self.yax)
xlen = self.displayCtx.bounds.getLen(self.xax)
ylen = self.displayCtx.bounds.getLen(self.yax)
xmax = xmin + ncols * xlen
ymax = ymin + nrows * ylen
......@@ -168,15 +168,19 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
return self._totalRows
def __init__(self, imageList, zax=0):
def __init__(self, imageList, displayCtx, zax=0):
"""Create a :class:`LightBoxCanvas` object.
:arg imageList: a :class:`~fsl.data.image.ImageList` object which
contains, or will contain, a list of images to be
displayed.
:arg imageList: a :class:`~fsl.data.image.ImageList` object which
contains, or will contain, a list of images to be
displayed.
:arg displayCtx: A :class:`~fsl.fslview.displaycontext.DisplayContext`
object which defines how that image list is to be
displayed.
:arg zax: Image axis to be used as the 'depth' axis. Can be
changed via the :attr:`LightBoxCanvas.zax` property.
:arg zax: Image axis to be used as the 'depth' axis. Can be
changed via the :attr:`LightBoxCanvas.zax` property.
"""
# These attributes are used to keep track of
......@@ -185,10 +189,10 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
self._nslices = 0
self._totalRows = 0
slicecanvas.SliceCanvas.__init__(self, imageList, zax)
slicecanvas.SliceCanvas.__init__(self, imageList, displayCtx, zax)
# default to showing the entire slice range
self.zrange.x = imageList.bounds.getRange(self.zax)
self.zrange.x = displayCtx.bounds.getRange(self.zax)
self._slicePropsChanged()
......@@ -237,8 +241,8 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
the total number of rows.
"""
xlen = self.imageList.bounds.getLen(self.xax)
ylen = self.imageList.bounds.getLen(self.yax)
xlen = self.displayCtx.bounds.getLen(self.xax)
ylen = self.displayCtx.bounds.getLen(self.yax)
zlen = self.zrange.xlen
width, height = self._getSize()
......@@ -318,7 +322,7 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
transformation matrices being changed).
"""
newZRange = self.imageList.bounds.getRange(self.zax)
newZRange = self.displayCtx.bounds.getRange(self.zax)
newZGap = self.sliceSpacing
# Pick a sensible default for the
......@@ -351,7 +355,7 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
slicecanvas.SliceCanvas._imageBoundsChanged(self)
zmin, zmax = self.imageList.bounds.getRange(self.zax)
zmin, zmax = self.displayCtx.bounds.getRange(self.zax)
oldzmin, oldzmax = self.zrange.getLimits(0)
self.zrange.setLimits(0, zmin, zmax)
......@@ -380,10 +384,10 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
be displayed, in real world coordinates.
"""
xmin = self.imageList.bounds.getLo( self.xax)
ymin = self.imageList.bounds.getLo( self.yax)
xlen = self.imageList.bounds.getLen(self.xax)
ylen = self.imageList.bounds.getLen(self.yax)
xmin = self.displayCtx.bounds.getLo( self.xax)
ymin = self.displayCtx.bounds.getLo( self.yax)
xlen = self.displayCtx.bounds.getLen(self.xax)
ylen = self.displayCtx.bounds.getLen(self.yax)
off = self._totalRows - self.nrows - self.topRow
ymin = ymin + ylen * off
......@@ -453,8 +457,8 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
row = int(np.floor(sliceno / ncols))
col = int(np.floor(sliceno % ncols))
xlen = self.imageList.bounds.getLen(self.xax)
ylen = self.imageList.bounds.getLen(self.yax)
xlen = self.displayCtx.bounds.getLen(self.xax)
ylen = self.displayCtx.bounds.getLen(self.yax)
translate = np.identity(4, dtype=np.float32)
translate[3, self.xax] = xlen * col
......@@ -467,10 +471,10 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
def _drawGridLines(self):
"""Draws grid lines between all the displayed slices."""
xlen = self.imageList.bounds.getLen(self.xax)
ylen = self.imageList.bounds.getLen(self.yax)
xmin = self.imageList.bounds.getLo( self.xax)
ymin = self.imageList.bounds.getLo( self.yax)
xlen = self.displayCtx.bounds.getLen(self.xax)
ylen = self.displayCtx.bounds.getLen(self.yax)
xmin = self.displayCtx.bounds.getLo( self.xax)
ymin = self.displayCtx.bounds.getLo( self.yax)
rowLines = np.zeros(((self.nrows - 1) * 2, 3), dtype=np.float32)
colLines = np.zeros(((self.ncols - 1) * 2, 3), dtype=np.float32)
......@@ -511,10 +515,10 @@ class LightBoxCanvas(slicecanvas.SliceCanvas):
sliceno = int(np.floor((self.pos.z - self.zrange.xlo) /
self.sliceSpacing))
xlen = self.imageList.bounds.getLen(self.xax)
ylen = self.imageList.bounds.getLen(self.yax)
xmin = self.imageList.bounds.getLo( self.xax)
ymin = self.imageList.bounds.getLo( self.yax)
xlen = self.displayCtx.bounds.getLen(self.xax)
ylen = self.displayCtx.bounds.getLen(self.yax)
xmin = self.displayCtx.bounds.getLo( self.xax)
ymin = self.displayCtx.bounds.getLo( self.yax)
row = int(np.floor(sliceno / self.ncols))
col = int(np.floor(sliceno % self.ncols))
......
......@@ -25,6 +25,7 @@ class OSMesaLightBoxCanvas(lightboxcanvas.LightBoxCanvas,
def __init__(self,
imageList,
displayCtx,
zax=0,
width=0,
height=0,
......
......@@ -25,6 +25,7 @@ class OSMesaSliceCanvas(sc.SliceCanvas,
def __init__(self,
imageList,
displayCtx,
zax=0,
width=0,
height=0,
......
......@@ -41,10 +41,11 @@ class SliceCanvas(props.HasProperties):
"""The currently displayed position. The ``pos.x`` and ``pos.y`` positions
denote the position of a 'cursor', which is highlighted with green
crosshairs. The ``pos.z`` position specifies the currently displayed
slice. While the values of this point are in the image list world
coordinates, the dimension ordering may not be the same as the image list
dimension ordering. For this position, the x and y dimensions correspond
to horizontal and vertical on the screen, and the z dimension to 'depth'.
slice. While the values of this point are in the display coordinate
system, the dimension ordering may not be the same as the display
coordinate dimension ordering. For this position, the x and y dimensions
correspond to horizontal and vertical on the screen, and the z dimension
to 'depth'.
"""
......@@ -59,7 +60,7 @@ class SliceCanvas(props.HasProperties):
displayBounds = props.Bounds(ndims=2)
"""The display bound x/y values specify the horizontal/vertical display
range of the canvas, in world coordinates. This may be a larger area
range of the canvas, in display coordinates. This may be a larger area
than the size of the displayed images, as it is adjusted to preserve the
aspect ratio.
"""
......@@ -107,11 +108,11 @@ class SliceCanvas(props.HasProperties):
def calcPixelDims(self):
"""Calculate and return the approximate size (width, height) of one
pixel in world space.
pixel in display space.
"""
xmin, xmax = self.imageList.bounds.getRange(self.xax)
ymin, ymax = self.imageList.bounds.getRange(self.yax)
xmin, xmax = self.displayCtx.bounds.getRange(self.xax)
ymin, ymax = self.displayCtx.bounds.getRange(self.yax)
w, h = self._getSize()
pixx = (xmax - xmin) / float(w)
......@@ -122,7 +123,7 @@ class SliceCanvas(props.HasProperties):
def canvasToWorld(self, xpos, ypos):
"""Given pixel x/y coordinates on this canvas, translates them
into the real world coordinates of the displayed slice.
into the display coordinates of the displayed slice.
"""
realWidth = self.displayBounds.xlen
......@@ -146,7 +147,7 @@ class SliceCanvas(props.HasProperties):
def panDisplayBy(self, xoff, yoff):
"""Pans the canvas display by the given x/y offsets (specified in
world coordinates).
display coordinates).
"""
bounds = self.displayBounds
......@@ -178,7 +179,7 @@ class SliceCanvas(props.HasProperties):
def panDisplayToShow(self, xpos, ypos):
"""Pans the display so that the given x/y position (in world
"""Pans the display so that the given x/y position (in display
coordinates) is visible.
"""
......@@ -200,7 +201,7 @@ class SliceCanvas(props.HasProperties):
self.panDisplayBy(xoff, yoff)
def __init__(self, imageList, zax=0):
def __init__(self, imageList, displayCtx, zax=0):
"""Creates a canvas object.
.. note:: It is assumed that each :class:`~fsl.data.image.Image`
......@@ -208,7 +209,10 @@ class SliceCanvas(props.HasProperties):
which refers to an :class:`~fsl.fslview.displaycontext.ImageDisplay`
instance defining how that image is to be displayed.
:arg imageList: An :class:`~fsl.data.image.ImageList` object.
:arg imageList: An :class:`~fsl.data.image.ImageList` object.
:arg displayCtx: A :class:`~fsl.fslview.displaycontext.DisplayContext`
object.
:arg zax: Image axis perpendicular to the plane to be displayed
(the 'depth' axis), default 0.
......@@ -220,8 +224,9 @@ class SliceCanvas(props.HasProperties):
props.HasProperties.__init__(self)
self.imageList = imageList
self.name = '{}_{}'.format(self.__class__.__name__, id(self))
self.imageList = imageList
self.displayCtx = displayCtx
self.name = '{}_{}'.format(self.__class__.__name__, id(self))
# The zax property is the image axis which maps to the
# 'depth' axis of this canvas. The _zAxisChanged method
......@@ -247,12 +252,12 @@ class SliceCanvas(props.HasProperties):
# When the image list changes, refresh the
# display, and update the display bounds
self.imageList.addListener('images',
self.name,
self._imageListChanged)
self.imageList.addListener('bounds',
self.name,
self._imageBoundsChanged)
self.imageList.addListener( 'images',
self.name,
self._imageListChanged)
self.displayCtx.addListener('bounds',
self.name,
self._imageBoundsChanged)
def _initGL(self):
......@@ -359,14 +364,14 @@ class SliceCanvas(props.HasProperties):
def _imageBoundsChanged(self, *a):
"""Called when the image list bounds are changed.
"""Called when the display bounds are changed.
Updates the constraints on the :attr:`pos` property so it is
limited to stay within a valid range, and then calls the
:meth:`_updateDisplayBounds` method.
"""
imgBounds = self.imageList.bounds
imgBounds = self.displayCtx.bounds
self.pos.setMin(0, imgBounds.getLo(self.xax))
self.pos.setMax(0, imgBounds.getHi(self.xax))
......@@ -438,10 +443,10 @@ class SliceCanvas(props.HasProperties):
:arg ymax: Maximum y value to be in the display bounds.
"""
if xmin is None: xmin = self.imageList.bounds.getLo(self.xax)
if xmax is None: xmax = self.imageList.bounds.getHi(self.xax)
if ymin is None: ymin = self.imageList.bounds.getLo(self.yax)
if ymax is None: ymax = self.imageList.bounds.getHi(self.yax)
if xmin is None: xmin = self.displayCtx.bounds.getLo(self.xax)
if xmax is None: xmax = self.displayCtx.bounds.getHi(self.xax)
if ymin is None: ymin = self.displayCtx.bounds.getLo(self.yax)
if ymax is None: ymax = self.displayCtx.bounds.getHi(self.yax)
log.debug('Required display bounds: X: ({}, {}) Y: ({}, {})'.format(
xmin, xmax, ymin, ymax))
......@@ -515,8 +520,8 @@ class SliceCanvas(props.HasProperties):
if xmax is None: xmax = self.displayBounds.xhi
if ymin is None: ymin = self.displayBounds.ylo
if ymax is None: ymax = self.displayBounds.yhi
if zmin is None: zmin = self.imageList.bounds.getLo(self.zax)
if zmax is None: zmax = self.imageList.bounds.getHi(self.zax)
if zmin is None: zmin = self.displayCtx.bounds.getLo(self.zax)
if zmax is None: zmax = self.displayCtx.bounds.getHi(self.zax)
# If there are no images to be displayed,
# or no space to draw, do nothing
......@@ -582,8 +587,8 @@ class SliceCanvas(props.HasProperties):
xverts = np.zeros((2, 3))
yverts = np.zeros((2, 3))
xmin, xmax = self.imageList.bounds.getRange(self.xax)
ymin, ymax = self.imageList.bounds.getRange(self.yax)
xmin, xmax = self.displayCtx.bounds.getRange(self.xax)
ymin, ymax = self.displayCtx.bounds.getRange(self.yax)
x = self.pos.x
y = self.pos.y
......
......@@ -26,22 +26,25 @@ class WXGLLightBoxCanvas(lightboxcanvas.LightBoxCanvas,
interactive 2D slice rendering from a collection of 3D images.
"""
def __init__(self, parent, imageList, zax=0):
def __init__(self, parent, imageList, displayCtx, zax=0):
"""Configures a few event handlers for cleaning up property
listeners when the canvas is destroyed, and for redrawing on
paint/resize events.
"""
wxgl.GLCanvas .__init__(self, parent)
lightboxcanvas.LightBoxCanvas.__init__(self, imageList, zax)
lightboxcanvas.LightBoxCanvas.__init__(self,
imageList,
displayCtx,
zax)
fslgl.WXGLCanvasTarget .__init__(self)
# the image list is probably going to outlive
# this SliceCanvas object, so we do the right
# thing and remove our listeners when we die
def onDestroy(ev):
self.imageList.removeListener('images', self.name)
self.imageList.removeListener('bounds', self.name)
self.imageList .removeListener('images', self.name)
self.displayCtx.removeListener('bounds', self.name)
for image in self.imageList:
disp = image.getAttribute('display')
disp.removeListener('imageType', self.name)
......
......@@ -31,22 +31,22 @@ class WXGLSliceCanvas(slicecanvas.SliceCanvas,
interactive 2D slice rendering from a collection of 3D images.
"""
def __init__(self, parent, imageList, zax=0):
def __init__(self, parent, imageList, displayCtx, zax=0):
"""Configures a few event handlers for cleaning up property
listeners when the canvas is destroyed, and for redrawing on
paint/resize events.
"""
wxgl.GLCanvas .__init__(self, parent)
slicecanvas.SliceCanvas.__init__(self, imageList, zax)
slicecanvas.SliceCanvas.__init__(self, imageList, displayCtx, zax)
fslgl.WXGLCanvasTarget .__init__(self)
# the image list is probably going to outlive
# this SliceCanvas object, so we do the right
# thing and remove our listeners when we die
def onDestroy(ev):
self.imageList.removeListener('images', self.name)
self.imageList.removeListener('bounds', self.name)
self.imageList .removeListener('images', self.name)
self.displayCtx.removeListener('bounds', self.name)
for image in self.imageList:
disp = image.getAttribute('display')
disp.removeListener('imageType', self.name)
......
......@@ -50,11 +50,11 @@ viewPanelConfigMenuText = {
orthoConfigMenu = '{} display'
lightBoxConfigMenu = '{} display'
locationPanelOutOfBounds = 'Out of bounds'
locationPanelSpaceLabel = '{} space'
locationPanelLocationLabel = 'World location (mm)'
locationPanelVoxelLabel = 'Voxel coordinates'
locationPanelVolumeLabel = 'Volume (index)'
locationPanelOutOfBounds = 'Out of bounds'
locationPanelSpaceLabel = '{} space'
locationPanelWorldLabel = 'World location (mm)'
locationPanelVoxelLabel = 'Voxel coordinates'
locationPanelVolumeLabel = 'Volume (index)'
imageAxisLowLongLabels = {
......@@ -94,7 +94,6 @@ imageAxisHighShortLabels = {
fslimage.ORIENT_UNKNOWN : '?'}
imageSpaceLabels = {
fslimage.NIFTI_XFORM_VOXEL : 'Voxel',
fslimage.NIFTI_XFORM_UNKNOWN : 'Unknown',
fslimage.NIFTI_XFORM_SCANNER_ANAT : 'Scanner anatomical',
fslimage.NIFTI_XFORM_ALIGNED_ANAT : 'Aligned anatomical',
......
......@@ -90,7 +90,8 @@ class LightBoxPanel(canvaspanel.CanvasPanel):
self._scrollbar = wx.ScrollBar(self, style=wx.SB_VERTICAL)
self._lbCanvas = lightboxcanvas.LightBoxCanvas(self.getCanvasPanel(),
imageList)
imageList,
displayCtx)
# My properties are the canvas properties
self.bindProps('sliceSpacing', self._lbCanvas)
......
......@@ -113,11 +113,17 @@ class OrthoPanel(canvaspanel.CanvasPanel):
# The canvases themselves - each one displays a
# slice along each of the three world axes
self._xcanvas = slicecanvas.WXGLSliceCanvas(self._xCanvasPanel,
imageList, zax=0)
imageList,
displayCtx,
zax=0)
self._ycanvas = slicecanvas.WXGLSliceCanvas(self._yCanvasPanel,
imageList, zax=1)
imageList,
displayCtx,
zax=1)
self._zcanvas = slicecanvas.WXGLSliceCanvas(self._zCanvasPanel,
imageList, zax=2)
imageList,
displayCtx,
zax=2)
# Attach each canvas as an attribute of its parent -
# see the _configureGridLayout/_configureFlatLayout
......@@ -284,18 +290,24 @@ class OrthoPanel(canvaspanel.CanvasPanel):
for changes on its affine transformation matrix.
"""
self._refreshLabels()
if len(self._imageList) == 0: return
for i, img in enumerate(self._imageList):
img.removeListener('transform', self._name)
display = img.getAttribute('display')
display.removeListener('transform', self._name)
# Update anatomy labels when the image
# transformation matrix changes
if i == self._displayCtx.selectedImage:
img.addListener('transform', self._name, self._refreshLabels)
display.addListener('transform',
self._name,
self._refreshLabels)
# _refreshLabels will in turn call
# _layoutChanged, which is needed
# in case the display bounds have
# changed
self._refreshLabels()
def _onDestroy(self, ev):
......@@ -319,7 +331,8 @@ class OrthoPanel(canvaspanel.CanvasPanel):
# listeners to individual images,
# so we have to remove them too
for img in self._imageList:
img.removeListener('transform', self._name)
display = img.getAttribute('display')
display.removeListener('transform', self._name)
def _resize(self, ev):
......@@ -348,8 +361,9 @@ class OrthoPanel(canvaspanel.CanvasPanel):
# Are we showing or hiding the labels?
if self.showLabels: show = True
else: show = False
if len(self._imageList) == 0: show = False
elif self.showLabels: show = True
else: show = False
for lbl in allLabels:
lbl.Show(show)
......@@ -364,25 +378,24 @@ class OrthoPanel(canvaspanel.CanvasPanel):
# changed to red
colour = 'white'
if len(self._imageList) > 0:
image = self._imageList[self._displayCtx.selectedImage]
image = self._imageList[self._displayCtx.selectedImage]
display = image.getAttribute('display')
# 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)
# The image is being displayed as it is stored on
# disk - the image.getOrientation method calculates
# and returns labels for each voxelwise axis.
if display.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'
......@@ -428,7 +441,7 @@ class OrthoPanel(canvaspanel.CanvasPanel):
self._configureFlatLayout(width, height, canvases, False)
return
bounds = self._imageList.bounds
bounds = self._displayCtx.bounds
canvasWidths = [bounds.getLen(c._canvas.xax) for c in canvases]
canvasHeights = [bounds.getLen(c._canvas.yax) for c in canvases]
......@@ -458,7 +471,7 @@ class OrthoPanel(canvaspanel.CanvasPanel):
ratio is maintained across them when laid out vertically
(``vert=True``) or horizontally (``vert=False``).
"""
bounds = self._imageList.bounds
bounds = self._displayCtx.bounds
# Get the canvas dimensions in world space
canvasWidths = [bounds.getLen(c._canvas.xax) for c in canvases]
......@@ -556,7 +569,10 @@ class OrthoPanel(canvaspanel.CanvasPanel):
self._zCanvasPanel]
show = [self.showXCanvas,
self.showYCanvas,
self.showZCanvas]
self.showZCanvas]
if len(self._imageList) == 0:
show = [False] * 3
# Pick out the canvases for which
# the 'show*Canvas' property is true
......@@ -572,6 +588,9 @@ class OrthoPanel(canvaspanel.CanvasPanel):
self._zCanvasPanel.Show(self.showZCanvas)
if len(canvases) == 0:
self.Layout()
self.getCanvasPanel().Layout()
self.Refresh()
return
# Regardless of the layout, we use a
......
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