From 0918a2695ed16d1ffe57c6ff5698a13209fffc9a Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Wed, 28 Oct 2015 15:35:06 +0000 Subject: [PATCH] Documentation for melodicimage and melodicresults modules. MelodicImage now automatically sets a TR property, if it can find the data file. TimeSeries scales melodic time series by this TR accordingly. --- fsl/data/melodicimage.py | 63 ++++++++++++++-- fsl/data/melodicresults.py | 116 +++++++++++++++++++++-------- fsl/fsleyes/plotting/timeseries.py | 10 ++- 3 files changed, 149 insertions(+), 40 deletions(-) diff --git a/fsl/data/melodicimage.py b/fsl/data/melodicimage.py index 6bdc25480..1456386c9 100644 --- a/fsl/data/melodicimage.py +++ b/fsl/data/melodicimage.py @@ -8,30 +8,57 @@ """This module provides the :class:`MelodicImage` class, an :class:`.Image` sub-class which encapsulates data from a MELODIC analysis. """ + + import os.path as op +import props + import image as fslimage import melodicresults as melresults class MelodicImage(fslimage.Image): + """The ``MelodicImage`` class is an :class:`.Image` which encapsulates + the results of a FSL MELODIC analysis. A ``MelodicImage`` corresponds to + the spatial component map file, generally called ``melodic_IC.nii.gz``. + + The ``MelodicImage`` class provides a few MELODIC-specific attributes and + methods: + + .. autosummary:: + + tr + getComponentTimeSeries + numComponents + getDataFile """ + + + tr = props.Real(default=1.0) + """The TR time of the raw data from which this ``MelodicImage`` was + generated. If it is possible to do so, this is automatically initialised + from the data file (see the :meth:`getDataFile` method). """ + - def __init__(self, image, *args, **kwargs): - """ - """ + def __init__(self, path, *args, **kwargs): + """Create a ``MelodicImage``. + :arg path: A path specifying the ``melodic_IC`` image file, or the + ``.ica`` directory. - if op.isdir(image): + All other arguments are passed through to the :meth:`.Image.__init__` + method. + """ - dirname = image + if op.isdir(path): + dirname = path filename = 'melodic_IC' - else: - dirname = op.dirname( image) - filename = op.basename(image) + dirname = op.dirname( path) + filename = op.basename(path) dirname = dirname.rstrip(op.sep) @@ -48,12 +75,32 @@ class MelodicImage(fslimage.Image): *args, **kwargs) + self.__meldir = dirname self.__melmix = melresults.getComponentTimeSeries(dirname) + # Automatically set the + # TR value if possible + dataFile = self.getDataFile() + + if dataFile is not None: + dataImage = fslimage.Image(dataFile, loadData=False) + if dataImage.is4DImage(): + self.tr = dataImage.pixdim[3] + def getComponentTimeSeries(self, component): + """Returns the time course for the specified (0-indexed) component. """ return self.__melmix[:, component] def numComponents(self): + """Returns the number of components in this ``MelodicImage``. """ return self.shape[3] + + + def getDataFile(self): + """Returns the file name of the data image from which this + ``MelodicImage`` was generated, if possible. See the + :func:`.melodicresults.getDataFile` function. + """ + return melresults.getDataFile(self.__meldir) diff --git a/fsl/data/melodicresults.py b/fsl/data/melodicresults.py index d05a25ab3..f020003fb 100644 --- a/fsl/data/melodicresults.py +++ b/fsl/data/melodicresults.py @@ -15,49 +15,46 @@ following functions are provided: isMelodicDir getMelodicDir + getTopLevelAnalysisDir + getDataFile getICFile + getMixFile getNumComponents getComponentTimeSeries """ -import os import os.path as op import numpy as np -import fsl.data.image as fslimage +import fsl.data.image as fslimage +import fsl.data.featresults as featresults def isMelodicDir(path): - """ + """Returns ``True`` if the given path looks like it is contained within + a MELODIC directory, ``False`` otherwise. """ # Must be named *.ica or *.gica - meldir = getMelodicDir(path) - - if meldir is None: - return False - - # Must contain an image file called melodic_IC - try: - fslimage.addExt(op.join(meldir, 'melodic_IC'), mustExist=True) - except ValueError: - return False - - # Must contain a file called melodic_mix - if not op.exists(op.join(meldir, 'melodic_mix')): - return False - - return True + return getMelodicDir(path) is not None def getMelodicDir(path): - """ + """Returns the MELODIC directory in which the given path is contained, + or ``None`` if it is not contained within a MELODIC directory. A melodic + directory: + + - Must be named ``*.ica`` or ``*.gica`` + - Must contain a file called ``melodic_IC.nii.gz`` + - Must contain a file called ``melodic_mix``. """ # TODO This code is identical to featresults.getFEATDir. # Can you generalise it and put it somewhere in fsl.utils? + path = op.abspath(path) + sufs = ['.ica', '.gica'] idxs = [(path.rfind(s), s) for s in sufs] idx, suf = max(idxs, key=lambda (i, s): i) @@ -66,28 +63,88 @@ def getMelodicDir(path): return None idx += len(suf) - path = path[:idx] + path = path[:idx].rstrip(op.sep) - if path.endswith(suf) or path.endswith('{}{}'.format(suf, op.sep)): - return path + if not path.endswith(suf): + return None + + # Must contain an image file called melodic_IC + try: + fslimage.addExt(op.join(path, 'melodic_IC'), mustExist=True) + except ValueError: + return None + + # Must contain a file called melodic_mix + if not op.exists(op.join(path, 'melodic_mix')): + return None - return None + return path -def getICFile(meldir): +def getTopLevelAnalysisDir(path): + """If the given path is a MELODIC directory, and it is contained within + a FEAT directory, or another MELODIC directory, the path to the latter + directory is returned. Otherwise, ``None`` is returned. """ + + meldir = getMelodicDir(path) + sufs = ['.feat', '.gfeat', '.ica', '.gica'] + + if meldir is None: + return None + + if featresults.isFEATDir(meldir): + return featresults.getFEATDir(meldir) + + parentDir = op.dirname(meldir) + parentDir = parentDir.rstrip(op.sep) + + if not any([parentDir.endswith(s) for s in sufs]): + return None + + # Must contain a file called filtered_func_data.nii.gz + dataFile = op.join(parentDir, 'filtered_func_data') + + try: + dataFile = fslimage.addExt(dataFile, mustExist=True) + except ValueError: + return None + + return parentDir + + +def getDataFile(meldir): + """If the given melodic directory is contained within another analysis + directory, the path to the data file is returned. Otherwise ``None`` is + returned. """ + + topDir = getTopLevelAnalysisDir(meldir) + + if topDir is None: + return None + + dataFile = op.join(topDir, 'filtered_func_data') + + try: + return fslimage.addExt(dataFile, mustExist=True) + except ValueError: + return None + + +def getICFile(meldir): + """Returns the path to the melodic IC image. """ return fslimage.addExt(op.join(meldir, 'melodic_IC')) def getMixFile(meldir): - """ - """ + """Returns the path to the melodic mix file. """ return op.join(meldir, 'melodic_mix') def getNumComponents(meldir): - """ + """Returns the number of components generated in the melodic analysis + contained in the given directrory. """ icImg = fslimage.Image(getICFile(meldir), loadData=False) @@ -95,7 +152,8 @@ def getNumComponents(meldir): def getComponentTimeSeries(meldir): - """ + """Returns a ``numpy`` array containing the melodic mix for the given + directory. """ mixfile = getMixFile(meldir) diff --git a/fsl/fsleyes/plotting/timeseries.py b/fsl/fsleyes/plotting/timeseries.py index 8d484aa16..094d450c8 100644 --- a/fsl/fsleyes/plotting/timeseries.py +++ b/fsl/fsleyes/plotting/timeseries.py @@ -26,8 +26,9 @@ import numpy as np import props -import dataseries -import fsl.data.strings as strings +import dataseries +import fsl.data.strings as strings +import fsl.data.melodicimage as fslmelimage class TimeSeries(dataseries.DataSeries): @@ -90,7 +91,10 @@ class TimeSeries(dataseries.DataSeries): ydata = np.array(ydata, dtype=np.float32) if self.tsPanel.usePixdim: - xdata *= self.overlay.pixdim[3] + if isinstance(self.overlay, fslmelimage.MelodicImage): + xdata *= self.overlay.tr + else: + xdata *= self.overlay.pixdim[3] if self.tsPanel.plotMode == 'demean': ydata = ydata - ydata.mean() -- GitLab