From 92441c66052f3feb54ff122a366647dc4133a794 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Wed, 16 Dec 2015 16:50:26 +0000 Subject: [PATCH] GLVector now supports a separate clipping image, in addition to the modulate image. Gl14, GLLineVector not yet supported, and things in the GLVolume class hierarchy are broken. --- fsl/data/strings.py | 8 +- fsl/fsleyes/controls/overlaydisplaypanel.py | 31 ++- fsl/fsleyes/controls/overlaydisplaytoolbar.py | 72 ++--- fsl/fsleyes/displaycontext/group.py | 5 +- fsl/fsleyes/displaycontext/vectoropts.py | 84 ++++-- fsl/fsleyes/fsleyes_parseargs.py | 119 +++++---- fsl/fsleyes/gl/gl21/glrgbvector_funcs.py | 91 ++++--- fsl/fsleyes/gl/gl21/gltensor_funcs.py | 141 +++++----- fsl/fsleyes/gl/gl21/glvector_frag.glsl | 38 ++- fsl/fsleyes/gl/gl21/glvolume_funcs.py | 106 ++++---- fsl/fsleyes/gl/glvector.py | 248 +++++++++++------- fsl/fsleyes/tooltips.py | 24 +- 12 files changed, 566 insertions(+), 401 deletions(-) diff --git a/fsl/data/strings.py b/fsl/data/strings.py index d5f46d795..acc7f1874 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -553,8 +553,9 @@ properties = TypeDict({ 'VectorOpts.suppressX' : 'Suppress X value', 'VectorOpts.suppressY' : 'Suppress Y value', 'VectorOpts.suppressZ' : 'Suppress Z value', - 'VectorOpts.modulate' : 'Modulate by', - 'VectorOpts.modThreshold' : 'Modulation threshold', + 'VectorOpts.modulateImage' : 'Modulate by', + 'VectorOpts.clipImage' : 'Clip by', + 'VectorOpts.clipThreshold' : 'Clipping threshold', 'RGBVectorOpts.interpolation' : 'Interpolation', @@ -615,7 +616,8 @@ choices = TypeDict({ 'VectorOpts.displayType.line' : 'Lines', 'VectorOpts.displayType.rgb' : 'RGB', - 'VectorOpts.modulate.none' : 'No modulation', + 'VectorOpts.modulateImage.none' : 'No modulation', + 'VectorOpts.clipImage.none' : 'No clipping', 'ModelOpts.refImage.none' : 'No reference image', diff --git a/fsl/fsleyes/controls/overlaydisplaypanel.py b/fsl/fsleyes/controls/overlaydisplaypanel.py index 6dae3a73b..00efee699 100644 --- a/fsl/fsleyes/controls/overlaydisplaypanel.py +++ b/fsl/fsleyes/controls/overlaydisplaypanel.py @@ -275,8 +275,8 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): def _imageName(img): - """Used to generate choice labels for the :attr`.VectorOpts.modulate` and - :attr:`.ModelOpts.refImage` properties. + """Used to generate choice labels for the :attr`.VectorOpts.modulateImage`, + :attr`.VectorOpts.clipImage` and :attr:`.ModelOpts.refImage` properties. """ if img is None: return 'None' else: return img.name @@ -333,8 +333,13 @@ _DISPLAY_PROPS = td.TypeDict({ props.Widget('suppressX'), props.Widget('suppressY'), props.Widget('suppressZ'), - props.Widget('modulate', labels=_imageName), - props.Widget('modThreshold', showLimits=False, spin=False)], + props.Widget('modulateImage', labels=_imageName), + props.Widget('clipImage', labels=_imageName), + props.Widget('clipThreshold', + showLimits=False, + spin=False, + dependencies=['clipImage'], + enabledWhen=lambda o, ci: ci is not None)], 'LineVectorOpts' : [ props.Widget('resolution', showLimits=False), @@ -346,8 +351,13 @@ _DISPLAY_PROPS = td.TypeDict({ props.Widget('suppressZ'), props.Widget('directed'), props.Widget('lineWidth', showLimits=False), - props.Widget('modulate', labels=_imageName), - props.Widget('modThreshold', showLimits=False, spin=False)], + props.Widget('modulateImage', labels=_imageName), + props.Widget('clipImage', labels=_imageName), + props.Widget('clipThreshold', + showLimits=False, + spin=False, + dependencies=['clipImage'], + enabledWhen=lambda o, ci: ci is not None)], 'ModelOpts' : [ props.Widget('colour'), @@ -373,8 +383,13 @@ _DISPLAY_PROPS = td.TypeDict({ props.Widget('suppressX'), props.Widget('suppressY'), props.Widget('suppressZ'), - props.Widget('modulate', labels=_imageName), - props.Widget('modThreshold', showLimits=False, spin=False)], + props.Widget('modulateImage', labels=_imageName), + props.Widget('clipImage', labels=_imageName), + props.Widget('clipThreshold', + showLimits=False, + spin=False, + dependencies=['clipImage'], + enabledWhen=lambda o, ci: ci is not None)], 'LabelOpts' : [ props.Widget('lut', labels=lambda l: l.name), diff --git a/fsl/fsleyes/controls/overlaydisplaytoolbar.py b/fsl/fsleyes/controls/overlaydisplaytoolbar.py index e6a4607ad..f860b7ced 100644 --- a/fsl/fsleyes/controls/overlaydisplaytoolbar.py +++ b/fsl/fsleyes/controls/overlaydisplaytoolbar.py @@ -330,8 +330,8 @@ class OverlayDisplayToolBar(fsltoolbar.FSLEyesToolBar): of the given :class:`.VectorOpts` instance. """ - modSpec = _TOOLBAR_PROPS[opts]['modulate'] - thresSpec = _TOOLBAR_PROPS[opts]['modThreshold'] + modSpec = _TOOLBAR_PROPS[opts]['modulateImage'] + thresSpec = _TOOLBAR_PROPS[opts]['clipThreshold'] panel = wx.Panel(self) sizer = wx.FlexGridSizer(2, 2) @@ -342,8 +342,8 @@ class OverlayDisplayToolBar(fsltoolbar.FSLEyesToolBar): modLabel = wx.StaticText(panel) thresLabel = wx.StaticText(panel) - modLabel .SetLabel(strings.properties[opts, 'modulate']) - thresLabel.SetLabel(strings.properties[opts, 'modThreshold']) + modLabel .SetLabel(strings.properties[opts, 'modulateImage']) + thresLabel.SetLabel(strings.properties[opts, 'clipThreshold']) sizer.Add(modLabel) sizer.Add(modWidget, flag=wx.EXPAND) @@ -406,7 +406,7 @@ def _modImageLabel(img): """Used to generate labels for the :attr:`.VectorOpts.modulate` property choices. """ - if img is None: return strings.choices['VectorOpts.modulate.none'] + if img is None: return strings.choices['VectorOpts.modulateImage.none'] else: return img.name @@ -432,15 +432,15 @@ _TOOLTIPS = td.TypeDict({ 'LabelOpts.outlineWidth' : fsltooltips.properties['LabelOpts.' 'outlineWidth'], - 'RGBVectorOpts.modulate' : fsltooltips.properties['VectorOpts.' - 'modulate'], - 'RGBVectorOpts.modThreshold' : fsltooltips.properties['VectorOpts.' - 'modThreshold'], + 'RGBVectorOpts.modulateImage' : fsltooltips.properties['VectorOpts.' + 'modulateImage'], + 'RGBVectorOpts.clipThreshold' : fsltooltips.properties['VectorOpts.' + 'clipThreshold'], - 'LineVectorOpts.modulate' : fsltooltips.properties['VectorOpts.' - 'modulate'], - 'LineVectorOpts.modThreshold' : fsltooltips.properties['VectorOpts.' - 'modThreshold'], + 'LineVectorOpts.modulateImage' : fsltooltips.properties['VectorOpts.' + 'modulateImage'], + 'LineVectorOpts.clipThreshold' : fsltooltips.properties['VectorOpts.' + 'clipThreshold'], 'LineVectorOpts.lineWidth' : fsltooltips.properties['LineVectorOpts.' 'lineWidth'], @@ -449,10 +449,10 @@ _TOOLTIPS = td.TypeDict({ 'ModelOpts.outlineWidth' : fsltooltips.properties['ModelOpts.' 'outlineWidth'], - 'TensorOpts.modulate' : fsltooltips.properties['VectorOpts.' - 'modulate'], - 'TensorOpts.modThreshold' : fsltooltips.properties['VectorOpts.' - 'modThreshold'], + 'TensorOpts.modulateImage' : fsltooltips.properties['VectorOpts.' + 'modulateImage'], + 'TensorOpts.clipThreshold' : fsltooltips.properties['VectorOpts.' + 'clipThreshold'], }) """This dictionary contains tooltips for :class:`.Display` and :class:`.DisplayOpts` properties. It is referenced in the @@ -532,26 +532,26 @@ _TOOLBAR_PROPS = td.TypeDict({ spin=False)}, 'RGBVectorOpts' : { - 'modulate' : props.Widget( - 'modulate', + 'modulateImage' : props.Widget( + 'modulateImage', labels=_modImageLabel, - tooltip=_TOOLTIPS['RGBVectorOpts.modulate']), - 'modThreshold' : props.Widget( - 'modThreshold', + tooltip=_TOOLTIPS['RGBVectorOpts.modulateImage']), + 'clipThreshold' : props.Widget( + 'clipThreshold', showLimits=False, spin=False, - tooltip=_TOOLTIPS['RGBVectorOpts.modThreshold'])}, + tooltip=_TOOLTIPS['RGBVectorOpts.clipThreshold'])}, 'LineVectorOpts' : { - 'modulate' : props.Widget( - 'modulate', + 'modulateImage' : props.Widget( + 'modulateImage', labels=_modImageLabel, - tooltip=_TOOLTIPS['LineVectorOpts.modulate']), - 'modThreshold' : props.Widget( - 'modThreshold', + tooltip=_TOOLTIPS['LineVectorOpts.modulateImage']), + 'clipThreshold' : props.Widget( + 'clipThreshold', showLimits=False, spin=False, - tooltip=_TOOLTIPS['LineVectorOpts.modThreshold']), + tooltip=_TOOLTIPS['LineVectorOpts.clipThreshold']), 'lineWidth' : props.Widget( 'lineWidth', showLimits=False, @@ -577,16 +577,16 @@ _TOOLBAR_PROPS = td.TypeDict({ enabledWhen=lambda i: i.outline)}, 'TensorOpts' : { - 'lighting' : props.Widget('lighting'), - 'modulate' : props.Widget( - 'modulate', + 'lighting' : props.Widget('lighting'), + 'modulateImage' : props.Widget( + 'modulateImage', labels=_modImageLabel, - tooltip=_TOOLTIPS['TensorOpts.modulate']), - 'modThreshold' : props.Widget( - 'modThreshold', + tooltip=_TOOLTIPS['TensorOpts.modulateImage']), + 'clipThreshold' : props.Widget( + 'clipThreshold', showLimits=False, spin=False, - tooltip=_TOOLTIPS['TensorOpts.modThreshold'])} + tooltip=_TOOLTIPS['TensorOpts.clipThreshold'])} }) """This dictionary defines specifications for all controls shown on an :class:`OverlayDisplayToolBar`. diff --git a/fsl/fsleyes/displaycontext/group.py b/fsl/fsleyes/displaycontext/group.py index ff40aa36c..3485e7432 100644 --- a/fsl/fsleyes/displaycontext/group.py +++ b/fsl/fsleyes/displaycontext/group.py @@ -74,8 +74,9 @@ class OverlayGroup(props.HasProperties): 'VectorOpts' : ['suppressX', 'suppressY', 'suppressZ', - 'modulate', - 'modThreshold'], + 'modulateImage', + 'clipImage', + 'clipThreshold'], 'LineVectorOpts' : ['lineWidth', 'directed'], 'RGBVectorOpts' : ['interpolation'], diff --git a/fsl/fsleyes/displaycontext/vectoropts.py b/fsl/fsleyes/displaycontext/vectoropts.py index 6311eeca8..2487eb2f7 100644 --- a/fsl/fsleyes/displaycontext/vectoropts.py +++ b/fsl/fsleyes/displaycontext/vectoropts.py @@ -49,17 +49,23 @@ class VectorOpts(volumeopts.Nifti1Opts): """Do not use the Z vector magnitude to colour vectors.""" - modulate = props.Choice() - """Modulate the vector colours by another image. Any image which is in the - :class:`.OverlayList`, and which has the same voxel dimensions as the - vector image can be selected for modulation. + modulateImage = props.Choice() + """Modulate the vector colour brightness by another image. Any image which + is in the :class:`.OverlayList`, and which has the same voxel dimensions as + the vector image can be selected for modulation. """ - # TODO This is currently a percentage - # of the modulation image data range. - # It should be an absolute value - modThreshold = props.Percentage(default=0.0) + clipImage = props.Choice() + """Clip voxels from the vector image according to another image. Any image + which is in the :class:`.OverlayList`, and which has the same voxel + dimensions as the vector image can be selected for clipping. The + :attr:`clipThreshold` dictates the value below which vector voxels are + clipped. + """ + + + clipThreshold = props.Real(default=0.0, minval=0, maxval=1) """Hide voxels for which the modulation value is below this threshold, as a percentage of the :attr:`modulate` image data range. """ @@ -76,8 +82,12 @@ class VectorOpts(volumeopts.Nifti1Opts): self.overlayList.addListener('overlays', self.name, self.__overlayListChanged) + self .addListener('clipImage', + self.name, + self.__clipImageChanged) self.__overlayListChanged() + self.__clipImageChanged() def destroy(self): @@ -93,14 +103,34 @@ class VectorOpts(volumeopts.Nifti1Opts): volumeopts.Nifti1Opts.destroy(self) + def __clipImageChanged(self, *a): + """Called when the :attr:`clipImage` property changes. Updates + the range of the :attr:`clipThreshold` property. + """ + + image = self.clipImage + + if image is None: + return + + opts = self.displayCtx.getOpts(image) + minval = opts.dataMin + maxval = opts.dataMax + + self.setConstraint('clipThreshold', 'minval', minval) + self.setConstraint('clipThreshold', 'maxval', maxval) + + def __overlayListChanged(self, *a): - """Called when the overlay list changes. Updates the :attr:`modulate` - property so that it contains a list of overlays which could be used - to modulate the vector image. + """Called when the overlay list changes. Updates the :attr:`modulateImage` + and :attr:`clipImage` properties so that they contain a list of + overlays which could be used to modulate the vector image. """ - modProp = self.getProp('modulate') - modVal = self.modulate + modProp = self.getProp('modulateImage') + clipProp = self.getProp('clipImage') + modVal = self.modulateImage + clipVal = self.clipImage overlays = self.displayCtx.getOrderedOverlays() # the image for this VectorOpts @@ -109,29 +139,30 @@ class VectorOpts(volumeopts.Nifti1Opts): self.overlayList.removeListener('overlays', self.name) return - modOptions = [None] + options = [None] for overlay in overlays: # It doesn't make sense to - # modulate the image by itself + # modulate/clip the image by + # itself. if overlay is self.overlay: continue - # The modulate image must - # be an image. Duh. + # The modulate/clip images + # must be images. if not isinstance(overlay, fslimage.Image): continue # an image can only be used to - # modulate the vector image if - # it shares the same dimensions - # as said vector image. 4D - # images are ok though. + # modulate/clip the vector image + # if it shares the same + # dimensions as said vector image. + # 4D images are ok though. if overlay.shape[:3] != self.overlay.shape[:3]: continue - modOptions.append(overlay) + options.append(overlay) display = self.displayCtx.getDisplay(overlay) display.addListener('name', @@ -139,10 +170,13 @@ class VectorOpts(volumeopts.Nifti1Opts): self.__overlayListChanged, overwrite=True) - modProp.setChoices(modOptions, instance=self) + modProp .setChoices(options, instance=self) + clipProp.setChoices(options, instance=self) - if modVal in overlays: self.modulate = modVal - else: self.modulate = None + if modVal in overlays: self.modulateImage = modVal + else: self.modulateImage = None + if clipVal in overlays: self.clipImage = clipVal + else: self.clipImage = None class LineVectorOpts(VectorOpts): diff --git a/fsl/fsleyes/fsleyes_parseargs.py b/fsl/fsleyes/fsleyes_parseargs.py index a4d98a98b..a6d3f1f30 100644 --- a/fsl/fsleyes/fsleyes_parseargs.py +++ b/fsl/fsleyes/fsleyes_parseargs.py @@ -205,8 +205,9 @@ OPTIONS = td.TypeDict({ 'suppressX', 'suppressY', 'suppressZ', - 'modulate', - 'modThreshold'], + 'modulateImage', + 'clipImage', + 'clipThreshold'], 'LineVectorOpts' : ['lineWidth', 'directed'], 'RGBVectorOpts' : ['interpolation'], @@ -338,14 +339,15 @@ ARGUMENTS = td.TypeDict({ 'MaskOpts.invert' : ('mi', 'maskInvert'), 'MaskOpts.threshold' : ('t', 'threshold'), - 'VectorOpts.xColour' : ('xc', 'xColour'), - 'VectorOpts.yColour' : ('yc', 'yColour'), - 'VectorOpts.zColour' : ('zc', 'zColour'), - 'VectorOpts.suppressX' : ('xs', 'suppressX'), - 'VectorOpts.suppressY' : ('ys', 'suppressY'), - 'VectorOpts.suppressZ' : ('zs', 'suppressZ'), - 'VectorOpts.modulate' : ('m', 'modulate'), - 'VectorOpts.modThreshold': ('mt', 'modThreshold'), + 'VectorOpts.xColour' : ('xc', 'xColour'), + 'VectorOpts.yColour' : ('yc', 'yColour'), + 'VectorOpts.zColour' : ('zc', 'zColour'), + 'VectorOpts.suppressX' : ('xs', 'suppressX'), + 'VectorOpts.suppressY' : ('ys', 'suppressY'), + 'VectorOpts.suppressZ' : ('zs', 'suppressZ'), + 'VectorOpts.modulateImage' : ('md', 'modulateImage'), + 'VectorOpts.clipImage' : ('cl', 'clipImage'), + 'VectorOpts.clipThreshold' : ('ct', 'clipThreshold'), 'LineVectorOpts.lineWidth' : ('lvw', 'lineWidth'), 'LineVectorOpts.directed' : ('lvi', 'directed'), @@ -443,16 +445,17 @@ HELP = td.TypeDict({ 'MaskOpts.invert' : 'Invert', 'MaskOpts.threshold' : 'Threshold', - 'VectorOpts.xColour' : 'X colour', - 'VectorOpts.yColour' : 'Y colour', - 'VectorOpts.zColour' : 'Z colour', - 'VectorOpts.suppressX' : 'Suppress X magnitude', - 'VectorOpts.suppressY' : 'Suppress Y magnitude', - 'VectorOpts.suppressZ' : 'Suppress Z magnitude', - 'VectorOpts.modulate' : 'Modulate vector colours', - 'VectorOpts.modThreshold' : 'Hide voxels where modulation ' - 'value is below this threshold ' - '(expressed as a percentage)', + 'VectorOpts.xColour' : 'X colour', + 'VectorOpts.yColour' : 'Y colour', + 'VectorOpts.zColour' : 'Z colour', + 'VectorOpts.suppressX' : 'Suppress X magnitude', + 'VectorOpts.suppressY' : 'Suppress Y magnitude', + 'VectorOpts.suppressZ' : 'Suppress Z magnitude', + 'VectorOpts.modulateImage' : 'Modulate vector brightness', + 'VectorOpts.clipImage' : 'Clip vector voxels', + 'VectorOpts.clipThreshold' : 'Hide voxels where clip image ' + 'value is below this threshold ' + '(expressed as a percentage)', 'LineVectorOpts.lineWidth' : 'Line width', 'LineVectorOpts.directed' : 'Interpret vectors as directed', @@ -523,10 +526,11 @@ TRANSFORMS = td.TypeDict({ # when reading in command line arguments - # the transform function specified here # is only used when generating arguments - 'VectorOpts.modulate' : _imageTrans, - 'ModelOpts.refImage' : _imageTrans, + 'VectorOpts.modulateImage' : _imageTrans, + 'VectorOpts.clipImage' : _imageTrans, + 'ModelOpts.refImage' : _imageTrans, - 'LabelOpts.lut' : _lutTrans, + 'LabelOpts.lut' : _lutTrans, }) """This dictionary defines any transformations for command line options where the value passed on the command line cannot be directly converted @@ -722,11 +726,16 @@ def _configOverlayParser(ovlParser): propNames = list(OPTIONS[target]) specialOptions = [] - # The VectorOpts.modulate - # option needs special treatment - if target == VectorOpts and 'modulate' in propNames: - specialOptions.append('modulate') - propNames.remove('modulate') + # The VectorOpts.modulateImage + # and clipImage options need + # special treatment + if target == VectorOpts and 'modulateImage' in propNames: + specialOptions.append('modulateImage') + propNames.remove('modulateImage') + + if target == VectorOpts and 'clipImage' in propNames: + specialOptions.append('clipImage') + propNames.remove('clipImage') # The same goes for the # ModelOpts.refImage option @@ -870,12 +879,13 @@ def parseArgs(mainParser, argv, name, desc, toolOptsDesc='[options]'): # short/long arguments into a 1D list. fileOpts = [] - # The VectorOpts.modulate option allows - # the user to specify another image file - # by which the vector image colours are - # to be modulated. The same goes for the - # ModelOpts.refImage option - fileOpts.extend(ARGUMENTS[fsldisplay.VectorOpts, 'modulate']) + # The VectorOpts.modulateImage and + # clipImage options allow the user + # to specify another image file. + # The same goes for the + # ModelOpts.refImage option. + fileOpts.extend(ARGUMENTS[fsldisplay.VectorOpts, 'modulateImage']) + fileOpts.extend(ARGUMENTS[fsldisplay.VectorOpts, 'clipImage']) fileOpts.extend(ARGUMENTS[fsldisplay.ModelOpts, 'refImage']) # There is a possibility that the user @@ -1156,12 +1166,13 @@ def applyOverlayArgs(args, overlayList, displayCtx, **kwargs): # DisplayOpts instance will be replaced opts = display.getDisplayOpts() - # VectorOpts.modulate is a Choice property, - # where the valid choices are defined by - # the current contents of the overlay list. - # So when the user specifies a modulation - # image, we need to do an explicit check - # to see if the specified image is vaid + # VectorOpts.modulateImage and clipImage + # are Choice properties, where the valid + # choices are defined by the current + # contents of the overlay list. So when + # the user specifies one of these images, + # we need to do an explicit check to see + # if the specified image is valid # # Here, I'm loading the image, and checking # to see if it can be used to modulate the @@ -1171,10 +1182,10 @@ def applyOverlayArgs(args, overlayList, displayCtx, **kwargs): # value. If the modulate file is not valid, # an error is raised. if isinstance(opts, fsldisplay.VectorOpts) and \ - args.overlays[i].modulate is not None: + args.overlays[i].modulateImage is not None: modImage = _findOrLoad(overlayList, - args.overlays[i].modulate, + args.overlays[i].modulateImage, fslimage.Image, overlay) @@ -1183,12 +1194,32 @@ def applyOverlayArgs(args, overlayList, displayCtx, **kwargs): 'Image {} cannot be used to modulate {} - ' 'dimensions don\'t match'.format(modImage, overlay)) - opts.modulate = modImage - args.overlays[i].modulate = None + opts.modulateImage = modImage + args.overlays[i].modulateImage = None log.debug('Set {} to be modulated by {}'.format( overlay, modImage)) + # Same process for VectorOpts.clipImage + if isinstance(opts, fsldisplay.VectorOpts) and \ + args.overlays[i].clipImage is not None: + + clipImage = _findOrLoad(overlayList, + args.overlays[i].clipImage, + fslimage.Image, + overlay) + + if clipImage.shape != overlay.shape[ :3]: + raise RuntimeError( + 'Image {} cannot be used to clip {} - ' + 'dimensions don\'t match'.format(clipImage, overlay)) + + opts.clipImage = clipImage + args.overlays[i].modulateImage = None + + log.debug('Set {} to be clipped by {}'.format( + overlay, clipImage)) + # A similar process is followed for # the ModelOpts.refImage property if isinstance(overlay, fslmodel.Model) and \ diff --git a/fsl/fsleyes/gl/gl21/glrgbvector_funcs.py b/fsl/fsleyes/gl/gl21/glrgbvector_funcs.py index ab0c9b7b7..d500cfec9 100644 --- a/fsl/fsleyes/gl/gl21/glrgbvector_funcs.py +++ b/fsl/fsleyes/gl/gl21/glrgbvector_funcs.py @@ -60,60 +60,67 @@ def compileShaders(self): self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc) - self.vertexPos = gl.glGetAttribLocation( self.shaders, - 'vertex') - self.voxCoordPos = gl.glGetAttribLocation( self.shaders, - 'voxCoord') - self.texCoordPos = gl.glGetAttribLocation( self.shaders, - 'texCoord') - self.imageTexturePos = gl.glGetUniformLocation(self.shaders, - 'imageTexture') - self.modTexturePos = gl.glGetUniformLocation(self.shaders, - 'modTexture') - self.xColourTexturePos = gl.glGetUniformLocation(self.shaders, - 'xColourTexture') - self.yColourTexturePos = gl.glGetUniformLocation(self.shaders, - 'yColourTexture') - self.zColourTexturePos = gl.glGetUniformLocation(self.shaders, - 'zColourTexture') - self.modThresholdPos = gl.glGetUniformLocation(self.shaders, - 'modThreshold') - self.useSplinePos = gl.glGetUniformLocation(self.shaders, - 'useSpline') - self.imageShapePos = gl.glGetUniformLocation(self.shaders, - 'imageShape') - self.voxValXformPos = gl.glGetUniformLocation(self.shaders, - 'voxValXform') - self.cmapXformPos = gl.glGetUniformLocation(self.shaders, - 'cmapXform') + shaderVars = {} + + vertUniforms = [] + vertAtts = ['vertex', 'voxCoord', 'texCoord'] + + fragUniforms = ['imageTexture', 'modulateTexture', 'clipTexture', + 'clipThreshold', 'xColourTexture', 'yColourTexture', + 'zColourTexture', 'voxValXform', 'cmapXform', + 'imageShape', 'useSpline'] + + for va in vertAtts: + shaderVars[va] = gl.glGetAttribLocation(self.shaders, va) + + for vu in vertUniforms: + shaderVars[va] = gl.glGetUniformLocation(self.shaders, vu) + + for fu in fragUniforms: + if fu in shaderVars: + continue + shaderVars[fu] = gl.glGetUniformLocation(self.shaders, fu) + + self.shaderVars = shaderVars def updateShaderState(self): """Updates all shader program variables. """ - opts = self.displayOpts + opts = self.displayOpts + svars = self.shaderVars # The coordinate transformation matrices for # each of the three colour textures are identical - voxValXform = self.imageTexture.voxValXform - cmapXform = self.xColourTexture.getCoordinateTransform() - useSpline = opts.interpolation == 'spline' - imageShape = np.array(self.vectorImage.shape, dtype=np.float32) - + voxValXform = self.imageTexture.voxValXform + invClipValXform = self.clipTexture .invVoxValXform + cmapXform = self.xColourTexture.getCoordinateTransform() + useSpline = opts.interpolation == 'spline' + imageShape = np.array(self.vectorImage.shape, dtype=np.float32) + clipThreshold = opts.clipThreshold + + # Transform the clip threshold into + # the texture value range, so the + # fragment shader can compare texture + # values directly to it. + clipThreshold = clipThreshold * invClipValXform[0, 0] + \ + invClipValXform[3, 0] + gl.glUseProgram(self.shaders) - gl.glUniform1f( self.useSplinePos, useSpline) - gl.glUniform3fv(self.imageShapePos, 1, imageShape) + gl.glUniform1f( svars['useSpline'], useSpline) + gl.glUniform3fv(svars['imageShape'], 1, imageShape) - gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, voxValXform) - gl.glUniformMatrix4fv(self.cmapXformPos, 1, False, cmapXform) + gl.glUniformMatrix4fv(svars['voxValXform'], 1, False, voxValXform) + gl.glUniformMatrix4fv(svars['cmapXform'], 1, False, cmapXform) - gl.glUniform1f(self.modThresholdPos, opts.modThreshold / 100.0) - gl.glUniform1i(self.imageTexturePos, 0) - gl.glUniform1i(self.modTexturePos, 1) - gl.glUniform1i(self.xColourTexturePos, 2) - gl.glUniform1i(self.yColourTexturePos, 3) - gl.glUniform1i(self.zColourTexturePos, 4) + gl.glUniform1f(svars['clipThreshold'], clipThreshold) + gl.glUniform1i(svars['imageTexture'], 0) + gl.glUniform1i(svars['modulateTexture'], 1) + gl.glUniform1i(svars['clipTexture'], 2) + gl.glUniform1i(svars['xColourTexture'], 3) + gl.glUniform1i(svars['yColourTexture'], 4) + gl.glUniform1i(svars['zColourTexture'], 5) gl.glUseProgram(0) diff --git a/fsl/fsleyes/gl/gl21/gltensor_funcs.py b/fsl/fsleyes/gl/gl21/gltensor_funcs.py index c999c31e8..9d1506a31 100644 --- a/fsl/fsleyes/gl/gl21/gltensor_funcs.py +++ b/fsl/fsleyes/gl/gl21/gltensor_funcs.py @@ -103,6 +103,8 @@ def compileShaders(self): self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc) + shaderVars = {} + vertUniforms = ['v1Texture', 'v2Texture', 'v3Texture', 'l1Texture', 'l2Texture', 'l3Texture', 'v1ValXform', 'v2ValXform', 'v3ValXform', @@ -113,46 +115,47 @@ def compileShaders(self): vertAtts = ['voxel', 'vertex'] - fragUniforms = ['imageTexture', 'modTexture', 'modThreshold', - 'xColourTexture', 'yColourTexture', 'zColourTexture', - 'voxValXform', 'cmapXform', 'imageShape', - 'useSpline'] + fragUniforms = ['imageTexture', 'modulateTexture', 'clipTexture', + 'clipThreshold', 'xColourTexture', 'yColourTexture', + 'zColourTexture', 'voxValXform', 'cmapXform', + 'imageShape', 'useSpline'] for vu in vertUniforms: - loc = gl.glGetUniformLocation(self.shaders, vu) - setattr(self, '{}Pos'.format(vu), loc) + shaderVars[vu] = gl.glGetUniformLocation(self.shaders, vu) + + for va in vertAtts: + shaderVars[va] = gl.glGetAttribLocation(self.shaders, va) for fu in fragUniforms: - if hasattr(self, '{}Pos'.format(fu)): + if fu in shaderVars: continue - loc = gl.glGetUniformLocation(self.shaders, fu) - setattr(self, '{}Pos'.format(fu), loc) + shaderVars[fu] = gl.glGetUniformLocation(self.shaders, fu) - for va in vertAtts: - loc = gl.glGetAttribLocation(self.shaders, va) - setattr(self, '{}Pos'.format(va), loc) + self.shaderVars = shaderVars def updateShaderState(self): gl.glUseProgram(self.shaders) - opts = self.displayOpts + opts = self.displayOpts + svars = self.shaderVars # Textures used by the fragment shader - gl.glUniform1i(self.imageTexturePos, 0) - gl.glUniform1i(self.modTexturePos, 1) - gl.glUniform1i(self.xColourTexturePos, 2) - gl.glUniform1i(self.yColourTexturePos, 3) - gl.glUniform1i(self.zColourTexturePos, 4) + gl.glUniform1i(svars['imageTexture'], 0) + gl.glUniform1i(svars['modulateTexture'], 1) + gl.glUniform1i(svars['clipTexture'], 2) + gl.glUniform1i(svars['xColourTexture'], 3) + gl.glUniform1i(svars['yColourTexture'], 4) + gl.glUniform1i(svars['zColourTexture'], 5) # Textures used by the vertex shader - gl.glUniform1i(self.v1TexturePos, 5) - gl.glUniform1i(self.v2TexturePos, 6) - gl.glUniform1i(self.v3TexturePos, 7) - gl.glUniform1i(self.l1TexturePos, 8) - gl.glUniform1i(self.l2TexturePos, 9) - gl.glUniform1i(self.l3TexturePos, 10) + gl.glUniform1i(svars['v1Texture'], 6) + gl.glUniform1i(svars['v2Texture'], 7) + gl.glUniform1i(svars['v3Texture'], 8) + gl.glUniform1i(svars['l1Texture'], 9) + gl.glUniform1i(svars['l2Texture'], 10) + gl.glUniform1i(svars['l3Texture'], 11) # Texture -> value value offsets/scales # used by the vertex and fragment shaders @@ -174,31 +177,35 @@ def updateShaderState(self): l2ValXform = np.array(l2ValXform, dtype=np.float32).ravel('C') l3ValXform = np.array(l3ValXform, dtype=np.float32).ravel('C') - gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, voxValXform) - gl.glUniformMatrix4fv(self.cmapXformPos, 1, False, cmapXform) - gl.glUniformMatrix4fv(self.v1ValXformPos, 1, False, v1ValXform) - gl.glUniformMatrix4fv(self.v2ValXformPos, 1, False, v2ValXform) - gl.glUniformMatrix4fv(self.v3ValXformPos, 1, False, v3ValXform) - gl.glUniformMatrix4fv(self.l1ValXformPos, 1, False, l1ValXform) - gl.glUniformMatrix4fv(self.l2ValXformPos, 1, False, l2ValXform) - gl.glUniformMatrix4fv(self.l3ValXformPos, 1, False, l3ValXform) + gl.glUniformMatrix4fv(svars['voxValXform'], 1, False, voxValXform) + gl.glUniformMatrix4fv(svars['cmapXform'], 1, False, cmapXform) + gl.glUniformMatrix4fv(svars['v1ValXform'], 1, False, v1ValXform) + gl.glUniformMatrix4fv(svars['v2ValXform'], 1, False, v2ValXform) + gl.glUniformMatrix4fv(svars['v3ValXform'], 1, False, v3ValXform) + gl.glUniformMatrix4fv(svars['l1ValXform'], 1, False, l1ValXform) + gl.glUniformMatrix4fv(svars['l2ValXform'], 1, False, l2ValXform) + gl.glUniformMatrix4fv(svars['l3ValXform'], 1, False, l3ValXform) # Other miscellaneous uniforms - imageShape = np.array(self.image.shape[:3], dtype=np.float32) - resolution = opts.tensorResolution - modThreshold = opts.modThreshold / 100.0 - lighting = 1 if opts.lighting else 0 - useSpline = 0 - - l1 = self.image. L1() - eigValNorm = 0.5 / abs(l1.data).max() - - gl.glUniform3fv(self.imageShapePos, 1, imageShape) - gl.glUniform1f( self.resolutionPos, resolution) - gl.glUniform1f( self.eigValNormPos, eigValNorm) - gl.glUniform1f( self.lightingPos, lighting) - gl.glUniform1f( self.modThresholdPos, modThreshold) - gl.glUniform1f( self.useSplinePos, useSpline) + imageShape = np.array(self.image.shape[:3], dtype=np.float32) + resolution = opts.tensorResolution + clipThreshold = opts.clipThreshold + lighting = 1 if opts.lighting else 0 + useSpline = 0 + + l1 = self.image.L1() + eigValNorm = 0.5 / abs(l1.data).max() + + invClipValXform = self.clipTexture .invVoxValXform + clipThreshold = clipThreshold * invClipValXform[0, 0] + \ + invClipValXform[3, 0] + + gl.glUniform3fv(svars['imageShape'], 1, imageShape) + gl.glUniform1f( svars['resolution'], resolution) + gl.glUniform1f( svars['eigValNorm'], eigValNorm) + gl.glUniform1f( svars['lighting'], lighting) + gl.glUniform1f( svars['clipThreshold'], clipThreshold) + gl.glUniform1f( svars['useSpline'], useSpline) # Vertices of a unit sphere. The vertex # shader will transform these vertices @@ -229,6 +236,8 @@ def preDraw(self): """ gl.glUseProgram(self.shaders) + svars = self.shaderVars + # Define the light position in # the world coordinate system lightPos = np.array([1, 1, -1], dtype=np.float32) @@ -246,19 +255,19 @@ def preDraw(self): # normal vectors - T(I(MV matrix)) normalMatrix = npla.inv(mvMat).T - gl.glUniform1f( self.zaxPos, self.zax) - gl.glUniform3fv( self.lightPosPos, 1, lightPos) - gl.glUniformMatrix3fv(self.normalMatrixPos, 1, False, normalMatrix) + gl.glUniform1f( svars['zax'], self.zax) + gl.glUniform3fv( svars['lightPos'], 1, lightPos) + gl.glUniformMatrix3fv(svars['normalMatrix'], 1, False, normalMatrix) - self.v1Texture.bindTexture(gl.GL_TEXTURE5) - self.v2Texture.bindTexture(gl.GL_TEXTURE6) - self.v3Texture.bindTexture(gl.GL_TEXTURE7) - self.l1Texture.bindTexture(gl.GL_TEXTURE8) - self.l2Texture.bindTexture(gl.GL_TEXTURE9) - self.l3Texture.bindTexture(gl.GL_TEXTURE10) + self.v1Texture.bindTexture(gl.GL_TEXTURE6) + self.v2Texture.bindTexture(gl.GL_TEXTURE7) + self.v3Texture.bindTexture(gl.GL_TEXTURE8) + self.l1Texture.bindTexture(gl.GL_TEXTURE9) + self.l2Texture.bindTexture(gl.GL_TEXTURE10) + self.l3Texture.bindTexture(gl.GL_TEXTURE11) - gl.glEnableVertexAttribArray(self.voxelPos) - gl.glEnableVertexAttribArray(self.vertexPos) + gl.glEnableVertexAttribArray(svars['voxel']) + gl.glEnableVertexAttribArray(svars['vertex']) gl.glEnable(gl.GL_CULL_FACE) gl.glCullFace(gl.GL_BACK) @@ -268,6 +277,7 @@ def draw(self, zpos, xform=None): image = self.image opts = self.displayOpts + svars = self.shaderVars v2dMat = opts.getTransform('voxel', 'display') d2vMat = opts.getTransform('display', 'voxel') @@ -297,21 +307,21 @@ def draw(self, zpos, xform=None): gl.glBufferData( gl.GL_ARRAY_BUFFER, voxels.nbytes, voxels, gl.GL_STATIC_DRAW) gl.glVertexAttribPointer( - self.voxelPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None) + svars['voxel'], 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None) # Use one set of voxel coordinates for every sphere drawn - arbia.glVertexAttribDivisorARB(self.voxelPos, 1) + arbia.glVertexAttribDivisorARB(svars['voxel'], 1) # Bind the vertex buffer gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.vertexBuffer) gl.glVertexAttribPointer( - self.vertexPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None) + svars['vertex'], 3, gl.GL_FLOAT, gl.GL_FALSE, 0, None) if xform is None: xform = v2dMat else: xform = transform.concat(v2dMat, xform) xform = np.array(xform, dtype=np.float32).ravel('C') - gl.glUniformMatrix4fv(self.voxToDisplayMatPos, 1, False, xform) + gl.glUniformMatrix4fv(svars['voxToDisplayMat'], 1, False, xform) # And the vertex index buffer gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.indexBuffer) @@ -321,6 +331,9 @@ def draw(self, zpos, xform=None): def postDraw(self): + + svars = self.shaderVars + gl.glUseProgram(0) gl.glDisable(gl.GL_CULL_FACE) @@ -335,5 +348,5 @@ def postDraw(self): self.l2Texture.unbindTexture() self.l3Texture.unbindTexture() - gl.glDisableVertexAttribArray(self.voxelPos) - gl.glDisableVertexAttribArray(self.vertexPos) + gl.glDisableVertexAttribArray(svars['voxel']) + gl.glDisableVertexAttribArray(svars['vertex']) diff --git a/fsl/fsleyes/gl/gl21/glvector_frag.glsl b/fsl/fsleyes/gl/gl21/glvector_frag.glsl index dabae9d8e..6dd2fe212 100644 --- a/fsl/fsleyes/gl/gl21/glvector_frag.glsl +++ b/fsl/fsleyes/gl/gl21/glvector_frag.glsl @@ -18,14 +18,21 @@ uniform sampler3D imageTexture; * Modulation texture containing values by * which the vector colours are to be modulated. */ -uniform sampler3D modTexture; +uniform sampler3D modulateTexture; + + +/* + * Texture containing values which determine + * whether a vector voxel should be clipped. + */ +uniform sampler3D clipTexture; + /* - * If the modulation value is below this - * threshold, the fragment is made - * transparent. + * If the clipping value is below this + * threshold, the fragment is clipped. */ -uniform float modThreshold; +uniform float clipThreshold; /* * Colour map for the X vector component. @@ -106,14 +113,23 @@ void main(void) { voxValue = texture3D(imageTexture, fragTexCoord).xyz; } - /* Look up the modulation value */ + /* Look up the modulation and clipping values */ float modValue; + float clipValue; if (useSpline) { - modValue = spline_interp(modTexture, fragTexCoord, imageShape, 0); + modValue = spline_interp(modulateTexture, fragTexCoord, imageShape, 0); + clipValue = spline_interp(clipTexture, fragTexCoord, imageShape, 0); } else { - modValue = texture3D(modTexture, fragTexCoord).x; - } + modValue = texture3D(modulateTexture, fragTexCoord).x; + clipValue = texture3D(clipTexture, fragTexCoord).x; + } + + /* Knock out voxels where the clipping value is below the threshold */ + if (clipValue < clipThreshold) { + gl_FragColor.a = 0.0; + return; + } /* * Transform the voxel texture values @@ -140,9 +156,5 @@ void main(void) { /* Take the highest alpha of the three colour maps */ voxColour.a = max(max(xColour.a, yColour.a), zColour.a); - /* Knock out voxels where the modulation value is below the threshold */ - if (modValue < modThreshold) - voxColour.a = 0.0; - gl_FragColor = voxColour * fragColourFactor; } diff --git a/fsl/fsleyes/gl/gl21/glvolume_funcs.py b/fsl/fsleyes/gl/gl21/glvolume_funcs.py index ce9fffc15..95b2c39a4 100644 --- a/fsl/fsleyes/gl/gl21/glvolume_funcs.py +++ b/fsl/fsleyes/gl/gl21/glvolume_funcs.py @@ -57,35 +57,23 @@ def compileShaders(self): fragShaderSrc = shaders.getFragmentShader(self) self.shaders = shaders.compileShaders(vertShaderSrc, fragShaderSrc) - # indices of all vertex/fragment shader parameters - self.vertexPos = gl.glGetAttribLocation( self.shaders, - 'vertex') - self.voxCoordPos = gl.glGetAttribLocation( self.shaders, - 'voxCoord') - self.texCoordPos = gl.glGetAttribLocation( self.shaders, - 'texCoord') - self.imageTexturePos = gl.glGetUniformLocation(self.shaders, - 'imageTexture') - self.colourTexturePos = gl.glGetUniformLocation(self.shaders, - 'colourTexture') - self.negColourTexturePos = gl.glGetUniformLocation(self.shaders, - 'negColourTexture') - self.useNegCmapPos = gl.glGetUniformLocation(self.shaders, - 'useNegCmap') - self.texZeroPos = gl.glGetUniformLocation(self.shaders, - 'texZero') - self.imageShapePos = gl.glGetUniformLocation(self.shaders, - 'imageShape') - self.useSplinePos = gl.glGetUniformLocation(self.shaders, - 'useSpline') - self.voxValXformPos = gl.glGetUniformLocation(self.shaders, - 'voxValXform') - self.clipLowPos = gl.glGetUniformLocation(self.shaders, - 'clipLow') - self.clipHighPos = gl.glGetUniformLocation(self.shaders, - 'clipHigh') - self.invertClipPos = gl.glGetUniformLocation(self.shaders, - 'invertClip') + shaderVars = {} + + vertAtts = ['vertex', 'voxCoord', 'texCoord'] + fragUniforms = ['imageTexture', 'colourTexture', 'negColourTexture', + 'useNegCmap', 'imageShape', 'useSpline', + 'voxValXform', 'clipLow', 'clipHigh', + 'texZero', 'invertClip'] + + for va in vertAtts: + shaderVars[va] = gl.glGetAttribLocation(self.shaders, va) + + for fu in fragUniforms: + if fu in shaderVars: + continue + shaderVars[fu] = gl.glGetUniformLocation(self.shaders, fu) + + self.shaderVars = shaderVars def updateShaderState(self): @@ -93,16 +81,10 @@ def updateShaderState(self): current display properties. """ - opts = self.displayOpts + opts = self.displayOpts + svars = self.shaderVars gl.glUseProgram(self.shaders) - - # bind the current interpolation setting, - # image shape, and image->screen axis - # mappings - gl.glUniform1f( self.useSplinePos, opts.interpolation == 'spline') - gl.glUniform3fv(self.imageShapePos, 1, np.array(self.image.shape, - dtype=np.float32)) # The clipping range options are in the voxel value # range, but the shader needs them to be in image @@ -113,12 +95,6 @@ def updateShaderState(self): clipHigh = opts.clippingRange[1] * xform[0, 0] + xform[3, 0] texZero = 0.0 * xform[0, 0] + xform[3, 0] - gl.glUniform1f(self.clipLowPos, clipLow) - gl.glUniform1f(self.clipHighPos, clipHigh) - gl.glUniform1f(self.texZeroPos, texZero) - gl.glUniform1f(self.invertClipPos, opts.invertClipping) - gl.glUniform1f(self.useNegCmapPos, opts.useNegativeCmap) - # Bind transformation matrix to transform # from image texture values to voxel values, # and and to scale said voxel values to @@ -126,13 +102,27 @@ def updateShaderState(self): vvx = transform.concat(self.imageTexture.voxValXform, self.colourTexture.getCoordinateTransform()) vvx = np.array(vvx, dtype=np.float32).ravel('C') - - gl.glUniformMatrix4fv(self.voxValXformPos, 1, False, vvx) + + + # bind the current interpolation setting, + # image shape, and image->screen axis + # mappings + gl.glUniform1f( svars['useSpline'], opts.interpolation == 'spline') + gl.glUniform3fv(svars['imageShape'], 1, np.array(self.image.shape, + dtype=np.float32)) + + gl.glUniform1f(svars['clipLow'], clipLow) + gl.glUniform1f(svars['clipHigh'], clipHigh) + gl.glUniform1f(svars['texZero'], texZero) + gl.glUniform1f(svars['invertClip'], opts.invertClipping) + gl.glUniform1f(svars['useNegCmap'], opts.useNegativeCmap) + + gl.glUniformMatrix4fv(svars['voxValXform'], 1, False, vvx) # Set up the colour and image textures - gl.glUniform1i(self.imageTexturePos, 0) - gl.glUniform1i(self.colourTexturePos, 1) - gl.glUniform1i(self.negColourTexturePos, 2) + gl.glUniform1i(svars['imageTexture'], 0) + gl.glUniform1i(svars['colourTexture'], 1) + gl.glUniform1i(svars['negColourTexture'], 2) gl.glUseProgram(0) @@ -153,9 +143,9 @@ def _prepareVertexAttributes(self, vertices, voxCoords, texCoords): """ buf = np.zeros((vertices.shape[0] * 3, 3), dtype=np.float32) - verPos = self.vertexPos - voxPos = self.voxCoordPos - texPos = self.texCoordPos + verPos = self.shaderVars['vertex'] + voxPos = self.shaderVars['voxCoord'] + texPos = self.shaderVars['texCoord'] # We store each of the three coordinate # sets in a single interleaved buffer @@ -174,10 +164,10 @@ def _prepareVertexAttributes(self, vertices, voxCoords, texCoords): texPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 36, ctypes.c_void_p(24)) gl.glVertexAttribPointer( voxPos, 3, gl.GL_FLOAT, gl.GL_FALSE, 36, ctypes.c_void_p(12)) - gl.glEnableVertexAttribArray(self.voxCoordPos) - - gl.glEnableVertexAttribArray(self.vertexPos) - gl.glEnableVertexAttribArray(self.texCoordPos) + + gl.glEnableVertexAttribArray(voxPos) + gl.glEnableVertexAttribArray(verPos) + gl.glEnableVertexAttribArray(texPos) def draw(self, zpos, xform=None): @@ -222,9 +212,9 @@ def postDraw(self): instance. """ - gl.glDisableVertexAttribArray(self.vertexPos) - gl.glDisableVertexAttribArray(self.texCoordPos) - gl.glDisableVertexAttribArray(self.voxCoordPos) + gl.glDisableVertexAttribArray(self.shaderVars['vertex']) + gl.glDisableVertexAttribArray(self.shaderVars['texCoord']) + gl.glDisableVertexAttribArray(self.shaderVars['voxCoord']) gl.glBindBuffer(gl.GL_ARRAY_BUFFER, 0) gl.glUseProgram(0) diff --git a/fsl/fsleyes/gl/glvector.py b/fsl/fsleyes/gl/glvector.py index 6f6cdd0de..dbcccb828 100644 --- a/fsl/fsleyes/gl/glvector.py +++ b/fsl/fsleyes/gl/glvector.py @@ -27,9 +27,10 @@ class GLVector(globject.GLImageObject): The ``GLVector`` class is a base class which is not intended to be - instantiated directly. The :class:`.GLRGBVector` and - :class:`.GLLineVector` subclasses should be used instead. These two - subclasses share the functionality provided by this class. + instantiated directly. The :class:`.GLRGBVector`, + :class:`.GLLineVector` and :class:`.GLTensor` subclasses should be + used instead. These subclasses share the functionality provided + by this class. The :class:`.Image` is stored on the GPU as a 3D RGB @@ -46,8 +47,13 @@ class GLVector(globject.GLImageObject): The colour of each vector may be modulated by another image, specified by - the :attr:`.VectorOpts.modulate` property. This modulation image is + the :attr:`.VectorOpts.modulateImage` property. This modulation image is stored as a 3D single-channel :class:`.ImageTexture`. + + + Vector voxels may be clipped according to the values of another image, + specified by the :attr:`.VectorOpts.clipImage` property. This clipping + image is stored as a 3D single-channel :class:`.ImageTexture`. """ @@ -94,19 +100,23 @@ 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)) - self.modImage = None - self.modOpts = None - self.modTexture = None - self.imageTexture = None - self.prefilter = prefilter + self.vectorImage = vectorImage + self.xColourTexture = textures.ColourMapTexture('{}_x'.format(name)) + self.yColourTexture = textures.ColourMapTexture('{}_y'.format(name)) + self.zColourTexture = textures.ColourMapTexture('{}_z'.format(name)) + self.modulateImage = None + self.clipImage = None + self.modulateOpts = None + self.clipOpts = None + self.modulateTexture = None + self.clipTexture = None + self.imageTexture = None + self.prefilter = prefilter self.addListeners() self.refreshImageTexture() - self.refreshModulateTexture() + self.refreshModClipTexture('modulate') + self.refreshModClipTexture('clip') self.refreshColourTextures() @@ -120,16 +130,21 @@ class GLVector(globject.GLImageObject): self.yColourTexture.destroy() self.zColourTexture.destroy() - glresources.delete(self.imageTexture.getTextureName()) - glresources.delete(self.modTexture .getTextureName()) + glresources.delete(self.imageTexture .getTextureName()) + glresources.delete(self.modulateTexture.getTextureName()) + glresources.delete(self.clipTexture .getTextureName()) self.removeListeners() - self.deregisterModulateImage() + self.deregisterModClipImage('modulate') + self.deregisterModClipImage('clip') - self.imageTexture = None - self.modTexture = None - self.modImage = None - self.modOpts = None + self.imageTexture = None + self.modulateTexture = None + self.clipTexture = None + self.modulateImage = None + self.clipImage = None + self.modulateOpts = None + self.clipOpts = None globject.GLImageObject.destroy(self) @@ -148,12 +163,19 @@ class GLVector(globject.GLImageObject): self.onUpdate() def modUpdate( *a): - self.deregisterModulateImage() - self.registerModulateImage() - self.refreshModulateTexture() + self.deregisterModClipImage('modulate') + self.registerModClipImage('modulate') + self.refreshModClipTexture('modulate') self.updateShaderState() self.onUpdate() + def clipUpdate( *a): + self.deregisterModClipImage('clip') + self.registerModClipImage('clip') + self.refreshModClipTexture('clip') + self.updateShaderState() + self.onUpdate() + def cmapUpdate(*a): self.refreshColourTextures() self.updateShaderState() @@ -188,8 +210,9 @@ class GLVector(globject.GLImageObject): opts .addListener('suppressX', name, cmapUpdate, weak=False) opts .addListener('suppressY', name, cmapUpdate, weak=False) opts .addListener('suppressZ', name, cmapUpdate, weak=False) - opts .addListener('modulate', name, modUpdate, weak=False) - opts .addListener('modThreshold', name, shaderUpdate, weak=False) + opts .addListener('modulateImage', name, modUpdate, weak=False) + opts .addListener('clipImage', name, clipUpdate, weak=False) + opts .addListener('clipThreshold', name, shaderUpdate, weak=False) opts .addListener('resolution', name, imageUpdate, weak=False) opts .addListener('transform', name, update, weak=False) @@ -210,20 +233,21 @@ class GLVector(globject.GLImageObject): opts = self.displayOpts name = self.name - display.removeListener('alpha', name) - display.removeListener('brightness', name) - display.removeListener('contrast', name) - opts .removeListener('xColour', name) - opts .removeListener('yColour', name) - opts .removeListener('zColour', name) - opts .removeListener('suppressX', name) - opts .removeListener('suppressY', name) - opts .removeListener('suppressZ', name) - opts .removeListener('modulate', name) - opts .removeListener('modThreshold', name) - opts .removeListener('volume', name) - opts .removeListener('resolution', name) - opts .removeListener('transform' , name) + display.removeListener('alpha', name) + display.removeListener('brightness', name) + display.removeListener('contrast', name) + opts .removeListener('xColour', name) + opts .removeListener('yColour', name) + opts .removeListener('zColour', name) + opts .removeListener('suppressX', name) + opts .removeListener('suppressY', name) + opts .removeListener('suppressZ', name) + opts .removeListener('modulateImage', name) + opts .removeListener('clipImage', name) + opts .removeListener('clipThreshold', name) + opts .removeListener('volume', name) + opts .removeListener('resolution', name) + opts .removeListener('transform' , name) if self.__syncListenersRegistered: opts.removeSyncChangeListener('resolution', name) @@ -286,50 +310,71 @@ class GLVector(globject.GLImageObject): '{} subclasses'.format(type(self).__name__)) - def registerModulateImage(self): - """Called when the :attr:`.VectorOpts.modulate` property changes. - Registers a listener with the :attr:`.Nifti1Opts.volume` property - of the modulate image, so the modulate texture can be updated when - the image volume changes. + + def registerModClipImage(self, which): + """Called when the :attr:`.VectorOpts.modulateImage` or + :attr:`.VectorOpts.clipImage` properties change. Registers a listener + with the :attr:`.Nifti1Opts.volume` property of the modulate/clip + image, so the modulate/clip textures can be updated when the image + volume changes. """ - - modImage = self.displayOpts.modulate - self.modOpts = None + imageAttr = '{}Image' .format(which) + optsAttr = '{}Opts' .format(which) + texAttr = '{}Texture'.format(which) - if modImage is None or modImage == 'none': self.modImage = None - else: self.modImage = modImage + image = getattr(self.displayOpts, imageAttr) + + if image is None or image == 'none': + image = None + + setattr(self, optsAttr, None) + setattr(self, imageAttr, image) - if self.modImage is None: + if image is None: return - self.modOpts = self.displayOpts.displayCtx.getOpts(modImage) + opts = self.displayOpts.displayCtx.getOpts(image) + tex = getattr(self, texAttr) + + setattr(self, optsAttr, opts) def volumeChange(*a): - self.modTexture.set(volume=self.modOpts.volume) - self.refreshModulateTexture() + tex.set(volume=opts.volume) + self.refreshModClipTexture(which) self.onUpdate() - self.modOpts.addListener('volume', self.name, volumeChange, weak=False) + # We set overwrite=True, because + # the modulate and clip images may + # be the same. + opts.addListener('volume', + self.name, + volumeChange, + overwrite=True, + weak=False) - def deregisterModulateImage(self): - """Called when the :attr:`.VectorOpts.modulate` property changes. + def deregisterModClipImage(self, which): + """Called when the :attr:`.VectorOpts.modulateImage` or + :attr:`.VectorOpts.clipImage` properties change. Deregisters the :attr:`.Nifti1Opts.volume` listener that was - registered in :meth:`registerModulateImage`. - """ + registered in :meth:`registerModClipImage`. + """ - if self.modImage is None: - return + imageAttr = '{}Image'.format(which) + optsAttr = '{}Opts' .format(which) - self.modOpts.removeListener('volume', self.name) + opts = getattr(self, optsAttr) - self.modImage = None - self.modOpts = None + if opts is not None: + opts.removeListener('volume', self.name) + setattr(self, imageAttr, None) + setattr(self, optsAttr, None) + - def refreshModulateTexture(self): + def refreshModClipTexture(self, which): """Called when the :attr`.VectorOpts.modulate` property changes. Reconfigures the modulation :class:`.ImageTexture`. If no modulation @@ -338,42 +383,49 @@ class GLVector(globject.GLImageObject): effect). """ - if self.modTexture is not None: - glresources.delete(self.modTexture.getTextureName()) + imageAttr = '{}Image' .format(which) + optsAttr = '{}Opts' .format(which) + texAttr = '{}Texture'.format(which) + + image = getattr(self, imageAttr) + opts = getattr(self, optsAttr) + tex = getattr(self, texAttr) - modImage = self.modImage - modOpts = self.modOpts + if tex is not None: + glresources.delete(tex.getTextureName()) - if modImage is None or modImage == 'none': - textureData = np.zeros((5, 5, 5), dtype=np.uint8) + if image is None: + textureData = np.zeros((5, 5, 5), dtype=np.uint8) textureData[:] = 255 - modImage = fslimage.Image(textureData) - norm = False + image = fslimage.Image(textureData) + norm = False else: norm = True - texName = '{}_{}_{}_modulate'.format( - type(self).__name__, id(self.image), id(modImage)) + texName = '{}_{}_{}_{}'.format( + type(self).__name__, id(self.image), id(image), which) - if modOpts is not None: - unsynced = (modOpts.getParent() is None or - not modOpts.isSyncedToParent('resolution') or - not modOpts.isSyncedToParent('volume')) + if opts is not None: + unsynced = (opts.getParent() is None or + not opts.isSyncedToParent('resolution') or + not opts.isSyncedToParent('volume')) # TODO If unsynced, this GLVector needs to - # update the modulate texture whenever its - # volume/resolution properties change. + # update the mod/clip textures whenever + # their volume/resolution properties change. # Right? if unsynced: - texName = '{}_unsync_{}'.format(texName, id(modOpts)) + texName = '{}_unsync_{}'.format(texName, id(opts)) - self.modTexture = glresources.get( + tex = glresources.get( texName, textures.ImageTexture, texName, - modImage, + image, normalise=norm) + + setattr(self, texAttr, tex) def refreshColourTextures(self, colourRes=256): @@ -432,26 +484,28 @@ class GLVector(globject.GLImageObject): def preDraw(self): """Must be called by subclass implementations. - Ensures that the five textures (the vector and modulation images, - and the three colour textures) are bound to texture units 0-4 + Ensures that the six textures (the vector, clip, and modulation + images, and the three colour textures) are bound to texture units 0-5 respectively. """ - self.imageTexture .bindTexture(gl.GL_TEXTURE0) - self.modTexture .bindTexture(gl.GL_TEXTURE1) - self.xColourTexture.bindTexture(gl.GL_TEXTURE2) - self.yColourTexture.bindTexture(gl.GL_TEXTURE3) - self.zColourTexture.bindTexture(gl.GL_TEXTURE4) + self.imageTexture .bindTexture(gl.GL_TEXTURE0) + self.modulateTexture.bindTexture(gl.GL_TEXTURE1) + self.clipTexture .bindTexture(gl.GL_TEXTURE2) + self.xColourTexture .bindTexture(gl.GL_TEXTURE3) + self.yColourTexture .bindTexture(gl.GL_TEXTURE4) + self.zColourTexture .bindTexture(gl.GL_TEXTURE5) def postDraw(self): """Must be called by subclass implementations. - Unbindes the five GL textures. + Unbindes the six GL textures. """ - self.imageTexture .unbindTexture() - self.modTexture .unbindTexture() - self.xColourTexture.unbindTexture() - self.yColourTexture.unbindTexture() - self.zColourTexture.unbindTexture() + self.imageTexture .unbindTexture() + self.modulateTexture.unbindTexture() + self.clipTexture .unbindTexture() + self.xColourTexture .unbindTexture() + self.yColourTexture .unbindTexture() + self.zColourTexture .unbindTexture() diff --git a/fsl/fsleyes/tooltips.py b/fsl/fsleyes/tooltips.py index de98df739..37834889d 100644 --- a/fsl/fsleyes/tooltips.py +++ b/fsl/fsleyes/tooltips.py @@ -135,19 +135,25 @@ properties = TypeDict({ 'colouring voxels.', 'VectorOpts.suppressZ' : 'Ignore the Z vector component when ' 'colouring voxels.', - 'VectorOpts.modulate' : 'Modulate the vector colours by another ' - 'image. The image selected here is ' - 'normalised to lie in the range (0, 1), ' - 'and the magnitude of each vector is ' + 'VectorOpts.modulateImage' : 'Modulate the vector colour brightness by ' + 'another image. The image selected here ' + 'is normalised to lie in the range (0, ' + '1), and the magnitude of each vector is ' 'scaled by the corresponding modulation ' 'value before it is coloured. The ' 'modulation image must have the same ' 'voxel dimensions as the vector image.', - 'VectorOpts.modThreshold' : 'Vector values which have a corresponding ' - 'modulation value that is less than this ' - 'threshold are not displayed. The ' - 'threshold is a proportion of the ' - 'modulation image data range.', + 'VectorOpts.clipImage' : 'Clip vector voxels according to the ' + 'values in another image. Vector voxels ' + 'which correspond to values in the ' + 'clipping image that have a value less ' + 'than the current clipping threshold are ' + 'not shown. The clipping image must have ' + 'the same voxel dimensions as the vector ' + 'image. ', + 'VectorOpts.clipThreshold' : 'Vector values which have a corresponding ' + 'clipping image value that is less than ' + 'this threshold are not displayed. ', 'LineVectorOpts.lineWidth' : 'The width of each vector line, in ' 'display pixels.', 'LineVectorOpts.directed' : 'If unchecked, the vector data is assumed ' -- GitLab