From c1db01a517da761479304f3426e97fdcd0eb17b4 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Thu, 25 Jun 2015 17:22:50 +0100 Subject: [PATCH] In process of improving time series functionality. --- fsl/data/strings.py | 7 +- fsl/fslview/controls/__init__.py | 1 + fsl/fslview/controls/timeserieslistpanel.py | 59 ++++++ fsl/fslview/views/timeseriespanel.py | 208 +++++++++----------- 4 files changed, 156 insertions(+), 119 deletions(-) create mode 100644 fsl/fslview/controls/timeserieslistpanel.py diff --git a/fsl/data/strings.py b/fsl/data/strings.py index fab0d8759..ada85f980 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -116,6 +116,7 @@ titles = TypeDict({ 'LookupTablePanel' : 'Lookup tables', 'LutLabelDialog' : 'New LUT label', 'NewLutDialog' : 'New LUT', + 'TimeSeriesListPanel' : 'Time series list', 'LookupTablePanel.loadLut' : 'Select a lookup table file', 'LookupTablePanel.labelExists' : 'Label already exists', @@ -148,9 +149,11 @@ actions = TypeDict({ 'LightBoxPanel.toggleLightBoxToolBar' : 'View properties', - 'PlotPanel.screenshot' : 'Take screenshot', + 'PlotPanel.screenshot' : 'Take screenshot', - 'HistogramPanel.toggleToolbar' : 'Histogram controls', + 'TimeSeriesPanel.toggleTimeSeriesList' : 'Time series list', + 'HistogramPanel.toggleToolbar' : 'Histogram controls', + 'OrthoViewProfile.centreCursor' : 'Centre cursor', diff --git a/fsl/fslview/controls/__init__.py b/fsl/fslview/controls/__init__.py index 9ee27b304..fbfd7642b 100644 --- a/fsl/fslview/controls/__init__.py +++ b/fsl/fslview/controls/__init__.py @@ -13,6 +13,7 @@ from lightboxsettingspanel import LightBoxSettingsPanel from locationpanel import LocationPanel from orthosettingspanel import OrthoSettingsPanel from lookuptablepanel import LookupTablePanel +from timeserieslistpanel import TimeSeriesListPanel from orthotoolbar import OrthoToolBar from orthoprofiletoolbar import OrthoProfileToolBar diff --git a/fsl/fslview/controls/timeserieslistpanel.py b/fsl/fslview/controls/timeserieslistpanel.py new file mode 100644 index 000000000..346be2209 --- /dev/null +++ b/fsl/fslview/controls/timeserieslistpanel.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# +# timeserieslistpanel.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import wx + +import pwidgets.elistbox as elistbox +import fsl.fslview.panel as fslpanel +import fsl.fslview.colourmaps as fslcm + + +class TimeSeriesListPanel(fslpanel.FSLViewPanel): + + def __init__(self, parent, overlayList, displayCtx, timeSeriesPanel): + + fslpanel.FSLViewPanel.__init__(self, parent, overlayList, displayCtx) + + self.__tsPanel = timeSeriesPanel + self.__currentLabel = wx.StaticText( self) + self.__tsList = elistbox.EditableListBox(self) + + self.__sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self.__sizer) + + self.__sizer.Add(self.__currentLabel, flag=wx.EXPAND) + self.__sizer.Add(self.__tsList, flag=wx.EXPAND, proportion=1) + + + displayCtx .addListener('selectedOverlay', + self._name, + self.__locationChanged) + overlayList.addListener('overlays', + self._name, + self.__locationChanged) + + self.Layout() + + + def destroy(self): + fslpanel.FSLViewPanel.destroy(self) + self._displayCtx .removeListener('selectedOverlay', self._name) + self._overlayList.removeListener('overlays', self._name) + + + def __locationChanged(self, *a): + pass + + + def __onListAdd(self, ev): + + ts = self.__tsPanel.getCurrent() + + ts.colour = fslcm.randomColour() + + self.__tsPanel.timeSeries.append(ts) + diff --git a/fsl/fslview/views/timeseriespanel.py b/fsl/fslview/views/timeseriespanel.py index bb61bfbca..310734d23 100644 --- a/fsl/fslview/views/timeseriespanel.py +++ b/fsl/fslview/views/timeseriespanel.py @@ -16,10 +16,14 @@ import logging import numpy as np +import props + import plotpanel import fsl.data.image as fslimage import fsl.data.strings as strings import fsl.fslview.displaycontext as fsldisplay +import fsl.fslview.colourmaps as fslcm +import fsl.fslview.controls as fslcontrols import fsl.utils.transform as transform @@ -36,6 +40,16 @@ log = logging.getLogger(__name__) # of each persistent time course +class TimeSeries(object): + + def __init__(self, overlay, coords, data, colour, label): + self.overlay = overlay + self.coords = coords + self.data = data + self.colour = colour + self.label = label + + class TimeSeriesPanel(plotpanel.PlotPanel): """A panel with a :mod:`matplotlib` canvas embedded within. @@ -44,38 +58,37 @@ class TimeSeriesPanel(plotpanel.PlotPanel): plotted on the canvas. """ + + timeSeries = props.List() + + def __init__(self, parent, overlayList, displayCtx): - plotpanel.PlotPanel.__init__(self, parent, overlayList, displayCtx) + actionz = { + 'toggleTimeSeriesList' : lambda *a: self.togglePanel( + fslcontrols.TimeSeriesListPanel, False, self) + } + + plotpanel.PlotPanel.__init__( + self, parent, overlayList, displayCtx, actionz=actionz) figure = self.getFigure() - canvas = self.getCanvas() figure.subplots_adjust( top=1.0, bottom=0.0, left=0.0, right=1.0) figure.patch.set_visible(False) - self._mouseDown = False - canvas.mpl_connect('button_press_event', self._onPlotMouseDown) - canvas.mpl_connect('button_release_event', self._onPlotMouseUp) - canvas.mpl_connect('motion_notify_event', self._onPlotMouseMove) - - self._overlayList.addListener( - 'overlays', - self._name, - self._selectedOverlayChanged) - self._displayCtx.addListener( - 'selectedOverlay', - self._name, - self._selectedOverlayChanged) - self._displayCtx.addListener( - 'location', - self._name, - self._locationChanged) + name = self._name + draw = self._draw + + self._overlayList.addListener('overlays', name, draw) + self._displayCtx .addListener('selectedOverlay', name, draw) + self._displayCtx .addListener('location', name, draw) + self .addListener('timeSeries', name, draw) self.Layout() - self._selectedOverlayChanged() + self._draw() def destroy(self): @@ -85,126 +98,87 @@ class TimeSeriesPanel(plotpanel.PlotPanel): self._displayCtx .removeListener('selectedOverlay', self._name) self._displayCtx .removeListener('location', self._name) - for ovl in self._overlayList: - opts = self._displayCtx.getOpts(ovl) - - if isinstance(opts, fsldisplay.ImageOpts): - opts.removeListener('volume', self._name) - - - def _selectedOverlayChanged(self, *a): - - overlay = self._displayCtx.getSelectedOverlay() - - for ovl in self._overlayList: - - if not isinstance(ovl, fslimage.Image): - continue - - opts = self._displayCtx.getOpts(ovl) - - if ovl is overlay: - opts.addListener('volume', - self._name, - self._locationChanged, - overwrite=True) - else: - opts.removeListener('volume', self._name) - - self._locationChanged() - - - def _locationChanged(self, *a): - - self.getAxis().clear() - if len(self._overlayList) == 0: - self.getCanvas().draw() - self.Refresh() - else: - self._drawPlot() - + def getCurrent(self): - def _drawPlot(self): - - axis = self.getAxis() - canvas = self.getCanvas() x, y, z = self._displayCtx.location.xyz overlay = self._displayCtx.getSelectedOverlay() + opts = self._displayCtx.getOpts(overlay) - if not isinstance(overlay, fslimage.Image): - self.message(strings.messages[self, 'noData']) - - elif not overlay.is4DImage(): - self.message(strings.messages[self, 'not4D']) + if not isinstance(overlay, fslimage.Image) or \ + not isinstance(opts, fsldisplay.VolumeOpts) or \ + not overlay.is4DImage(): + return None + + xform = opts.getTransform('display', 'voxel') + vox = transform.transform([[x, y, z]], xform)[0] + vox = np.floor(vox + 0.5) - else: - opts = self._displayCtx.getOpts(overlay) - xform = opts.getTransform('display', 'voxel') + if vox[0] < 0 or \ + vox[1] < 0 or \ + vox[2] < 0 or \ + vox[0] >= overlay.shape[0] or \ + vox[1] >= overlay.shape[1] or \ + vox[2] >= overlay.shape[2]: + return None - vox = transform.transform([[x, y, z]], xform)[0] - vox = np.floor(vox + 0.5) + return TimeSeries( + overlay, + vox, + overlay.data[vox[0], vox[1], vox[2], :], + [0.5, 0.5, 0.5], + '{} [{} {} {}]'.format(overlay.name, vox[0], vox[1], vox[2])) - if vox[0] < 0 or \ - vox[1] < 0 or \ - vox[2] < 0 or \ - vox[0] >= overlay.shape[0] or \ - vox[1] >= overlay.shape[1] or \ - vox[2] >= overlay.shape[2]: - - self.message(strings.messages[self, 'outOfBounds']) - else: - self._drawPlotOneOverlay(overlay, *vox) - axis.axvline(opts.volume, c='#000080', lw=2, alpha=0.4) + def _draw(self, *a): - canvas.draw() - self.Refresh() + axis = self.getAxis() + canvas = self.getCanvas() + axis.clear() - def _drawPlotOneOverlay(self, overlay, x, y, z): + toPlot = self.timeSeries[:] + currentTs = self.getCurrent() - display = self._displayCtx.getDisplay(overlay) + if currentTs is not None: + toPlot = [currentTs] + toPlot - if not overlay.is4DImage(): return None - if not display.enabled: return None + if len(toPlot) == 0: + canvas.draw() + self.Refresh() + return - for vox, shape in zip((x, y, z), overlay.shape): - if vox >= shape or vox < 0: - return None + xlims = [] + ylims = [] - data = overlay.data[x, y, z, :] - self.getAxis().plot(data, lw=2) + for ts in toPlot: + xlim, ylim = self._drawTimeSeries(ts) + xlims.append(xlim) + ylims.append(ylim) - return data.min(), data.max(), len(data) + # Set x/ylim + xmin = min([lim[0] for lim in xlims]) + xmax = max([lim[1] for lim in xlims]) + ymin = min([lim[0] for lim in ylims]) + ymax = max([lim[1] for lim in ylims]) + axis.set_xlim((xmin, xmax)) + axis.set_ylim((ymin, ymax)) - def _onPlotMouseDown(self, ev): - if ev.inaxes != self.getAxis(): return + canvas.draw() + self.Refresh() - overlay = self._displayCtx.getSelectedOverlay() - if not isinstance(overlay, fslimage.Image) or not overlay.is4DImage(): - return - self._mouseDown = True + def _drawTimeSeries(self, ts): - opts = self._displayCtx.getOpts(overlay) - opts.volume = np.floor(ev.xdata) - + display = self._displayCtx.getDisplay(ts.overlay) - def _onPlotMouseUp(self, ev): - self._mouseDown = False + if not display.enabled: + return None + data = ts.data - def _onPlotMouseMove(self, ev): - if not self._mouseDown: return - if ev.inaxes != self.getAxis(): return - - overlay = self._displayCtx.getSelectedOverlay() + self.getAxis().plot(data, lw=2, c=ts.colour, label=ts.label) - if not isinstance(overlay, fslimage.Image) or not overlay.is4DImage(): - return - - opts = self._displayCtx.getOpts(overlay) - - opts.volume = np.floor(ev.xdata) + # TODO take into account TR + return (0, len(data)), (data.min(), data.max()) -- GitLab