diff --git a/fsl/fsleyes/controls/overlaydisplaypanel.py b/fsl/fsleyes/controls/overlaydisplaypanel.py index 96d394199908b4b17ff3ea6bff965a621960c74a..21e968d9d211d197e3117a5680f8d58601f9ff84 100644 --- a/fsl/fsleyes/controls/overlaydisplaypanel.py +++ b/fsl/fsleyes/controls/overlaydisplaypanel.py @@ -392,7 +392,7 @@ _DISPLAY_PROPS = td.TypeDict({ # props.Widget('showName'), props.Widget('coordSpace', enabledWhen=lambda o, ri: ri != 'none', - labels=strings.choices['Nifti1Opts.transform'], + labels=strings.choices['ModelOpts.coordSpace'], dependencies=['refImage'])], 'TensorOpts' : [ diff --git a/fsl/fsleyes/displaycontext/modelopts.py b/fsl/fsleyes/displaycontext/modelopts.py index 8ff9ff0a0faa8a887f7e739c13f29ea100c6f482..626adf98b05e6db9c0a35c11bd22a21cf1156850 100644 --- a/fsl/fsleyes/displaycontext/modelopts.py +++ b/fsl/fsleyes/displaycontext/modelopts.py @@ -9,8 +9,6 @@ for displaying :class:`.Model` overlays. """ -import copy - import numpy as np import props @@ -21,8 +19,6 @@ import fsl.fsleyes.colourmaps as colourmaps import fsl.data.image as fslimage import fsl.utils.transform as transform -import volumeopts - class ModelOpts(fsldisplay.DisplayOpts): """The ``ModelOpts`` class defines settings for displaying :class:`.Model` @@ -65,10 +61,37 @@ class ModelOpts(fsldisplay.DisplayOpts): """ - coordSpace = copy.copy(volumeopts.Nifti1Opts.transform) + # This property is implicitly tightly-coupled to + # the Nifti1Opts.getTransform method - the choices + # defined in this property are assumed to be valid + # inputs to that method. + coordSpace = props.Choice(('affine', 'pixdim', 'pixdim-flip', 'id'), + default='pixdim-flip') """If :attr:`refImage` is not ``None``, this property defines the reference image coordinate space in which the model coordinates are defined (i.e. voxels, scaled voxels, or world coordinates). + + =============== ========================================================= + ``affine`` The model coordinates are defined in the reference image + world coordinate system. + + ``id`` The model coordinates are defined in the reference image + voxel coordinate system. + + ``pixdim`` The model coordinates are defined in the reference image + voxel coordinate system, scaled by the voxel pixdims. + + ``pixdim-flip`` The model coordinates are defined in the reference image + voxel coordinate system, scaled by the voxel pixdims. If + the reference image transformation matrix has a positive + determinant, the X axis is flipped. + =============== ========================================================= + + The default value is ``pixdim-flip``, as this is the coordinate system + used in the VTK sub-cortical segmentation model files output by FIRST. + See also the :ref:`note on coordinate systems + <volumeopts-coordinate-systems>`, and the :meth:`.Nifti1Opts.getTransform` + method. """ @@ -77,12 +100,6 @@ class ModelOpts(fsldisplay.DisplayOpts): to the :class:`.DisplayOpts` constructor. """ - # The Nifti1Opts.transform property has a - # 'custom' option which is not applicable - # to our coordSpace property. - coordSpace = self.getProp('coordSpace') - coordSpace.removeChoice('custom', self) - # Create a random, highly # saturated colour colour = colourmaps.randomBrightColour() @@ -162,11 +179,12 @@ class ModelOpts(fsldisplay.DisplayOpts): the :class:`.Model` vertex coordinates into the display coordinate system. - If no :attr:`refImage` is selected, this method returns ``None``. + If no :attr:`refImage` is selected, this method returns an identity + transformation. """ if self.refImage is None: - return None + return np.eye(4) opts = self.displayCtx.getOpts(self.refImage) @@ -346,11 +364,9 @@ class ModelOpts(fsldisplay.DisplayOpts): lo, hi = self.overlay.getBounds() xform = self.getCoordSpaceTransform() - if xform is not None: - - lohi = transform.transform([lo, hi], xform) - lohi.sort(axis=0) - lo, hi = lohi[0, :], lohi[1, :] + lohi = transform.transform([lo, hi], xform) + lohi.sort(axis=0) + lo, hi = lohi[0, :], lohi[1, :] self.bounds = [lo[0], hi[0], lo[1], hi[1], lo[2], hi[2]] diff --git a/fsl/fsleyes/gl/glmodel.py b/fsl/fsleyes/gl/glmodel.py index 0e348889ee4716e777f7d36b571e24ed68c7e74b..d11bbc8e621ffca7ed8ff1d3009882caf3c4cad0 100644 --- a/fsl/fsleyes/gl/glmodel.py +++ b/fsl/fsleyes/gl/glmodel.py @@ -9,8 +9,9 @@ to render :class:`.Model` overlays. """ -import numpy as np -import OpenGL.GL as gl +import numpy as np +import numpy.linalg as npla +import OpenGL.GL as gl import globject import fsl.utils.transform as transform @@ -173,10 +174,9 @@ class GLModel(globject.GLObject): vertices = self.overlay.vertices indices = self.overlay.indices + xform = self.opts.getCoordSpaceTransform() - xform = self.opts.getCoordSpaceTransform() - - if xform is not None: + if not np.all(xform == np.eye(4)): vertices = transform.transform(vertices, xform) self.vertices = np.array(vertices, dtype=np.float32) @@ -330,16 +330,19 @@ class GLModel(globject.GLObject): # plane. gl.glStencilFunc(gl.GL_ALWAYS, 0, 0) - # I don't understand why, but if any of the - # display system axes are inverted, we need - # to render the back faces first, otherwise - # the cross-section mask will not be created - # correctly. + # If the model coordinate transformation + # has a positive determinant, we need to + # render the back faces first, otherwise + # the cross-section mask will not be + # created correctly. Something to do with + # the vertex unwinding order, I guess. direction = [gl.GL_INCR, gl.GL_DECR] - - if np.any(np.array(hi) < 0.0): faceOrder = [gl.GL_FRONT, gl.GL_BACK] - else: faceOrder = [gl.GL_BACK, gl.GL_FRONT] + if npla.det(opts.getCoordSpaceTransform()) > 0: + faceOrder = [gl.GL_BACK, gl.GL_FRONT] + else: + faceOrder = [gl.GL_FRONT, gl.GL_BACK] + for face, direction in zip(faceOrder, direction): gl.glStencilOp(gl.GL_KEEP, gl.GL_KEEP, direction) diff --git a/fsl/fsleyes/strings.py b/fsl/fsleyes/strings.py index 31030ec4b52f9f705ce0b620dba9f762388c465b..14bacad2a4e144f1b48ad88f921d17766dc8e959 100644 --- a/fsl/fsleyes/strings.py +++ b/fsl/fsleyes/strings.py @@ -416,16 +416,22 @@ labels = TypeDict({ 'OverlayInfoPanel.MelodicImage' : 'NIFTI1 image ' '(MELODIC analysis)', 'OverlayInfoPanel.MelodicImage.melodicInfo' : 'MELODIC information', - 'OverlayInfoPanel.Model' : 'VTK model', - 'OverlayInfoPanel.Model.numVertices' : 'Number of vertices', - 'OverlayInfoPanel.Model.numIndices' : 'Number of indices', - 'OverlayInfoPanel.Model.displaySpace' : 'Display space', - 'OverlayInfoPanel.Model.refImage' : 'Reference image', - 'OverlayInfoPanel.Model.coordSpace' : 'Vertices defined in', - 'OverlayInfoPanel.Model.coordSpace.id' : 'Voxels ({})', - 'OverlayInfoPanel.Model.coordSpace.pixdim' : 'Scaled voxels ({})', - 'OverlayInfoPanel.Model.coordSpace.affine' : 'World coordinates ({})', - 'OverlayInfoPanel.Model.coordSpace.display' : 'Display coordinate system', + + 'OverlayInfoPanel.Model' : 'VTK model', + 'OverlayInfoPanel.Model.numVertices' : 'Number of vertices', + 'OverlayInfoPanel.Model.numIndices' : 'Number of indices', + 'OverlayInfoPanel.Model.displaySpace' : 'Display space', + 'OverlayInfoPanel.Model.refImage' : 'Reference image', + 'OverlayInfoPanel.Model.coordSpace' : 'Vertices defined in', + 'OverlayInfoPanel.Model.coordSpace.id' : 'Voxels ({})', + 'OverlayInfoPanel.Model.coordSpace.pixdim' : 'Scaled voxels ({})', + 'OverlayInfoPanel.Model.coordSpace.pixdim-flip' : 'Scaled voxels forced ' + 'to radiological ' + 'orientation ({})', + 'OverlayInfoPanel.Model.coordSpace.affine' : 'World coordinates ({})', + 'OverlayInfoPanel.Model.coordSpace.display' : 'Display coordinate ' + 'system', + 'OverlayInfoPanel.dataSource' : 'Data source', 'OverlayInfoPanel.TensorImage' : 'NIFTI1 diffusion ' @@ -651,6 +657,12 @@ choices = TypeDict({ 'ModelOpts.refImage.none' : 'No reference image', + 'ModelOpts.coordSpace' : {'affine' : 'World coordinates', + 'pixdim' : 'Scaled voxels', + 'pixdim-flip' : 'Scaled voxels forced to ' + 'radiological orientation', + 'id' : 'Voxels'}, + 'TensorOpts.tensorResolution.min' : 'Low', 'TensorOpts.tensorResolution.max' : 'High',