diff --git a/fsl/data/strings.py b/fsl/data/strings.py index 1691e050f59bcc8bd096bed08548f6e1ccd1f570..e44661918c450926956342bcbd3bc4576d7f0ce6 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -527,15 +527,16 @@ properties = TypeDict({ 'ImageOpts.transform' : 'Image transform', 'ImageOpts.volume' : 'Volume', - 'VolumeOpts.displayRange' : 'Display range', - 'VolumeOpts.clippingRange' : 'Clipping range', - 'VolumeOpts.centreRanges' : 'Centre display/clipping ranges at 0', - 'VolumeOpts.linkLowRanges' : 'Link low display/clipping ranges', - 'VolumeOpts.linkHighRanges' : 'Link high display/clipping ranges', - 'VolumeOpts.cmap' : 'Colour map', - 'VolumeOpts.invert' : 'Invert colour map', - 'VolumeOpts.invertClipping' : 'Invert clipping range', - 'VolumeOpts.interpolation' : 'Interpolation', + 'VolumeOpts.displayRange' : 'Display range', + 'VolumeOpts.clippingRange' : 'Clipping range', + 'VolumeOpts.linkLowRanges' : 'Link low display/clipping ranges', + 'VolumeOpts.linkHighRanges' : 'Link high display/clipping ranges', + 'VolumeOpts.cmap' : 'Colour map', + 'VolumeOpts.negativeCmap' : '-ve colour map', + 'VolumeOpts.enableNegativeCmap' : '-ve colour map', + 'VolumeOpts.invert' : 'Invert colour map', + 'VolumeOpts.invertClipping' : 'Invert clipping range', + 'VolumeOpts.interpolation' : 'Interpolation', 'MaskOpts.colour' : 'Colour', 'MaskOpts.invert' : 'Invert', diff --git a/fsl/fsleyes/colourmaps.py b/fsl/fsleyes/colourmaps.py index a8b551ca0f393a6b5cbf1917d1e82c20e3e2a6b0..f5b8b763b47e609e4009feec8ef4e89eb99d1bf7 100644 --- a/fsl/fsleyes/colourmaps.py +++ b/fsl/fsleyes/colourmaps.py @@ -275,16 +275,19 @@ def registerColourMap(cmapFile, # update the VolumeOpts colour map property # for any existing VolumeOpts instances - cmapProp = fsldisplay.VolumeOpts.getProp('cmap') + cmapProp = fsldisplay.VolumeOpts.getProp('cmap') + negCmapProp = fsldisplay.VolumeOpts.getProp('negativeCmap') for overlay in overlayList: opts = displayCtx.getOpts(overlay) if isinstance(opts, fsldisplay.VolumeOpts): - cmapProp.addColourMap(key, opts) + cmapProp .addColourMap(key, opts) + negCmapProp.addColourMap(key, opts) # and for all future volume overlays - cmapProp.addColourMap(key) + cmapProp .addColourMap(key) + negCmapProp.addColourMap(key) def registerLookupTable(lut, diff --git a/fsl/fsleyes/controls/overlaydisplaypanel.py b/fsl/fsleyes/controls/overlaydisplaypanel.py index a8f4842e4b535b08eb8555ac4b414a47c5c8a7c3..0c9fe561fbbfe8d04d06c7ad2be85a62f8f1b6af 100644 --- a/fsl/fsleyes/controls/overlaydisplaypanel.py +++ b/fsl/fsleyes/controls/overlaydisplaypanel.py @@ -216,15 +216,11 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): for p in dispProps: - widget = props.buildGUI(self.__widgets, - target, - p, - showUnlink=False) + widget = props.buildGUI(self.__widgets, target, p) - # Add a 'load colour map' button next - # to the VolumeOpts.cmap control + # Build a panel for the VolumeOpts colour map controls. if isinstance(target, displayctx.VolumeOpts) and p.key == 'cmap': - widget = self.__buildColourMapWidget(widget) + widget = self.__buildColourMapWidget(target, widget) widgets.append(widget) @@ -238,24 +234,43 @@ class OverlayDisplayPanel(fslpanel.FSLEyesPanel): self.Layout() - def __buildColourMapWidget(self, cmapWidget): - """Creates a control which allows the user to load a custom colour - map. This control is added to the settings for :class:`.Image` - overlays with a :attr:`.Display.overlayType` of ``'volume'``. + def __buildColourMapWidget(self, target, cmapWidget): + """Builds a panel which contains widgets for controlling the + :attr:`.VolumeOpts.cmap`, :attr:`.VolumeOpts.negativeCmap`, and + :attr:`.VolumeOpts.enableNegativeCmap`. """ - action = loadcmap.LoadColourMapAction(self._displayCtx, - self._overlayList) + widgets = self.__widgets - button = wx.Button(self.__widgets) - button.SetLabel(strings.labels[self, 'loadCmap']) + # Button to load a new + # colour map from file + loadAction = loadcmap.LoadColourMapAction(self._displayCtx, + self._overlayList) - action.bindToWidget(self, wx.EVT_BUTTON, button) + loadButton = wx.Button(widgets) + loadButton.SetLabel(strings.labels[self, 'loadCmap']) - sizer = wx.BoxSizer(wx.HORIZONTAL) + loadAction.bindToWidget(self, wx.EVT_BUTTON, loadButton) - sizer.Add(cmapWidget, flag=wx.EXPAND, proportion=1) - sizer.Add(button, flag=wx.EXPAND) + # Negative colour map widget + negCmap = props.Widget('negativeCmap', + enabledWhen=lambda i, enc: enc, + dependencies=['enableNegativeCmap']) + enableNegCmap = props.Widget('enableNegativeCmap') + + negCmap = props.buildGUI(widgets, target, negCmap) + enableNegCmap = props.buildGUI(widgets, target, enableNegCmap) + + enableNegCmap.SetLabel( + strings.properties[target, 'enableNegativeCmap']) + + sizer = wx.FlexGridSizer(2, 2) + sizer.AddGrowableCol(0) + + sizer.Add(cmapWidget, flag=wx.EXPAND) + sizer.Add(loadButton, flag=wx.EXPAND) + sizer.Add(negCmap, flag=wx.EXPAND) + sizer.Add(enableNegCmap, flag=wx.EXPAND) return sizer @@ -288,7 +303,6 @@ _DISPLAY_PROPS = td.TypeDict({ props.Widget('cmap'), props.Widget('invert'), props.Widget('invertClipping'), - props.Widget('centreRanges'), props.Widget('linkLowRanges'), props.Widget('linkHighRanges'), props.Widget('displayRange', diff --git a/fsl/fsleyes/displaycontext/volumeopts.py b/fsl/fsleyes/displaycontext/volumeopts.py index a4135b83197b04ef8f97789e9989d034203e717d..4e303f0ec971d8566d2ba5167f443db9f3e67ced 100644 --- a/fsl/fsleyes/displaycontext/volumeopts.py +++ b/fsl/fsleyes/displaycontext/volumeopts.py @@ -492,6 +492,28 @@ class VolumeOpts(ImageOpts): cmap = props.ColourMap() """The colour map, a :class:`matplotlib.colors.Colourmap` instance.""" + + negativeCmap = props.ColourMap() + """A second colour map, used if :attr:`enableNegativeCmap` is ``True``. + When active, the :attr:`cmap` is used to colour positive values, and + the :attr:`negativeCmap` is used to colour negative values. + """ + + + enableNegativeCmap = props.Boolean(default=False) + """When ``True``, the :attr:`cmap` is used to colour positive values, + and the :attr:`negativeCmap` is used to colour negative values. + When this property is enabled, the minimum value for both the + :attr:`displayRange` and :attr:`clippingRange` is set to zero. Both + ranges are applied to positive values, and negated/inverted for negative + values. + + .. note:: When this property is set to ``True``, the + :attr:`.Display.brightness` and :attr:`.Display.contrast` + properties are disabled, as managing the interaction between + them would be far too complicated. + """ + interpolation = props.Choice(('none', 'linear', 'spline')) """How the value shown at a real world location is derived from the @@ -506,19 +528,6 @@ class VolumeOpts(ImageOpts): """ - centreRanges = props.Boolean(default=False) - """If ``True``, the :attr:`displayRange` and :attr:`clippingRange` ranges - will be kept centred at zero. A change to the negative end of either range - will result in the positive end being changed, and vice versa. - - .. note:: When this property is set to ``True``, the - :attr:`.Display.brightness`, :attr:`.Display.contrast`, - :attr:`.linkLowRanges` and :attr:`.linkHighRanges` properties - are all disabled, as managing the interaction between all of - them would be far too complicated. - """ - - linkLowRanges = props.Boolean(default=True) """If ``True``, the low bounds on both the :attr:`displayRange` and :attr:`clippingRange` ranges will be linked together. @@ -598,9 +607,9 @@ class VolumeOpts(ImageOpts): self .addListener('displayRange', self.name, self.__displayRangeChanged) - self .addListener('centreRanges', + self .addListener('enableNegativeCmap', self.name, - self.__centreRangesChanged) + self.__enableNegativeCmapChanged) self .addListener('linkLowRanges', self.name, self.__linkLowRangesChanged) @@ -621,14 +630,13 @@ class VolumeOpts(ImageOpts): display, display.getSyncPropertyName('contrast')) - # If centreRanges, linkLowRanges or linkHighRanges + # If enableNegativeCmap, linkLowRanges or linkHighRanges # have been set to True (this will happen if they # are true on the parent VolumeOpts instance), make # sure the property / listener states are up to date. - if self.centreRanges: self.__centreRangesChanged() - else: - if self.linkLowRanges: self.__linkLowRangesChanged() - if self.linkHighRanges: self.__linkHighRangesChanged() + if self.enableNegativeCmap: self.__enableNegativeCmapChanged() + if self.linkLowRanges: self.__linkLowRangesChanged() + if self.linkHighRanges: self.__linkHighRangesChanged() @actions.action @@ -733,7 +741,7 @@ class VolumeOpts(ImageOpts): See :func:`.colourmaps.displayRangeToBricon`. """ - if self.centreRanges: + if self.enableNegativeCmap: return brightness, contrast = fslcm.displayRangeToBricon( @@ -749,32 +757,24 @@ class VolumeOpts(ImageOpts): self.__toggleListeners(True) - def __centreRangesChanged(self, *a): - """Called when the :attr:`centreRanges` property changes. Configures - property listeners on the :attr:`clippingRange` and - :attr:`displayRange` properties. + def __enableNegativeCmapChanged(self, *a): + """Called when the :attr:`enableNegativeCmap` property changes. + Enables/disables the :attr:`.Display.brightness` and + :attr:`.Display.contrast` properties, and configures limits + on the :attr:`clippingRange` and :attr:`displayRange` properties. """ - if self.centreRanges: + if self.enableNegativeCmap: self.display.disableProperty('brightness') self.display.disableProperty('contrast') - self .disableProperty('linkLowRanges') - self .disableProperty('linkHighRanges') - self.setConstraint('displayRange', 'dimCentres', [0.0]) - self.setConstraint('clippingRange', 'dimCentres', [0.0]) - - # Make sure that lowLinkRanges and - # highLinkRanges are not active - self.__linkRangesChanged(False, 0) - self.__linkRangesChanged(False, 1) + self.displayRange .xmin = 0.0 + self.clippingRange.xmin = 0.0 else: self.display.enableProperty('brightness') self.display.enableProperty('contrast') - self .enableProperty('linkLowRanges') - self .enableProperty('linkHighRanges') - self.setConstraint('displayRange', 'dimCentres', [None]) - self.setConstraint('clippingRange', 'dimCentres', [None]) + self.displayRange .xmin = self.dataMin + self.clippingRange.xmin = self.dataMin def __linkLowRangesChanged(self, *a): diff --git a/fsl/fsleyes/tooltips.py b/fsl/fsleyes/tooltips.py index 47f2b9bd4023e9e8a14e1921124076e03300829d..f5c62a995f1a2249c63b3d2579ef56415a14c8e2 100644 --- a/fsl/fsleyes/tooltips.py +++ b/fsl/fsleyes/tooltips.py @@ -80,12 +80,12 @@ properties = TypeDict({ 'voxels outside of the range are displayed.' 'This option is useful for displaying ' 'statistic images.', - 'VolumeOpts.centreRanges' : 'If checked, the low and high values ' - 'of both the clipping and display ' - 'ranges will be yoked together, so ' - 'that both ranges stay centered at ' - 'zero.' , - 'VolumeOpts.cmap' : 'The colour map to use.', + 'VolumeOpts.cmap' : 'The colour map to use.', + 'VolumeOpts.negativeCmap' : 'The colour map to use for negative ' + 'values.', + 'VolumeOpts.enableNegativeCmap' : 'Enable the negative colour map - ' + 'this allows positive and negative ' + 'values to be coloured independently.', 'VolumeOpts.interpolation' : 'Interpolate the image data for display ' 'purposes. You can choose no ' 'interpolation (equivalent to nearest '