diff --git a/fsl/fsleyes/controls/timeserieslistpanel.py b/fsl/fsleyes/controls/timeserieslistpanel.py index 86f406b419d320a57fe7a77d97492b1e7f9520ac..c3d500fb48be3bc98aa70c8d1b0947d43b7ea45d 100644 --- a/fsl/fsleyes/controls/timeserieslistpanel.py +++ b/fsl/fsleyes/controls/timeserieslistpanel.py @@ -13,12 +13,12 @@ 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.fsleyes.plotting as plotting -import fsl.data.strings as strings +import props +import pwidgets.elistbox as elistbox +import fsl.fsleyes.panel as fslpanel +import fsl.fsleyes.tooltips as fsltooltips +import fsl.fsleyes.plotting.timeseries as timeseries +import fsl.data.strings as strings class TimeSeriesListPanel(fslpanel.FSLEyesPanel): @@ -161,7 +161,7 @@ class TimeSeriesListPanel(fslpanel.FSLEyesPanel): if ts is None: return - if isinstance(ts, plotting.FEATTimeSeries): + if isinstance(ts, timeseries.FEATTimeSeries): toAdd = list(ts.getModelTimeSeries()) else: toAdd = [ts] @@ -170,7 +170,9 @@ class TimeSeriesListPanel(fslpanel.FSLEyesPanel): for ts in toAdd: - copy = plotting.DataSeries(ts.overlay) + copy = timeseries.TimeSeries(self.__tsPanel, + overlay, + self._displayCtx) copy.alpha = ts.alpha copy.lineWidth = ts.lineWidth @@ -182,7 +184,7 @@ class TimeSeriesListPanel(fslpanel.FSLEyesPanel): # This is hacky, and is here in order to # make the __onLIstSelect method work. - if isinstance(ts, plotting.MelodicTimeSeries): + if isinstance(ts, timeseries.MelodicTimeSeries): copy.tsLoc = 'volume' copy.coord = ts.getComponent() else: diff --git a/fsl/fsleyes/plotting/__init__.py b/fsl/fsleyes/plotting/__init__.py index 202ec69c2b4bc5ce95c0253311cf5535e021cc00..b991030c900898e40c24d874fd37db05ad17de0a 100644 --- a/fsl/fsleyes/plotting/__init__.py +++ b/fsl/fsleyes/plotting/__init__.py @@ -9,7 +9,8 @@ import dataseries import timeseries DataSeries = dataseries.DataSeries -TimeSeries = timeseries.TimeSeries +TimeSeries = timeseries.TimeSeries +VoxelTimeSeries = timeseries.VoxelTimeSeries FEATTimeSeries = timeseries.FEATTimeSeries FEATPartialFitTimeSeries = timeseries.FEATPartialFitTimeSeries FEATEVTimeSeries = timeseries.FEATEVTimeSeries diff --git a/fsl/fsleyes/plotting/timeseries.py b/fsl/fsleyes/plotting/timeseries.py index c60cfdb9911bd6cd744309971937c5ede849ecb6..8d484aa16aed370f6a7cdb12149774f16aeff847 100644 --- a/fsl/fsleyes/plotting/timeseries.py +++ b/fsl/fsleyes/plotting/timeseries.py @@ -11,6 +11,7 @@ are use by the :class:`.TimeSeriesPanel`. The following classes are provided: :nosignatures: TimeSeries + VoxelTimeSeries FEATTimeSeries FEATModelTimeSeries FEATPartialFitTimeSeries @@ -20,7 +21,6 @@ are use by the :class:`.TimeSeriesPanel`. The following classes are provided: MelodicTimeSeries """ -import logging import numpy as np @@ -30,26 +30,19 @@ import dataseries import fsl.data.strings as strings -log = logging.getLogger(__name__) - - class TimeSeries(dataseries.DataSeries): - """Encapsulates time series data from a specific voxel in an - :class:`.Image` overlay. The voxel data may be accessed through - the :meth:`getData` method, where the voxel is defined by the - :attr:`.DisplayContext.location` property (transformed into the - image voxel coordinate system). - - The ``TimeSeries`` class is the base-class for all other classes - in this module. - - A ``TimeSeries`` instance provides the following methods: + """Encapsulates time series data from an overlay. :class:`.Image` + overlay. The ``TimeSeries`` class is the base-class for all other classes + in this module - its :meth:`getData` method implements some pre-processing + routines which are required by the :class:`.TimeSeriesPanel`. + The following methods are intended to be overridden and/or called by + sub-class implementations: + .. autosummary:: :nosignatures: makeLabel - getVoxel getData """ @@ -69,9 +62,78 @@ class TimeSeries(dataseries.DataSeries): self.tsPanel = tsPanel self.displayCtx = displayCtx + + def makeLabel(self): + """Return a label for this ``TimeSeries``. """ + display = self.displayCtx.getDisplay(self.overlay) + return display.name + + + def getData(self, xdata=None, ydata=None): + """Overrides :meth:`.DataSeries.getData`. Returns the data associated + with this ``TimeSeries`` instance, pre-processed according to the + current :class:`.TimeSeriesPanel` settings. + + The ``xdata`` and ``ydata`` arguments may be used by sub-classes to + override the x/y data in the event that they have already performed + some processing on the data. The default implementation returns + whatever has been set through :meth:`.DataSeries.setData`. + """ + + dsXData, dsYData = dataseries.DataSeries.getData(self) + + if xdata is None: xdata = dsXData + if ydata is None: ydata = dsYData + if xdata is None or len(xdata) == 0: xdata = np.arange(len(ydata)) + + xdata = np.array(xdata, dtype=np.float32) + ydata = np.array(ydata, dtype=np.float32) + + if self.tsPanel.usePixdim: + xdata *= self.overlay.pixdim[3] + + if self.tsPanel.plotMode == 'demean': + ydata = ydata - ydata.mean() + + elif self.tsPanel.plotMode == 'normalise': + ymin = ydata.min() + ymax = ydata.max() + ydata = 2 * (ydata - ymin) / (ymax - ymin) - 1 + + elif self.tsPanel.plotMode == 'percentChange': + mean = ydata.mean() + ydata = 100 * (ydata / mean) - 100 + + return xdata, ydata + + +class VoxelTimeSeries(TimeSeries): + """A :class:`TimeSeries` sub-class which encapsulates data from a + specific voxel of a :class:`.Image` overlay. + The voxel data may be accessed through the :meth:`getData` method, where + the voxel is defined by current value of the + :attr:`.DisplayContext.location` property (transformed into the image + voxel coordinate system). + """ + + def __init__(self, tsPanel, overlay, displayCtx): + """Create a ``VoxelTimeSeries`` instance. + + :arg tsPanel: The :class:`TimeSeriesPanel` which owns this + ``VoxelTimeSeries``. + + :arg overlay: The :class:`.Image` instance to extract the data from. + + :arg displayCtx: The :class:`.DisplayContext`. + """ + TimeSeries.__init__(self, tsPanel, overlay, displayCtx) + + def makeLabel(self): - """Returns a string representation of this ``TimeSeries`` instance. """ + """Returns a string representation of this ``VoxelTimeSeries`` + instance. + """ display = self.displayCtx.getDisplay(self.overlay) coords = self.getVoxel() @@ -88,7 +150,7 @@ class TimeSeries(dataseries.DataSeries): def getVoxel(self): """Calculates and returns the voxel coordinates corresponding to the current :attr:`.DisplayContext.location` for the overlay associated - with this ``TimeSeries``. + with this ``VoxelTimeSeries``. Returns ``None`` if the current location is outside of the image bounds. @@ -111,14 +173,11 @@ class TimeSeries(dataseries.DataSeries): return vox - - def getData(self, xdata=None, ydata=None): - """Overrides :meth:`.DataSeries.getData`. Returns the data associated - with this ``TimeSeries`` instance. - The ``xdata`` and ``ydata`` arguments may be used by subclasses to - override the x/y data in the event that they have already performed - some processing on the data. + def getData(self, xdata=None, ydata=None): + """Returns the data at the current voxel location. The ``xdata`` and + ``ydata`` parameters may be used by sub-classes to override this + default behaviour. """ if ydata is None: @@ -128,32 +187,18 @@ class TimeSeries(dataseries.DataSeries): return [], [] x, y, z = xyz - ydata = np.array(self.overlay.data[x, y, z, :], dtype=np.float32) - if xdata is None: - xdata = np.arange(len(ydata), dtype=np.float32) + ydata = self.overlay.data[x, y, z, :] - if self.tsPanel.usePixdim: - xdata *= self.overlay.pixdim[3] + if xdata is None: + xdata = np.arange(len(ydata)) - if self.tsPanel.plotMode == 'demean': - ydata = ydata - ydata.mean() - - elif self.tsPanel.plotMode == 'normalise': - ymin = ydata.min() - ymax = ydata.max() - ydata = 2 * (ydata - ymin) / (ymax - ymin) - 1 - - elif self.tsPanel.plotMode == 'percentChange': - mean = ydata.mean() - ydata = 100 * (ydata / mean) - 100 - - return xdata, ydata + return TimeSeries.getData(self, xdata=xdata, ydata=ydata) -class FEATTimeSeries(TimeSeries): - """A :class:`TimeSeries` class for use with :class:`FEATImage` instances, - containing some extra FEAT specific options. +class FEATTimeSeries(VoxelTimeSeries): + """A :class:`VoxelTimeSeries` class for use with :class:`FEATImage` + instances, containing some extra FEAT specific options. The ``FEATTimeSeries`` class acts as a container for several @@ -237,11 +282,11 @@ class FEATTimeSeries(TimeSeries): def __init__(self, *args, **kwargs): """Create a ``FEATTimeSeries``. - All arguments are passed through to the :class:`TimeSeries` + All arguments are passed through to the :class:`VoxelTimeSeries` constructor. """ - TimeSeries.__init__(self, *args, **kwargs) + VoxelTimeSeries.__init__(self, *args, **kwargs) self.name = '{}_{}'.format(type(self).__name__, id(self)) numEVs = self.overlay.numEVs() @@ -489,10 +534,10 @@ class FEATTimeSeries(TimeSeries): FEATModelFitTimeSeries, self.__getContrast('full', -1), 'full', -1) -class FEATPartialFitTimeSeries(TimeSeries): - """A :class:`TimeSeries` class which represents the partial model fit - of an EV or contrast from a FEAT analysis. Instances of this class - are created by the :class:`FEATTimeSeries` class. +class FEATPartialFitTimeSeries(VoxelTimeSeries): + """A :class:`VoxelTimeSeries` class which represents the partial model + fit of an EV or contrast from a FEAT analysis at a specific voxel. + Instances of this class are created by the :class:`FEATTimeSeries` class. """ def __init__(self, tsPanel, @@ -523,7 +568,7 @@ class FEATPartialFitTimeSeries(TimeSeries): :arg idx: If the model fit type is ``'pe'`` or ``'cope'``, the EV/contrast index. """ - TimeSeries.__init__(self, tsPanel, overlay, displayCtx) + VoxelTimeSeries.__init__(self, tsPanel, overlay, displayCtx) self.parentTs = parentTs self.contrast = contrast @@ -543,7 +588,7 @@ class FEATPartialFitTimeSeries(TimeSeries): return [], [] data = self.overlay.partialFit(self.contrast, coords, False) - return TimeSeries.getData(self, ydata=data) + return VoxelTimeSeries.getData(self, ydata=data) class FEATEVTimeSeries(TimeSeries): @@ -577,23 +622,25 @@ class FEATEVTimeSeries(TimeSeries): """Returns a string representation of this ``FEATEVTimeSeries`` instance. """ + + display = self.displayCtx.getDisplay(self.overlay) - return '{} (EV{} - {})'.format( - self.parentTs.makeLabel(), + return '{} EV{} ({})'.format( + display.name, self.idx + 1, self.overlay.evNames()[self.idx]) def getData(self): """Returns the time course of the EV specified in the constructor. """ - data = np.array(self.overlay.getDesign()[:, self.idx]) + data = self.overlay.getDesign()[:, self.idx] return TimeSeries.getData(self, ydata=data) -class FEATResidualTimeSeries(TimeSeries): - """A :class:`TimeSeries` class which represents the time course of the - residuals from a FEAT analysis. Instances of this class are created by - the :class:`FEATTimeSeries` class. +class FEATResidualTimeSeries(VoxelTimeSeries): + """A :class:`VoxelTimeSeries` class which represents the time course of + the residuals from a FEAT analysis at a specific voxel. Instances of this + class are created by the :class:`FEATTimeSeries` class. """ def __init__(self, tsPanel, overlay, displayCtx, parentTs): @@ -609,7 +656,7 @@ class FEATResidualTimeSeries(TimeSeries): :arg parentTs: The :class:`.FEATTimeSeries` instance that has created this ``FEATResidualTimeSeries``. """ - TimeSeries.__init__(self, tsPanel, overlay, displayCtx) + VoxelTimeSeries.__init__(self, tsPanel, overlay, displayCtx) self.parentTs = parentTs @@ -629,15 +676,15 @@ class FEATResidualTimeSeries(TimeSeries): return [], [] x, y, z = voxel - data = np.array(self.overlay.getResiduals().data[x, y, z, :]) + data = self.overlay.getResiduals().data[x, y, z, :] - return TimeSeries.getData(self, ydata=data) + return VoxelTimeSeries.getData(self, ydata=data) -class FEATModelFitTimeSeries(TimeSeries): +class FEATModelFitTimeSeries(VoxelTimeSeries): """A :class:`TimeSeries` class which represents the time course for - a model fit from a FEAT analysis. Instances of this class are created by - the :class:`FEATTimeSeries` class. + a model fit from a FEAT analysis at a specific voxel. Instances of this + class are created by the :class:`FEATTimeSeries` class. """ def __init__(self, @@ -673,7 +720,7 @@ class FEATModelFitTimeSeries(TimeSeries): if fitType not in ('full', 'cope', 'pe'): raise ValueError('Unknown model fit type {}'.format(fitType)) - TimeSeries.__init__(self, tsPanel, overlay, displayCtx) + VoxelTimeSeries.__init__(self, tsPanel, overlay, displayCtx) self.parentTs = parentTs self.fitType = fitType self.idx = idx @@ -712,16 +759,13 @@ class FEATModelFitTimeSeries(TimeSeries): data = self.overlay.fit(contrast, voxel, fitType == 'full') - return TimeSeries.getData(self, ydata=data) - + return VoxelTimeSeries.getData(self, ydata=data) class MelodicTimeSeries(TimeSeries): """A :class:`.TimeSeries` class which encapsulates the time course for - one component of a :class:`.MelodicImage`. Where the :class:`.TimeSeries` - class returns the time course of the voxel at the current - :class:`.DisplayContext.location`, the :class:`.MelodicTimeSeries` returns - the time course of the component specified by the current + one component of a :class:`.MelodicImage`. The :meth:`getData` method + returns the time course of the component specified by the current :class:`.ImageOpts.volume`. """ @@ -747,6 +791,7 @@ class MelodicTimeSeries(TimeSeries): def makeLabel(self): """Returns a string representation of this ``MelodicTimeSeries``. """ + display = self.displayCtx.getDisplay(self.overlay) return '{} [component {}]'.format(display.name, self.getComponent()) @@ -755,5 +800,5 @@ class MelodicTimeSeries(TimeSeries): """Returns the time course of the current Melodic component. """ component = self.getComponent() - ydata = np.array(self.overlay.getComponentTimeSeries(component)) + ydata = self.overlay.getComponentTimeSeries(component) return TimeSeries.getData(self, ydata=ydata) diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py index 02d2c6dceddad0f3388f3c80e0edcf7aac1bb879..5a879e526528b542ece4f59efe961faa81221d1b 100644 --- a/fsl/fsleyes/views/timeseriespanel.py +++ b/fsl/fsleyes/views/timeseriespanel.py @@ -413,7 +413,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel): propNames = ['volume'] else: - ts = timeseries.TimeSeries(self, overlay, self._displayCtx) + ts = timeseries.VoxelTimeSeries(self, overlay, self._displayCtx) targets = [self._displayCtx] propNames = ['location']