From 62896be361ff74741cd1e1e055c95bcfc31c4f98 Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Mon, 13 Jul 2015 14:18:46 +0100
Subject: [PATCH] Other feat data files are also loaded as FEAT images.
 ClusterPanel can add Z statistics and cluster masks as overlays.

---
 fsl/data/featimage.py                | 68 +++++++++++++++++++-----
 fsl/data/featresults.py              | 66 +++++++++++++++++-------
 fsl/data/image.py                    |  8 ++-
 fsl/data/strings.py                  |  2 +-
 fsl/fslview/controls/clusterpanel.py | 77 +++++++++++++++++-----------
 fsl/fslview/views/timeseriespanel.py |  4 +-
 6 files changed, 158 insertions(+), 67 deletions(-)

diff --git a/fsl/data/featimage.py b/fsl/data/featimage.py
index 3f0e30061..2ed7b2c17 100644
--- a/fsl/data/featimage.py
+++ b/fsl/data/featimage.py
@@ -4,7 +4,7 @@
 #
 # Author: Paul McCarthy <pauldmccarthy@gmail.com>
 #
-"""This module provides the :class:`FeatImage` class, a subclass of
+"""This module provides the :class:`FEATImage` class, a subclass of
 :class:`.Image` designed for the ``filtered_func_data`` file of a FEAT
 analysis.
 """
@@ -13,8 +13,6 @@ import os.path as op
 
 import numpy   as np
 
-import nibabel as nib
-
 import image   as fslimage
 import            featresults
 
@@ -31,13 +29,15 @@ class FEATImage(fslimage.Image):
             raise ValueError('{} does not appear to be data from a '
                              'FEAT analysis'.format(path))
 
-        featDir     = op.dirname(path)
+        if op.isdir(path):
+            path = op.join(path, 'filtered_func_data')
+
+        featDir     = featresults.getFEATDir(path)
         settings    = featresults.loadSettings( featDir)
         design      = featresults.loadDesign(   featDir)
         names, cons = featresults.loadContrasts(featDir)
-        datafile    = featresults.getDataFile(  featDir)
         
-        fslimage.Image.__init__(self, datafile, **kwargs)
+        fslimage.Image.__init__(self, path, **kwargs)
 
         self.__analysisName  = op.splitext(op.basename(featDir))[0]
         self.__featDir       = featDir
@@ -50,6 +50,8 @@ class FEATImage(fslimage.Image):
         self.__residuals     =  None
         self.__pes           = [None] * self.numEVs()
         self.__copes         = [None] * self.numContrasts()
+        self.__zstats        = [None] * self.numContrasts()
+        self.__clustMasks    = [None] * self.numContrasts()
 
         if 'name' not in kwargs:
             self.name = '{}: {}'.format(self.__analysisName, self.name)
@@ -98,7 +100,12 @@ class FEATImage(fslimage.Image):
 
         if self.__pes[ev] is None:
             pefile = featresults.getPEFile(self.__featDir, ev)
-            self.__pes[ev] = nib.load(pefile).get_data()
+            self.__pes[ev] = FEATImage(
+                pefile,
+                name='{}: PE{} ({})'.format(
+                    self.__analysisName,
+                    ev + 1,
+                    self.evNames()[ev]))
 
         return self.__pes[ev]
 
@@ -107,7 +114,9 @@ class FEATImage(fslimage.Image):
         
         if self.__residuals is None:
             resfile = featresults.getResidualFile(self.__featDir)
-            self.__residuals = nib.load(resfile).get_data()
+            self.__residuals = FEATImage(
+                resfile,
+                name='{}: residuals'.format(self.__analysisName))
         
         return self.__residuals
 
@@ -116,10 +125,45 @@ class FEATImage(fslimage.Image):
         
         if self.__copes[con] is None:
             copefile = featresults.getPEFile(self.__featDir, con)
-            self.__copes[con] = nib.load(copefile).get_data()
+            self.__copes[con] = FEATImage(
+                copefile,
+                name='{}: COPE{} ({})'.format(
+                    self.__analysisName,
+                    con + 1,
+                    self.contrastNames()[con]))
+
+        return self.__copes[con]
 
-        return self.__copes[con] 
+
+    def getZStats(self, con):
         
+        if self.__zstats[con] is None:
+            zfile = featresults.getZStatFile(self.__featDir, con)
+
+            self.__zstats[con] = FEATImage(
+                zfile,
+                name='{}: zstat{} ({})'.format(
+                    self.__analysisName,
+                    con + 1,
+                    self.contrastNames()[con]))
+
+        return self.__zstats[con] 
+
+
+    def getClusterMask(self, con):
+        
+        if self.__clustMasks[con] is None:
+            mfile = featresults.getClusterMaskFile(self.__featDir, con)
+
+            self.__clustMasks[con] = FEATImage(
+                mfile,
+                name='{}: cluster mask for zstat{} ({})'.format(
+                    self.__analysisName,
+                    con + 1,
+                    self.contrastNames()[con]))
+
+        return self.__clustMasks[con] 
+            
 
     def fit(self, contrast, xyz, fullmodel=False):
         """
@@ -146,7 +190,7 @@ class FEATImage(fslimage.Image):
 
         for i in range(numEVs):
 
-            pe        = self.getPE(i)[x, y, z]
+            pe        = self.getPE(i).data[x, y, z]
             modelfit += X[:, i] * pe * contrast[i]
 
         return modelfit + data.mean()
@@ -160,7 +204,7 @@ class FEATImage(fslimage.Image):
         """
 
         x, y, z   = xyz
-        residuals = self.getResiduals()[x, y, z, :]
+        residuals = self.getResiduals().data[x, y, z, :]
         modelfit  = self.fit(contrast, xyz, fullmodel)
 
         return residuals + modelfit
diff --git a/fsl/data/featresults.py b/fsl/data/featresults.py
index 1b27dacc7..a9e1139dc 100644
--- a/fsl/data/featresults.py
+++ b/fsl/data/featresults.py
@@ -24,32 +24,44 @@ def isFEATDir(path):
     looks like the input data for a FEAT analysis, ``False`` otherwise.
     """
 
-    if op.isfile(path):
 
-        dirname, filename = op.split(path)
+    dirname, filename = op.split(path)
 
-        if filename.startswith('filtered_func_data'):
-            return True
+    featDir   = getFEATDir(dirname)
+    isfeatdir = featDir is not None
+
+    try:
+        hasdesfsf = op.exists(op.join(featDir, 'design.fsf'))
+        hasdesmat = op.exists(op.join(featDir, 'design.mat'))
+        hasdescon = op.exists(op.join(featDir, 'design.con'))
+        
+        isfeat    = (isfeatdir and
+                     hasdesmat and
+                     hasdescon and
+                     hasdesfsf)
+
+        return isfeat
+    
+    except:
         return False
 
-    dirname = path
-    keys    = ['.feat',
-               '.gfeat',
-               '.feat{}' .format(op.sep),
-               '.gfeat{}'.format(op.sep)]
 
-    isfeatdir = any([path.endswith(k) for k in keys])
+def getFEATDir(path):
 
-    hasdesfsf = op.exists(op.join(dirname, 'design.fsf'))
-    hasdesmat = op.exists(op.join(dirname, 'design.mat'))
-    hasdescon = op.exists(op.join(dirname, 'design.con'))
+    sufs     = ['.feat', '.gfeat']
+    idxs     = [(path.rfind(s), s) for s in sufs]
+    idx, suf = max(idxs, key=lambda (i, s): i)
 
-    isfeat    = (isfeatdir and
-                 hasdesmat and
-                 hasdescon and
-                 hasdesfsf)
-    
-    return isfeat
+    if idx == -1:
+        return None
+
+    idx  += len(suf)
+    path  = path[:idx]
+
+    if path.endswith(suf) or path.endswith('{}{}'.format(suf, op.sep)):
+        return path
+                                           
+    return None
 
 
 def loadDesign(featdir):
@@ -298,6 +310,22 @@ def getCOPEFile(featdir, contrast):
     return glob.glob(copefile)[0]
 
 
+def getZStatFile(featdir, contrast):
+    """Returns the path of the Z-statistic file for the specified
+    ``contrast``, which is assumed to be 0-indexed. 
+    """
+    zfile = op.join(featdir, 'stats', 'zstat{}.*'.format(contrast + 1))
+    return glob.glob(zfile)[0]
+
+
+def getClusterMaskFile(featdir, contrast):
+    """Returns the path of the cluster mask file for the specified
+    ``contrast``, which is assumed to be 0-indexed. 
+    """
+    mfile = op.join(featdir, 'cluster_mask_zstat{}.*'.format(contrast + 1))
+    return glob.glob(mfile)[0]
+
+
 def getEVNames(settings):
     """Returns the names of every EV in the FEAT analysis which has the given
     ``settings`` (see the :func:`loadSettings` function).
diff --git a/fsl/data/image.py b/fsl/data/image.py
index 7fd432e9b..cb4bdf27a 100644
--- a/fsl/data/image.py
+++ b/fsl/data/image.py
@@ -121,11 +121,15 @@ class Image(props.HasProperties):
             # the provided file name, that means that the
             # image was opened from a temporary file
             if filename != image:
-                self.name     = removeExt(op.basename(self.dataSource))
+                filepref      = removeExt(op.basename(self.dataSource))
                 self.tempFile = nibImage.get_filename()
             else:
-                self.name     = removeExt(op.basename(self.dataSource))
+                filepref      = removeExt(op.basename(self.dataSource))
 
+            if name is None:
+                name = filepref
+            
+            self.name  = name
             self.saved = True
                 
         # Or a numpy array - we wrap it in a nibabel image,
diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 80165a32f..321b1d6e5 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -240,7 +240,7 @@ labels = TypeDict({
 
     'FEATResidualTimeSeries'     : 'Residuals',
 
-    'ClusterPanel.clustName'     : 'Z statistics for COPE {} ({})',
+    'ClusterPanel.clustName'     : 'Z statistics for COPE{} ({})',
     
     'ClusterPanel.index'         : 'Cluster index',
     'ClusterPanel.nvoxels'       : 'Size (voxels)',
diff --git a/fsl/fslview/controls/clusterpanel.py b/fsl/fslview/controls/clusterpanel.py
index ab98ec474..68ee9c5fb 100644
--- a/fsl/fslview/controls/clusterpanel.py
+++ b/fsl/fslview/controls/clusterpanel.py
@@ -56,12 +56,12 @@ class ClusterPanel(fslpanel.FSLViewPanel):
         self.__topSizer.Add(self.__addClusterMask, **args)
 
         self.__mainSizer.Add(self.__topSizer,    flag=wx.EXPAND)
-        self.__mainSizer.Add(self.__clusterList, flag=wx.EXPAND, proportion=1)
+        self.__mainSizer.Add(self.__clusterList, **args)
 
         # Only one of the disabledText or
         # mainSizer are shown at any one time
-        self.__sizer.Add(self.__disabledText, flag=wx.EXPAND, proportion=1)
-        self.__sizer.Add(self.__mainSizer,    flag=wx.EXPAND, proportion=1)
+        self.__sizer.Add(self.__disabledText, **args)
+        self.__sizer.Add(self.__mainSizer,    **args)
 
         overlayList.addListener('overlays',
                                 self._name,
@@ -70,7 +70,9 @@ class ClusterPanel(fslpanel.FSLViewPanel):
                                 self._name,
                                 self.__selectedOverlayChanged)
 
-        self.__statSelect.Bind(wx.EVT_COMBOBOX, self.__statSelected)
+        self.__statSelect    .Bind(wx.EVT_COMBOBOX, self.__statSelected)
+        self.__addZStats     .Bind(wx.EVT_BUTTON,   self.__addZStatsClick)
+        self.__addClusterMask.Bind(wx.EVT_BUTTON,   self.__addClusterMaskClick)
 
         self.__selectedOverlay = None
         self.__selectedOverlayChanged()
@@ -80,39 +82,59 @@ class ClusterPanel(fslpanel.FSLViewPanel):
         self._overlayList.removeListener('overlays',        self._name)
         self._displayCtx .removeListener('selectedOverlay', self ._name)
 
-        if self.__selctedOverlay is not None:
-            try:
-                display = self._displayCtx.getDisplay(self.__selectedOverlay)
-                display.removeListener('name', self._name)
-            except:
-                pass
-
-            
+        
     def __disable(self, message):
 
         self.__disabledText.SetLabel(message)
         self.__sizer.Show(self.__disabledText, True)
         self.__sizer.Show(self.__mainSizer,    False)
-        self.Layout() 
-        
+        self.Layout()
+
+
+    def __addZStatsClick(self, ev):
+
+        overlay  = self.__selectedOverlay
+        contrast = self.__statSelect.GetSelection()
+        zstats   = overlay.getZStats(contrast)
+
+        for ol in self._overlayList:
+            
+            # Already in overlay list
+            if ol.dataSource == zstats.dataSource:
+                return
+
+        log.debug('Adding Z-statistic {} to overlay list'.format(zstats.name))
+        self._overlayList.append(zstats)
+
+    
+    def __addClusterMaskClick(self, ev):
+        overlay  = self.__selectedOverlay
+        contrast = self.__statSelect.GetSelection()
+        mask     = overlay.getClusterMask(contrast)
+
+        for ol in self._overlayList:
+            
+            # Already in overlay list
+            if ol.dataSource == mask.dataSource:
+                return
+
+        log.debug('Adding Cluster mask {} to overlay list'.format(mask.name))
+        self._overlayList.append(mask)
+
 
     def __selectedOverlayChanged(self, *a):
 
         self.__statSelect .Clear()
         self.__clusterList.ClearGrid()
 
+        self.__selectedOverlay = None
+
         # No overlays are loaded
         if len(self._overlayList) == 0:
             self.__disable(strings.messages[self, 'noOverlays'])
             return
 
-        if self.__selectedOverlay is not None:
-            display = self._displayCtx.getDisplay(self.__selectedOverlay)
-            display.removeListener('name', self._name)
-            self.__selectedOverlay = None
-
         overlay = self._displayCtx.getSelectedOverlay()
-        display = self._displayCtx.getDisplay(overlay)
 
         # Not a FEAT image, can't 
         # do anything with that
@@ -125,8 +147,8 @@ class ClusterPanel(fslpanel.FSLViewPanel):
         self.__sizer.Show(self.__disabledText, False)
         self.__sizer.Show(self.__mainSizer,    True)
 
-        numCons  =  overlay.numContrasts()
-        conNames =  overlay.contrastNames()
+        numCons  = overlay.numContrasts()
+        conNames = overlay.contrastNames()
 
         try:
             # clusts is a list of (contrast, clusterList) tuples 
@@ -148,21 +170,14 @@ class ClusterPanel(fslpanel.FSLViewPanel):
 
         for contrast, clusterList in clusts:
             name = conNames[contrast]
-            name = strings.labels[self, 'clustName'].format(contrast, name)
+            name = strings.labels[self, 'clustName'].format(contrast + 1, name)
 
             self.__statSelect.Append(name, clusterList)
             
-        self.__overlayName.SetLabel(display.name)
+        self.__overlayName.SetLabel(overlay.getAnalysisName())
         self.__statSelect.SetSelection(0)
         self.__displayClusterData(clusts[0][1])
 
-        # Update displayed name if
-        # overlay name is changed
-        def nameChanged(*a):
-            self.__overlayName.setLabel(display.name)
-
-        display.addListener('name', self._name, nameChanged)
-        
         self.Layout()
         return
 
diff --git a/fsl/fslview/views/timeseriespanel.py b/fsl/fslview/views/timeseriespanel.py
index 90f31c76a..6ef492eb3 100644
--- a/fsl/fslview/views/timeseriespanel.py
+++ b/fsl/fslview/views/timeseriespanel.py
@@ -357,9 +357,9 @@ class FEATEVTimeSeries(TimeSeries):
 class FEATResidualTimeSeries(TimeSeries):
     def getData(self):
         x, y, z = self.coords
-        data    = self.overlay.getResiduals()[x, y, z, :]
+        data    = self.overlay.getResiduals().data[x, y, z, :]
         
-        return TimeSeries.getData(self, ydata=data)
+        return TimeSeries.getData(self, ydata=np.array(data))
             
 
 class FEATModelFitTimeSeries(TimeSeries):
-- 
GitLab