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