From 8e11cc847bd9296f9a9722d00c5ca679005eebf0 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Mon, 7 Jul 2014 17:11:01 +0100 Subject: [PATCH] First go at time series panel, using matplotlib. It's slow. --- fsl/data/fslimage.py | 17 ++++- fsl/fslview/fslviewframe.py | 82 +++++++++++++-------- fsl/fslview/strings.py | 5 +- fsl/fslview/views/__init__.py | 6 +- fsl/fslview/views/timeseriespanel.py | 105 +++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 37 deletions(-) create mode 100644 fsl/fslview/views/timeseriespanel.py diff --git a/fsl/data/fslimage.py b/fsl/data/fslimage.py index f07748dfd..1766fa3ee 100644 --- a/fsl/data/fslimage.py +++ b/fsl/data/fslimage.py @@ -229,6 +229,12 @@ class Image(props.HasProperties): return self.__str__() + def is4DImage(self): + """Returns ``True`` if this image is 4D, ``False`` otherwise. + """ + return len(self.shape) > 3 and self.shape[3] > 1 + + def _transformChanged(self, *a): """This method is called when the :attr:`transform` property value changes. It updates the :attr:`voxToWorldMat`, :attr:`worldToVoxMat`, @@ -434,35 +440,44 @@ class ImageDisplay(props.HasProperties): the associated :class:`ImageDisplay` object. """ + enabled = props.Boolean(default=True) """Should this image be displayed at all?""" + alpha = props.Real(minval=0.0, maxval=1.0, default=1.0) """Transparency - 1.0 is fully opaque, and 0.0 is fully transparent.""" + displayRange = props.Bounds(ndims=1, editLimits=True, labels=['Min.', 'Max.']) """Image values which map to the minimum and maximum colour map colours.""" + samplingRate = props.Int(minval=1, maxval=16, default=1, clamped=True) """Only display every Nth voxel (a performance tweak).""" + rangeClip = props.Boolean(default=False) """If ``True``, don't display voxel values which are beyond the :attr:`displayRange`. """ + cmap = props.ColourMap(default=mplcm.Greys_r) """The colour map, a :class:`matplotlib.colors.Colourmap` instance.""" + volume = props.Int(minval=0, maxval=0, default=0, clamped=True) """If a 4D image, the current volume to display.""" + def is4DImage(self): """Returns ``True`` if this image is 4D, ``False`` otherwise. """ - return len(self.image.shape) > 3 and self.image.shape[3] > 1 + return self.image.is4DImage() + _view = props.VGroup(('enabled', props.Widget('volume', enabledWhen=is4DImage), 'displayRange', diff --git a/fsl/fslview/fslviewframe.py b/fsl/fslview/fslviewframe.py index bb7ab3f30..780f351a8 100644 --- a/fsl/fslview/fslviewframe.py +++ b/fsl/fslview/fslviewframe.py @@ -81,61 +81,74 @@ class FSLViewFrame(wx.Frame): self.Bind(wx.EVT_CLOSE, self._onClose) - def _addViewPanel(self, panelCls): + def _addViewPanel(self, panel, title, configMenuText=None): """Adds a view panel to the centre of the frame, and a menu item allowing the user to configure the view. """ - panel = panelCls(self._centrePane, - self._imageList, - glContext=self._glContext) - - if panelCls == views.OrthoPanel: - title = strings.orthoTitle - menuText = strings.configOrtho - - if self._glContext is None: - self._glContext = panel.xcanvas.glContext - - elif panelCls == views.LightBoxPanel: - title = strings.lightBoxTitle - menuText = strings.configLightBox - - if self._glContext is None: - self._glContext = panel.canvas.glContext - + self._viewPanelCount = self._viewPanelCount + 1 title = '{} {}'.format(title, self._viewPanelCount) - menuText = menuText.format(title) + self._viewPanelTitles[id(panel)] = title self._centrePane.AddPage(panel, title) self._centrePane.SetSelection(self._centrePane.GetPageIndex(panel)) - configAction = self._viewMenu.Append(wx.ID_ANY, menuText) + if configMenuText is not None: + configMenuText = configMenuText.format(title) + configAction = self._viewMenu.Append(wx.ID_ANY, configMenuText) - def onConfig(ev): - self._addViewConfigPanel(panel, title) - - def onDestroy(ev): - ev.Skip() - try: self._viewMenu.RemoveItem(configAction) - except wx._core.PyDeadObjectError: pass + def onConfig(ev): + self._addViewConfigPanel(panel, title) - self .Bind(wx.EVT_MENU, onConfig, configAction) - panel.Bind(wx.EVT_WINDOW_DESTROY, onDestroy) + def onDestroy(ev): + ev.Skip() + try: self._viewMenu.RemoveItem(configAction) + except wx._core.PyDeadObjectError: pass + + self .Bind(wx.EVT_MENU, onConfig, configAction) + panel.Bind(wx.EVT_WINDOW_DESTROY, onDestroy) def addOrthoPanel(self): """Adds an :class:`~fsl.fslview.views.orthopanel.OrthoPanel` display to the central :class:`~wx.aui.AuiNotebook` widget. """ - self._addViewPanel(views.OrthoPanel) + + panel = views.OrthoPanel(self._centrePane, + self._imageList, + glContext=self._glContext) + + if self._glContext is None: + self._glContext = panel.xcanvas.glContext + + self._addViewPanel(panel, + strings.orthoTitle, + strings.orthoConfigMenu) def addLightBoxPanel(self): """Adds a :class:`~fsl.fslview.views.lightboxpanel.LightBoxPanel` display to the central :class:`~wx.aui.AuiNotebook` widget. """ - self._addViewPanel(views.LightBoxPanel) + panel = views.LightBoxPanel(self._centrePane, + self._imageList, + glContext=self._glContext) + + if self._glContext is None: + self._glContext = panel.canvas.glContext + + self._addViewPanel(panel, + strings.lightBoxTitle, + strings.lightBoxConfigMenu) + + + def addTimeSeriesPanel(self): + """Adds a :class:`~fsl.fslview.views.lightboxpanel.LightBoxPanel` + display to the central :class:`~wx.aui.AuiNotebook` widget. + """ + + panel = views.TimeSeriesPanel(self._centrePane, self._imageList) + self._addViewPanel(panel, strings.timeSeriesTitle) def _addViewConfigPanel(self, viewPanel, title): @@ -389,6 +402,8 @@ class FSLViewFrame(wx.Frame): orthoAction = viewMenu.Append(wx.ID_ANY, strings.orthoTitle) lightboxAction = viewMenu.Append(wx.ID_ANY, strings.lightBoxTitle) + timeSeriesAction = viewMenu.Append(wx.ID_ANY, + strings.timeSeriesTitle) imageDisplayAction = viewMenu.Append(wx.ID_ANY, strings.imageDisplayTitle) imageListAction = viewMenu.Append(wx.ID_ANY, strings.imageListTitle) @@ -402,6 +417,9 @@ class FSLViewFrame(wx.Frame): self.Bind(wx.EVT_MENU, lambda ev: self.addLightBoxPanel(), lightboxAction) + self.Bind(wx.EVT_MENU, + lambda ev: self.addTimeSeriesPanel(), + timeSeriesAction) self.Bind(wx.EVT_MENU, lambda ev: self.addImageDisplayPanel(), imageDisplayAction) diff --git a/fsl/fslview/strings.py b/fsl/fslview/strings.py index ca139615b..b8dea45cc 100644 --- a/fsl/fslview/strings.py +++ b/fsl/fslview/strings.py @@ -7,6 +7,7 @@ orthoTitle = 'Ortho view' lightBoxTitle = 'Lightbox view' +timeSeriesTitle = 'Time series' imageDisplayTitle = 'Image display properties' imageListTitle = 'Image list' locationTitle = 'Cursor location' @@ -14,5 +15,5 @@ locationTitle = 'Cursor location' openFile = 'Add image file' openStd = 'Add standard' -configOrtho = '{} display' -configLightBox = '{} display' +orthoConfigMenu = '{} display' +lightBoxConfigMenu = '{} display' diff --git a/fsl/fslview/views/__init__.py b/fsl/fslview/views/__init__.py index 86062b802..43ad54a2a 100644 --- a/fsl/fslview/views/__init__.py +++ b/fsl/fslview/views/__init__.py @@ -12,6 +12,8 @@ which provide a view of an image collection (see import orthopanel import lightboxpanel +import timeseriespanel -OrthoPanel = orthopanel .OrthoPanel -LightBoxPanel = lightboxpanel.LightBoxPanel +OrthoPanel = orthopanel .OrthoPanel +LightBoxPanel = lightboxpanel .LightBoxPanel +TimeSeriesPanel = timeseriespanel.TimeSeriesPanel diff --git a/fsl/fslview/views/timeseriespanel.py b/fsl/fslview/views/timeseriespanel.py new file mode 100644 index 000000000..83c61c491 --- /dev/null +++ b/fsl/fslview/views/timeseriespanel.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# +# timeseriespanel.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import logging +log = logging.getLogger(__name__) + +import wx + +import props + +import matplotlib as mpl + +mpl.use('WXAgg') + +import matplotlib.pyplot as plt +from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas + +class TimeSeriesPanel(wx.Panel, props.HasProperties): + + + def __init__(self, parent, imageList): + + wx.Panel.__init__(self, parent) + props.HasProperties.__init__(self) + + self._imageList = imageList + self._name = '{}_{}'.format(self.__class__.__name__, id(self)) + + self._figure = plt.Figure() + self._axis = self._figure.add_subplot(1, 1, 1) + self._canvas = Canvas(self, -1, self._figure) + + self._sizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._sizer) + self._sizer.Add(self._canvas, flag=wx.EXPAND, proportion=1) + + self._imageList.addListener( + 'selectedImage', + self._name, + self._selectedImageChanged) + + self._imageList.addListener( + 'location', + self._name, + self._locationChanged) + + def onDestroy(ev): + ev.Skip() + self._imageList.removeListener('selectedImage', self._name) + self._imageList.removeListener('location', self._name) + + self.Bind(wx.EVT_WINDOW_DESTROY, onDestroy) + + self._selectedImageChanged() + + + self.Layout() + + + def _selectedImageChanged(self, *a): + + self._axis.clear() + + if len(self._imageList) == 0: + return + + image = self._imageList[self._imageList.selectedImage] + + if not image.is4DImage(): + return + + self._voxPlot(image, *self._imageList.location.xyz) + + + def _locationChanged(self, *a): + + self._axis.clear() + + if len(self._imageList) == 0: + return + + image = self._imageList[self._imageList.selectedImage] + + if not image.is4DImage(): + return + + self._voxPlot(image, *self._imageList.location.xyz) + + + def _voxPlot(self, image, x, y, z): + + x, y, z = image.worldToVox([[x, y, z]])[0] + + data = image.data[x, y, z, :] + + print + print 'Plot ({}, {}, {}): {}'.format(x, y, z, data) + print + + self._axis.plot(data, 'r-', lw=2) + self._canvas.draw() -- GitLab