From 118dbbc8e2ae1f415172fc79620f1d8269e277aa Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Wed, 8 Jul 2015 16:53:06 +0100 Subject: [PATCH] Ability to plot 'reduced' data (with respect to a specific PE/COPE), and residuals. --- fsl/data/featimage.py | 33 +++++-- fsl/data/strings.py | 2 + .../controls/timeseriescontrolpanel.py | 13 +++ fsl/fslview/views/timeseriespanel.py | 89 +++++++++++++++++-- 4 files changed, 124 insertions(+), 13 deletions(-) diff --git a/fsl/data/featimage.py b/fsl/data/featimage.py index 384dfd0a5..b5eb96ea8 100644 --- a/fsl/data/featimage.py +++ b/fsl/data/featimage.py @@ -154,6 +154,7 @@ class FEATImage(fslimage.Image): self.__contrasts = cons self.__settings = settings + self.__residuals = None self.__pes = [None] * self.numEVs() self.__copes = [None] * self.numContrasts() @@ -183,24 +184,37 @@ class FEATImage(fslimage.Image): return [list(c) for c in self.__contrasts] - def __getPEFile(self, prefix, ev): - prefix = op.join(self.__featDir, 'stats', '{}{}'.format( - prefix, ev + 1)) + 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 getPE(self, ev): if self.__pes[ev] is None: - pefile = self.__getPEFile('pe', ev) + pefile = self.__getStatsFile('pe', ev) self.__pes[ev] = nib.load(pefile).get_data() return self.__pes[ev] + + def getResiduals(self): + + if self.__residuals is None: + resfile = self.__getStatsFile('res4d') + self.__residuals = nib.load(resfile).get_data() + + return self.__residuals + def getCOPE(self, num): + if self.__copes[num] is None: - copefile = self.__getPEFile('cope', num) + copefile = self.__getStatsFile('cope', num) self.__copes[num] = nib.load(copefile).get_data() return self.__copes[num] @@ -229,6 +243,15 @@ class FEATImage(fslimage.Image): return modelfit + data.mean() + + def reducedData(self, xyz, contrast, fullmodel=False): + + x, y, z = xyz + residuals = self.getResiduals()[x, y, z, :] + modelfit = self.fit(contrast, xyz, fullmodel) + + return residuals + modelfit + # def getThresholdedZStats(self): # pass diff --git a/fsl/data/strings.py b/fsl/data/strings.py index 01a649224..d3f168057 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -290,6 +290,8 @@ properties = TypeDict({ 'FEATTimeSeries.plotFullModelFit' : 'Plot full model fit', 'FEATTimeSeries.plotPEFits' : 'Plot PE{} fit', 'FEATTimeSeries.plotCOPEFits' : 'Plot COPE{} fit ({})', + 'FEATTimeSeries.plotResiduals' : 'Show residuals', + 'FEATTimeSeries.reduceAgainst' : 'Reduce data against', 'OrthoEditProfile.selectionSize' : 'Selection size', 'OrthoEditProfile.selectionIs3D' : '3D selection', diff --git a/fsl/fslview/controls/timeseriescontrolpanel.py b/fsl/fslview/controls/timeseriescontrolpanel.py index bdec0f8e0..f3302a830 100644 --- a/fsl/fslview/controls/timeseriescontrolpanel.py +++ b/fsl/fslview/controls/timeseriescontrolpanel.py @@ -198,12 +198,25 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel): display.name)) full = props.makeWidget( self.__widgets, ts, 'plotFullModelFit') + res = props.makeWidget( self.__widgets, ts, 'plotResiduals') pes = props.makeListWidgets(self.__widgets, ts, 'plotPEFits') copes = props.makeListWidgets(self.__widgets, ts, 'plotCOPEFits') + reduce = props.makeWidget( self.__widgets, ts, 'reduceAgainst') + self.__widgets.AddWidget( full, displayName=strings.properties[ts, 'plotFullModelFit'], groupName='currentFEATSettings') + + self.__widgets.AddWidget( + res, + displayName=strings.properties[ts, 'plotResiduals'], + groupName='currentFEATSettings') + + self.__widgets.AddWidget( + reduce, + displayName=strings.properties[ts, 'reduceAgainst'], + groupName='currentFEATSettings') for i, pe in enumerate(pes): self.__widgets.AddWidget( diff --git a/fsl/fslview/views/timeseriespanel.py b/fsl/fslview/views/timeseriespanel.py index e5c26f64c..4ddb0a88a 100644 --- a/fsl/fslview/views/timeseriespanel.py +++ b/fsl/fslview/views/timeseriespanel.py @@ -56,9 +56,16 @@ class TimeSeries(plotpanel.DataSeries): return True - def getData(self): - ydata = np.array( self.data, dtype=np.float32) - xdata = np.arange(len(ydata), dtype=np.float32) + def getData(self, xdata=None, ydata=None): + """ + + :arg xdata: + :arg ydata: Used by subclasses in case they have already done some + processing on the data. + """ + + if xdata is None: xdata = np.arange(len(self.data), dtype=np.float32) + if ydata is None: ydata = np.array( self.data, dtype=np.float32) if self.tsPanel.usePixdim: xdata *= self.overlay.pixdim[3] @@ -77,19 +84,28 @@ class FEATTimeSeries(TimeSeries): plotFullModelFit = props.Boolean(default=False) + plotResiduals = props.Boolean(default=False) plotPEFits = props.List(props.Boolean(default=False)) plotCOPEFits = props.List(props.Boolean(default=False)) - - # TODO 'None', or any PE/COPE - reduceDataAgainst = props.Choice() + reduceAgainst = props.Choice() def __init__(self, *args, **kwargs): TimeSeries.__init__(self, *args, **kwargs) self.name = '{}_{}'.format(type(self).__name__, id(self)) - numEVs = self.overlay.numEVs() - numCOPEs = self.overlay.numContrasts() + numEVs = self.overlay.numEVs() + numCOPEs = self.overlay.numContrasts() + copeNames = self.overlay.contrastNames() + + reduceOpts = ['none'] + \ + ['PE{}'.format(i + 1) for i in range(numEVs)] + + for i in range(numCOPEs): + name = 'COPE{} ({})'.format(i + 1, copeNames[i]) + reduceOpts.append(name) + + self.getProp('reduceAgainst').setChoices(reduceOpts, instance=self) for i in range(numEVs): self.plotPEFits.append(False) @@ -98,12 +114,17 @@ class FEATTimeSeries(TimeSeries): self.plotCOPEFits.append(False) self.__fullModelTs = None + self.__resTs = None self.__peTs = [None] * numEVs self.__copeTs = [None] * numCOPEs self.addListener('plotFullModelFit', self.name, self.__plotFullModelFitChanged) + + self.addListener('plotResiduals', + self.name, + self.__plotResidualsChanged) for i, plotPEFit in enumerate( self.plotPEFits.getPropertyValueList()): @@ -121,6 +142,28 @@ class FEATTimeSeries(TimeSeries): plotCOPEFit.addListener(self.name, onChange) + + def getData(self): + + reduce = self.reduceAgainst + + if reduce == 'none': + data = None + + else: + idx = int(reduce.split()[0][-1]) - 1 + numEVs = self.overlay.numEVs() + + if reduce.startswith('PE'): + contrast = [0] * numEVs + contrast[idx] = 1 + else: + contrast = self.overlay.contrasts()[idx] + + data = self.overlay.reducedData(self.coords, contrast, False) + + return TimeSeries.getData(self, ydata=data) + def __copy__(self): copy = type(self)(self.tsPanel, self.overlay, self.coords) @@ -148,6 +191,9 @@ class FEATTimeSeries(TimeSeries): if self.plotFullModelFit: modelts.append(self.__fullModelTs) + if self.plotResiduals: + modelts.append(self.__resTs) + for i in range(self.overlay.numEVs()): if self.plotPEFits[i]: modelts.append(self.__peTs[i]) @@ -158,6 +204,25 @@ class FEATTimeSeries(TimeSeries): return modelts + + def __plotResidualsChanged(self, *a): + if not self.plotResiduals: + self.__resTs = None + return + + rts = FEATResidualTimeSeries( + self.tsPanel, + self.overlay, + self.coords) + + rts.colour = (0.8, 0.4, 0) + rts.alpha = self.alpha + rts.label = self.label + rts.lineWidth = self.lineWidth + rts.lineStyle = self.lineStyle + + self.__resTs = rts + def __plotCOPEFitChanged(self, copenum): if not self.plotCOPEFits[copenum]: @@ -237,6 +302,14 @@ class FEATTimeSeries(TimeSeries): return True +class FEATResidualTimeSeries(TimeSeries): + def getData(self): + x, y, z = self.coords + data = self.overlay.getResiduals()[x, y, z, :] + + return TimeSeries.getData(self, ydata=data) + + class FEATModelFitTimeSeries(TimeSeries): -- GitLab