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