Skip to content
Snippets Groups Projects
Commit 7136ab8c authored by Paul McCarthy's avatar Paul McCarthy
Browse files

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.
parent 635fa0ee
No related branches found
No related tags found
No related merge requests found
...@@ -10,146 +10,34 @@ analysis. ...@@ -10,146 +10,34 @@ analysis.
""" """
import os.path as op import os.path as op
import glob
import numpy as np import numpy as np
import nibabel as nib import nibabel as nib
import image as fslimage import image as fslimage
import featresults
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
class FEATImage(fslimage.Image): class FEATImage(fslimage.Image):
def __init__(self, image, **kwargs): def __init__(self, path, **kwargs):
fslimage.Image.__init__(self, image, **kwargs) """
The specified ``path`` may be a FEAT analysis directory, or the model
if not isFEATData(self.dataSource): 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 ' raise ValueError('{} does not appear to be data from a '
'FEAT analysis'.format(self.dataSource)) 'FEAT analysis'.format(path))
featDir = op.dirname(self.dataSource) featDir = op.dirname(path)
settings = loadDesignFsf(op.join(featDir, 'design.fsf')) settings = featresults.loadSettings( featDir)
design = loadDesignMat(op.join(featDir, 'design.mat')) design = featresults.loadDesign( featDir)
names, cons = loadDesignCon(op.join(featDir, 'design.con')) names, cons = featresults.loadContrasts(featDir)
datafile = featresults.getDataFile( featDir)
fslimage.Image.__init__(self, datafile, **kwargs)
self.__analysisName = op.splitext(op.basename(featDir))[0] self.__analysisName = op.splitext(op.basename(featDir))[0]
self.__featDir = featDir self.__featDir = featDir
...@@ -157,58 +45,14 @@ class FEATImage(fslimage.Image): ...@@ -157,58 +45,14 @@ class FEATImage(fslimage.Image):
self.__contrastNames = names self.__contrastNames = names
self.__contrasts = cons self.__contrasts = cons
self.__settings = settings self.__settings = settings
self.__evNames = self.__getEVNames() self.__evNames = featresults.getEVNames(settings)
self.__residuals = None self.__residuals = None
self.__pes = [None] * self.numEVs() self.__pes = [None] * self.numEVs()
self.__copes = [None] * self.numContrasts() self.__copes = [None] * self.numContrasts()
if 'name' not in kwargs: if 'name' not in kwargs:
self.name = '{}.feat: {}'.format( self.name = '{}: {}'.format(self.__analysisName, self.name)
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
def getAnalysisName(self): def getAnalysisName(self):
...@@ -243,19 +87,14 @@ class FEATImage(fslimage.Image): ...@@ -243,19 +87,14 @@ class FEATImage(fslimage.Image):
return [list(c) for c in self.__contrasts] return [list(c) for c in self.__contrasts]
def __getStatsFile(self, prefix, ev=None): def clusterResults(self, contrast):
pass
if ev is not None: prefix = '{}{}'.format(prefix, ev + 1)
prefix = op.join(self.__featDir, 'stats', prefix)
return glob.glob('{}.*'.format(prefix))[0]
def getPE(self, ev): def getPE(self, ev):
if self.__pes[ev] is None: 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() self.__pes[ev] = nib.load(pefile).get_data()
return self.__pes[ev] return self.__pes[ev]
...@@ -264,19 +103,19 @@ class FEATImage(fslimage.Image): ...@@ -264,19 +103,19 @@ class FEATImage(fslimage.Image):
def getResiduals(self): def getResiduals(self):
if self.__residuals is None: if self.__residuals is None:
resfile = self.__getStatsFile('res4d') resfile = featresults.getResidualFile(self.__featDir)
self.__residuals = nib.load(resfile).get_data() self.__residuals = nib.load(resfile).get_data()
return self.__residuals return self.__residuals
def getCOPE(self, num): def getCOPE(self, con):
if self.__copes[num] is None: if self.__copes[con] is None:
copefile = self.__getStatsFile('cope', num) copefile = featresults.getPEFile(self.__featDir, con)
self.__copes[num] = nib.load(copefile).get_data() self.__copes[con] = nib.load(copefile).get_data()
return self.__copes[num] return self.__copes[con]
def fit(self, contrast, xyz, fullmodel=False): def fit(self, contrast, xyz, fullmodel=False):
......
#!/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
...@@ -17,10 +17,11 @@ import os.path as op ...@@ -17,10 +17,11 @@ import os.path as op
import props import props
import fsl.data.image as fslimage import fsl.data.image as fslimage
import fsl.data.featimage as fslfeatimage import fsl.data.featresults as featresults
import fsl.data.strings as strings import fsl.data.featimage as fslfeatimage
import fsl.data.model as fslmodel import fsl.data.strings as strings
import fsl.data.model as fslmodel
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -140,8 +141,8 @@ class OverlayList(props.HasProperties): ...@@ -140,8 +141,8 @@ class OverlayList(props.HasProperties):
def guessDataSourceType(filename): def guessDataSourceType(filename):
"""A convenience function which, given the name of a file, figures out a """A convenience function which, given the name of a file or directory,
suitable data source type. figures out a suitable data source type.
Returns a tuple containing two values - a type which should be able to 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 load the filename, and the filename, possibly adjusted. If the file type
...@@ -150,14 +151,22 @@ def guessDataSourceType(filename): ...@@ -150,14 +151,22 @@ def guessDataSourceType(filename):
if filename.endswith('.vtk'): if filename.endswith('.vtk'):
return fslmodel.Model, filename return fslmodel.Model, filename
else: else:
filename = fslimage.addExt(filename, False)
if any([filename.endswith(e) for e in fslimage.ALLOWED_EXTENSIONS]): if op.isdir(filename):
if fslfeatimage.isFEATData(filename): if featresults.isFEATDir(filename):
return fslfeatimage.FEATImage, filename return fslfeatimage.FEATImage, filename
else: else:
return fslimage.Image, filename
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 return None, filename
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment