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

MelodicClassificationPanel can now load/save FIX files. Need some more

work on managing case-sensitive component labels/LUT labels/tags.
parent 4a8aad30
No related branches found
No related tags found
No related merge requests found
......@@ -34,6 +34,7 @@ class MelodicImage(fslimage.Image):
getComponentTimeSeries
getComponentPowerSpectrum
numComponents
getMelodicDir
getTopLevelAnalysisDir
getDataFile
getICClassification
......@@ -83,8 +84,7 @@ class MelodicImage(fslimage.Image):
self.__meldir = dirname
self.__melmix = melresults.getComponentTimeSeries( dirname)
self.__melFTmix = melresults.getComponentPowerSpectra(dirname)
self.__melICClass = melresults.MelodicClassification(
self.numComponents())
self.__melICClass = melresults.MelodicClassification( self)
# Automatically set the
# TR value if possible
......@@ -116,11 +116,18 @@ class MelodicImage(fslimage.Image):
"""Returns the number of components in this ``MelodicImage``. """
return self.shape[3]
def getMelodicDir(self):
"""Returns the melodic output directory in which this image is
contained.
"""
return self.__meldir
def getTopLevelAnalysisDir(self):
"""Returns the top level analysis, if the melodic analysis for this
``MelodicImage`` is contained within another analysis. Otherwise,
returnsa ``None``. See the
returns ``None``. See the
:func:`.melodicresults.getTopLevelAnalysisDir` function.
"""
return melresults.getTopLevelAnalysisDir(self.__meldir)
......
......@@ -192,7 +192,6 @@ def getComponentPowerSpectra(meldir):
return np.loadtxt(ftmixfile)
class MelodicClassification(props.HasProperties):
"""The ``MelodicClassification`` class is a convenience class for managing
a collection of component classification labels.
......@@ -211,6 +210,9 @@ class MelodicClassification(props.HasProperties):
removeComponent
clearLabels
clearComponents
.. note:: All component labels are converted to lower case.
.. warning:: Do not modify the :attr:`labels` list directly - use the
......@@ -229,21 +231,163 @@ class MelodicClassification(props.HasProperties):
"""
def __init__(self, ncomps):
def __init__(self, melimage):
"""Create a ``MelodicClassification`` instance.
"""
self.__ncomps = ncomps
self.__melimage = melimage
self.__ncomps = melimage.numComponents()
self.clear()
def clear(self):
"""Removes all labels from all components. """
notifState = self.getNotificationState('labels')
self.disableNotification('labels')
self.__components = {}
self.labels = [[] for i in range(ncomps)]
self.labels = [[] for i in range(self.__ncomps)]
self.setNotificationState('labels', notifState)
self.notify('labels')
def load(self, filename):
pass
"""Loads component labels from the specified file. The file is assuemd
to be of the format generated by FIX or Melview; such a file should
have a structure resembling the following::
filtered_func_data.ica
1, Signal, False
2, Unclassified Noise, True
3, Unknown, False
4, Signal, False
5, Unclassified Noise, True
6, Unclassified Noise, True
7, Unclassified Noise, True
8, Signal, False
[2, 5, 6, 7]
The first line of the file contains the name of the melodic directory.
Then, one line is present for each component, containing the following,
separated by commas:
- The component index (starting from 1).
- One or more labels for the component (multiple labels must be
comma-separated).
- ``'True'`` if the component has been classified as *bad*,
``'False'`` otherwise.
The last line of the file contains the index (starting from 1) of all
*bad* components, i.e. those components which are not classified as
signal or unknown.
.. note:: This method adds to, but does not replace, any existing
component classifications stored by this
``MelodicClassification``. Call the :meth:`clear` method,
before calling ``load``, if you want to discard any existing
classifications.
"""
with open(filename, 'rt') as f:
lines = f.readlines()
if len(lines) < 3:
raise InvalidFixFileError('Invalid FIX classification '
'file - not enough lines')
lines = [l.strip() for l in lines]
# Ignore the first and last
# lines - we're only interested
# in the component labels
compLines = lines[1:-1]
if len(compLines) != self.__ncomps:
raise InvalidFixFileError('Invalid FIX classification '
'file - number of components '
'do not match')
# Parse the labels for every component
# We dot not add the labels as we go
# as, if something is wrong with the
# file contents, we don't want this
# MelodicClassification instance to
# be modified. So we'll assign the
# labels afterwards
allLabels = []
for i, compLine in enumerate(compLines):
tokens = compLine.split(',')
tokens = [t.strip() for t in tokens]
if len(tokens) < 3:
raise InvalidFixFileError('Invalid FIX classification '
'file - component line {} does '
'not have enough '
'tokens'.format(i + 1))
compIdx = int(tokens[0])
compLabels = tokens[1:-1]
if compIdx != i + 1:
raise InvalidFixFileError('Invalid FIX classification '
'file - component line {} has '
'wrong component number '
'({})'.format(i, compIdx))
allLabels.append(compLabels)
# Now that all the labels are
# read in, we can store them
notifState = self.getNotificationState('labels')
self.disableNotification('labels')
for i, labels in enumerate(allLabels):
for label in labels:
self.addLabel(i, label)
self.setNotificationState('labels', notifState)
self.notify('labels')
def save(self, filename):
pass
"""Saves the component classifications stored by this
``MeloidicClassification`` to the specified file. The classifications
are saved in the format described in the :meth:`load` method.
.. TODO:: Accept a dictionary of ``{label : display label}`` mappings,
so we can output cased labels (e.g. ``'Signal'`` instead of
``'signal'``).
"""
lines = []
badComps = []
image = self.__melimage
# The first line - the melodic directory name
lines.append(op.basename(image.getMelodicDir()))
# A line for each component
for comp in range(self.__ncomps):
noise = not (self.hasLabel(comp, 'signal') or
self.hasLabel(comp, 'unknown'))
tokens = [str(comp + 1)] + self.getLabels(comp) + [str(noise)]
lines.append(', '.join(tokens))
if noise:
badComps.append(comp)
# A line listing the bad components
lines.append('[' + ', '.join([str(c + 1) for c in badComps]) + ']')
with open(filename, 'wt') as f:
f.write('\n'.join(lines) + '\n')
def getLabels(self, component):
......@@ -255,12 +399,14 @@ class MelodicClassification(props.HasProperties):
"""Returns ``True`` if the specified component has the specified label,
``False`` otherwise.
"""
label = label.lower()
return label in self.labels[component]
def addLabel(self, component, label):
"""Adds the given label to the given component. """
label = label.lower()
labels = list(self.labels[component])
comps = list(self.__components.get(label, []))
......@@ -284,6 +430,7 @@ class MelodicClassification(props.HasProperties):
def removeLabel(self, component, label):
"""Removes the given label from the given component. """
label = label.lower()
labels = list(self.labels[component])
comps = list(self.__components.get(label, []))
......@@ -318,6 +465,7 @@ class MelodicClassification(props.HasProperties):
def getComponents(self, label):
"""Returns a list of all components which have the given label. """
label = label.lower()
return list(self.__components.get(label, []))
......@@ -350,3 +498,9 @@ class MelodicClassification(props.HasProperties):
self.enableNotification('labels')
self.notify('labels')
class InvalidFixFileError(Exception):
"""
"""
pass
......@@ -125,6 +125,11 @@ messages = TypeDict({
'MelodicClassificationPanel.disabled' : 'Choose a melodic image.',
'MelodicClassificationPanel.loadError' : 'An error occurred while loading '
'the file {}.\n\nDetails: {}',
'MelodicClassificationPanel.saveError' : 'An error occurred while saving '
'the file {}.\n\nDetails: {}',
})
......@@ -180,6 +185,11 @@ titles = TypeDict({
'LookupTablePanel.loadLut' : 'Select a lookup table file',
'LookupTablePanel.labelExists' : 'Label already exists',
'MelodicClassificationPanel.loadDialog' : 'Load FIX/Melview file...',
'MelodicClassificationPanel.saveDialog' : 'Save FIX/Melview file...',
'MelodicClassificationPanel.loadError' : 'Error loading FIX/Melview file',
'MelodicClassificationPanel.saveError' : 'Error saving FIX/Melview file',
})
......@@ -356,6 +366,9 @@ labels = TypeDict({
'MelodicClassificationPanel.componentTab' : 'Components',
'MelodicClassificationPanel.labelTab' : 'Labels',
'MelodicClassificationPanel.loadButton' : 'Load from file',
'MelodicClassificationPanel.saveButton' : 'Save to file',
'MelodicClassificationPanel.clearButton' : 'Clear all labels',
'ComponentGrid.componentColumn' : 'IC #',
'ComponentGrid.labelColumn' : 'Labels',
......
......@@ -785,7 +785,7 @@ class LutLabel(object):
"""
if value is None: raise ValueError('LutLabel value cannot be None')
if name is None: name = 'Label'
if name is None: name = 'label'
if colour is None: colour = (0, 0, 0)
if enabled is None: enabled = True
......@@ -872,6 +872,9 @@ class LookupTable(props.HasProperties):
names/colours modified, via the :meth:`set` method. Label values can be
removed via the meth:`delete` method.
.. note:: All label names are converted to lower case.
.. warning:: Do not directly modify the :attr:`labels` list. If you do,
it will be your fault when things break. Use the :meth:`set`
and :meth:`delete` methods instead.
......@@ -970,8 +973,10 @@ class LookupTable(props.HasProperties):
def getByName(self, name):
"""Returns the :class:`LutLabel` instance associated with the given
``name``, or ``None`` if there is no ``LutLabel`.
``name``, or ``None`` if there is no ``LutLabel`. The name comparison
is case-insensitive.
"""
name = name.lower()
for i, ll in enumerate(self.labels):
if ll.name() == name:
......@@ -980,6 +985,22 @@ class LookupTable(props.HasProperties):
return None
def new(self, name, colour=None, enabled=True):
"""Create a new label. The new label is given the value ``max() + 1``.
:arg name: Label name
:arg colour: Label colour. If not previded, a random colour is used.
:arg enabled: Label enabled state .
"""
if colour is None:
colour = randomBrightColour()
return self.set(self.max() + 1,
name=name,
colour=colour,
enabled=enabled)
def set(self, value, **kwargs):
"""Create a new label with the given value, or updates the
colour/name/enabled states associated with the given value.
......@@ -1008,7 +1029,7 @@ class LookupTable(props.HasProperties):
# Create a new LutLabel instance with the
# new, existing, or default label settings
name = kwargs.get('name', label.name())
name = kwargs.get('name', label.name()).lower()
colour = kwargs.get('colour', label.colour())
enabled = kwargs.get('enabled', label.enabled())
label = LutLabel(value, name, colour, enabled)
......@@ -1037,6 +1058,8 @@ class LookupTable(props.HasProperties):
if lutChanged:
self.saved = False
return label
def delete(self, value):
"""Removes the given label value from the lookup table."""
......
......@@ -404,7 +404,13 @@ class LookupTablePanel(fslpanel.FSLEyesPanel):
current :class:`.LookupTable` instance.
"""
dlg = LutLabelDialog(self.GetTopLevelParent())
lut = self.__selectedLut
value = lut.max() + 1
name = strings.labels['LutLabelDialog.newLabel']
colour = fslcmaps.randomBrightColour()
colour = [int(round(c * 255.0)) for c in colour]
dlg = LutLabelDialog(self.GetTopLevelParent(), value, name, colour)
if dlg.ShowModal() != wx.ID_OK:
return
......@@ -702,7 +708,7 @@ class LutLabelDialog(wx.Dialog):
"""
def __init__(self, parent):
def __init__(self, parent, value, name, colour):
"""Create a ``LutLabelDialog``.
:arg parent: The :mod:`wx` paren object.
......@@ -726,8 +732,10 @@ class LutLabelDialog(wx.Dialog):
self.__colourLabel.SetLabel(strings.labels[self, 'colour'])
self.__ok .SetLabel(strings.labels[self, 'ok'])
self.__cancel .SetLabel(strings.labels[self, 'cancel'])
self.__name .SetValue(strings.labels[self, 'newLabel'])
self.__value .SetValue(0)
self.__value .SetValue( value)
self.__name .SetValue( name)
self.__colour .SetColour(colour)
self.__sizer = wx.GridSizer(4, 2)
self.SetSizer(self.__sizer)
......
......@@ -209,11 +209,9 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
for label in melclass.getLabels(i):
if label in labels:
continue
colour = self.__addNewLutLabel(label).colour()
labels .append(label)
colours.append(colour)
colours.append(fslcm.randomBrightColour())
for i in range(len(colours)):
colours[i] = [int(round(c * 255)) for c in colours[i]]
......@@ -228,26 +226,6 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
tags.AddTag(label)
def __addNewLutLabel(self, label, colour=None):
"""Called when a new tag is added to a :class:`.TextTagPanel`. Adds a
corresponding label to the :class:`.LookupTable`.
"""
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)
self.__refreshTags()
return lut.get(value)
def __onTagAdded(self, ev):
"""Called when a tag is added to a :class:`.TextTagPanel`. Adds the
corresponding component-label mapping to the
......@@ -267,9 +245,9 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
# If the tag panel previously just contained
# the 'Unknown' tag, remove that tag
if tags.TagCount() == 2 and tags.HasTag('Unknown'):
melclass.removeLabel(component, 'Unknown')
tags.RemoveTag('Unknown')
if tags.TagCount() == 2 and tags.HasTag('unknown'):
melclass.removeLabel(component, 'unknown')
tags.RemoveTag('unknown')
melclass.enableListener('labels', self._name)
......@@ -278,7 +256,10 @@ class ComponentGrid(fslpanel.FSLEyesPanel):
if lut.getByName(label) is None:
colour = tags.GetTagColour(label)
colour = [c / 255.0 for c in colour]
self.__addNewLutLabel(label, colour)
lut.disableListener('labels', self._name)
lut.new(name=label, colour=colour)
lut.enableListener('labels', self._name)
self.__grid.FitInside()
......
......@@ -9,8 +9,10 @@
of a :class:`.MelodicImage`.
"""
import wx
import logging
import wx
import pwidgets.notebook as notebook
......@@ -21,13 +23,31 @@ import fsl.fsleyes.panel as fslpanel
import melodicclassificationgrid as melodicgrid
log = logging.getLogger(__name__)
class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
"""The ``MelodicClassificationPanel``
"""
#
# Choose label colours
# Load/save from/to file
#
# File format:
# First line: ica directory name
# Lines 2-(N+1): One line for each component
# Last line: List of bad components
#
# A component line:
# Component index, Label1, Label2, True|False
#
#
#
# Save to a FSLeyes label file:
#
# Save to a FIX/MELview file:
# - Component has 'Signal' label
# - Component has 'Unknown' label
# - All other labels are output as 'Unclassified Noise'
# (these are added to the list on the last line of the file)
#
def __init__(self, parent, overlayList, displayCtx):
"""Create a ``MelodicClassificationPanel``.
......@@ -46,42 +66,61 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
wx.ALIGN_CENTRE_VERTICAL))
lut = fslcm.getLookupTable('melodic-classes')
self.__lut = fslcm.getLookupTable('melodic-classes')
self.__notebook = notebook.Notebook(self)
self.__componentGrid = melodicgrid.ComponentGrid(
self.__notebook,
self._overlayList,
self._displayCtx,
lut)
self.__lut)
self.__labelGrid = melodicgrid.LabelGrid(
self.__notebook,
self._overlayList,
self._displayCtx,
lut)
# self.__labelGrid = melodicgrid.LabelGrid(
# self.__notebook,
# self._overlayList,
# self._displayCtx,
# self.__lut)
self.__labelGrid = wx.Panel(self.__notebook)
self.__loadButton = wx.Button(self)
self.__saveButton = wx.Button(self)
self.__clearButton = wx.Button(self)
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.Add(self.__notebook, flag=wx.EXPAND, proportion=1)
# TODO Things which you don't want shown when
# a melodic image is not selected should
# be added to __mainSizer. Things which
# you always want displayed should be
# added to __sizer (but need to be laid
# out w.r.t. __disabledText/__mainSizer)
self.__loadButton .SetLabel(strings.labels[self, 'loadButton'])
self.__saveButton .SetLabel(strings.labels[self, 'saveButton'])
self.__clearButton.SetLabel(strings.labels[self, 'clearButton'])
# Things which you don't want shown when
# a melodic image is not selected should
# be added to __mainSizer. Things which
# you always want displayed should be
# added to __sizer (but need to be laid
# out w.r.t. __disabledText/__mainSizer)
self.__mainSizer = wx.BoxSizer(wx.VERTICAL)
self.__btnSizer = wx.BoxSizer(wx.HORIZONTAL)
self.__sizer = wx.BoxSizer(wx.VERTICAL)
self.__btnSizer .Add(self.__loadButton, flag=wx.EXPAND, proportion=1)
self.__btnSizer .Add(self.__saveButton, flag=wx.EXPAND, proportion=1)
self.__btnSizer .Add(self.__clearButton, flag=wx.EXPAND, proportion=1)
self.__mainSizer.Add(self.__notebook, flag=wx.EXPAND, proportion=1)
self.__mainSizer.Add(self.__btnSizer, flag=wx.EXPAND)
self.__sizer = wx.BoxSizer(wx.HORIZONTAL)
self.__sizer.Add(self.__disabledText, flag=wx.EXPAND, proportion=1)
self.__sizer.Add(self.__mainSizer, flag=wx.EXPAND, proportion=1)
self.__sizer .Add(self.__disabledText, flag=wx.EXPAND, proportion=1)
self.__sizer .Add(self.__mainSizer, flag=wx.EXPAND, proportion=1)
self.SetSizer(self.__sizer)
self.__loadButton .Bind(wx.EVT_BUTTON, self.__onLoadButton)
self.__saveButton .Bind(wx.EVT_BUTTON, self.__onSaveButton)
self.__clearButton.Bind(wx.EVT_BUTTON, self.__onClearButton)
overlayList.addListener('overlays',
self._name,
self.__selectedOverlayChanged)
......@@ -118,6 +157,8 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
def __selectedOverlayChanged(self, *a):
"""
"""
overlay = self._displayCtx.getSelectedOverlay()
......@@ -127,3 +168,99 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
return
self.__enable(True)
def __onLoadButton(self, ev):
"""
"""
lut = self.__lut
overlay = self._displayCtx.getSelectedOverlay()
melclass = overlay.getICClassification()
dlg = wx.FileDialog(
self,
message=strings.titles[self, 'loadDialog'],
defaultDir=overlay.getMelodicDir(),
style=wx.FD_OPEN)
if dlg.ShowModal() != wx.ID_OK:
return
filename = dlg.GetPath()
# Disable notification during the load,
# so the component/label grids don't
# confuse themselves. We'll manually
# refresh them below.
melclass.disableNotification('labels')
lut .disableNotification('labels')
try:
melclass.clear()
melclass.load(filename)
except Exception as e:
e = str(e)
msg = strings.messages[self, 'loadError'].format(filename, e)
title = strings.titles[ self, 'loadError']
log.debug('Error loading classification file '
'({}), ({})'.format(filename, e), exc_info=True)
wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK)
# Make sure a colour in the melodic
# lookup table exists for all labels
for comp, labels in enumerate(melclass.labels):
for label in labels:
lutLabel = lut.getByName(label)
if lutLabel is not None:
print 'Label {} is already in lookup table'.format(label)
continue
print 'New melodic classification label: {}'.format(label)
log.debug('New melodic classification label: {}'.format(label))
lut.new(label)
melclass.enableNotification('labels')
lut .enableNotification('labels')
lut .notify('labels')
melclass.notify('labels')
def __onSaveButton(self, ev):
"""
"""
overlay = self._displayCtx.getSelectedOverlay()
melclass = overlay.getICClassification()
dlg = wx.FileDialog(
self,
message=strings.titles[self, 'saveDialog'],
defaultDir=overlay.getMelodicDir(),
style=wx.FD_SAVE)
if dlg.ShowModal() != wx.ID_OK:
return
filename = dlg.GetPath()
try:
melclass.save(filename)
except Exception as e:
e = str(e)
msg = strings.messages[self, 'saveError'].format(filename, e)
title = strings.titles[ self, 'saveError']
log.debug('Error saving classification file '
'({}), ({})'.format(filename, e), exc_info=True)
wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK)
def __onClearButton(self, ev):
"""
"""
overlay = self._displayCtx.getSelectedOverlay()
melclass = overlay.getICClassification()
melclass.clear()
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