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