Commit 06eddfec authored by Paul McCarthy's avatar Paul McCarthy
Browse files

FIX/AROMA label i/o moved to its own module - fixlabels. Removed all

classification-related stuff from MelodicImage. Renamed MelodicClassification
to VolumeLabel - intent is that it will be usable with non-melodic images.
parent af6c1d24
#!/usr/bin/env python
#
# melodiclabels.py - Loading/saving/managing melodic IC labels.
# fixlabels.py - Functions for loading/saving FIX/ICA-AROMA label files.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`MelodicClassification` class, which is
used to manage component classifications of a :class:`.MelodicImage`.
A handful of standalone IO functions are also contained in this module, for
saving/loading component classifications to/from file:
"""This module contains functions for loading/saving FIX/ICA-AROMA label files.
.. autosummary::
:nosignatures:
loadLabelFile
saveLabelFile
isNoisyComponent
InvalidLabelFileError
"""
import logging
import os.path as op
import fsl.utils.notifier as notifier
log = logging.getLogger(__name__)
class MelodicClassification(notifier.Notifier):
"""The ``MelodicClassification`` class is a convenience class for managing
a collection of component classification labels.
.. autosummary::
:nosignatures:
hasLabel
hasComponent
getLabels
getComponents
addLabel
addComponent
removeLabel
removeComponent
clearLabels
clearComponents
The ``MelodicClassification`` class uses the :class:`.Notifier` interface
to notify listeners about changes to the labels. Listeners can be
registered to be notified on the following topics:
- ``added``: A new label was added to a component.
- ``removed``: A label was removed from a component.
When either of these events occur, the value passed to registered
listeners will contain a list of ``(component, label)``) tuples,
which specify the labels that were added/removed.
.. note:: All component labels are internally stored as lower case;
their cased version (whatever is initially used) is accessible
via the :meth:`getDisplayLabel` method.
"""
def __init__(self, melimage):
"""Create a ``MelodicClassification`` instance.
:arg melimage: A :class:`.MelodicImage` instance.
"""
self.__melimage = melimage
self.__ncomps = melimage.numComponents()
self.__displayLabels = {}
# __labels is a list of lists, one list
# for each component, containing the
# labels for that component.
#
# __components is a dictionary of
#
# { label : [component] } mappings
#
# containing the same information, but
# making lookup by label a bit quicker.
#
# These are initialised in clear()
self.__labels = None
self.__components = None
self.clear()
def getDisplayLabel(self, label):
"""Returns the display name for the given label. """
return self.__displayLabels.get(label.lower(), label)
def clear(self):
"""Removes all labels from all components. """
self.__components = {}
self.__labels = [[] for i in range(self.__ncomps)]
def load(self, filename):
"""Loads component labels from the specified file. See the
:func:`loadLabelFile` function.
.. 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.
"""
# Read the labels in
_, allLabels = loadLabelFile(filename)
# More labels in the file than there are in
# melodic_IC - that doesn't make any sense.
if len(allLabels) > self.__ncomps:
raise InvalidLabelFileError(
'The number of components in {} does '
'not match the number of components in '
'{}!'.format(filename, self.__melimage.dataSource))
# Less labels in the file than there are in
# the melodic_IC image - this is ok, as the
# file may have only contained a list of
# noisy components. We'll label the remaining
# components as 'Unknown'.
elif len(allLabels) < self.__ncomps:
for i in range(len(allLabels), self.__ncomps):
allLabels.append(['Unknown'])
# Add the labels to this melclass object
with self.skipAll(topic='added'):
for i, labels in enumerate(allLabels):
for label in labels:
self.addLabel(i, label)
def save(self, filename):
"""Saves the component classifications stored by this
``MeloidicClassification`` to the specified file. See the
:func:`saveMelodicLabelFile` function.
"""
allLabels = []
for c in range(self.__ncomps):
labels = [self.getDisplayLabel(l) for l in self.getLabels(c)]
allLabels.append(labels)
saveLabelFile(self.__melimage.getMelodicDir(),
allLabels,
filename)
def getLabels(self, component):
"""Returns all labels of the specified component. """
return list(self.__labels[component])
def getAllLabels(self):
"""Returns all labels that are currently associated with any
component.
"""
return list(self.__components.keys())
def hasLabel(self, component, label):
"""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, notify=True):
"""Adds the given label to the given component.
:arg notify: If ``True`` (the default), the :meth:`.Notifier.notify`
method will be called, with the ``'added'`` topic.
This parameter is only intended for uses interal to the
``MelodicClassification`` class.
:returns: ``True`` if the label was added, ``False`` if the label was
already present.
"""
display = label
label = label.lower()
labels = list(self.__labels[component])
comps = list(self.__components.get(label, []))
if label in labels:
return False
labels.append(label)
comps .append(component)
self.__displayLabels[label] = display
self.__components[ label] = comps
self.__labels[ component] = labels
log.debug('Label added to component: {} <-> {}'.format(component,
label))
if notify:
self.notify(topic='added', value=[(component, label)])
return True
def removeLabel(self, component, label, notify=True):
"""Removes the given label from the given component.
:returns: ``True`` if the label was removed, ``False`` if the
component did not have this label.
"""
label = label.lower()
labels = list(self.__labels[component])
comps = list(self.__components.get(label, []))
if label not in labels:
return False
labels.remove(label)
comps .remove(component)
self.__components[label] = comps
self.__labels[ component] = labels
log.debug('Label removed from component: {} <-> {}'.format(component,
label))
if notify:
self.notify(topic='removed', value=[(component, label)])
return True
def clearLabels(self, component):
"""Removes all labels from the given component. """
labels = self.getLabels(component)
removed = []
for label in labels:
if self.removeLabel(component, label, notify=False):
removed.append((component, label))
log.debug('Labels cleared from component: {}'.format(component))
if len(removed) > 0:
self.notify(topic='removed', value=removed)
def getComponents(self, label):
"""Returns a list of all components which have the given label. """
label = label.lower()
return list(self.__components.get(label, []))
def hasComponent(self, label, component):
"""Returns ``True`` if the given compoennt has the given label,
``False`` otherwise.
"""
return component in self.getComponents(label)
def addComponent(self, label, component):
"""Adds the given label to the given component. """
self.addLabel(component, label)
def removeComponent(self, label, component, notify=True):
"""Removes the given label from the given component. """
self.removeLabel(component, label, notify)
def clearComponents(self, label):
"""Removes the given label from all components. """
components = self.getComponents(label)
removed = []
for comp in components:
if self.removeComponent(label, comp, notify=False):
removed.append((comp, label))
if len(removed) > 0:
self.notify(topic='removed', value=removed)
def loadLabelFile(filename, includeLabel=None, excludeLabel=None):
"""Loads component labels from the specified file. The file is assuemd
......@@ -435,12 +158,6 @@ def loadLabelFile(filename, includeLabel=None, excludeLabel=None):
melDir = op.join(op.dirname(filename), melDir)
# Parse the labels for every component
# We do not add the labels as we go
# because, 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(lines[1:-1]):
......@@ -495,17 +212,32 @@ def loadLabelFile(filename, includeLabel=None, excludeLabel=None):
return melDir, allLabels
def saveLabelFile(melDir, allLabels, filename):
"""Saves the component classifications stored by this
``MeloidicClassification`` to the specified file. The classifications
are saved in the format described in the :meth:`loadLabelFile` method.
def saveLabelFile(allLabels, filename, dirname=None, listBad=True):
"""Saves the given classification labels to the specified file. The
classifications are saved in the format described in the
:func:`loadLabelFile` method.
:arg allLabels: A list of lists, one list for each component, where
each list contains the labels for the corresponding
component.
:arg filename: Name of the file to which the labels should be saved.
:arg dirname: If provided, is output as the first line of the file.
Intended to be a relative path to the MELODIC analysis
directory with which this label file is associated.
:arg listBad: If ``True`` (the default), the last line of the file
will contain a comma separated list of components which
are deemed 'noisy' (see :func:`isNoisyComponent`).
"""
lines = []
noisyComps = []
# The first line - the melodic directory name
lines.append(op.abspath(melDir))
if dirname is not None:
lines.append(op.abspath(dirname))
# A line for each component
for i, labels in enumerate(allLabels):
......@@ -524,7 +256,8 @@ def saveLabelFile(melDir, allLabels, filename):
noisyComps.append(comp)
# A line listing the bad components
lines.append('[' + ', '.join([str(c) for c in noisyComps]) + ']')
if listBad:
lines.append('[' + ', '.join([str(c) for c in noisyComps]) + ']')
with open(filename, 'wt') as f:
f.write('\n'.join(lines) + '\n')
......@@ -542,8 +275,7 @@ def isNoisyComponent(labels):
class InvalidLabelFileError(Exception):
"""Exception raised by the :meth:`MelodicClassification.load` method and
the :func:`loadMelodicLabelFile` function when an attempt is made to load
an invalid label file.
"""Exception raised by the :func:`loadLabelFile` function when an attempt
is made to load an invalid label file.
"""
pass
......@@ -36,7 +36,6 @@ class MelodicImage(fslimage.Image):
getMelodicDir
getTopLevelAnalysisDir
getDataFile
getICClassification
The :attr:`tr` time of the ``MelodicImage`` may not be known when it is
......@@ -70,7 +69,6 @@ class MelodicImage(fslimage.Image):
self.__meldir = meldir
self.__melmix = melanalysis.getComponentTimeSeries( meldir)
self.__melFTmix = melanalysis.getComponentPowerSpectra(meldir)
self.__melICClass = mellabels .MelodicClassification( self)
# Automatically set the
# TR value if possible
......@@ -83,10 +81,6 @@ class MelodicImage(fslimage.Image):
if dataImage.is4DImage():
self.__tr = dataImage.pixdim[3]
# TODO load classifications if present
for i in range(self.numComponents()):
self.__melICClass.addLabel(i, 'Unknown')
@property
def tr(self):
......@@ -163,10 +157,3 @@ class MelodicImage(fslimage.Image):
function.
"""
return melanalysis.getMeanFile(self.__meldir)
def getICClassification(self):
"""Return the :class:`.MelodicClassification` instance associated with
this ``MelodicImage``.
"""
return self.__melICClass
#!/usr/bin/env python
#
# volumelabels.py - The VolumeLabels class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`VolumeLabels` class, which is used to
manage labels associated with the volumes of an :class:`.Image`. This class
is primarily used for managing component classifications of a
:class:`.MelodicImage`.
"""
import logging
import fsl.utils.notifier as notifier
import fsl.data.fixlabels as fixlabels
log = logging.getLogger(__name__)
class VolumeLabels(notifier.Notifier):
"""The ``VolumeLabels`` class is a convenience class for managing a
collection of classification labels fgor the volumes of an :class:`.Image`.
The ``VolumeLabels`` class refers to *volumes* as *components*, because
it was originally written to manage classification labels associated wirh
the components of a Melodic analysis.
The following methods are available on a ``VolumeLabels`` instance:
.. autosummary::
:nosignatures:
clear
load
save
hasLabel
hasComponent
getLabels
getAllLabels
getDisplayLabel
getComponents
addLabel
addComponent
removeLabel
removeComponent
clearLabels
clearComponents
The ``VolumeLabels`` class uses the :class:`.Notifier` interface
to notify listeners about changes to the labels. Listeners can be
registered to be notified on the following topics:
- ``added``: A new label was added to a component.
- ``removed``: A label was removed from a component.
When either of these events occur, the value passed to registered
listeners will contain a list of ``(component, label)``) tuples,
which specify the labels that were added/removed.
.. note:: All component labels are internally stored as lower case;
however, their cased version (whatever is initially used) is
accessible via the :meth:`getDisplayLabel` method.
"""
def __init__(self, nvolumes):
"""Create a ``VolumeLabels`` instance.
:arg nvolumes: Number of volumes to be classified. This cannot be
changed.
"""
self.__ncomps = nvolumes
self.__displayLabels = {}
# __labels is a list of lists, one list
# for each component, containing the
# labels for that component.
#
# __components is a dictionary of
#
# { label : [component] } mappings
#
# containing the same information, but
# making lookup by label a bit quicker.
#
# These are initialised in clear()
self.__labels = None
self.__components = None
self.clear()
def getDisplayLabel(self, label):
"""Returns the display name for the given label. """
return self.__displayLabels.get(label.lower(), label)
def clear(self):
"""Removes all labels from all components. """
self.__components = {}
self.__labels = [[] for i in range(self.__ncomps)]
def load(self, filename):
"""Loads component labels from the specified file. See the
:func:`.fixlabels.loadLabelFile` function.
.. note:: This method adds to, but does not replace, any existing
component classifications stored by this
``VolumeLabels`` instance. Call the :meth:`clear` method,
before calling ``load``, if you want to discard any existing
classifications.
"""
# Read the labels in
_, allLabels = fixlabels.loadLabelFile(filename)
# More labels in the file than there are in
# this labels instance - that doesn't make
# any sense.
if len(allLabels) > self.__ncomps:
raise fixlabels.InvalidLabelFileError(
'Wrong number of components in {}!'.format(filename))
# Less labels in the file than there are in
# this labels instance - this is ok, as the
# file may have only contained a list of
# noisy components. We'll label the remaining
# components as 'Unknown'.
elif len(allLabels) < self.__ncomps:
for i in range(len(allLabels), self.__ncomps):
allLabels.append(['Unknown'])
# Add the labels to this object
with self.skipAll(topic='added'):
for i, labels in enumerate(allLabels):
for label in labels:
self.addLabel(i, label)
def save(self, filename, dirname=None):
"""Saves the component classifications stored by this ``VolumeLabels``
instance to the specified file. See the
:func:`.fixlabels.saveLabelFile` function.
"""
allLabels = []
for c in range(self.__ncomps):
labels = [self.getDisplayLabel(l) for l in self.getLabels(c)]
allLabels.append(labels)
fixlabels.saveLabelFile(allLabels, filename, dirname=dirname)
def getLabels(self, component):
"""Returns all labels of the specified component. """
return list(self.__labels[component])
def getAllLabels(self):
"""Returns all labels that are currently associated with any
component.
"""
return list(self.__components.keys())
def hasLabel(self, component, label):
"""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, notify=True):
"""Adds the given label to the given component.
:arg notify: If ``True`` (the default), the :meth:`.Notifier.notify`
method will be called, with the ``'added'`` topic.
This parameter is only intended for uses interal to the
``VolumeLabels`` class.
:returns: ``True`` if the label was added, ``False`` if the label was
already present.
"""
display = label
label = label.lower()
labels = list(self.__labels[component])
comps = list(self.__components.get(label, []))
if label in labels:
return False
labels.append(label)
comps .append(component)
self.__displayLabels[label] = display
self.__components[ label] = comps
self.__labels[ component] = labels
log.debug('Label added to component: {} <-> {}'.format(component,
label))
if notify:
self.notify(topic='added', value=[(component, label)])
return True
def removeLabel(self, component, label, notify=True):
"""Removes the given label from the given component.
:returns: ``True`` if the label was removed, ``False`` if the
component did not have this label.
"""
label = label.lower()