From 0ee9e94bcdb1b81a91cc20d7a0a6ca61a4205749 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Mon, 29 Jun 2015 11:19:54 +0100
Subject: [PATCH] Functional histogram list, allows add/remove histogram plots.
 Histograms are not recalculated if they've already been calculated.

---
 fsl/data/strings.py                        |  7 +-
 fsl/fslview/controls/__init__.py           |  1 +
 fsl/fslview/controls/histogramlistpanel.py | 88 ++++++++++++++++++++++
 fsl/fslview/views/histogrampanel.py        | 73 ++++++++----------
 4 files changed, 123 insertions(+), 46 deletions(-)
 create mode 100644 fsl/fslview/controls/histogramlistpanel.py

diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 6d4840e5f..540eb5ff4 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -112,12 +112,12 @@ titles = TypeDict({
     'OrthoSettingsPanel'     : 'Ortho view settings',
     'LightBoxToolBar'        : 'Lightbox view toolbar',
     'LightBoxSettingsPanel'  : 'Lightbox view settings',
-    'HistogramToolBar'       : 'Histogram settings',
     'LookupTablePanel'       : 'Lookup tables',
     'LutLabelDialog'         : 'New LUT label',
     'NewLutDialog'           : 'New LUT',
     'TimeSeriesListPanel'    : 'Time series list',
     'TimeSeriesControlPanel' : 'Time series control',
+    'HistogramListPanel'     : 'Histogram list',
 
     'LookupTablePanel.loadLut'     : 'Select a lookup table file',
     'LookupTablePanel.labelExists' : 'Label already exists',
@@ -149,13 +149,10 @@ actions = TypeDict({
     
     'LightBoxPanel.toggleLightBoxToolBar' : 'View properties',
 
-
     'PlotPanel.screenshot'                    : 'Take screenshot',
     'TimeSeriesPanel.toggleTimeSeriesList'    : 'Time series list',
     'TimeSeriesPanel.toggleTimeSeriesControl' : 'Time series control', 
-    'HistogramPanel.toggleToolbar'            : 'Histogram controls',
-    
-
+    'HistogramPanel.toggleHistogramList'      : 'Histogram list',
 
     'OrthoViewProfile.centreCursor' : 'Centre cursor',
     'OrthoViewProfile.resetZoom'    : 'Reset zoom',
diff --git a/fsl/fslview/controls/__init__.py b/fsl/fslview/controls/__init__.py
index bf45f4d1a..328ec2b27 100644
--- a/fsl/fslview/controls/__init__.py
+++ b/fsl/fslview/controls/__init__.py
@@ -15,6 +15,7 @@ from orthosettingspanel     import OrthoSettingsPanel
 from lookuptablepanel       import LookupTablePanel
 from timeserieslistpanel    import TimeSeriesListPanel
 from timeseriescontrolpanel import TimeSeriesControlPanel
+from histogramlistpanel     import HistogramListPanel
 
 from orthotoolbar          import OrthoToolBar
 from orthoprofiletoolbar   import OrthoProfileToolBar
diff --git a/fsl/fslview/controls/histogramlistpanel.py b/fsl/fslview/controls/histogramlistpanel.py
new file mode 100644
index 000000000..33c0ef887
--- /dev/null
+++ b/fsl/fslview/controls/histogramlistpanel.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+#
+# histogramlistpanel.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
+
+import                           timeserieslistpanel 
+    
+
+class HistogramListPanel(fslpanel.FSLViewPanel):
+
+    def __init__(self, parent, overlayList, displayCtx, histPanel):
+
+        fslpanel.FSLViewPanel.__init__(self, parent, overlayList, displayCtx)
+
+        self.__hsPanel      = histPanel
+        self.__hsList       = elistbox.EditableListBox(
+            self, style=(elistbox.ELB_NO_MOVE |
+                         elistbox.ELB_EDITABLE))
+
+        self.__sizer = wx.BoxSizer(wx.VERTICAL)
+        self.SetSizer(self.__sizer)
+
+        self.__sizer.Add(self.__hsList, flag=wx.EXPAND, proportion=1)
+
+        self.__hsList.Bind(elistbox.EVT_ELB_ADD_EVENT,    self.__onListAdd)
+        self.__hsList.Bind(elistbox.EVT_ELB_REMOVE_EVENT, self.__onListRemove)
+        self.__hsList.Bind(elistbox.EVT_ELB_EDIT_EVENT,   self.__onListEdit)
+        self.__hsList.Bind(elistbox.EVT_ELB_SELECT_EVENT, self.__onListSelect)
+        
+        self.__hsPanel.addListener('dataSeries',
+                                   self._name,
+                                   self.__histSeriesChanged)
+
+        self.__histSeriesChanged()
+        self.Layout()
+
+        
+    def destroy(self):
+        fslpanel.FSLViewPanel.destroy(self)
+        self.__hsPanel.removeListener('dataSeries', self._name)
+
+
+    def __histSeriesChanged(self, *a):
+
+        self.__hsList.Clear()
+
+        for hs in self.__hsPanel.dataSeries:
+            widg = timeserieslistpanel.TimeSeriesWidget(self, hs)
+            self.__hsList.Append(hs.overlay.name,
+                                 clientData=hs,
+                                 extraWidget=widg)
+
+    
+    def __onListAdd(self, ev):
+        
+        hs = self.__hsPanel.getCurrent()
+
+        if hs is None:
+            return
+        
+        hs.alpha     = 1
+        hs.lineWidth = 2
+        hs.lineStyle = '-'
+        hs.colour    = fslcm.randomColour()
+        hs.label     = hs.overlay.name
+        self.__hsPanel.dataSeries.append(hs)
+
+        
+    def __onListEdit(self, ev):
+        ev.data.label = ev.label
+
+        
+    def __onListSelect(self, ev):
+        overlay = ev.data.overlay
+        self._displayCtx.selectedOverlay = self._overlayList.index(overlay)
+
+        
+    def __onListRemove(self, ev):
+        self.__hsPanel.dataSeries.remove(ev.data)
diff --git a/fsl/fslview/views/histogrampanel.py b/fsl/fslview/views/histogrampanel.py
index 46301d75d..44323757e 100644
--- a/fsl/fslview/views/histogrampanel.py
+++ b/fsl/fslview/views/histogrampanel.py
@@ -15,7 +15,7 @@ import props
 
 import fsl.data.image         as fslimage
 import fsl.data.strings       as strings
-import fsl.fslview.colourmaps as fslcm
+import fsl.fslview.controls   as fslcontrols
 import                           plotpanel
 
 
@@ -50,26 +50,6 @@ def autoBin(data, dataRange):
     return nbins
 
 
-#
-# Ideas:
-#
-#    - Plot histogram for multiple images (how to select them?)
-#
-#    - Ability to apply a label mask image, and plot separate
-#      histograms for each label
-# 
-#    - Ability to put an overlay on the display, showing the
-#      voxels that are within the histogram range
-#
-#    - For 4D images, add an option to plot the histogram for
-#      the current volume only, or for all volumes
-#
-#    - For different image types (e.g. vector), add anoption
-#      to plot the histogram of calculated values, e.g.
-#      magnitude, or separate histogram lines for xyz
-#      components?
-#
-
 class HistogramSeries(plotpanel.DataSeries):
 
     nbins       = props.Int(minval=10, maxval=500, default=100, clamped=True)
@@ -92,13 +72,13 @@ class HistogramSeries(plotpanel.DataSeries):
         if overlay.is4DImage():
             self.setConstraint('volume', 'maxval', overlay.shape[3] - 1)
 
+        self.__calcInitDataRange()
+        self.histPropsChanged()
+
         hsPanel.addListener('autoBin',   self.name, self.histPropsChanged)
         self   .addListener('nbins',     self.name, self.histPropsChanged)
         self   .addListener('dataRange', self.name, self.histPropsChanged)
 
-        self.__calcInitDataRange()
-        self.histPropsChanged()
-
         
     def __del__(self):
         self.hsPanel.removeListener('autoBin', self.name)
@@ -133,7 +113,13 @@ class HistogramSeries(plotpanel.DataSeries):
         if self.ignoreZeros:
             data = data[data != 0]
 
+        nvals     = data.size
         dataRange = self.dataRange.x
+
+        log.debug('Calculating histogram for overlay '
+                  '{} (number of values {})'.format(
+                      self.overlay.name,
+                      nvals))
         
         if self.hsPanel.autoBin: nbins = autoBin(data, dataRange)
         else:                    nbins = self.nbins
@@ -153,10 +139,18 @@ class HistogramSeries(plotpanel.DataSeries):
 
         self.xdata = np.array(histX, dtype=np.float32)
         self.ydata = np.array(histY, dtype=np.float32)
+        self.nvals = nvals
 
 
     def getData(self):
-        return self.xdata, self.ydata
+
+        xdata    = self.xdata
+        ydata    = self.ydata
+        nvals    = self.nvals
+        histType = self.hsPanel.histType
+
+        if   histType == 'count':       return xdata, ydata
+        elif histType == 'probability': return xdata, ydata / nvals
 
     
 class HistogramPanel(plotpanel.PlotPanel):
@@ -164,13 +158,15 @@ class HistogramPanel(plotpanel.PlotPanel):
 
     autoBin       = props.Boolean(default=True)
     showCurrent   = props.Boolean(default=True)
-
     enableOverlay = props.Boolean(default=False)
+    histType      = props.Choice(('probability', 'count'))
     
 
     def __init__(self, parent, overlayList, displayCtx):
 
-        actionz = {}
+        actionz = {'toggleHistogramList' : lambda *a: self.togglePanel(
+            fslcontrols.HistogramListPanel, False, self)
+        }
 
         plotpanel.PlotPanel.__init__(
             self, parent, overlayList, displayCtx, actionz)
@@ -185,13 +181,14 @@ class HistogramPanel(plotpanel.PlotPanel):
         self._overlayList.addListener(
             'overlays',
             self._name,
-            self.__selectedOverlayChanged) 
+            self.__updateCurrent) 
         self._displayCtx.addListener(
             'selectedOverlay',
             self._name,
-            self.__selectedOverlayChanged)
+            self.__updateCurrent)
+        self.addGlobalListener(self._name, self.__updateCurrent)
 
-        self.__selectedOverlayChanged()
+        self.__updateCurrent()
 
         self.Layout()
 
@@ -204,7 +201,8 @@ class HistogramPanel(plotpanel.PlotPanel):
         self._displayCtx .removeListener('selectedOverlay', self._name)
 
 
-    def __calcCurrent(self):
+    def __updateCurrent(self, *a):
+
         self.__current = None
 
         if self._overlayList == 0:
@@ -215,6 +213,9 @@ class HistogramPanel(plotpanel.PlotPanel):
         if not isinstance(overlay, fslimage.Image):
             return
 
+        if overlay in [hs.overlay for hs in self.dataSeries]:
+            return
+
         hs             = HistogramSeries(self, overlay)
         hs.colour      = [0.2, 0.2, 0.2]
         hs.alpha       = 1
@@ -228,19 +229,9 @@ class HistogramPanel(plotpanel.PlotPanel):
     def getCurrent(self): 
         return self.__current
 
-    
-    def __selectedOverlayChanged(self, *a):
-
-        if len(self._overlayList) == 0:
-            return
-
-
-        self.draw()
-
 
     def draw(self, *a):
 
-        self.__calcCurrent()
         current = self.getCurrent()
 
         if current is not None: self.drawDataSeries([current])
-- 
GitLab