From 2537e9f1cd6aefc726723d353ae912806e2cddee Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Thu, 17 Dec 2015 15:36:14 +0000 Subject: [PATCH] Infrastructure for colouring vector images by another image. --- fsl/data/strings.py | 2 + fsl/fsleyes/colourmaps.py | 38 +++++---- fsl/fsleyes/controls/overlaydisplaypanel.py | 85 +++++++++------------ fsl/fsleyes/displaycontext/vectoropts.py | 60 ++++++++++----- fsl/fsleyes/tooltips.py | 38 ++++++--- 5 files changed, 130 insertions(+), 93 deletions(-) diff --git a/fsl/data/strings.py b/fsl/data/strings.py index d54391c7a..b80b0962e 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -553,6 +553,8 @@ properties = TypeDict({ 'VectorOpts.suppressX' : 'Suppress X value', 'VectorOpts.suppressY' : 'Suppress Y value', 'VectorOpts.suppressZ' : 'Suppress Z value', + 'VectorOpts.colourImage' : 'Colour by', + 'VectorOpts.cmap' : 'Colour map', 'VectorOpts.modulateImage' : 'Modulate by', 'VectorOpts.clipImage' : 'Clip by', 'VectorOpts.clippingRange' : 'Clipping range', diff --git a/fsl/fsleyes/colourmaps.py b/fsl/fsleyes/colourmaps.py index f5b8b763b..0de096d15 100644 --- a/fsl/fsleyes/colourmaps.py +++ b/fsl/fsleyes/colourmaps.py @@ -265,29 +265,35 @@ def registerColourMap(cmapFile, _cmaps[key] = _Map(key, name, cmap, None, False) - # TODO Any new DisplayOpts sub-types which have a - # colour map will need to be patched here - - log.debug('Patching VolumeOpts instances and class ' + log.debug('Patching DisplayOpts instances and class ' 'to support new colour map {}'.format(key)) import fsl.fsleyes.displaycontext as fsldisplay - # update the VolumeOpts colour map property - # for any existing VolumeOpts instances - cmapProp = fsldisplay.VolumeOpts.getProp('cmap') - negCmapProp = fsldisplay.VolumeOpts.getProp('negativeCmap') - + # A list of all DisplayOpts colour map properties + # + # TODO Any new DisplayOpts sub-types which have a + # colour map will need to be patched here + cmapProps = [] + cmapProps.append((fsldisplay.VolumeOpts, 'cmap')) + cmapProps.append((fsldisplay.VolumeOpts, 'negativeCmap')) + cmapProps.append((fsldisplay.VectorOpts, 'cmap')) + + # Update the colour map properties + # for any existing instances for overlay in overlayList: opts = displayCtx.getOpts(overlay) - - if isinstance(opts, fsldisplay.VolumeOpts): - cmapProp .addColourMap(key, opts) - negCmapProp.addColourMap(key, opts) - # and for all future volume overlays - cmapProp .addColourMap(key) - negCmapProp.addColourMap(key) + for cls, propName in cmapProps: + if isinstance(opts, cls): + prop = opts.getProp(propName) + prop.addColourMap(key, opts) + + # and for all future overlays + for cls, propName in cmapProps: + + prop = cls.getProp(propName) + prop.addColourMap(key) def registerLookupTable(lut, diff --git a/fsl/fsleyes/controls/overlaydisplaypanel.py b/fsl/fsleyes/controls/overlaydisplaypanel.py index d30f90fcb..1d4f86863 100644 --- a/fsl/fsleyes/controls/overlaydisplaypanel.py +++ b/fsl/fsleyes/controls/overlaydisplaypanel.py @@ -10,6 +10,7 @@ control* panel which allows the user to change overlay display settings. import logging +import functools import wx import props @@ -206,7 +207,9 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): self.__widgets.ClearGroup(groupName) - dispProps = _DISPLAY_PROPS.get(target, []) + dispProps = _DISPLAY_PROPS.get(target, [], allhits=True) + dispProps = functools.reduce(lambda a, b: a + b, dispProps) + labels = [strings.properties.get((target, p.key), p.key) for p in dispProps] tooltips = [fsltooltips.properties.get((target, p.key), None) @@ -214,6 +217,7 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): widgets = [] + for p in dispProps: widget = props.buildGUI(self.__widgets, target, p) @@ -276,7 +280,8 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): def _imageName(img): """Used to generate choice labels for the :attr`.VectorOpts.modulateImage`, - :attr`.VectorOpts.clipImage` and :attr:`.ModelOpts.refImage` properties. + :attr`.VectorOpts.clipImage`, :attr`.VectorOpts.colourImage` and + :attr:`.ModelOpts.refImage` properties. """ if img is None: return 'None' else: return img.name @@ -324,45 +329,46 @@ _DISPLAY_PROPS = td.TypeDict({ props.Widget('invert'), props.Widget('threshold', showLimits=False)], - 'RGBVectorOpts' : [ - props.Widget('resolution', showLimits=False), - props.Widget('interpolation', - labels=strings.choices['VolumeOpts.interpolation']), - props.Widget('xColour'), - props.Widget('yColour'), - props.Widget('zColour'), - props.Widget('suppressX'), - props.Widget('suppressY'), - props.Widget('suppressZ'), + 'VectorOpts' : [ + props.Widget('colourImage', labels=_imageName), props.Widget('modulateImage', labels=_imageName), props.Widget('clipImage', labels=_imageName), + props.Widget('cmap', + dependencies=['colourImage'], + enabledWhen=lambda o, ci: ci is not None), props.Widget('clippingRange', showLimits=False, slider=True, labels=[strings.choices['VectorOpts.clippingRange.min'], strings.choices['VectorOpts.clippingRange.max']], dependencies=['clipImage'], - enabledWhen=lambda o, ci: ci is not None)], + enabledWhen=lambda o, ci: ci is not None), + props.Widget('xColour', + dependencies=['colourImage'], + enabledWhen=lambda o, ci: ci is None), + props.Widget('yColour', + dependencies=['colourImage'], + enabledWhen=lambda o, ci: ci is None), + props.Widget('zColour', + dependencies=['colourImage'], + enabledWhen=lambda o, ci: ci is None), + props.Widget('suppressX', + dependencies=['colourImage'], + enabledWhen=lambda o, ci: ci is None), + props.Widget('suppressY', + dependencies=['colourImage'], + enabledWhen=lambda o, ci: ci is None), + props.Widget('suppressZ', + dependencies=['colourImage'], + enabledWhen=lambda o, ci: ci is None)], - 'LineVectorOpts' : [ + 'RGBVectorOpts' : [ props.Widget('resolution', showLimits=False), - props.Widget('xColour'), - props.Widget('yColour'), - props.Widget('zColour'), - props.Widget('suppressX'), - props.Widget('suppressY'), - props.Widget('suppressZ'), - props.Widget('directed'), - props.Widget('lineWidth', showLimits=False), - props.Widget('modulateImage', labels=_imageName), - props.Widget('clipImage', labels=_imageName), - props.Widget('clippingRange', - showLimits=False, - slider=True, - labels=[strings.choices['VectorOpts.clippingRange.min'], - strings.choices['VectorOpts.clippingRange.max']], - dependencies=['clipImage'], - enabledWhen=lambda o, ci: ci is not None)], + props.Widget('interpolation', + labels=strings.choices['VolumeOpts.interpolation'])], + + 'LineVectorOpts' : [ + props.Widget('resolution', showLimits=False)], 'ModelOpts' : [ props.Widget('colour'), @@ -382,22 +388,7 @@ _DISPLAY_PROPS = td.TypeDict({ spin=False, labels=[strings.choices['TensorOpts.tensorResolution.min'], strings.choices['TensorOpts.tensorResolution.max']]), - props.Widget('tensorScale', showLimits=False, spin=False), - props.Widget('xColour'), - props.Widget('yColour'), - props.Widget('zColour'), - props.Widget('suppressX'), - props.Widget('suppressY'), - props.Widget('suppressZ'), - props.Widget('modulateImage', labels=_imageName), - props.Widget('clipImage', labels=_imageName), - props.Widget('clippingRange', - showLimits=False, - slider=True, - labels=[strings.choices['VectorOpts.clippingRange.min'], - strings.choices['VectorOpts.clippingRange.max']], - dependencies=['clipImage'], - enabledWhen=lambda o, ci: ci is not None)], + props.Widget('tensorScale', showLimits=False, spin=False)], 'LabelOpts' : [ props.Widget('lut', labels=lambda l: l.name), diff --git a/fsl/fsleyes/displaycontext/vectoropts.py b/fsl/fsleyes/displaycontext/vectoropts.py index cdc412d41..53cb00b72 100644 --- a/fsl/fsleyes/displaycontext/vectoropts.py +++ b/fsl/fsleyes/displaycontext/vectoropts.py @@ -49,6 +49,22 @@ class VectorOpts(volumeopts.Nifti1Opts): """Do not use the Z vector magnitude to colour vectors.""" + cmap = props.ColourMap() + """If an image is selected as the :attr:`colourImage`, this colour map + is used to colour the vector voxels. + """ + + + colourImage = props.Choice() + """Colour vector voxels by the values contained in this 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. If a ``colourImage`` is + selected, the :attr:`xColour`, :attr:`yColour`, :attr:`zColour`, + :attr:`suppressX`, :attr:`suppressY`, and :attr:`suppressZ` properties are + all ignored. + """ + + 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 @@ -124,16 +140,19 @@ class VectorOpts(volumeopts.Nifti1Opts): def __overlayListChanged(self, *a): - """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. + """Called when the overlay list changes. Updates the :attr:`modulateImage`, + :attr:`colourImage` and :attr:`clipImage` properties so that they + contain a list of overlays which could be used to modulate the vector + image. """ - modProp = self.getProp('modulateImage') - clipProp = self.getProp('clipImage') - modVal = self.modulateImage - clipVal = self.clipImage - overlays = self.displayCtx.getOrderedOverlays() + modProp = self.getProp('modulateImage') + clipProp = self.getProp('clipImage') + colourProp = self.getProp('colourImage') + modVal = self.modulateImage + clipVal = self.clipImage + colourVal = self.clipImage + overlays = self.displayCtx.getOrderedOverlays() # the image for this VectorOpts # instance has been removed @@ -151,14 +170,14 @@ class VectorOpts(volumeopts.Nifti1Opts): if overlay is self.overlay: continue - # The modulate/clip images - # must be images. + # The modulate/clip/colour + # images must be images. if not isinstance(overlay, fslimage.Image): continue # an image can only be used to - # modulate/clip the vector image - # if it shares the same + # modulate/clip/colour 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]: @@ -172,13 +191,16 @@ class VectorOpts(volumeopts.Nifti1Opts): self.__overlayListChanged, overwrite=True) - modProp .setChoices(options, instance=self) - clipProp.setChoices(options, instance=self) - - if modVal in overlays: self.modulateImage = modVal - else: self.modulateImage = None - if clipVal in overlays: self.clipImage = clipVal - else: self.clipImage = None + modProp .setChoices(options, instance=self) + clipProp .setChoices(options, instance=self) + colourProp.setChoices(options, instance=self) + + if modVal in overlays: self.modulateImage = modVal + else: self.modulateImage = None + if clipVal in overlays: self.clipImage = clipVal + else: self.clipImage = None + if colourVal in overlays: self.colourImage = colourVal + else: self.colourImage = None class LineVectorOpts(VectorOpts): diff --git a/fsl/fsleyes/tooltips.py b/fsl/fsleyes/tooltips.py index a03f67579..50a2ba657 100644 --- a/fsl/fsleyes/tooltips.py +++ b/fsl/fsleyes/tooltips.py @@ -120,28 +120,37 @@ properties = TypeDict({ 'VectorOpts.xColour' : 'The colour corresponding to the X ' 'component of the vector - the brightness ' 'of the colour corresponds to the ' - 'magnitude of the X component', + 'magnitude of the X component. This ' + 'option has no effect if a colour image ' + 'is selected.', 'VectorOpts.yColour' : 'The colour corresponding to the Y ' 'component of the vector - the brightness ' 'of the colour corresponds to the ' - 'magnitude of the Y component.', + 'magnitude of the Y component. This ' + 'option has no effect if a colour image ' + 'is selected.', 'VectorOpts.zColour' : 'The colour corresponding to the Z ' 'component of the vector - the brightness ' 'of the colour corresponds to the ' - 'magnitude of the Z component.', + 'magnitude of the Z component. This ' + 'option has no effect if a colour image ' + 'is selected.', 'VectorOpts.suppressX' : 'Ignore the X vector component when ' - 'colouring voxels.', + 'colouring voxels. This option has no ' + 'effect if a colour image is selected.', 'VectorOpts.suppressY' : 'Ignore the Y vector component when ' - 'colouring voxels.', + 'colouring voxels. This option has no ' + 'effect if a colour image is selected.', 'VectorOpts.suppressZ' : 'Ignore the Z vector component when ' - 'colouring voxels.', + 'colouring voxels. This option has no ' + 'effect if a colour image is selected.', '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 ' + '1), and the brightness of each vector ' + 'colour 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.clipImage' : 'Clip vector voxels according to the ' 'values in another image. Vector voxels ' @@ -150,10 +159,17 @@ properties = TypeDict({ 'than the current clipping threshold are ' 'not shown. The clipping image must have ' 'the same voxel dimensions as the vector ' - 'image. ', + 'image. ', + 'VectorOpts.colourImage' : 'Colour the vectors according to the ' + 'values in another image, and by the ' + 'selected colour map. The colour image ' + 'must have the same voxel dimensions as ' + 'the vector image. ', 'VectorOpts.clippingRange' : 'Vector values which have a corresponding ' 'clipping image value that is outside of ' 'this range are not displayed. ', + 'VectorOpts.cmap' : 'Colour map to use for colouring vector ' + 'voxels, if a colour image is selected.', 'LineVectorOpts.lineWidth' : 'The width of each vector line, in ' 'display pixels.', 'LineVectorOpts.directed' : 'If unchecked, the vector data is assumed ' -- GitLab