From 04a7d414e9c61a47c834b02619220160ee39d653 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Wed, 3 Jun 2015 16:03:00 +0100 Subject: [PATCH] Slightly functional lookup table panel, allowing user to change label colours, and to toggle labels on/off. --- fsl/data/strings.py | 14 +- fsl/fslview/colourmaps.py | 5 +- fsl/fslview/controls/__init__.py | 1 + fsl/fslview/controls/lookuptablepanel.py | 287 +++++++++++++++++++++++ fsl/fslview/displaycontext/labelopts.py | 7 +- fsl/fslview/views/canvaspanel.py | 2 + 6 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 fsl/fslview/controls/lookuptablepanel.py diff --git a/fsl/data/strings.py b/fsl/data/strings.py index d92a2bf3a..d144f9937 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -67,6 +67,10 @@ messages = TypeDict({ 'SpacePanel.nonVolumetric' : 'Non-volumetric overlays ' 'are not supported', + + 'LookupTablePanel.notLutOverlay' : 'Choose an overlay which ' + 'uses a lookup table', + }) @@ -102,7 +106,8 @@ titles = TypeDict({ 'OrthoSettingsPanel' : 'Ortho view settings', 'LightBoxToolBar' : 'Lightbox view toolbar', 'LightBoxSettingsPanel' : 'Lightbox view settings', - 'HistogramToolBar' : 'Histogram settings', + 'HistogramToolBar' : 'Histogram settings', + 'LookupTablePanel' : 'Lookup tables' }) @@ -120,6 +125,7 @@ actions = TypeDict({ 'CanvasPanel.toggleDisplayProperties' : 'Overlay display properties', 'CanvasPanel.toggleLocationPanel' : 'Location panel', 'CanvasPanel.toggleAtlasPanel' : 'Atlas panel', + 'CanvasPanel.toggleLookupTablePanel' : 'Lookup tables', 'OrthoPanel.toggleOrthoToolBar' : 'View properties', 'OrthoPanel.toggleProfileToolBar' : 'Mode controls', @@ -161,6 +167,12 @@ labels = TypeDict({ 'CanvasPanel.screenshot.notSaved.skip' : 'Skip overlay (will not appear ' 'in screenshot)', 'CanvasPanel.screenshot.notSaved.cancel' : 'Cancel screenshot', + + + 'LookupTablePanel.addLabel' : 'Add label', + 'LookupTablePanel.newLut' : 'New LUT', + 'LookupTablePanel.saveLut' : 'Save LUT', + 'LookupTablePanel.loadLut' : 'Load LUT', }) diff --git a/fsl/fslview/colourmaps.py b/fsl/fslview/colourmaps.py index d3c8e15cf..9d00e60c7 100644 --- a/fsl/fslview/colourmaps.py +++ b/fsl/fslview/colourmaps.py @@ -179,7 +179,10 @@ class _Map(object): def __repr__(self): return self.__str__() - + +# TODO Maybe this should be a HasProps class, so +# that interested parties can be# notified +# of changes to colours/names/etc. class LookupTable(object): """Class which encapsulates a list of labels and associated colours and names, defining a lookup table to be used for colouring label images. diff --git a/fsl/fslview/controls/__init__.py b/fsl/fslview/controls/__init__.py index 4da850160..9ee27b304 100644 --- a/fsl/fslview/controls/__init__.py +++ b/fsl/fslview/controls/__init__.py @@ -12,6 +12,7 @@ from overlayselectpanel import OverlaySelectPanel from lightboxsettingspanel import LightBoxSettingsPanel from locationpanel import LocationPanel from orthosettingspanel import OrthoSettingsPanel +from lookuptablepanel import LookupTablePanel from orthotoolbar import OrthoToolBar from orthoprofiletoolbar import OrthoProfileToolBar diff --git a/fsl/fslview/controls/lookuptablepanel.py b/fsl/fslview/controls/lookuptablepanel.py new file mode 100644 index 000000000..28b511678 --- /dev/null +++ b/fsl/fslview/controls/lookuptablepanel.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# +# lookuptablepanel.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging + +import wx + +import numpy as np + +import props + +import pwidgets.elistbox as elistbox + +import fsl.fslview.panel as fslpanel +import fsl.fslview.displaycontext as fsldisplay +import fsl.data.strings as strings + + +log = logging.getLogger(__name__) + + + +class LabelWidget(wx.Panel): + + def __init__(self, lutPanel, overlayOpts, lut, value): + wx.Panel.__init__(self, lutPanel) + + self.lutPanel = lutPanel + self.opts = overlayOpts + self.lut = lut + self.value = value + + # TODO Change the enable box to a toggle + # button with an eye icon + + self.valueLabel = wx.StaticText(self, + style=wx.ALIGN_CENTRE_VERTICAL | + wx.ALIGN_RIGHT) + self.enableBox = wx.CheckBox(self) + self.colourButton = wx.ColourPickerCtrl(self) + + self.sizer = wx.BoxSizer(wx.HORIZONTAL) + self.SetSizer(self.sizer) + self.sizer.Add(self.valueLabel, flag=wx.ALIGN_CENTRE, proportion=1) + self.sizer.Add(self.enableBox, flag=wx.ALIGN_CENTRE, proportion=1) + self.sizer.Add(self.colourButton, flag=wx.ALIGN_CENTRE, proportion=1) + + colour = [np.floor(c * 255.0) for c in lut.colour(value)] + + self.valueLabel .SetLabel(str(value)) + self.colourButton.SetColour(colour) + self.enableBox .SetValue(lut.enabled(value)) + + self.enableBox .Bind(wx.EVT_CHECKBOX, self.__onEnable) + self.colourButton.Bind(wx.EVT_COLOURPICKER_CHANGED, self.__onColour) + + + def __onEnable(self, ev): + + self.lut.set(self.value, enabled=self.enableBox.GetValue()) + self.__notifyLut() + + + def __notifyLut(self): + + # Disable the LookupTablePanel listener + # on the lut property, otherwise it will + # re-create the label list + self.opts.disableListener('lut', self.lutPanel._name) + self.opts.notify('lut') + self.opts.enableListener('lut', self.lutPanel._name) + + + def __onColour(self, ev): + + newColour = self.colourButton.GetColour() + newColour = [c / 255.0 for c in newColour] + + self.lut.set(self.value, colour=newColour) + self.__notifyLut() + + + +class LookupTablePanel(fslpanel.FSLViewPanel): + + def __init__(self, parent, overlayList, displayCtx): + + fslpanel.FSLViewPanel.__init__(self, parent, overlayList, displayCtx) + + # If non-lut image is shown, just show a message + + # Overlay name + # Change lookup table + # Add label + # Save lut + # Load lut + + self.__controlRow = wx.Panel(self) + + self.__disabledLabel = wx.StaticText(self, + style=wx.ALIGN_CENTER_VERTICAL | + wx.ALIGN_CENTER_HORIZONTAL) + self.__labelList = elistbox.EditableListBox( + self, + style=elistbox.ELB_NO_MOVE | elistbox.ELB_EDITABLE) + + self.__overlayNameLabel = wx.StaticText(self, + style=wx.ST_ELLIPSIZE_MIDDLE) + + self.__lutWidget = None + self.__newLutButton = wx.Button(self.__controlRow) + self.__saveLutButton = wx.Button(self.__controlRow) + self.__loadLutButton = wx.Button(self.__controlRow) + + self.__controlRowSizer = wx.BoxSizer(wx.HORIZONTAL) + self.__sizer = wx.BoxSizer(wx.VERTICAL) + + self.__controlRow.SetSizer(self.__controlRowSizer) + self .SetSizer(self.__sizer) + + self.__controlRowSizer.Add(self.__newLutButton, + flag=wx.EXPAND, proportion=1) + self.__controlRowSizer.Add(self.__loadLutButton, + flag=wx.EXPAND, proportion=1) + self.__controlRowSizer.Add(self.__saveLutButton, + flag=wx.EXPAND, proportion=1) + + self.__sizer.Add(self.__overlayNameLabel, flag=wx.EXPAND) + self.__sizer.Add(self.__controlRow, flag=wx.EXPAND) + self.__sizer.Add(self.__disabledLabel, flag=wx.EXPAND, proportion=1) + self.__sizer.Add(self.__labelList, flag=wx.EXPAND, proportion=1) + + # Label the labels and buttons + self.__disabledLabel.SetLabel(strings.messages[self, 'notLutOverlay']) + self.__newLutButton .SetLabel(strings.labels[ self, 'newLut']) + self.__loadLutButton.SetLabel(strings.labels[ self, 'loadLut']) + self.__saveLutButton.SetLabel(strings.labels[ self, 'saveLut']) + + # Make the label name a bit smaller + font = self.__overlayNameLabel.GetFont() + font.SetPointSize(font.GetPointSize() - 2) + font.SetWeight(wx.FONTWEIGHT_LIGHT) + self.__overlayNameLabel.SetFont(font) + + # Listen for listbox events + self.__labelList.Bind(elistbox.EVT_ELB_ADD_EVENT, + self.__onLabelAdd) + self.__labelList.Bind(elistbox.EVT_ELB_REMOVE_EVENT, + self.__onLabelRemove) + self.__labelList.Bind(elistbox.EVT_ELB_EDIT_EVENT, + self.__onLabelEdit) + + self.__selectedOpts = None + self.__selectedOverlay = None + + overlayList.addListener('overlays', + self._name, + self.__selectedOverlayChanged) + displayCtx .addListener('selectedOverlay', + self._name, + self.__selectedOverlayChanged) + + self.__selectedOverlayChanged() + + + def __selectedOverlayChanged(self, *a): + + newOverlay = self._displayCtx.getSelectedOverlay() + + if self.__selectedOverlay == newOverlay: + return + + if self.__selectedOverlay is not None: + + display = self._displayCtx.getDisplay(self.__selectedOverlay) + + display.removeListener('name', self._name) + display.removeListener('overlayType', self._name) + + self.__selectedOverlay = newOverlay + + if newOverlay is not None: + display = self._displayCtx.getDisplay(newOverlay) + display.addListener('name', + self._name, + self.__overlayNameChanged) + display.addListener('overlayType', + self._name, + self.__overlayTypeChanged) + + self.__overlayNameChanged() + self.__overlayTypeChanged() + + + def __overlayNameChanged(self, *a): + + overlay = self.__selectedOverlay + + if overlay is None: + self.__overlayNameLabel.SetLabel('') + return + + display = self._displayCtx.getDisplay(overlay) + + self.__overlayNameLabel.SetLabel(display.name) + + + def __overlayTypeChanged(self, *a): + + if self.__lutWidget is not None: + self.__controlRowSizer.Detach(self.__lutWidget) + self.__lutWidget.Destroy() + self.__lutWidget = None + + if self.__selectedOpts is not None: + self.__selectedOpts.removeListener('lut', self._name) + self.__selectedOpts = None + + overlay = self.__selectedOverlay + enabled = False + + if overlay is not None: + opts = self._displayCtx.getOpts(overlay) + + if isinstance(opts, fsldisplay.LabelOpts): + enabled = True + + self.__overlayNameLabel.Show( enabled) + self.__controlRow .Show( enabled) + self.__labelList .Show( enabled) + self.__disabledLabel .Show(not enabled) + + if not enabled: + self.Layout() + return + + opts = self._displayCtx.getOpts(overlay) + + opts.addListener('lut', self._name, self.__initLabelList) + + self.__selectedOpts = opts + self.__lutWidget = props.makeWidget( + self.__controlRow, opts, 'lut') + + self.__controlRowSizer.Insert( + 0, self.__lutWidget, flag=wx.EXPAND, proportion=1) + + self.__initLabelList() + + self.Layout() + + + def __initLabelList(self, *a): + + self.__labelList.Clear() + + if self.__selectedOpts is None: + return + + opts = self.__selectedOpts + lut = opts.lut + + names = lut.names() + colours = lut.colours() + values = lut.values() + + for i, (name, colour, value) in enumerate(zip(names, colours, values)): + self.__labelList.Append(name) + + widget = LabelWidget(self, opts, lut, value) + self.__labelList.SetItemWidget(i, widget) + + + def __onLabelAdd(self, ev): + pass + + + def __onLabelRemove(self, ev): + pass + + + def __onLabelEdit(self, ev): + pass diff --git a/fsl/fslview/displaycontext/labelopts.py b/fsl/fslview/displaycontext/labelopts.py index e8ccbb095..e8d63d0c3 100644 --- a/fsl/fslview/displaycontext/labelopts.py +++ b/fsl/fslview/displaycontext/labelopts.py @@ -27,7 +27,12 @@ class LabelOpts(volumeopts.ImageOpts): names = [lut.lutName() for lut in luts] self.getProp('lut').setChoices(luts, names, self) - + + # TODO create a copy of this LUT? If the user + # modifies a LUT for a specific image, do we + # want those modfications to be propagated to + # other images which also use the same LUT? + # Because that's what's happening currently. self.lut = luts[0] volumeopts.ImageOpts.__init__(self, overlay, *args, **kwargs) diff --git a/fsl/fslview/views/canvaspanel.py b/fsl/fslview/views/canvaspanel.py index 880321523..c9e44f0e9 100644 --- a/fsl/fslview/views/canvaspanel.py +++ b/fsl/fslview/views/canvaspanel.py @@ -192,6 +192,8 @@ class CanvasPanel(viewpanel.ViewPanel): fslcontrols.OverlayDisplayToolBar, False, self), 'toggleLocationPanel' : lambda *a: self.togglePanel( fslcontrols.LocationPanel), + 'toggleLookupTablePanel' : lambda *a: self.togglePanel( + fslcontrols.LookupTablePanel), }.items() + extraActions.items()) viewpanel.ViewPanel.__init__( -- GitLab