Skip to content
Snippets Groups Projects
Commit dca9c05d authored by Paul McCarthy's avatar Paul McCarthy
Browse files

Continuing work on melodic classification panel. Re-working pwidgets

controls, so this is going to stall for a bit.
parent 4c8e0c10
No related branches found
No related tags found
No related merge requests found
...@@ -36,6 +36,7 @@ class MelodicImage(fslimage.Image): ...@@ -36,6 +36,7 @@ class MelodicImage(fslimage.Image):
numComponents numComponents
getTopLevelAnalysisDir getTopLevelAnalysisDir
getDataFile getDataFile
getICClassification
""" """
...@@ -95,6 +96,8 @@ class MelodicImage(fslimage.Image): ...@@ -95,6 +96,8 @@ class MelodicImage(fslimage.Image):
self.tr = dataImage.pixdim[3] self.tr = dataImage.pixdim[3]
# TODO load classifications if present # TODO load classifications if present
for i in range(self.numComponents()):
self.__melICClass.addLabel(i, 'Unknown')
def getComponentTimeSeries(self, component): def getComponentTimeSeries(self, component):
...@@ -132,4 +135,7 @@ class MelodicImage(fslimage.Image): ...@@ -132,4 +135,7 @@ class MelodicImage(fslimage.Image):
def getICClassification(self): def getICClassification(self):
"""Return the :class:`.MelodicClassification` instance associated with
this ``MelodicImage``.
"""
return self.__melICClass return self.__melICClass
...@@ -27,13 +27,20 @@ following functions are provided: ...@@ -27,13 +27,20 @@ following functions are provided:
""" """
import logging
import os.path as op import os.path as op
import numpy as np import numpy as np
import props
import fsl.data.image as fslimage import fsl.data.image as fslimage
import fsl.data.featresults as featresults import fsl.data.featresults as featresults
log = logging.getLogger(__name__)
def isMelodicDir(path): def isMelodicDir(path):
"""Returns ``True`` if the given path looks like it is contained within """Returns ``True`` if the given path looks like it is contained within
a MELODIC directory, ``False`` otherwise. a MELODIC directory, ``False`` otherwise.
...@@ -182,19 +189,41 @@ def getComponentPowerSpectra(meldir): ...@@ -182,19 +189,41 @@ def getComponentPowerSpectra(meldir):
class MelodicClassification(object): class MelodicClassification(props.HasProperties):
""" """The ``MelodicClassification`` class is a convenience class for managing
a collection of component classification labels.
.. autosummary::
:nosignatures:
addLabel
addComponent
removeLabel
removeComponent
clearLabels
clearComponents
.. warning:: Do not modify the :attr:`labels` list directly - use the
methods listed above. A ``MelodicClassification`` needs to
manage some internal state whenever the component labels
change, so directly modifying the ``labels`` list will corrupt
this internal state.
""" """
labels = props.List()
def __init__(self, ncomps): def __init__(self, ncomps):
"""Create a ``MelodicClassification`` instance. """Create a ``MelodicClassification`` instance.
""" """
self.__ncomps = ncomps self.__ncomps = ncomps
self.__components = {}
self.__componentLabels = [[] for i in range(ncomps)] self.labels = [[] for i in range(ncomps)]
self.__labelComponents = {}
def load(self, filename): def load(self, filename):
pass pass
...@@ -205,43 +234,62 @@ class MelodicClassification(object): ...@@ -205,43 +234,62 @@ class MelodicClassification(object):
def getLabels(self, component): def getLabels(self, component):
return list(self.__componentLabels[component]) return list(self.labels[component])
def addLabel(self, component, label): def addLabel(self, component, label):
cmpLabels = self.__componentLabels[component] labels = list(self.labels[component])
labelCmps = self.__labelComponents.get(label, []) comps = self.__components.get(label, [])
if label in cmpLabels: if label in labels:
return return
cmpLabels[component].append(label) labels.append(label)
labelCmps[label] .append(component) comps .append(component)
self.labels[ component] = labels
self.__components[label] = comps
self.__componentLabels[component] = cmpLabels log.debug('Label added to component: {} <-> {}'.format(component,
self.__labelComponents[label] = labelCmps label))
def removeLabel(self, component, label): def removeLabel(self, component, label):
if label not in self.__componentLabels[component]: labels = list(self.labels[component])
comps = self.__components.get(label, [])
if label not in labels:
return return
self.__componentLabels[component].remove(label) labels.remove(label)
self.__labelComponents[label] .remove(component) comps .remove(component)
self.labels[ component] = labels
self.__components[label] = comps
log.debug('Label removed from component: {} <-> {}'.format(component,
label))
def clearLabels(self, component): def clearLabels(self, component):
labels = self.getLabels(component) labels = self.getLabels(component)
self.disableNotification('labels')
for l in labels: for l in labels:
self.removeLabel(component, l) self.removeLabel(component, l)
self.enableNotification('labels')
self.notify('labels')
log.debug('Labels cleared from component: {}'.format(component))
def getComponents(self, label): def getComponents(self, label):
return list(self.__labelComponents.get(label, [])) return list(self.__components.get(label, []))
def addComponent(self, label, component): def addComponent(self, label, component):
...@@ -256,5 +304,10 @@ class MelodicClassification(object): ...@@ -256,5 +304,10 @@ class MelodicClassification(object):
components = self.getComponents(label) components = self.getComponents(label)
self.disableNotification('labels')
for c in components: for c in components:
self.removeComponent(label, c) self.removeComponent(label, c)
self.enableNotification('labels')
self.notify('labels')
...@@ -353,6 +353,12 @@ labels = TypeDict({ ...@@ -353,6 +353,12 @@ labels = TypeDict({
'OverlayInfoPanel.Model.numVertices' : 'Number of vertices', 'OverlayInfoPanel.Model.numVertices' : 'Number of vertices',
'OverlayInfoPanel.Model.numIndices' : 'Number of indices', 'OverlayInfoPanel.Model.numIndices' : 'Number of indices',
'OverlayInfoPanel.dataSource' : 'Data source', 'OverlayInfoPanel.dataSource' : 'Data source',
'MelodicClassificationPanel.componentTab' : 'Components',
'MelodicClassificationPanel.labelTab' : 'Labels',
'ComponentGrid.componentColumn' : 'IC #',
'ComponentGrid.labelColumn' : 'Labels',
}) })
......
...@@ -968,6 +968,18 @@ class LookupTable(props.HasProperties): ...@@ -968,6 +968,18 @@ class LookupTable(props.HasProperties):
return self.__find(value)[1] return self.__find(value)[1]
def getByName(self, name):
"""Returns the :class:`LutLabel` instance associated with the given
``name``, or ``None`` if there is no ``LutLabel`.
"""
for i, ll in enumerate(self.labels):
if ll.name() == name:
return ll
return None
def set(self, value, **kwargs): def set(self, value, **kwargs):
"""Create a new label with the given value, or updates the """Create a new label with the given value, or updates the
colour/name/enabled states associated with the given value. colour/name/enabled states associated with the given value.
......
#!/usr/bin/env python
#
# melodicclassificationgrid.py - the ComponentGrid and LabelGrid classes.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
import wx
import pwidgets.widgetgrid as widgetgrid
import pwidgets.texttag as texttag
import fsl.fsleyes.panel as fslpanel
import fsl.fsleyes.colourmaps as fslcm
import fsl.fsleyes.displaycontext as fsldisplay
import fsl.data.melodicimage as fslmelimage
import fsl.data.strings as strings
class ComponentGrid(fslpanel.FSLEyesPanel):
def __init__(self, parent, overlayList, displayCtx, lut):
fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx)
self.__lut = lut
self.__grid = widgetgrid.WidgetGrid(
self,
style=(wx.VSCROLL |
widgetgrid.WG_SELECTABLE_ROWS |
widgetgrid.WG_KEY_NAVIGATION))
self.__grid.ShowRowLabels(False)
self.__grid.ShowColLabels(True)
self.__sizer = wx.BoxSizer(wx.HORIZONTAL)
self.__sizer.Add(self.__grid, flag=wx.EXPAND, proportion=1)
self.SetSizer(self.__sizer)
self.__grid.Bind(widgetgrid.EVT_WG_SELECT, self.__onGridSelect)
self.__grid.Bind(wx.EVT_CHAR_HOOK, self.__onGridKeyboard)
lut .addListener('labels', self._name, self.__lutChanged)
displayCtx .addListener('selectedOverlay',
self._name,
self.__selectedOverlayChanged)
overlayList.addListener('overlays',
self._name,
self.__selectedOverlayChanged)
self.__overlay = None
self.__selectedOverlayChanged()
def destroy(self):
"""
"""
self._displayCtx .removeListener('selectedOverlay', self._name)
self._overlayList.removeListener('overlays', self._name)
self.__lut .removeListener('labels', self._name)
self.__deregisterCurrentOverlay()
self.__lut = None
fslpanel.FSLEyesPanel.destroy(self)
def __deregisterCurrentOverlay(self):
"""
"""
if self.__overlay is None:
return
overlay = self.__overlay
self.__overlay = None
melclass = overlay.getICClassification()
melclass.removeListener('labels', self._name)
try:
display = self._displayCtx.getDisplay(overlay)
opts = display.getDisplayOpts()
opts .removeListener('volume', self._name)
display.removeListener('overlayType', self._name)
except fsldisplay.InvalidOverlayError:
pass
def __selectedOverlayChanged(self, *a):
"""
"""
self.__deregisterCurrentOverlay()
self.__grid.ClearGrid()
overlay = self._displayCtx.getSelectedOverlay()
if not isinstance(overlay, fslmelimage.MelodicImage):
return
self.__overlay = overlay
display = self._displayCtx.getDisplay(overlay)
opts = display.getDisplayOpts()
melclass = overlay.getICClassification()
ncomps = overlay.numComponents()
self.__grid.SetGridSize(ncomps, 2, growCols=[1])
self.__grid.SetColLabel(0, strings.labels[self, 'componentColumn'])
self.__grid.SetColLabel(1, strings.labels[self, 'labelColumn'])
opts .addListener('volume', self._name, self.__volumeChanged)
melclass.addListener('labels', self._name, self.__labelsChanged)
display .addListener('overlayType',
self._name,
self.__selectedOverlayChanged)
self.__recreateTags()
self.__volumeChanged()
def __recreateTags(self):
"""
"""
overlay = self.__overlay
lut = self.__lut
melclass = overlay.getICClassification()
numComps = overlay.numComponents()
labels = [l.name() for l in lut.labels]
colours = [l.colour() for l in lut.labels]
# Compile lists of all the existing
# labels, and the colours for each one
for i in range(numComps):
for label in melclass.getLabels(i):
if label in labels:
continue
colour = self.__addNewLutLabel(label).colour()
labels .append(label)
colours.append(colour)
for i in range(len(colours)):
colours[i] = [int(round(c * 255)) for c in colours[i]]
for i in range(numComps):
tags = texttag.TextTagPanel(self.__grid,
style=(texttag.TTP_ALLOW_NEW_TAGS |
texttag.TTP_ADD_NEW_TAGS |
texttag.TTP_NO_DUPLICATES))
tags.SetOptions(labels, colours)
# Store the component number on the tag
# panel, so we know which component we
# are dealing with in the __onTagAdded
# and __onTagRemoved methods.
tags._melodicComponent = i
self.__grid.SetText( i, 0, str(i))
self.__grid.SetWidget(i, 1, tags)
for label in melclass.getLabels(i):
tags.AddTag(label)
tags.Bind(texttag.EVT_TTP_TAG_ADDED_EVENT, self.__onTagAdded)
tags.Bind(texttag.EVT_TTP_TAG_REMOVED_EVENT, self.__onTagRemoved)
self.Layout()
def __addNewLutLabel(self, label, colour=None):
"""
"""
lut = self.__lut
value = lut.max() + 1
if colour is None:
colour = fslcm.randomBrightColour()
lut.disableListener('labels', self._name)
lut.set(value, name=label, colour=colour)
lut.enableListener('labels', self._name)
return lut.get(value)
def __onTagAdded(self, ev):
"""
"""
tags = ev.GetEventObject()
label = ev.tag
component = tags._melodicComponent
overlay = self.__overlay
lut = self.__lut
melclass = overlay.getICClassification()
if lut.getByName(label) is None:
colour = tags.GetTagColour(label)
colour = [c / 255.0 for c in colour]
self.__addNewLutLabel(label, colour)
melclass.disableListener('labels', self._name)
melclass.addLabel(component, label)
melclass.enableListener('labels', self._name)
self.__grid.FitInside()
def __onTagRemoved(self, ev):
"""
"""
tags = ev.GetEventObject()
label = ev.tag
component = tags._melodicComponent
overlay = self.__overlay
melclass = overlay.getICClassification()
melclass.disableListener('labels', self._name)
melclass.removeLabel(component, label)
melclass.enableListener('labels', self._name)
self.__grid.FitInside()
def __onGridSelect(self, ev):
component = ev.row
opts = self._displayCtx.getOpts(self.__overlay)
opts.disableListener('volume', self._name)
opts.volume = component
opts.enableListener('volume', self._name)
def __onGridKeyboard(self, ev):
key = ev.GetKeyCode()
print 'MC Grid keyboard event ({})'.format(key)
if key != wx.WXK_RETURN:
ev.Skip()
return
row = self.__grid.GetSelection()[0]
tags = self.__grid.GetWidget(row, 1)
if tags.HasFocus():
ev.Skip()
return
print ' -> Focusing row {}'.format(row)
tags.FocusComboBox()
def __volumeChanged(self, *a):
opts = self._displayCtx.getOpts(self.__overlay)
self.__grid.SetSelection(opts.volume, -1)
def __labelsChanged(self, *a):
self.__recreateTags()
def __lutChanged(self, *a):
self.__recreateTags()
class LabelGrid(fslpanel.FSLEyesPanel):
def __init__(self, parent, overlayList, displayCtx, lut):
fslpanel.FSLEyesPanel.__init__(self, parent, overlayList, displayCtx)
self.__grid = widgetgrid.WidgetGrid(self)
self.__sizer = wx.BoxSizer(wx.HORIZONTAL)
self.__sizer.Add(self.__grid, flag=wx.EXPAND, proportion=1)
self.SetSizer(self.__sizer)
...@@ -12,13 +12,13 @@ of a :class:`.MelodicImage`. ...@@ -12,13 +12,13 @@ of a :class:`.MelodicImage`.
import wx import wx
import pwidgets.widgetgrid as widgetgrid import pwidgets.notebook as notebook
import pwidgets.texttag as texttag
import pwidgets.notebook as notebook
import fsl.data.strings as strings import fsl.data.strings as strings
import fsl.data.melodicimage as fslmelimage import fsl.data.melodicimage as fslmelimage
import fsl.fsleyes.panel as fslpanel import fsl.fsleyes.colourmaps as fslcm
import fsl.fsleyes.panel as fslpanel
import melodicclassificationgrid as melodicgrid
class MelodicClassificationPanel(fslpanel.FSLEyesPanel): class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
...@@ -45,12 +45,26 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): ...@@ -45,12 +45,26 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
style=(wx.ALIGN_CENTRE_HORIZONTAL | style=(wx.ALIGN_CENTRE_HORIZONTAL |
wx.ALIGN_CENTRE_VERTICAL)) wx.ALIGN_CENTRE_VERTICAL))
self.__notebook = notebook.Notebook(self)
self.__componentGrid = ComponentGrid( self.__notebook)
self.__labelGrid = LabelGrid( self.__notebook)
self.__notebook.AddPage(self.__componentGrid, 'Components') lut = fslcm.getLookupTable('melodic-classes')
self.__notebook.AddPage(self.__labelGrid, 'Labels')
self.__notebook = notebook.Notebook(self)
self.__componentGrid = melodicgrid.ComponentGrid(
self.__notebook,
self._overlayList,
self._displayCtx,
lut)
self.__labelGrid = melodicgrid.LabelGrid(
self.__notebook,
self._overlayList,
self._displayCtx,
lut)
self.__notebook.AddPage(self.__componentGrid,
strings.labels[self, 'componentTab'])
self.__notebook.AddPage(self.__labelGrid,
strings.labels[self, 'labelTab'])
self.__mainSizer = wx.BoxSizer(wx.HORIZONTAL) self.__mainSizer = wx.BoxSizer(wx.HORIZONTAL)
self.__mainSizer.Add(self.__notebook, flag=wx.EXPAND, proportion=1) self.__mainSizer.Add(self.__notebook, flag=wx.EXPAND, proportion=1)
...@@ -75,15 +89,19 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): ...@@ -75,15 +89,19 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
self._name, self._name,
self.__selectedOverlayChanged) self.__selectedOverlayChanged)
self.__overlay = None
self.__selectedOverlayChanged() self.__selectedOverlayChanged()
self.SetMinSize((400, 100))
def destroy(self): def destroy(self):
""" """
""" """
self._displayCtx .removeListener('selectedOverlay', self._name) self._displayCtx .removeListener('selectedOverlay', self._name)
self._overlayList.removeListener('overlays', self._name) self._overlayList.removeListener('overlays', self._name)
self.__componentGrid.destroy()
self.__labelGrid .destroy()
fslpanel.FSLEyesPanel.destroy(self) fslpanel.FSLEyesPanel.destroy(self)
...@@ -108,67 +126,4 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): ...@@ -108,67 +126,4 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
self.__enable(False, strings.messages[self, 'disabled']) self.__enable(False, strings.messages[self, 'disabled'])
return return
self.__overlay = overlay
self.__componentGrid.setOverlay(overlay)
self.__labelGrid .setOverlay(overlay)
self.__enable(True) self.__enable(True)
class ComponentGrid(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.__grid = widgetgrid.WidgetGrid(self)
self.__sizer = wx.BoxSizer(wx.HORIZONTAL)
self.__grid.ShowRowLabels(False)
self.__grid.ShowColLabels(True)
self.__sizer.Add(self.__grid, flag=wx.EXPAND, proportion=1)
self.SetSizer(self.__sizer)
def setOverlay(self, overlay):
numComps = overlay.numComponents()
self.__grid.ClearGrid()
self.__grid.SetGridSize(numComps, 2, growCols=[1])
self.__grid.SetColLabel(0, 'Component #')
self.__grid.SetColLabel(1, 'Labels')
for i in range(numComps):
tags = texttag.TextTagPanel(self.__grid,
style=(texttag.TTP_ALLOW_NEW_TAGS |
texttag.TTP_ADD_NEW_TAGS |
texttag.TTP_NO_DUPLICATES))
self.__grid.SetText( i, 0, str(i))
self.__grid.SetWidget(i, 1, tags)
self.Layout()
class LabelGrid(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.__grid = widgetgrid.WidgetGrid(self)
self.__sizer = wx.BoxSizer(wx.HORIZONTAL)
self.__sizer.Add(self.__grid, flag=wx.EXPAND, proportion=1)
self.SetSizer(self.__sizer)
def setOverlay(self, overlay):
pass
...@@ -386,7 +386,7 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): ...@@ -386,7 +386,7 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel):
for i, (infName, infData) in enumerate(secInf): for i, (infName, infData) in enumerate(secInf):
if i % 2: bgColour = '#ffffff' if i % 2: bgColour = '#ffffff'
else: bgColour = '#ffeeee' else: bgColour = '#cdcdff'
lines.append('<tr bgcolor="{}">' lines.append('<tr bgcolor="{}">'
'<td><b>{}</b></td>' '<td><b>{}</b></td>'
......
...@@ -270,14 +270,14 @@ class ListItemWidget(wx.Panel): ...@@ -270,14 +270,14 @@ class ListItemWidget(wx.Panel):
""" """
unsavedDefaultBG = '#ffaaaa' unsavedDefaultBG = '#ffeeee'
"""This colour is used as the default background colour for """This colour is used as the default background colour for
:class:`.Image` overlays with an :attr:`.Image.saved` property :class:`.Image` overlays with an :attr:`.Image.saved` property
of ``False``. of ``False``.
""" """
unsavedSelectedBG = '#aa4444' unsavedSelectedBG = '#ffcdcd'
"""This colour is used as the background colour for :class:`.Image` """This colour is used as the background colour for :class:`.Image`
overlays with an :attr:`.Image.saved` property of ``False``, when overlays with an :attr:`.Image.saved` property of ``False``, when
they are selected in the :class:`OverlayListPanel`. they are selected in the :class:`OverlayListPanel`.
......
random Random random Random
harvard-oxford-cortical MGH Cortical harvard-oxford-cortical MGH Cortical
harvard-oxford-subcortical MGH Sub-cortical harvard-oxford-subcortical MGH Sub-cortical
melodic-classes Melodic IC classification
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment