From eb1b217b94578e061bb47ba0a5517aff49019e8a Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Sun, 6 Dec 2015 15:10:50 +0000 Subject: [PATCH] The melodic classification panel 'load labels' button can be used to load a melodic overlay. --- fsl/data/melodicresults.py | 33 ++ fsl/data/strings.py | 39 ++- .../controls/melodicclassificationpanel.py | 290 ++++++++++++++---- 3 files changed, 302 insertions(+), 60 deletions(-) diff --git a/fsl/data/melodicresults.py b/fsl/data/melodicresults.py index 6de7dc48f..ab159fbb6 100644 --- a/fsl/data/melodicresults.py +++ b/fsl/data/melodicresults.py @@ -113,6 +113,11 @@ def getDataFile(meldir): 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): """Returns the path to the melodic IC image. """ return fslimage.addExt(op.join(meldir, 'melodic_IC')) @@ -406,6 +411,7 @@ def loadMelodicLabelFile(filename): 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 @@ -417,6 +423,7 @@ def loadMelodicLabelFile(filename): 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: @@ -427,10 +434,29 @@ def loadMelodicLabelFile(filename): - ``'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. + + :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: lines = f.readlines() @@ -462,6 +488,13 @@ def loadMelodicLabelFile(filename): melDir = lines[0] 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 # We dot not add the labels as we go # as, if something is wrong with the diff --git a/fsl/data/strings.py b/fsl/data/strings.py index ca43e65ff..1691e050f 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -141,12 +141,35 @@ messages = TypeDict({ '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 ' - 'the file {}.\n\nDetails: {}', - 'MelodicClassificationPanel.saveError' : 'An error occurred while saving ' - 'the file {}.\n\nDetails: {}', + 'An error occurred while ' + 'saving the file {}.' + '\n\nDetails: {}', }) @@ -387,9 +410,9 @@ labels = TypeDict({ 'MelodicClassificationPanel.componentTab' : 'Components', 'MelodicClassificationPanel.labelTab' : 'Labels', - 'MelodicClassificationPanel.loadButton' : 'Load from file', - 'MelodicClassificationPanel.saveButton' : 'Save to file', - 'MelodicClassificationPanel.clearButton' : 'Clear all labels', + 'MelodicClassificationPanel.loadButton' : 'Load labels', + 'MelodicClassificationPanel.saveButton' : 'Save labels', + 'MelodicClassificationPanel.clearButton' : 'Clear labels', 'ComponentGrid.componentColumn' : 'IC #', 'ComponentGrid.labelColumn' : 'Labels', diff --git a/fsl/fsleyes/controls/melodicclassificationpanel.py b/fsl/fsleyes/controls/melodicclassificationpanel.py index 380d75769..af2b2a90e 100644 --- a/fsl/fsleyes/controls/melodicclassificationpanel.py +++ b/fsl/fsleyes/controls/melodicclassificationpanel.py @@ -10,16 +10,22 @@ of a :class:`.MelodicImage`. """ +import os +import os.path as op import logging import wx import pwidgets.notebook as notebook +import fsl.utils.settings as fslsettings 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.fsleyes.colourmaps as fslcm import fsl.fsleyes.panel as fslpanel +import fsl.fsleyes.autodisplay as autodisplay import melodicclassificationgrid as melodicgrid @@ -30,25 +36,7 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): """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): """Create a ``MelodicClassificationPanel``. @@ -109,10 +97,10 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): 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 .Add(self.__disabledText, 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) @@ -152,6 +140,9 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): self.__sizer.Show(self.__disabledText, not enable) self.__sizer.Show(self.__mainSizer, enable) + self.__saveButton .Enable(enable) + self.__clearButton.Enable(enable) + self.Layout() @@ -173,35 +164,131 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): 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 - overlay = self._displayCtx.getSelectedOverlay() - melclass = overlay.getICClassification() - dlg = wx.FileDialog( + # The aim of the code beneath this function + # is to load a set of component labels, and + # to figure out which overlay they should + # 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, message=strings.titles[self, 'loadDialog'], - defaultDir=overlay.getMelodicDir(), + defaultDir=loadDir, style=wx.FD_OPEN) + # User cancelled the dialog if dlg.ShowModal() != wx.ID_OK: return + # Load the specified label file 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) - + melDir, allLabels = fslmelresults.loadMelodicLabelFile(filename) + + # Problem loading the file except Exception as e: + e = str(e) msg = strings.messages[self, 'loadError'].format(filename, e) title = strings.titles[ self, 'loadError'] @@ -209,24 +296,123 @@ class MelodicClassificationPanel(fslpanel.FSLEyesPanel): '({}), ({})'.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: + return + + # Ok we've got the labels, now + # we need to figure out which + # overlay to add them to. - label = melclass.getDisplayLabel(label) - lutLabel = lut.getByName(label) - - if lutLabel is None: - log.debug('New melodic classification ' - 'label: {}'.format(label)) - lut.new(label) + # If the label file does not refer + # to a Melodic directory, and the + # current overlay is a melodic + # image, apply the labels to the image. + if selectedIsMelodic and (melDir is None): + applyLabels(filename, overlay, allLabels) + return - melclass.enableNotification('labels') - lut .enableNotification('labels') + # If the label file refers to a + # Melodic directory, and the + # current overlay is a melodic + # image. + if selectedIsMelodic and (melDir is not None): - lut .notify('labels') - melclass.notify('labels') + overlayDir = overlay.getMelodicDir() + + # 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): -- GitLab