From 731437527f9ef0e43c96e4ffdfa231cc792e2bcb Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Tue, 27 Oct 2015 09:00:31 +0000 Subject: [PATCH] Basic (buggy) support for MelodicImage overlays in TimeSeriesPanel. Things are going to change. --- fsl/data/melodicimage.py | 40 ++++++- fsl/data/melodicresults.py | 87 ++++++++++++-- fsl/fsleyes/controls/timeserieslistpanel.py | 25 ++-- fsl/fsleyes/overlay.py | 20 +++- fsl/fsleyes/views/plotpanel.py | 3 +- fsl/fsleyes/views/timeseriespanel.py | 122 +++++++++++--------- 6 files changed, 215 insertions(+), 82 deletions(-) diff --git a/fsl/data/melodicimage.py b/fsl/data/melodicimage.py index d0c4b5f8a..6bdc25480 100644 --- a/fsl/data/melodicimage.py +++ b/fsl/data/melodicimage.py @@ -8,6 +8,7 @@ """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 image as fslimage import melodicresults as melresults @@ -17,7 +18,42 @@ class MelodicImage(fslimage.Image): """ """ - def __init__(self, path): + def __init__(self, image, *args, **kwargs): """ """ - pass + + + if op.isdir(image): + + dirname = image + filename = 'melodic_IC' + + + else: + dirname = op.dirname( image) + filename = op.basename(image) + + dirname = dirname.rstrip(op.sep) + + if not melresults.isMelodicDir(dirname): + raise ValueError('{} does not appear to be a ' + 'MELODIC directory'.format(dirname)) + + if not filename.startswith('melodic_IC'): + raise ValueError('{} does not appear to be a MELODIC ' + 'component file'.format(filename)) + + fslimage.Image.__init__(self, + op.join(dirname, filename), + *args, + **kwargs) + + self.__melmix = melresults.getComponentTimeSeries(dirname) + + + def getComponentTimeSeries(self, component): + return self.__melmix[:, component] + + + def numComponents(self): + return self.shape[3] diff --git a/fsl/data/melodicresults.py b/fsl/data/melodicresults.py index 492a76e62..d05a25ab3 100644 --- a/fsl/data/melodicresults.py +++ b/fsl/data/melodicresults.py @@ -13,21 +13,90 @@ following functions are provided: .. autosummary:: nosignatures: - isMELODICDir - getMELODICDir - + isMelodicDir + getMelodicDir getICFile - getNumComponents getComponentTimeSeries """ -def isMELODICDir(path): +import os +import os.path as op +import numpy as np + +import fsl.data.image as fslimage + + +def isMelodicDir(path): + """ + """ + + # 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 + + +def getMelodicDir(path): + """ + """ + + # TODO This code is identical to featresults.getFEATDir. + # Can you generalise it and put it somewhere in fsl.utils? + + sufs = ['.ica', '.gica'] + idxs = [(path.rfind(s), s) for s in sufs] + idx, suf = max(idxs, key=lambda (i, s): i) + + if idx == -1: + return None + + idx += len(suf) + path = path[:idx] + + if path.endswith(suf) or path.endswith('{}{}'.format(suf, op.sep)): + return path + + return None + + +def getICFile(meldir): + """ + """ + return fslimage.addExt(op.join(meldir, 'melodic_IC')) + + +def getMixFile(meldir): + """ + """ + return op.join(meldir, 'melodic_mix') + + +def getNumComponents(meldir): + """ + """ + + icImg = fslimage.Image(getICFile(meldir), loadData=False) + return icImg.shape[3] + + +def getComponentTimeSeries(meldir): """ """ - # A MELODIC directory: - # - Must be called *.ica - # - Must contain melodic_IC.nii.gz - # - Must contain melodic_mix + mixfile = getMixFile(meldir) + return np.loadtxt(mixfile) diff --git a/fsl/fsleyes/controls/timeserieslistpanel.py b/fsl/fsleyes/controls/timeserieslistpanel.py index 7d94536c8..a4da0a512 100644 --- a/fsl/fsleyes/controls/timeserieslistpanel.py +++ b/fsl/fsleyes/controls/timeserieslistpanel.py @@ -15,12 +15,12 @@ import copy import wx import numpy as np -import props -import pwidgets.elistbox as elistbox -import fsl.fsleyes.panel as fslpanel -import fsl.fsleyes.tooltips as fsltooltips -import fsl.data.strings as strings -import fsl.fsleyes.colourmaps as fslcm +import props +import pwidgets.elistbox as elistbox +import fsl.fsleyes.panel as fslpanel +import fsl.fsleyes.tooltips as fsltooltips +import fsl.data.strings as strings +import fsl.fsleyes.colourmaps as fslcm class TimeSeriesListPanel(fslpanel.FSLEyesPanel): @@ -114,12 +114,17 @@ class TimeSeriesListPanel(fslpanel.FSLEyesPanel): """Creates a label to use for the given :class:`.TimeSeries` instance. """ + import fsl.fsleyes.views.timeseriespanel as tsp + display = self._displayCtx.getDisplay(ts.overlay) - return '{} [{} {} {}]'.format(display.name, - ts.coords[0], - ts.coords[1], - ts.coords[2]) + if isinstance(ts, tsp.MelodicTimeSeries): + return '{} [component {}]'.format(display.name, ts.coords) + else: + return '{} [{} {} {}]'.format(display.name, + ts.coords[0], + ts.coords[1], + ts.coords[2]) def __makeFEATModelTSLabel(self, parentTs, modelTs): diff --git a/fsl/fsleyes/overlay.py b/fsl/fsleyes/overlay.py index c6d6fbf24..2ee9a540e 100644 --- a/fsl/fsleyes/overlay.py +++ b/fsl/fsleyes/overlay.py @@ -191,19 +191,24 @@ def guessDataSourceType(filename): is unrecognised, the first tuple value will be ``None``. """ - import fsl.data.image as fslimage - import fsl.data.model as fslmodel - import fsl.data.featimage as fslfeatimage - import fsl.data.featresults as featresults + import fsl.data.image as fslimage + import fsl.data.model as fslmodel + import fsl.data.featimage as fslfeatimage + import fsl.data.melodicimage as fslmelimage + import fsl.data.melodicresults as melresults + import fsl.data.featresults as featresults + + filename = op.abspath(filename) if filename.endswith('.vtk'): return fslmodel.Model, filename else: - if op.isdir(filename): if featresults.isFEATDir(filename): return fslfeatimage.FEATImage, filename + elif melresults.isMelodicDir(filename): + return fslmelimage.MelodicImage, filename else: filename = fslimage.addExt(filename, False) @@ -212,6 +217,8 @@ def guessDataSourceType(filename): if featresults.isFEATDir(filename): return fslfeatimage.FEATImage, filename + elif melresults.isMelodicDir(filename): + return fslmelimage.MelodicImage, filename else: return fslimage.Image, filename @@ -288,7 +295,8 @@ def loadOverlays(paths, loadFunc='default', errorFunc='default', saveDir=True): e = str(e) msg = strings.messages['overlay.loadOverlays.error'].format(s, e) title = strings.titles[ 'overlay.loadOverlays.error'] - log.debug('Error loading overlay ({}), ({})'.format(s, e)) + log.debug('Error loading overlay ({}), ({})'.format(s, e), + exc_info=True) wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) # If loadFunc or errorFunc are explicitly set to diff --git a/fsl/fsleyes/views/plotpanel.py b/fsl/fsleyes/views/plotpanel.py index d8079e8f0..c69b7c484 100644 --- a/fsl/fsleyes/views/plotpanel.py +++ b/fsl/fsleyes/views/plotpanel.py @@ -488,7 +488,8 @@ class PlotPanel(viewpanel.ViewPanel): if ds.alpha == 0: return (0, 0), (0, 0) - log.debug('Drawing plot for {}'.format(ds.overlay)) + log.debug('Drawing {} for {}'.format(type(ds).__name__, + ds.overlay)) xdata, ydata = ds.getData() diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py index 7a39d1217..79bfcf41d 100644 --- a/fsl/fsleyes/views/timeseriespanel.py +++ b/fsl/fsleyes/views/timeseriespanel.py @@ -23,6 +23,7 @@ details): TimeSeries FEATTimeSeries + MelodicTimeSeries """ @@ -37,6 +38,7 @@ import props import plotpanel import fsl.data.featimage as fslfeatimage +import fsl.data.melodicimage as fslmelimage import fsl.data.image as fslimage import fsl.fsleyes.displaycontext as fsldisplay import fsl.fsleyes.colourmaps as fslcmaps @@ -66,8 +68,9 @@ class TimeSeries(plotpanel.DataSeries): plotpanel.DataSeries.__init__(self, overlay) self.tsPanel = tsPanel - self.coords = map(int, coords) - self.data = overlay.data[coords[0], coords[1], coords[2], :] + self.coords = None + self.data = None + self.update(coords) def __copy__(self): @@ -91,15 +94,15 @@ class TimeSeries(plotpanel.DataSeries): destroyed/recreated whenever the :attr:`.DisplayContext.location` changes. """ - - coords = map(int, coords) - if coords == self.coords: - return False - + self.coords = coords - self.data = self.overlay.data[coords[0], coords[1], coords[2], :] + self.data = self._getData(coords) return True + + def _getData(self, coords): + return self.overlay.data[coords[0], coords[1], coords[2], :] + def getData(self, xdata=None, ydata=None): """Overrides :meth:`.DataSeries.getData` Returns the data associated @@ -648,6 +651,17 @@ class FEATModelFitTimeSeries(TimeSeries): self.data = self.overlay.fit(contrast, xyz, fitType == 'full') + +class MelodicTimeSeries(TimeSeries): + + def __init__(self, tsPanel, overlay, component): + TimeSeries.__init__(self, tsPanel, overlay, component) + + + def _getData(self, component): + return self.overlay.getComponentTimeSeries(component) + + class TimeSeriesPanel(plotpanel.PlotPanel): """A :class:`.PlotPanel` which plots time series data from :class:`.Image` overlays. @@ -707,12 +721,12 @@ class TimeSeriesPanel(plotpanel.PlotPanel): The ``TimeSeriesPanel`` has some extra functionality for - :class:`.FEATImage` overlays. For these overlays, a :class:`FEATTimeSeries` - instance is plotted, instead of a regular :class:`TimeSeries` instance. The - ``FEATTimeSeries`` class, in turn, has the ability to generate more - ``TimeSeries`` instances which represent various aspects of the FEAT model - fit. See the :class:`FEATTimeSeries` and the - :class:`.TimeSeriesControlPanel` classes for more details. + :class:`.FEATImage` overlays. For these overlays, a + :class:`.FEATTimeSeries` instance is plotted, instead of a regular + :class:`.TimeSeries` instance. The ``FEATTimeSeries`` class, in turn, has + the ability to generate more ``TimeSeries`` instances which represent + various aspects of the FEAT model fit. See the :class:`.FEATTimeSeries` + and the :class:`.TimeSeriesControlPanel` classes for more details. """ @@ -844,7 +858,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel): def getCurrent(self): - """Returns the :class:`TimeSeries` instance for the current time + """Returns the :class:`.TimeSeries` instance for the current time course. If :attr:`showCurrent` is ``False``, or the currently selected overlay is not a :class:`.Image` (see :attr:`.DisplayContext.selectedOverlay`) this method will return @@ -881,34 +895,25 @@ class TimeSeriesPanel(plotpanel.PlotPanel): overlays = [o for o in self._overlayList if o is not currOverlay] - # Remove overlays for which the - # current location is out of bounds - locs = map(self.__getTimeSeriesLocation, overlays) - locovl = filter(lambda (l, o): l is not None, - zip(locs, overlays)) - - if len(locovl) > 0: - locs, overlays = zip(*locovl) - - tss = map(self.__genTimeSeries, overlays, locs) + tss = map(self.__genTimeSeries, overlays) - extras.extend([ts for ts in tss if ts is not None]) + extras.extend([ts for ts in tss if ts is not None]) - for ts in tss: - ts.alpha = 1 - ts.lineWidth = 0.5 - - # Use a random colour for each overlay, - # but use the same random colour each time - colour = self.__overlayColours.get( - ts.overlay, - fslcmaps.randomBrightColour()) + for ts in tss: + ts.alpha = 1 + ts.lineWidth = 0.5 + + # Use a random colour for each overlay, + # but use the same random colour each time + colour = self.__overlayColours.get( + ts.overlay, + fslcmaps.randomBrightColour()) - ts.colour = colour - self.__overlayColours[ts.overlay] = colour + ts.colour = colour + self.__overlayColours[ts.overlay] = colour - if isinstance(ts, FEATTimeSeries): - extras.extend(ts.getModelTimeSeries()) + if isinstance(ts, FEATTimeSeries): + extras.extend(ts.getModelTimeSeries()) self.drawDataSeries(extras) else: @@ -918,9 +923,9 @@ class TimeSeriesPanel(plotpanel.PlotPanel): def __currentSettingsChanged(self, *a): """Called when the settings controlling the display of the current time course(s) changes. If the current time course is a - :class:`FEATTimeSeries`, the display settings are propagated to all of + :class:`.FEATTimeSeries`, the display settings are propagated to all of its associated time courses (see the - :meth:`FEATTimeSeries.getModelTimeSeries` method). + :meth:`.FEATTimeSeries.getModelTimeSeries` method). """ if self.__currentTs is None: return @@ -948,7 +953,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel): def __overlaysChanged(self, *a): """Called when the :class:`.OverlayList` changes. Makes sure - that there are no :class:`TimeSeries` instances in the + that there are no :class:`.TimeSeries` instances in the :attr:`.PlotPanel.dataSeries` list which refer to overlays that no longer exist. """ @@ -962,7 +967,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel): def __bindCurrentProps(self, ts, bind=True): - """Binds or unbinds the properties of the given :class:`TimeSeries` + """Binds or unbinds the properties of the given :class:`.TimeSeries` instance with the current display settings (e.g. :attr:`currentColour`, :attr:`currentAlpha`, etc). """ @@ -992,6 +997,9 @@ class TimeSeriesPanel(plotpanel.PlotPanel): not overlay.is4DImage(): return None + if isinstance(overlay, fslmelimage.MelodicImage): + return opts.volume + vox = opts.transformCoords([[x, y, z]], 'display', 'voxel')[0] vox = np.round(vox) @@ -1006,15 +1014,25 @@ class TimeSeriesPanel(plotpanel.PlotPanel): return vox - def __genTimeSeries(self, overlay, vox): - """Creates and returns a :class:`TimeSeries` or :class:`FEATTimeSeries` - instance for the specified voxel of the specified overlay. + def __genTimeSeries(self, overlay): + """Creates and returns a :class:`.TimeSeries` or + :class:`.FEATTimeSeries` instance for the specified voxel of the + specified overlay. """ + loc = self.__getTimeSeriesLocation(overlay) + + if loc is None: + return None + if isinstance(overlay, fslfeatimage.FEATImage): - ts = FEATTimeSeries(self, overlay, vox) + ts = FEATTimeSeries(self, overlay, loc) + + elif isinstance(overlay, fslmelimage.MelodicImage): + ts = MelodicTimeSeries(self, overlay, loc) + else: - ts = TimeSeries(self, overlay, vox) + ts = TimeSeries(self, overlay, loc) ts.colour = self.currentColour ts.alpha = self.currentAlpha @@ -1043,18 +1061,14 @@ class TimeSeriesPanel(plotpanel.PlotPanel): return overlay = self._displayCtx.getSelectedOverlay() - vox = self.__getTimeSeriesLocation(overlay) - - if vox is None: - return if overlay is prevOverlay: self.__currentOverlay = prevOverlay self.__currentTs = prevTs - prevTs.update(vox) + prevTs.update(self.__getTimeSeriesLocation(overlay)) else: - ts = self.__genTimeSeries(overlay, vox) + ts = self.__genTimeSeries(overlay) self.__currentTs = ts self.__currentOverlay = overlay -- GitLab