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

The melodic classification panel 'load labels' button can be used to

load a melodic overlay.
parent e8f1d0ad
No related branches found
No related tags found
No related merge requests found
...@@ -113,6 +113,11 @@ def getDataFile(meldir): ...@@ -113,6 +113,11 @@ def getDataFile(meldir):
return None return None
def getMeanFile(meldir):
"""Return a path to the mean image of the meloidic input data. """
return fslimage.addExt(op.join(meldir, 'mean'))
def getICFile(meldir): def getICFile(meldir):
"""Returns the path to the melodic IC image. """ """Returns the path to the melodic IC image. """
return fslimage.addExt(op.join(meldir, 'melodic_IC')) return fslimage.addExt(op.join(meldir, 'melodic_IC'))
...@@ -406,6 +411,7 @@ def loadMelodicLabelFile(filename): ...@@ -406,6 +411,7 @@ def loadMelodicLabelFile(filename):
to be of the format generated by FIX or Melview; such a file should to be of the format generated by FIX or Melview; such a file should
have a structure resembling the following:: have a structure resembling the following::
filtered_func_data.ica filtered_func_data.ica
1, Signal, False 1, Signal, False
2, Unclassified Noise, True 2, Unclassified Noise, True
...@@ -417,6 +423,7 @@ def loadMelodicLabelFile(filename): ...@@ -417,6 +423,7 @@ def loadMelodicLabelFile(filename):
8, Signal, False 8, Signal, False
[2, 5, 6, 7] [2, 5, 6, 7]
The first line of the file contains the name of the melodic directory. The first line of the file contains the name of the melodic directory.
Then, one line is present for each component, containing the following, Then, one line is present for each component, containing the following,
separated by commas: separated by commas:
...@@ -427,10 +434,29 @@ def loadMelodicLabelFile(filename): ...@@ -427,10 +434,29 @@ def loadMelodicLabelFile(filename):
- ``'True'`` if the component has been classified as *bad*, - ``'True'`` if the component has been classified as *bad*,
``'False'`` otherwise. ``'False'`` otherwise.
The last line of the file contains the index (starting from 1) of all 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 *bad* components, i.e. those components which are not classified as
signal or unknown. signal or unknown.
:arg filename: Name of the label file to load.
:returns: A tuple containing the path to the melodic directory
as specified in the label file, and a list of lists, one
list per component, with each list containing the labels for
the corresponding component.
.. note:: This function will also parse files which only contain a
bad component list, e.g.::
[2, 5, 6, 7]
In this case, the returned melodic directory path will be
``None``.
""" """
filename = op.abspath(filename)
with open(filename, 'rt') as f: with open(filename, 'rt') as f:
lines = f.readlines() lines = f.readlines()
...@@ -462,6 +488,13 @@ def loadMelodicLabelFile(filename): ...@@ -462,6 +488,13 @@ def loadMelodicLabelFile(filename):
melDir = lines[0] melDir = lines[0]
noisyComps = map(int, lines[-1][1:-1].split(', ')) noisyComps = map(int, lines[-1][1:-1].split(', '))
# The melodic directory path should
# either be an absolute path, or
# be specified relative to the location
# of the label file.
if not op.isabs(melDir):
melDir = op.join(op.dirname(filename), melDir)
# Parse the labels for every component # Parse the labels for every component
# We dot not add the labels as we go # We dot not add the labels as we go
# as, if something is wrong with the # as, if something is wrong with the
......
...@@ -141,12 +141,35 @@ messages = TypeDict({ ...@@ -141,12 +141,35 @@ messages = TypeDict({
'is necessary for editing.', 'is necessary for editing.',
'MelodicClassificationPanel.disabled' : 'Choose a melodic image.', 'MelodicClassificationPanel.disabled' : 'Choose a melodic image.',
'MelodicClassificationPanel.loadError' : 'An error occurred while '
'loading the file {}.'
'\n\nDetails: {}',
'MelodicClassificationPanel.noMelDir' : 'The label file {} does not '
'specify a path to a Melodic '
'directory!',
'MelodicClassificationPanel.saveError' : 'An error occurred while '
'saving the file {}.'
'\n\nDetails: {}',
'MelodicClassificationPanel.wrongNComps' : 'The mumber of components in '
'the label file {} is greater '
'than the number of components '
'in the overlay {}!',
'MelodicClassificationPanel.diffMelDir' : 'The label file {} does not '
'refer to the melodic '
'directory of the selected '
'overlay ({}). What do you '
'want to do?',
'MelodicClassificationPanel.diffMelDir.labels' : 'Load the overlay in '
'the label file',
'MelodicClassificationPanel.diffMelDir.overlay' : 'Apply the labels to '
'the current overlay'
'MelodicClassificationPanel.loadError' : 'An error occurred while loading ' 'An error occurred while '
'the file {}.\n\nDetails: {}', 'saving the file {}.'
'MelodicClassificationPanel.saveError' : 'An error occurred while saving ' '\n\nDetails: {}',
'the file {}.\n\nDetails: {}',
}) })
...@@ -387,9 +410,9 @@ labels = TypeDict({ ...@@ -387,9 +410,9 @@ labels = TypeDict({
'MelodicClassificationPanel.componentTab' : 'Components', 'MelodicClassificationPanel.componentTab' : 'Components',
'MelodicClassificationPanel.labelTab' : 'Labels', 'MelodicClassificationPanel.labelTab' : 'Labels',
'MelodicClassificationPanel.loadButton' : 'Load from file', 'MelodicClassificationPanel.loadButton' : 'Load labels',
'MelodicClassificationPanel.saveButton' : 'Save to file', 'MelodicClassificationPanel.saveButton' : 'Save labels',
'MelodicClassificationPanel.clearButton' : 'Clear all labels', 'MelodicClassificationPanel.clearButton' : 'Clear labels',
'ComponentGrid.componentColumn' : 'IC #', 'ComponentGrid.componentColumn' : 'IC #',
'ComponentGrid.labelColumn' : 'Labels', 'ComponentGrid.labelColumn' : 'Labels',
......
...@@ -10,16 +10,22 @@ of a :class:`.MelodicImage`. ...@@ -10,16 +10,22 @@ of a :class:`.MelodicImage`.
""" """
import os
import os.path as op
import logging import logging
import wx import wx
import pwidgets.notebook as notebook import pwidgets.notebook as notebook
import fsl.utils.settings as fslsettings
import fsl.data.strings as strings import fsl.data.strings as strings
import fsl.data.image as fslimage
import fsl.data.melodicresults as fslmelresults
import fsl.data.melodicimage as fslmelimage import fsl.data.melodicimage as fslmelimage
import fsl.fsleyes.colourmaps as fslcm import fsl.fsleyes.colourmaps as fslcm
import fsl.fsleyes.panel as fslpanel import fsl.fsleyes.panel as fslpanel
import fsl.fsleyes.autodisplay as autodisplay
import melodicclassificationgrid as melodicgrid import melodicclassificationgrid as melodicgrid
...@@ -30,25 +36,7 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): ...@@ -30,25 +36,7 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
"""The ``MelodicClassificationPanel`` """The ``MelodicClassificationPanel``
""" """
#
# 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): def __init__(self, parent, overlayList, displayCtx):
"""Create a ``MelodicClassificationPanel``. """Create a ``MelodicClassificationPanel``.
...@@ -109,10 +97,10 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): ...@@ -109,10 +97,10 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
self.__btnSizer .Add(self.__clearButton, 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.__notebook, flag=wx.EXPAND, proportion=1)
self.__mainSizer.Add(self.__btnSizer, flag=wx.EXPAND)
self.__sizer .Add(self.__disabledText, 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.__sizer .Add(self.__mainSizer, flag=wx.EXPAND, proportion=1)
self.__sizer .Add(self.__btnSizer, flag=wx.EXPAND)
self.SetSizer(self.__sizer) self.SetSizer(self.__sizer)
...@@ -152,6 +140,9 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): ...@@ -152,6 +140,9 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
self.__sizer.Show(self.__disabledText, not enable) self.__sizer.Show(self.__disabledText, not enable)
self.__sizer.Show(self.__mainSizer, enable) self.__sizer.Show(self.__mainSizer, enable)
self.__saveButton .Enable(enable)
self.__clearButton.Enable(enable)
self.Layout() self.Layout()
...@@ -173,35 +164,131 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): ...@@ -173,35 +164,131 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
def __onLoadButton(self, ev): def __onLoadButton(self, ev):
""" """Called when the *Load labels* button is pushed. Prompts the user
to select a label file to load, then does the following:
1. If the selected label file refers to the currently selected
melodic_IC overlay, the labels are applied to the overlay.
2. If the selected label file refers to a different melodic_IC
overlay, the user is asked whether they want to load the
different melodic_IC file (the default), or whether they
want the labels applied to the existing overlay.
3. If the selected label file does not refer to any overlay
(it only contains the bad component list), the user is asked
whether they want the labels applied to the current melodic_IC
overlay.
If the number of labels in the file is less than the number of
melodic_IC components, thew remaining components are labelled
as unknown. If the number of labels in the file is greater than
the number of melodic_IC components, an error is shown, and
nothing is done.
""" """
lut = self.__lut # The aim of the code beneath this function
overlay = self._displayCtx.getSelectedOverlay() # is to load a set of component labels, and
melclass = overlay.getICClassification() # to figure out which overlay they should
dlg = wx.FileDialog( # be added to. When it has done this, it
# calls this function.
def applyLabels(labelFile, overlay, allLabels):
lut = self.__lut
melclass = overlay.getICClassification()
ncomps = overlay.numComponents()
nlabels = len(allLabels)
# Error: number of labels in the
# file is greater than the number
# of components in the overlay.
if ncomps < nlabels:
msg = strings.messages[self, 'wrongNComps'].format(
labelFile, overlay.dataSource)
title = strings.titles[ self, 'loadError']
wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK)
return
# Number of labels in the file is
# less than number of components
# in the overlay - we pad the
# labels with 'Unknown'
elif ncomps > nlabels:
for i in range(nlabels, ncomps):
allLabels.append(['Unknown'])
# Disable notification while applying
# labels so the component/label grids
# don't confuse themselves. We'll
# manually refresh them below.
melclass.disableNotification('labels')
lut .disableNotification('labels')
melclass.clear()
for comp, lbls in enumerate(allLabels):
for lbl in lbls:
melclass.addLabel(comp, lbl)
# Make sure a colour in the melodic
# lookup table exists for all labels
for comp, labels in enumerate(melclass.labels):
for label in labels:
label = melclass.getDisplayLabel(label)
lutLabel = lut.getByName(label)
if lutLabel is None:
log.debug('New melodic classification '
'label: {}'.format(label))
lut.new(label)
melclass.enableNotification('labels')
lut .enableNotification('labels')
# If we have just loaded a MelodicImage,
# make sure it is selected. If we loaded
# labels for an existing MelodicImage,
# this will have no effect.
self._displayCtx.disableListener('selectedOverlay', self._name)
self._displayCtx.selectOverlay(overlay)
self._displayCtx.enableListener('selectedOverlay', self._name)
self.__selectedOverlayChanged()
# If the current overlay is a MelodicImage,
# the open file dialog starting point will
# be the melodic directory.
overlay = self._displayCtx.getSelectedOverlay()
selectedIsMelodic = isinstance(overlay, fslmelimage.MelodicImage)
if selectedIsMelodic:
loadDir = overlay.getMelodicDir()
# Otherwise it will be the most
# recent overlay load directory.
else:
loadDir = fslsettings.read('loadOverlayLastDir', os.getcwd())
# Ask the user to select a label file
dlg = wx.FileDialog(
self, self,
message=strings.titles[self, 'loadDialog'], message=strings.titles[self, 'loadDialog'],
defaultDir=overlay.getMelodicDir(), defaultDir=loadDir,
style=wx.FD_OPEN) style=wx.FD_OPEN)
# User cancelled the dialog
if dlg.ShowModal() != wx.ID_OK: if dlg.ShowModal() != wx.ID_OK:
return return
# Load the specified label file
filename = dlg.GetPath() 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: try:
melclass.clear() melDir, allLabels = fslmelresults.loadMelodicLabelFile(filename)
melclass.load(filename)
# Problem loading the file
except Exception as e: except Exception as e:
e = str(e) e = str(e)
msg = strings.messages[self, 'loadError'].format(filename, e) msg = strings.messages[self, 'loadError'].format(filename, e)
title = strings.titles[ self, 'loadError'] title = strings.titles[ self, 'loadError']
...@@ -209,24 +296,123 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): ...@@ -209,24 +296,123 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel):
'({}), ({})'.format(filename, e), exc_info=True) '({}), ({})'.format(filename, e), exc_info=True)
wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK)
# Make sure a colour in the melodic return
# lookup table exists for all labels
for comp, labels in enumerate(melclass.labels): # Ok we've got the labels, now
for label in labels: # we need to figure out which
# overlay to add them to.
label = melclass.getDisplayLabel(label) # If the label file does not refer
lutLabel = lut.getByName(label) # to a Melodic directory, and the
# current overlay is a melodic
if lutLabel is None: # image, apply the labels to the image.
log.debug('New melodic classification ' if selectedIsMelodic and (melDir is None):
'label: {}'.format(label)) applyLabels(filename, overlay, allLabels)
lut.new(label) return
melclass.enableNotification('labels') # If the label file refers to a
lut .enableNotification('labels') # Melodic directory, and the
# current overlay is a melodic
# image.
if selectedIsMelodic and (melDir is not None):
lut .notify('labels') overlayDir = overlay.getMelodicDir()
melclass.notify('labels')
# And both the current overlay and
# the label file refer to the same
# melodic directory, then we apply
# the labels to the curent overlay.
if op.abspath(melDir) == op.abspath(overlayDir):
applyLabels(filename, overlay, allLabels)
return
# Otherwise, if the overlay and the
# label file refer to different
# melodic directories...
# Ask the user whether they want to
dlg = wx.MessageDialog(
self,
strings.messages[self, 'diffMelDir'].format(
melDir, overlayDir),
style=wx.ICON_QUESTION | wx.YES_NO | wx.CANCEL)
dlg.SetYesNoLabels(
strings.messages[self, 'diffMelDir.labels'],
strings.messages[self, 'diffMelDir.overlay'])
response = dlg.ShowModal()
# User cancelled the dialog
if response == wx.ID_CANCEL:
return
# User chose to load the melodic
# image specified in the label
# file. We'll carry on with this
# processing below.
elif response == wx.ID_YES:
pass
# Apply the labels to the current
# overlay, even though they are
# from different analyses.
else:
applyLabels(filename, overlay, allLabels)
return
# If we've reached this far, we are
# going to attempt to identify the
# melodic image associated with the
# label file, load that image, and
# then apply the labels.
# The label file does not
# specify a melodic directory
if melDir is None:
msg = strings.messages[self, 'noMelDir'].format(filename)
title = strings.titles[ self, 'loadError']
wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK)
return
# Try loading the melodic_IC image
# specified in the label file. We'll
# load the mean image as well, as an
# underlay.
try:
overlay = fslmelimage.MelodicImage( melDir)
mean = fslmelresults.getMeanFile(melDir)
mean = fslimage.Image( mean)
log.debug('Adding {} and {} to overlay list'.format(overlay, mean))
self._overlayList.disableListener('overlays', self._name)
self._displayCtx .disableListener('selectedOverlay', self._name)
self._overlayList.extend([mean, overlay])
self._overlayList.enableListener('overlays', self._name)
self._displayCtx .enableListener('selectedOverlay', self._name)
if self._displayCtx.autoDisplay:
for o in [overlay, mean]:
autodisplay.autoDisplay(o,
self._overlayList,
self._displayCtx)
fslsettings.write('loadOverlayLastDir', op.abspath(melDir))
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)
# Apply the loaded labels
# to the loaded overlay.
applyLabels(filename, overlay, allLabels)
def __onSaveButton(self, ev): def __onSaveButton(self, ev):
......
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