From 7136ab8c0786d614572c2a58277984c918aad76b Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Fri, 10 Jul 2015 12:01:19 +0100
Subject: [PATCH] Restructuring FEAT stuff a bit - all FEAT directory speciofic
 things are now in a module called featresults.py, which allows query/load of
 settings, files, etc.

Also added ability to load an overlay from a FEAT directory (instead of
having to specify the filtered_func_data). But WX does not have a file
dialog which allows selection of either files or directories, so this
new feature can only be done via command line at present.
---
 fsl/data/featimage.py   | 215 +++++------------------------------
 fsl/data/featresults.py | 242 ++++++++++++++++++++++++++++++++++++++++
 fsl/fslview/overlay.py  |  33 ++++--
 3 files changed, 290 insertions(+), 200 deletions(-)
 create mode 100644 fsl/data/featresults.py

diff --git a/fsl/data/featimage.py b/fsl/data/featimage.py
index 4f9e1385f..14e3af287 100644
--- a/fsl/data/featimage.py
+++ b/fsl/data/featimage.py
@@ -10,146 +10,34 @@ analysis.
 """
 
 import os.path as op
-import            glob
 
 import numpy   as np
 
 import nibabel as nib
 
 import image   as fslimage
-
-
-def loadDesignMat(designmat):
-    """Loads a FEAT ``design.mat`` file. Returns a ``numpy`` array
-    containing the design matrix data, where the first dimension
-    corresponds to the data points, and the second to the EVs.
-    """
-
-    matrix = None
-    with open(designmat, 'rt') as f:
-
-        while True:
-            line = f.readline()
-            if line.strip() == '/Matrix':
-                break
-
-        matrix = np.loadtxt(f)
-
-    if matrix is None or matrix.size == 0:
-        raise RuntimeError('{} does not appear to be a '
-                           'valid design.mat file'.format(designmat))
-
-    return matrix
-
-
-def loadDesignCon(designcon):
-    """Loads a FEAT ``design.con`` file. Returns a tuple containing:
-    
-      - A dictionary of ``{contrastnum : name}`` mappings
-    
-      - A list of contrast vectors (each of which is a list itself).
-    """
-
-    matrix       = None
-    numContrasts = 0
-    names        = {}
-    with open(designcon, 'rt') as f:
-
-        while True:
-            line = f.readline().strip()
-
-            if line.startswith('/ContrastName'):
-                tkns       = line.split(None, 1)
-                num        = [c for c in tkns[0] if c.isdigit()]
-                num        = int(''.join(num))
-                name       = tkns[1].strip()
-                names[num] = name
-
-            elif line.startswith('/NumContrasts'):
-                numContrasts = int(line.split()[1])
-
-            elif line == '/Matrix':
-                break
-
-        matrix = np.loadtxt(f)
-
-    if matrix       is None             or \
-       numContrasts != matrix.shape[0]:
-        raise RuntimeError('{} does not appear to be a '
-                           'valid design.con file'.format(designcon))
-
-    # Fill in any missing contrast names
-    if len(names) != numContrasts:
-        for i in range(numContrasts):
-            if i + 1 not in names:
-                names[i + 1] = str(i + 1)
-
-    names     = [names[c + 1] for c in range(numContrasts)]
-    contrasts = []
-
-    for row in matrix:
-        contrasts.append(list(row))
-
-    return names, contrasts
-
-
-def loadDesignFsf(designfsf):
-    """
-    """
-
-    settings = {}
-
-    with open(designfsf, 'rt') as f:
-
-        for line in f.readlines():
-            line = line.strip()
-
-            if not line.startswith('set '):
-                continue
-
-            tkns = line.split(None, 2)
-
-            key = tkns[1].strip()
-            val = tkns[2].strip().strip("'").strip('"')
-
-            settings[key] = val
-    
-    return settings
-
-
-def isFEATData(path):
-    
-    keys = ['.feat{}filtered_func_data' .format(op.sep),
-            '.gfeat{}filtered_func_data'.format(op.sep)]
-
-    isfeatdir = any([k in path for k in keys])
-
-    dirname   = op.dirname(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'))
-
-    isfeat    = (isfeatdir and
-                 hasdesmat and
-                 hasdescon and
-                 hasdesfsf)
-    
-    return isfeat
+import            featresults
 
 
 class FEATImage(fslimage.Image):
 
-    def __init__(self, image, **kwargs):
-        fslimage.Image.__init__(self, image, **kwargs)
-
-        if not isFEATData(self.dataSource):
+    def __init__(self, path, **kwargs):
+        """
+        The specified ``path`` may be a FEAT analysis directory, or the model
+        data input file (e.g. ``analysis.feat/filtered_func_data.nii.gz``).
+        """
+        
+        if not featresults.isFEATDir(path):
             raise ValueError('{} does not appear to be data from a '
-                             'FEAT analysis'.format(self.dataSource))
+                             'FEAT analysis'.format(path))
 
-        featDir     = op.dirname(self.dataSource)
-        settings    = loadDesignFsf(op.join(featDir, 'design.fsf'))
-        design      = loadDesignMat(op.join(featDir, 'design.mat'))
-        names, cons = loadDesignCon(op.join(featDir, 'design.con'))
+        featDir     = op.dirname(path)
+        settings    = featresults.loadSettings( featDir)
+        design      = featresults.loadDesign(   featDir)
+        names, cons = featresults.loadContrasts(featDir)
+        datafile    = featresults.getDataFile(  featDir)
+        
+        fslimage.Image.__init__(self, datafile, **kwargs)
 
         self.__analysisName  = op.splitext(op.basename(featDir))[0]
         self.__featDir       = featDir
@@ -157,58 +45,14 @@ class FEATImage(fslimage.Image):
         self.__contrastNames = names
         self.__contrasts     = cons
         self.__settings      = settings
-        self.__evNames       = self.__getEVNames()
+        self.__evNames       = featresults.getEVNames(settings)
 
         self.__residuals     =  None
         self.__pes           = [None] * self.numEVs()
         self.__copes         = [None] * self.numContrasts()
 
         if 'name' not in kwargs:
-            self.name = '{}.feat: {}'.format(
-                self.__analysisName, self.name)
-
-            
-    def __getEVNames(self):
-
-        numEVs = self.numEVs()
-        
-        titleKeys = filter(
-            lambda s: s.startswith('fmri(evtitle'),
-            self.__settings.keys())
-
-        derivKeys = filter(
-            lambda s: s.startswith('fmri(deriv_yn'),
-            self.__settings.keys())
-
-        evnames = []
-
-        for titleKey, derivKey in zip(titleKeys, derivKeys):
-
-            # Figure out the ev number from
-            # the design.fsf key - skip over
-            # 'fmri(evtitle' (an offset of 12)
-            evnum = int(titleKey[12:-1])
-
-            # Sanity check - the evnum
-            # for the deriv_yn key matches
-            # that for the evtitle key
-            if evnum != int(derivKey[13:-1]):
-                raise RuntimeError('design.fsf seem to be corrupt')
-
-            title = self.__settings[titleKey]
-            deriv = self.__settings[derivKey]
-
-            if deriv == '0':
-                evnames.append(title)
-            else:
-                evnames.append(title)
-                evnames.append('{} - {}'.format(title, 'temporal derivative'))
-
-        if len(evnames) != numEVs:
-            raise RuntimeError('The number of EVs in design.fsf does not '
-                               'match the number of EVs in design.mat')
-
-        return evnames
+            self.name = '{}: {}'.format(self.__analysisName, self.name)
 
 
     def getAnalysisName(self):
@@ -243,19 +87,14 @@ class FEATImage(fslimage.Image):
         return [list(c) for c in self.__contrasts]
 
 
-    def __getStatsFile(self, prefix, ev=None):
-
-        if ev is not None: prefix = '{}{}'.format(prefix, ev + 1)
-
-        prefix = op.join(self.__featDir, 'stats', prefix)
-        
-        return glob.glob('{}.*'.format(prefix))[0]
+    def clusterResults(self, contrast):
+        pass
 
 
     def getPE(self, ev):
 
         if self.__pes[ev] is None:
-            pefile = self.__getStatsFile('pe', ev)
+            pefile = featresults.getPEFile(self.__featDir, ev)
             self.__pes[ev] = nib.load(pefile).get_data()
 
         return self.__pes[ev]
@@ -264,19 +103,19 @@ class FEATImage(fslimage.Image):
     def getResiduals(self):
         
         if self.__residuals is None:
-            resfile          = self.__getStatsFile('res4d')
+            resfile = featresults.getResidualFile(self.__featDir)
             self.__residuals = nib.load(resfile).get_data()
         
         return self.__residuals
 
     
-    def getCOPE(self, num):
+    def getCOPE(self, con):
         
-        if self.__copes[num] is None:
-            copefile = self.__getStatsFile('cope', num)
-            self.__copes[num] = nib.load(copefile).get_data()
+        if self.__copes[con] is None:
+            copefile = featresults.getPEFile(self.__featDir, con)
+            self.__copes[con] = nib.load(copefile).get_data()
 
-        return self.__copes[num] 
+        return self.__copes[con] 
         
 
     def fit(self, contrast, xyz, fullmodel=False):
diff --git a/fsl/data/featresults.py b/fsl/data/featresults.py
new file mode 100644
index 000000000..23ef9c300
--- /dev/null
+++ b/fsl/data/featresults.py
@@ -0,0 +1,242 @@
+#!/usr/bin/env python
+#
+# featresults.py - Utility functions for loading/querying the contents of
+# a FEAT analysis directory.
+#
+# Author: Paul McCarthy <pauldmccarthy@gmail.com>
+#
+"""This module provides a few utility functions for loading/querying the
+contents of a FEAT analysis directory.
+"""
+
+
+import            glob
+import os.path as op
+import numpy   as np
+
+
+def isFEATDir(path):
+    """Returns ``True`` if the given path looks like a FEAT directory, or
+    looks like the input data for a FEAT analysis, ``False`` otherwise.
+    """
+
+    if op.isfile(path):
+
+        dirname, filename = op.splitext(path)
+
+        if not filename.startswith('filtered_func_data'):
+            return False
+
+    dirname = path
+    keys    = ['.feat',
+               '.gfeat',
+               '.feat{}' .format(op.sep),
+               '.gfeat{}'.format(op.sep)]
+
+    isfeatdir = any([path.endswith(k) for k in keys])
+
+    hasdesfsf = op.exists(op.join(dirname, 'design.fsf'))
+    hasdesmat = op.exists(op.join(dirname, 'design.mat'))
+    hasdescon = op.exists(op.join(dirname, 'design.con'))
+
+    isfeat    = (isfeatdir and
+                 hasdesmat and
+                 hasdescon and
+                 hasdesfsf)
+    
+    return isfeat
+
+
+def loadDesign(featdir):
+    """Loads the design matrix from a FEAT folder.
+
+    Returns a ``numpy`` array containing the design matrix data, where the
+    first dimension corresponds to the data points, and the second to the EVs.
+    """
+
+    matrix    = None 
+    designmat = op.join(featdir, 'design.mat')
+
+    with open(designmat, 'rt') as f:
+
+        while True:
+            line = f.readline()
+            if line.strip() == '/Matrix':
+                break
+
+        matrix = np.loadtxt(f)
+
+    if matrix is None or matrix.size == 0:
+        raise RuntimeError('{} does not appear to be a '
+                           'valid design.mat file'.format(designmat))
+
+    return matrix
+
+
+def loadContrasts(featdir):
+    """Loads the contrasts from a FEAT folder. Returns a tuple containing:
+    
+      - A dictionary of ``{contrastnum : name}`` mappings
+    
+      - A list of contrast vectors (each of which is a list itself).
+    """
+
+    matrix       = None
+    numContrasts = 0
+    names        = {}
+    designcon    = op.join(featdir, 'design.con')
+    
+    with open(designcon, 'rt') as f:
+
+        while True:
+            line = f.readline().strip()
+
+            if line.startswith('/ContrastName'):
+                tkns       = line.split(None, 1)
+                num        = [c for c in tkns[0] if c.isdigit()]
+                num        = int(''.join(num))
+                name       = tkns[1].strip()
+                names[num] = name
+
+            elif line.startswith('/NumContrasts'):
+                numContrasts = int(line.split()[1])
+
+            elif line == '/Matrix':
+                break
+
+        matrix = np.loadtxt(f)
+
+    if matrix       is None             or \
+       numContrasts != matrix.shape[0]:
+        raise RuntimeError('{} does not appear to be a '
+                           'valid design.con file'.format(designcon))
+
+    # Fill in any missing contrast names
+    if len(names) != numContrasts:
+        for i in range(numContrasts):
+            if i + 1 not in names:
+                names[i + 1] = str(i + 1)
+
+    names     = [names[c + 1] for c in range(numContrasts)]
+    contrasts = []
+
+    for row in matrix:
+        contrasts.append(list(row))
+
+    return names, contrasts
+
+
+def loadSettings(featdir):
+    """Loads the analysis settings from a a FEAT folder.
+
+    Returns a dict containing the settings specified in the given file.
+    """
+
+    settings  = {}
+    designfsf = op.join(featdir, 'design.fsf')
+
+    with open(designfsf, 'rt') as f:
+
+        for line in f.readlines():
+            line = line.strip()
+
+            if not line.startswith('set '):
+                continue
+
+            tkns = line.split(None, 2)
+
+            key = tkns[1].strip()
+            val = tkns[2].strip().strip("'").strip('"')
+
+            if key.startswith('fmri(') and key.endswith(')'):
+                key = key[5:-1]
+            
+            settings[key] = val
+    
+    return settings
+
+
+def getDataFile(featdir):
+    """Returns the name of the file in the FEAT results which contains
+    the model input data (typically called ``filtered_func_data.nii.gz``).
+    """
+    
+    # Assuming here that there is only
+    # one file called filtered_func_data.*
+    return glob.glob((op.join(featdir, 'filtered_func_data.*')))[0]
+
+
+def getResidualFile(featdir):
+    """Returns the name of the file in the FEAT results which contains
+    the model fit residuals (typically called ``res4d.nii.gz``).
+    """
+    
+    # Assuming here that there is only
+    # one file called stats/res4d.*
+    return glob.glob((op.join(featdir, 'stats', 'res4d.*')))[0]
+
+    
+def getPEFile(featdir, ev):
+    """Returns the path of the PE file for the specified ``ev``, which is
+    assumed to be 0-indexed. 
+    """
+
+    pefile = op.join(featdir, 'stats', 'pe{}.*'.format(ev + 1))
+    return glob.glob(pefile)[0]
+
+
+def getCOPEFile(featdir, contrast):
+    """Returns the path of the COPE file for the specified ``contrast``, which
+    is assumed to be 0-indexed. 
+    """
+    copefile = op.join(featdir, 'stats', 'cope{}.*'.format(contrast + 1))
+    return glob.glob(copefile)[0]
+
+
+def getEVNames(settings):
+    """Returns the names of every EV in the FEAT analysis which has the given
+    ``settings`` (see the :func:`loadSettings` function).
+    """
+
+    numEVs = int(settings['evs_real'])
+
+    titleKeys = filter(lambda s: s.startswith('evtitle'),  settings.keys())
+    derivKeys = filter(lambda s: s.startswith('deriv_yn'), settings.keys())
+
+    def _cmp(key1, key2):
+        key1 = ''.join([c for c in key1 if c.isdigit()])
+        key2 = ''.join([c for c in key2 if c.isdigit()])
+
+        return cmp(int(key1), int(key2))
+
+    titleKeys = sorted(titleKeys, cmp=_cmp)
+    derivKeys = sorted(derivKeys, cmp=_cmp)
+    evnames  = []
+
+    for titleKey, derivKey in zip(titleKeys, derivKeys):
+
+        # Figure out the ev number from
+        # the design.fsf key - skip over
+        # 'evtitle' (an offset of 7)
+        evnum = int(titleKey[7:])
+
+        # Sanity check - the evnum
+        # for the deriv_yn key matches
+        # that for the evtitle key
+        if evnum != int(derivKey[8:]):
+            raise RuntimeError('design.fsf seem to be corrupt')
+
+        title = settings[titleKey]
+        deriv = settings[derivKey]
+
+        if deriv == '0':
+            evnames.append(title)
+        else:
+            evnames.append(title)
+            evnames.append('{} - {}'.format(title, 'temporal derivative'))
+
+    if len(evnames) != numEVs:
+        raise RuntimeError('The number of EVs in design.fsf does not '
+                           'match the number of EVs in design.mat')
+
+    return evnames
diff --git a/fsl/fslview/overlay.py b/fsl/fslview/overlay.py
index d979a439c..6bf830d5f 100644
--- a/fsl/fslview/overlay.py
+++ b/fsl/fslview/overlay.py
@@ -17,10 +17,11 @@ import os.path as op
 
 import props
 
-import fsl.data.image     as fslimage
-import fsl.data.featimage as fslfeatimage
-import fsl.data.strings   as strings
-import fsl.data.model     as fslmodel
+import fsl.data.image       as fslimage
+import fsl.data.featresults as featresults
+import fsl.data.featimage   as fslfeatimage
+import fsl.data.strings     as strings
+import fsl.data.model       as fslmodel
 
 
 log = logging.getLogger(__name__)
@@ -140,8 +141,8 @@ class OverlayList(props.HasProperties):
 
 
 def guessDataSourceType(filename):
-    """A convenience function which, given the name of a file, figures out a
-    suitable data source type.
+    """A convenience function which, given the name of a file or directory,
+    figures out a suitable data source type.
 
     Returns a tuple containing two values - a type which should be able to
     load the filename, and the filename, possibly adjusted. If the file type
@@ -150,14 +151,22 @@ def guessDataSourceType(filename):
 
     if filename.endswith('.vtk'):
         return fslmodel.Model, filename
-    
+
     else:
-        filename = fslimage.addExt(filename, False)
-        if any([filename.endswith(e) for e in fslimage.ALLOWED_EXTENSIONS]):
-            if fslfeatimage.isFEATData(filename):
+
+        if op.isdir(filename):
+            if featresults.isFEATDir(filename):
                 return fslfeatimage.FEATImage, filename
-            else:
-                return fslimage.Image,         filename
+        else:
+        
+            filename = fslimage.addExt(filename, False)
+            if any([filename.endswith(e)
+                    for e in fslimage.ALLOWED_EXTENSIONS]):
+
+                if featresults.isFEATDir(filename):
+                    return fslfeatimage.FEATImage, filename
+                else:
+                    return fslimage.Image, filename
 
     return None, filename
 
-- 
GitLab