diff --git a/fsl/data/tensorimage.py b/fsl/data/tensorimage.py index 02e5fc4e4189fb29316543a414f205b252f51cc7..5bfeb009e9bef893d86593b2529921510e861de9 100644 --- a/fsl/data/tensorimage.py +++ b/fsl/data/tensorimage.py @@ -61,13 +61,13 @@ def isPathToTensorData(path): return getTensorDataPrefix(path) is not None -class TensorImage(fslimage.Image): +class TensorImage(fslimage.Nifti1): """The ``TensorImage`` class is able to load and encapsulate the diffusion tensor data generated by the FSL ``dtifit`` tool. """ - def __init__(self, path, *args, **kwargs): + def __init__(self, path): """Create a ``TensorImage``. :arg path: A path to a ``dtifit`` directory. Alternately, the ``path`` @@ -75,8 +75,6 @@ class TensorImage(fslimage.Image): ``{'v1', 'v2', 'v3', 'l1', 'l2', 'l3'}``, which specify paths to images containing the tensor eigenvectors and eigenvalues. - - All other arguments are passed through to :meth:`.Image.__init__`. """ dtifitDir = isinstance(path, basestring) @@ -95,20 +93,15 @@ class TensorImage(fslimage.Image): l2 = op.join(path, '{}_L2'.format(prefix)) l3 = op.join(path, '{}_L3'.format(prefix)) - paths = { - 'v1' : v1, - 'v2' : v2, - 'v3' : v3, - 'l1' : l1, - 'l2' : l2, - 'l3' : l3 - } + paths = {'v1' : v1, 'v2' : v2, 'v3' : v3, + 'l1' : l1, 'l2' : l2, 'l3' : l3} + else: paths = path - fslimage.Image.__init__(self, paths['v1'], *args, **kwargs) + fslimage.Nifti1.__init__(self, paths['v1'], loadData=False) - self.__v1 = self + self.__v1 = fslimage.Image(paths['v1']) self.__v2 = fslimage.Image(paths['v2']) self.__v3 = fslimage.Image(paths['v3']) self.__l1 = fslimage.Image(paths['l1']) diff --git a/fsl/fsleyes/displaycontext/display.py b/fsl/fsleyes/displaycontext/display.py index 627f6b6266240b3b9fe96624c0d0244f72a0da96..890e74769ef320deae290ec0f8ab51eafc827df1 100644 --- a/fsl/fsleyes/displaycontext/display.py +++ b/fsl/fsleyes/displaycontext/display.py @@ -497,7 +497,7 @@ OVERLAY_TYPES = td.TypeDict({ 'Image' : ['volume', 'mask', 'rgbvector', 'linevector', 'label'], 'Model' : ['model'], - 'TensorImage' : ['tensor', 'volume', 'rgbvector', 'linevector'], + 'TensorImage' : ['tensor', 'rgbvector', 'linevector'], }) """This dictionary provides a mapping between all overlay classes, and the possible values that the :attr:`Display.overlayType` property diff --git a/fsl/fsleyes/gl/gl14/gllinevector_funcs.py b/fsl/fsleyes/gl/gl14/gllinevector_funcs.py index 1bb585acdc335ee0100263a0476aee5ccb5ed0be..0afb7fde1914ea2c8f4e10dfac0e4e11e0936f81 100644 --- a/fsl/fsleyes/gl/gl14/gllinevector_funcs.py +++ b/fsl/fsleyes/gl/gl14/gllinevector_funcs.py @@ -47,7 +47,7 @@ def init(self): self.lineVertices = None self._vertexResourceName = '{}_{}_vertices'.format( - type(self).__name__, id(self.image)) + type(self).__name__, id(self.vectorImage)) compileShaders( self) updateShaderState(self) @@ -114,15 +114,15 @@ def updateVertices(self): :meth:`.GLLineVertices.calculateHash` method), this function does nothing. """ - image = self.image - if self.lineVertices is None: self.lineVertices = glresources.get( self._vertexResourceName, gllinevector.GLLineVertices, self) if hash(self.lineVertices) != self.lineVertices.calculateHash(self): - log.debug('Re-generating line vertices for {}'.format(image)) + log.debug('Re-generating line vertices ' + 'for {}'.format(self.vectorImage)) + self.lineVertices.refresh(self) glresources.set(self._vertexResourceName, self.lineVertices, @@ -131,8 +131,9 @@ def updateVertices(self): def updateShaderState(self): """Updates all fragment/vertex shader program variables. """ - - opts = self.displayOpts + + image = self.vectorImage + opts = self.displayOpts gl.glEnable(arbvp.GL_VERTEX_PROGRAM_ARB) gl.glEnable(arbfp.GL_FRAGMENT_PROGRAM_ARB) @@ -144,7 +145,7 @@ def updateShaderState(self): voxValXform = self.imageTexture.voxValXform cmapXform = self.xColourTexture.getCoordinateTransform() - shape = np.array(list(self.image.shape[:3]) + [0], dtype=np.float32) + shape = np.array(list(image.shape[:3]) + [0], dtype=np.float32) invShape = 1.0 / shape modThreshold = [opts.modThreshold / 100.0, 0.0, 0.0, 0.0] offset = [0.5, 0.5, 0.5, 0.0] diff --git a/fsl/fsleyes/gl/gl14/glrgbvector_funcs.py b/fsl/fsleyes/gl/gl14/glrgbvector_funcs.py index b420a1c31e6caca39dca56e07ea425d0f0fad26b..7d912e69b27890795a7232b85e2aeb2ee175f19f 100644 --- a/fsl/fsleyes/gl/gl14/glrgbvector_funcs.py +++ b/fsl/fsleyes/gl/gl14/glrgbvector_funcs.py @@ -80,7 +80,7 @@ def updateShaderState(self): voxValXform = self.imageTexture.voxValXform cmapXform = self.xColourTexture.getCoordinateTransform() - shape = list(self.image.shape[:3]) + [0] + shape = list(self.vectorImage.shape[:3]) + [0] modThreshold = [opts.modThreshold / 100.0, 0.0, 0.0, 0.0] shaders.setFragmentProgramMatrix(0, voxValXform) diff --git a/fsl/fsleyes/gl/gl21/gllinevector_funcs.py b/fsl/fsleyes/gl/gl21/gllinevector_funcs.py index 8617f60fe715dec76d99fa222eea16c113481e34..c82a87d158987715ecbd04350ca7a2174caaad3f 100644 --- a/fsl/fsleyes/gl/gl21/gllinevector_funcs.py +++ b/fsl/fsleyes/gl/gl21/gllinevector_funcs.py @@ -48,9 +48,6 @@ def init(self): self.texCoordBuffer = gl.glGenBuffers(1) self.vertexIDBuffer = gl.glGenBuffers(1) - self._vertexResourceName = '{}_{}_vertices'.format( - type(self).__name__, id(self.image)) - opts = self.displayOpts def vertexUpdate(*a): @@ -137,8 +134,9 @@ def compileShaders(self): def updateShaderState(self): """Updates all variables used by the vertex/fragment shaders. """ - - opts = self.displayOpts + + image = self.vectorImage + opts = self.displayOpts # The coordinate transformation matrices for # each of the three colour textures are identical, @@ -146,7 +144,7 @@ def updateShaderState(self): cmapXform = self.xColourTexture.getCoordinateTransform() voxValXform = self.imageTexture.voxValXform useSpline = False - imageShape = np.array(self.image.shape[:3], dtype=np.float32) + imageShape = np.array(image.shape[:3], dtype=np.float32) voxValXform = np.array(voxValXform, dtype=np.float32).ravel('C') cmapXform = np.array(cmapXform, dtype=np.float32).ravel('C') @@ -168,7 +166,7 @@ def updateShaderState(self): gl.glUniform1i(self.zColourTexturePos, 4) directed = opts.directed - imageDims = self.image.pixdim[:3] + imageDims = image.pixdim[:3] d2vMat = opts.getTransform('display', 'voxel') v2dMat = opts.getTransform('voxel', 'display') @@ -207,7 +205,7 @@ def draw(self, zpos, xform=None): """ - image = self.image + image = self.vectorImage opts = self.displayOpts v2dMat = opts.getTransform('voxel', 'display') resolution = np.array([opts.resolution] * 3) diff --git a/fsl/fsleyes/gl/gl21/glrgbvector_funcs.py b/fsl/fsleyes/gl/gl21/glrgbvector_funcs.py index de9009af559d77fcd6934293de8c4d39516e13e9..ab0c9b7b7f9c12c07dc0d6e45c3269baa8090d05 100644 --- a/fsl/fsleyes/gl/gl21/glrgbvector_funcs.py +++ b/fsl/fsleyes/gl/gl21/glrgbvector_funcs.py @@ -98,7 +98,7 @@ def updateShaderState(self): voxValXform = self.imageTexture.voxValXform cmapXform = self.xColourTexture.getCoordinateTransform() useSpline = opts.interpolation == 'spline' - imageShape = np.array(self.image.shape, dtype=np.float32) + imageShape = np.array(self.vectorImage.shape, dtype=np.float32) gl.glUseProgram(self.shaders) diff --git a/fsl/fsleyes/gl/gllabel.py b/fsl/fsleyes/gl/gllabel.py index ae358a136aef4dd2ec4407b061741dff6f52d5ea..f9b4d38d69b602487cdc712ffad6403441910714 100644 --- a/fsl/fsleyes/gl/gllabel.py +++ b/fsl/fsleyes/gl/gllabel.py @@ -82,6 +82,7 @@ class GLLabel(globject.GLImageObject): representation can be updated when they change. """ + image = self.image display = self.display opts = self.displayOpts name = self.name @@ -137,6 +138,7 @@ class GLLabel(globject.GLImageObject): opts .addListener('volume', name, imageUpdate, weak=False) opts .addListener('resolution', name, imageUpdate, weak=False) opts .addListener('transform', name, update, weak=False) + image .addListener('data', name, update, weak=False) opts.lut.addListener('labels', name, lutUpdate, weak=False) # See comment in GLVolume.addDisplayListeners about this @@ -153,6 +155,8 @@ class GLLabel(globject.GLImageObject): """Called by :meth:`destroy`. Removes all of the listeners that were added by :meth:`addListeners`. """ + + image = self.image display = self.display opts = self.displayOpts name = self.name @@ -166,6 +170,7 @@ class GLLabel(globject.GLImageObject): opts .removeListener( 'volume', name) opts .removeListener( 'resolution', name) opts .removeListener( 'transform', name) + image .removeListener( 'data', name) opts.lut.removeListener( 'labels', name) if self.__syncListenersRegistered: diff --git a/fsl/fsleyes/gl/gllinevector.py b/fsl/fsleyes/gl/gllinevector.py index f26812089c2bc87197b9595dec15cbd46a12da86..d9c834a0e144956f3cb103c354acfd7ac77bd53c 100644 --- a/fsl/fsleyes/gl/gllinevector.py +++ b/fsl/fsleyes/gl/gllinevector.py @@ -19,6 +19,7 @@ import logging import numpy as np import fsl.utils.transform as transform +import fsl.data.tensorimage as tensorimage import fsl.fsleyes.gl as fslgl import fsl.fsleyes.gl.glvector as glvector import fsl.fsleyes.gl.routines as glroutines @@ -55,20 +56,32 @@ class GLLineVector(glvector.GLVector): def __init__(self, image, display): """Create a ``GLLineVector`` instance. - :arg image: The :class:`.Image` instance. + :arg image: An :class:`.Image` or :class:`.TensorImage` instance. :arg display: The associated :class:`.Display` instance. """ - glvector.GLVector.__init__(self, image, display) + # If the overlay is a TensorImage, use the + # V1 image is the vector data. Otherwise, + # assume that the overlay is the vector image. + if isinstance(image, tensorimage.TensorImage): vecImage = image.V1() + else: vecImage = image + + glvector.GLVector.__init__(self, image, display, vectorImage=vecImage) fslgl.gllinevector_funcs.init(self) def update(*a): self.onUpdate() - self.displayOpts.addListener( - 'lineWidth', self.name, update, weak=False) + self.displayOpts.addListener('lineWidth', + self.name, + update, + weak=False) + self.vectorImage.addListener('data', + self.name, + update, + weak=False) def destroy(self): @@ -78,6 +91,7 @@ class GLLineVector(glvector.GLVector): function, and calls the :meth:`.GLVector.destroy` method. """ self.displayOpts.removeListener('lineWidth', self.name) + self.vectorImage.removeListener('data', self.name) fslgl.gllinevector_funcs.destroy(self) glvector.GLVector.destroy(self) @@ -247,7 +261,7 @@ class GLLineVertices(object): """ opts = glvec.displayOpts - image = glvec.image + image = glvec.vectorImage # Extract a sub-sample of the vector image # at the current display resolution diff --git a/fsl/fsleyes/gl/globject.py b/fsl/fsleyes/gl/globject.py index 1b0687f8338b2c7cf571b5d77e2c39a0878344d3..4eb8aacb38c0315371e8c5fd8ad5caebff928f05 100644 --- a/fsl/fsleyes/gl/globject.py +++ b/fsl/fsleyes/gl/globject.py @@ -314,7 +314,7 @@ class GLSimpleObject(GLObject): class GLImageObject(GLObject): """The ``GLImageObject`` class is the base class for all GL representations - of :class:`.Image` instances. + of :class:`.Nifti1` instances. """ def __init__(self, image, display): @@ -323,7 +323,8 @@ class GLImageObject(GLObject): This constructor adds the following attributes to this instance: =============== ======================================================= - ``image`` A reference to the :class:`.Image` being displayed. + ``image`` A reference to the :class:`.Nifti1` overlay being + displayed. ``display`` A reference to the :class:`.Display` instance associated with the ``image``. ``displayOpts`` A reference to the :class:`.DisplayOpts` instance, @@ -331,7 +332,7 @@ class GLImageObject(GLObject): is assumed to be a sub-class of :class:`.ImageOpts`. =============== ======================================================= - :arg image: The :class:`.Image` instance + :arg image: The :class:`.Nifti1` instance :arg display: An associated :class:`.Display` instance. """ @@ -341,18 +342,9 @@ class GLImageObject(GLObject): self.display = display self.displayOpts = display.getDisplayOpts() - self.image.addListener('data', self.name, self.__imageDataChanged) - log.memory('{}.init ({})'.format(type(self).__name__, id(self))) - def __imageDataChanged(self, *a): - """Called when the :attr:`.Image.data` changes. Calls - :meth:`GLObject.onUpdate`. - """ - self.onUpdate() - - def __del__(self): """Prints a log message.""" log.memory('{}.del ({})'.format(type(self).__name__, id(self))) @@ -363,7 +355,6 @@ class GLImageObject(GLObject): implementation. It clears references to the :class:`.Image`, :class:`.Display`, and :class:`.DisplayOpts` instances. """ - self.image.removeListener('data', self.name) self.image = None self.display = None self.displayOpts = None diff --git a/fsl/fsleyes/gl/glrgbvector.py b/fsl/fsleyes/gl/glrgbvector.py index b995085b0362f9a8469736215709ba696d0dac7e..fcdd272cc10de35c605759dc76fce3291fd20a14 100644 --- a/fsl/fsleyes/gl/glrgbvector.py +++ b/fsl/fsleyes/gl/glrgbvector.py @@ -12,6 +12,7 @@ vector :class:`.Image` overlays in RGB mode. import numpy as np import OpenGL.GL as gl +import fsl.data.tensorimage as tensorimage import fsl.fsleyes.gl as fslgl import fsl.fsleyes.gl.glvector as glvector @@ -60,17 +61,31 @@ class GLRGBVector(glvector.GLVector): def __init__(self, image, display): """Create a ``GLRGBVector``. - :arg image: The :class:`.Image` instance. + :arg image: An :class:`.Image` or :class:`.TensorImage` instance. :arg display: The associated :class:`.Display` instance. """ - glvector.GLVector.__init__(self, image, display, self.__prefilter) + # If the overlay is a TensorImage, use the + # V1 image is the vector data. Otherwise, + # assume that the overlay is the vector image. + if isinstance(image, tensorimage.TensorImage): vecImage = image.V1() + else: vecImage = image + + glvector.GLVector.__init__(self, + image, + display, + prefilter=np.abs, + vectorImage=vecImage) + fslgl.glrgbvector_funcs.init(self) self.displayOpts.addListener('interpolation', self.name, self.__interpChanged) - + self.vectorImage.addListener('data', + self.name, + self.__dataChanged) + def destroy(self): """Must be called when this ``GLRGBVector`` is no longer needed. @@ -91,13 +106,6 @@ class GLRGBVector(glvector.GLVector): self.__setInterp() - def __prefilter(self, data): - """This method is passed to :meth:`.GLVector.__init__` as the - ``prefilter`` parameter. It removes the sign from the given data. - """ - return np.abs(data) - - def __setInterp(self): """Updates the interpolation setting on the :class:`.ImageTexture` that contains the vector :class:`.Image` being displayed. @@ -109,7 +117,14 @@ class GLRGBVector(glvector.GLVector): self.imageTexture.set(interp=interp) + + def __dataChanged(self, *a): + """Called when the :attr:`.Image.data` of the vector image + changes. Calls :meth:`.GLObject.onUpdate`. + """ + self.onUpdate() + def __interpChanged(self, *a): """Called when the :attr:`.RGBVectorOpts.interpolation` property changes. Updates the :class:`.ImageTexture` interpolation. diff --git a/fsl/fsleyes/gl/gltensor.py b/fsl/fsleyes/gl/gltensor.py index 1be569b2c5daba93bd4e8874cb3869c5211492d9..ee76085e95c539746750a5b4564715d3232e1801 100644 --- a/fsl/fsleyes/gl/gltensor.py +++ b/fsl/fsleyes/gl/gltensor.py @@ -5,8 +5,11 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # + import logging +import numpy as np + import glvector import fsl.fsleyes.gl as fslgl @@ -18,8 +21,11 @@ class GLTensor(glvector.GLVector): def __init__(self, image, display): - glvector.GLVector.__init__(self, image, display) - + glvector.GLVector.__init__(self, + image, + display, + prefilter=np.abs, + vectorImage=image.V1()) fslgl.gltensor_funcs.init(self) diff --git a/fsl/fsleyes/gl/glvector.py b/fsl/fsleyes/gl/glvector.py index 5a8ea15b39d51535b7b80261261eaf3352ef88c3..7a905fa1bd65537c9a57a4061318c20a183bf8d7 100644 --- a/fsl/fsleyes/gl/glvector.py +++ b/fsl/fsleyes/gl/glvector.py @@ -51,7 +51,7 @@ class GLVector(globject.GLImageObject): """ - def __init__(self, image, display, prefilter=None): + def __init__(self, image, display, prefilter=None, vectorImage=None): """Create a ``GLVector`` object bound to the given image and display. Initialises the OpenGL data required to render the given image. @@ -64,19 +64,29 @@ class GLVector(globject.GLImageObject): instances, so the textures and geometry can be updated when necessary. - :arg image: An :class:`.Image` object. + :arg image: An :class:`.Nifti1` object. - :arg display: A :class:`.Display` object which describes how the - image is to be displayed. - - :arg prefilter: An optional function which filters the data before it - is stored as a 3D texture. See :class:`.ImageTexture`. - Whether or not this function is provided, the data is - transposed so that the fourth dimension is the fastest - changing. + :arg display: A :class:`.Display` object which describes how the + image is to be displayed. + + :arg prefilter: An optional function which filters the data before it + is stored as a 3D texture. See + :class:`.ImageTexture`. Whether or not this function + is provided, the data is transposed so that the + fourth dimension is the fastest changing. + + :arg vectorImage: Optional. If ``None``, the ``image`` is assumed to + be a 4D :class:`.Image` instance which contains + the vector data. If this is not the case, the + ``vectorImage`` parameter can be used to specify + an ``Image`` instance which does contain the + vector data. """ - if not image.is4DImage() or image.shape[3] != 3: + if vectorImage is None: + vectorImage = image + + if len(vectorImage.shape) != 4 or vectorImage.shape[3] != 3: raise ValueError('Image must be 4 dimensional, with 3 volumes ' 'representing the XYZ vector angles') @@ -84,6 +94,7 @@ class GLVector(globject.GLImageObject): name = self.name + self.vectorImage = vectorImage self.xColourTexture = textures.ColourMapTexture('{}_x'.format(name)) self.yColourTexture = textures.ColourMapTexture('{}_y'.format(name)) self.zColourTexture = textures.ColourMapTexture('{}_z'.format(name)) @@ -225,7 +236,8 @@ class GLVector(globject.GLImageObject): opts = self.displayOpts prefilter = self.prefilter - texName = '{}_{}'.format(type(self).__name__, id(self.image)) + vecImage = self.vectorImage + texName = '{}_{}'.format(type(self).__name__, id(vecImage)) if self.imageTexture is not None: glresources.delete(self.imageTexture.getTextureName()) @@ -248,7 +260,7 @@ class GLVector(globject.GLImageObject): texName, textures.ImageTexture, texName, - self.image, + vecImage, nvals=3, normalise=True, prefilter=realPrefilter) diff --git a/fsl/fsleyes/gl/glvolume.py b/fsl/fsleyes/gl/glvolume.py index 227a2042945ecf92e28cdf9c5228ca38a333bf82..aadf49cf05b052edbf70fb863a1a4f110b8a8080 100644 --- a/fsl/fsleyes/gl/glvolume.py +++ b/fsl/fsleyes/gl/glvolume.py @@ -186,6 +186,7 @@ class GLVolume(globject.GLImageObject): display properties are changed. """ + image = self.image display = self.display opts = self.displayOpts lName = self.name @@ -234,6 +235,7 @@ class GLVolume(globject.GLImageObject): opts .addListener('resolution', lName, imageUpdate, weak=False) opts .addListener('interpolation', lName, imageUpdate, weak=False) opts .addListener('transform', lName, update, weak=False) + image .addListener('data', lName, update, weak=False) # Save a flag so the removeDisplayListeners # method knows whether it needs to de-register @@ -258,6 +260,7 @@ class GLVolume(globject.GLImageObject): were added by :meth:`addDisplayListeners`. """ + image = self.image display = self.display opts = self.displayOpts @@ -276,6 +279,7 @@ class GLVolume(globject.GLImageObject): opts .removeListener( 'resolution', lName) opts .removeListener( 'interpolation', lName) opts .removeListener( 'transform', lName) + image .removeListener( 'data', lName) if self.__syncListenersRegistered: opts.removeSyncChangeListener('volume', lName)