From 234aa550e92b73a08a8e3fcd0ce62d7ade593e88 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Thu, 9 Jul 2015 11:30:25 +0100 Subject: [PATCH] Removed 'demean' option - replaced with 'plotMode' option which allows demeaning, or scaling to percent-changed. Added ability to plot data reduced against a PE/COPE, and the ability to hide the raw data plot for a currently-selected FEAT time series (as the plot can get pretty messy). --- fsl/data/featimage.py | 27 ++- fsl/data/strings.py | 24 ++- .../controls/timeseriescontrolpanel.py | 17 +- fsl/fslview/controls/timeserieslistpanel.py | 17 +- fsl/fslview/views/histogrampanel.py | 5 +- fsl/fslview/views/timeseriespanel.py | 184 ++++++++++++------ 6 files changed, 180 insertions(+), 94 deletions(-) diff --git a/fsl/data/featimage.py b/fsl/data/featimage.py index b5eb96ea8..4346d51c8 100644 --- a/fsl/data/featimage.py +++ b/fsl/data/featimage.py @@ -221,6 +221,13 @@ class FEATImage(fslimage.Image): def fit(self, contrast, xyz, fullmodel=False): + """ + + Passing in a contrast of all 1s, and ``fullmodel=True`` will + get you the full model fit. Pass in ``fullmodel=False`` for + all other contrasts, otherwise the model fit values will not + be scaled correctly. + """ if not fullmodel: contrast = np.array(contrast) @@ -245,24 +252,14 @@ class FEATImage(fslimage.Image): def reducedData(self, xyz, contrast, fullmodel=False): + """ + + Passing in a contrast of all 1s, and ``fullmodel=True`` will + get you the model fit residuals. + """ x, y, z = xyz residuals = self.getResiduals()[x, y, z, :] modelfit = self.fit(contrast, xyz, fullmodel) return residuals + modelfit - - - # def getThresholdedZStats(self): - # pass - - - # def getSomethingForClusters(self): - # pass - - - # # Return a copy of this image, transformed - # # to the specified spaced (e.g. MNI152, - # # structural, functional, etc) - # def getInSpace(self, space): - # pass diff --git a/fsl/data/strings.py b/fsl/data/strings.py index d3f168057..3ef12575f 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -218,10 +218,16 @@ labels = TypeDict({ 'TimeSeriesControlPanel.currentFEATSettings' : 'FEAT settings for ' 'selected overlay ({})', + 'TimeSeriesListPanel.featReduced' : 'Reduced against {}', + 'FEATModelFitTimeSeries.full' : 'Full model fit', 'FEATModelFitTimeSeries.cope' : 'COPE{} fit: {}', 'FEATModelFitTimeSeries.pe' : 'PE{} fit', - + + 'FEATReducedTimeSeries.cope' : 'Reduced against COPE{}: {}', + 'FEATReducedTimeSeries.pe' : 'Reduced against PE{}', + + 'FEATResidualTimeSeries' : 'Residuals', }) @@ -265,7 +271,7 @@ properties = TypeDict({ 'PlotPanel.xlabel' : 'X label', 'PlotPanel.ylabel' : 'Y label', - 'TimeSeriesPanel.demean' : 'Demean', + 'TimeSeriesPanel.plotMode' : 'Plotting mode', 'TimeSeriesPanel.usePixdim' : 'Use pixdims', 'TimeSeriesPanel.showCurrent' : 'Plot time series for current voxel', 'TimeSeriesPanel.currentColour' : 'Colour for current time course', @@ -290,8 +296,9 @@ 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', + 'FEATTimeSeries.plotResiduals' : 'Plot residuals', + 'FEATTimeSeries.plotReduced' : 'Plot data reduced against', + 'FEATTimeSeries.plotData' : 'Plot data', 'OrthoEditProfile.selectionSize' : 'Selection size', 'OrthoEditProfile.selectionIs3D' : '3D selection', @@ -420,7 +427,14 @@ choices = TypeDict({ 'Display.overlayType.label' : 'Label image', 'Display.overlayType.rgbvector' : '3-direction vector image (RGB)', 'Display.overlayType.linevector' : '3-direction vector image (Line)', - 'Display.overlayType.model' : '3D model' + 'Display.overlayType.model' : '3D model', + + 'HistogramPanel.histType.probability' : 'Probability', + 'HistogramPanel.histType.count' : 'Count', + + 'TimeSeriesPanel.plotMode.normal' : 'Normal', + 'TimeSeriesPanel.plotMode.demean' : 'Demeaned', + 'TimeSeriesPanel.plotMode.percentChange' : 'Percent changed', }) diff --git a/fsl/fslview/controls/timeseriescontrolpanel.py b/fsl/fslview/controls/timeseriescontrolpanel.py index f3302a830..bf4a6082c 100644 --- a/fsl/fslview/controls/timeseriescontrolpanel.py +++ b/fsl/fslview/controls/timeseriescontrolpanel.py @@ -27,7 +27,7 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel): self.SetSizer(self.__sizer) self.__sizer.Add(self.__widgets, flag=wx.EXPAND, proportion=1) - tsProps = ['demean', + tsProps = ['plotMode', 'usePixdim', 'showCurrent'] plotProps = ['xLogScale', @@ -201,8 +201,13 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel): 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') - + reduced = props.makeWidget( self.__widgets, ts, 'plotReduced') + data = props.makeWidget( self.__widgets, ts, 'plotData') + + self.__widgets.AddWidget( + data, + displayName=strings.properties[ts, 'plotData'], + groupName='currentFEATSettings') self.__widgets.AddWidget( full, displayName=strings.properties[ts, 'plotFullModelFit'], @@ -214,9 +219,9 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel): groupName='currentFEATSettings') self.__widgets.AddWidget( - reduce, - displayName=strings.properties[ts, 'reduceAgainst'], - groupName='currentFEATSettings') + reduced, + displayName=strings.properties[ts, 'plotReduced'], + groupName='currentFEATSettings') for i, pe in enumerate(pes): self.__widgets.AddWidget( diff --git a/fsl/fslview/controls/timeserieslistpanel.py b/fsl/fslview/controls/timeserieslistpanel.py index df7b1ea55..7fbefc46d 100644 --- a/fsl/fslview/controls/timeserieslistpanel.py +++ b/fsl/fslview/controls/timeserieslistpanel.py @@ -102,13 +102,21 @@ class TimeSeriesListPanel(fslpanel.FSLViewPanel): def __makeLabel(self, ts): + return '{} [{} {} {}]'.format(ts.overlay.name, ts.coords[0], ts.coords[1], ts.coords[2]) - def __makeFEATModelFitLabel(self, parentTs, modelTs): + def __makeFEATModelTSLabel(self, parentTs, modelTs): + + import fsl.fslview.views.timeseriespanel as tsp + + if isinstance(modelTs, tsp.FEATResidualTimeSeries): + return '{} ({})'.format( + parentTs.label, + strings.labels[modelTs]) label = '{} ({})'.format( parentTs.label, @@ -156,7 +164,7 @@ class TimeSeriesListPanel(fslpanel.FSLViewPanel): if ts is None: return - ts = copy.copy(ts) + ts = copy.copy(ts) ts.alpha = 1 ts.lineWidth = 2 @@ -167,13 +175,16 @@ class TimeSeriesListPanel(fslpanel.FSLViewPanel): self.__tsPanel.dataSeries.append(ts) if isinstance(ts, tsp.FEATTimeSeries): + modelTs = ts.getModelTimeSeries() + modelTs.remove(ts) for mts in modelTs: + mts.alpha = 1 mts.lineWidth = 2 mts.lineStyle = '-' - mts.label = self.__makeFEATModelFitLabel(ts, mts) + mts.label = self.__makeFEATModelTSLabel(ts, mts) self.__tsPanel.dataSeries.extend(modelTs) diff --git a/fsl/fslview/views/histogrampanel.py b/fsl/fslview/views/histogrampanel.py index 865b4960d..cfcf7c998 100644 --- a/fsl/fslview/views/histogrampanel.py +++ b/fsl/fslview/views/histogrampanel.py @@ -341,7 +341,10 @@ class HistogramPanel(plotpanel.PlotPanel): autoBin = props.Boolean(default=True) showCurrent = props.Boolean(default=True) - histType = props.Choice(('probability', 'count')) + histType = props.Choice( + ('probability', 'count'), + labels=[strings.choices['HistogramPanel.histType.probability'], + strings.choices['HistogramPanel.histType.count']]) selectedSeries = props.Int(minval=0, clamped=True) diff --git a/fsl/fslview/views/timeseriespanel.py b/fsl/fslview/views/timeseriespanel.py index 4ddb0a88a..aad982f89 100644 --- a/fsl/fslview/views/timeseriespanel.py +++ b/fsl/fslview/views/timeseriespanel.py @@ -22,6 +22,7 @@ import props import plotpanel import fsl.data.featimage as fslfeatimage import fsl.data.image as fslimage +import fsl.data.strings as strings import fsl.fslview.displaycontext as fsldisplay import fsl.fslview.controls as fslcontrols import fsl.utils.transform as transform @@ -70,9 +71,13 @@ class TimeSeries(plotpanel.DataSeries): if self.tsPanel.usePixdim: xdata *= self.overlay.pixdim[3] - if self.tsPanel.demean: + if self.tsPanel.plotMode == 'demean': ydata = ydata - ydata.mean() + elif self.tsPanel.plotMode == 'percentChange': + mean = ydata.mean() + ydata = 100 * (ydata / mean) - 100 + return xdata, ydata @@ -82,13 +87,14 @@ class FEATTimeSeries(TimeSeries): containing some extra FEAT specific options. """ - + + plotData = props.Boolean(default=True) plotFullModelFit = props.Boolean(default=False) plotResiduals = props.Boolean(default=False) plotPEFits = props.List(props.Boolean(default=False)) plotCOPEFits = props.List(props.Boolean(default=False)) - reduceAgainst = props.Choice() - + plotReduced = props.Choice() + def __init__(self, *args, **kwargs): TimeSeries.__init__(self, *args, **kwargs) @@ -105,7 +111,7 @@ class FEATTimeSeries(TimeSeries): name = 'COPE{} ({})'.format(i + 1, copeNames[i]) reduceOpts.append(name) - self.getProp('reduceAgainst').setChoices(reduceOpts, instance=self) + self.getProp('plotReduced').setChoices(reduceOpts, instance=self) for i in range(numEVs): self.plotPEFits.append(False) @@ -114,6 +120,7 @@ class FEATTimeSeries(TimeSeries): self.plotCOPEFits.append(False) self.__fullModelTs = None + self.__reducedTs = None self.__resTs = None self.__peTs = [None] * numEVs self.__copeTs = [None] * numCOPEs @@ -121,10 +128,12 @@ class FEATTimeSeries(TimeSeries): self.addListener('plotFullModelFit', self.name, self.__plotFullModelFitChanged) - self.addListener('plotResiduals', self.name, self.__plotResidualsChanged) + self.addListener('plotReduced', + self.name, + self.__plotReducedChanged) for i, plotPEFit in enumerate( self.plotPEFits.getPropertyValueList()): @@ -142,30 +151,9 @@ 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) copy.colour = self.colour @@ -181,31 +169,77 @@ class FEATTimeSeries(TimeSeries): copy.plotFullModelFit = self.plotFullModelFit copy.plotPEFits[ :] = self.plotPEFits[ :] copy.plotCOPEFits[:] = self.plotCOPEFits[:] + copy.plotReduced = self.plotReduced + copy.plotResiduals = self.plotResiduals return copy def getModelTimeSeries(self): + modelts = [] - if self.plotFullModelFit: - modelts.append(self.__fullModelTs) - - if self.plotResiduals: - modelts.append(self.__resTs) - + if self.plotData: modelts.append(self) + if self.plotFullModelFit: modelts.append(self.__fullModelTs) + if self.plotResiduals: modelts.append(self.__resTs) + if self.plotReduced != 'none': modelts.append(self.__reducedTs) + for i in range(self.overlay.numEVs()): if self.plotPEFits[i]: modelts.append(self.__peTs[i]) for i in range(self.overlay.numContrasts()): if self.plotCOPEFits[i]: - modelts.append(self.__copeTs[i]) - + modelts.append(self.__copeTs[i]) + return modelts + def __getContrast(self, fitType, idx): + + if fitType == 'full': + return [1] * self.overlay.numEVs() + elif fitType == 'pe': + con = [0] * self.overlay.numEVs() + con[idx] = 1 + return con + elif fitType == 'cope': + return self.overlay.getContrasts()[idx] + + + def __plotReducedChanged(self, *a): + + reduced = self.plotReduced + + if reduced == 'none' and self.__reducedTs is not None: + self.__reducedTs = None + return + + reduced = reduced.split()[0] + + # fitType is either 'cope' or 'pe' + fitType = reduced[:-1].lower() + idx = int(reduced[-1]) - 1 + + rts = FEATReducedTimeSeries( + self.__getContrast(fitType, idx), + fitType, + idx, + self.tsPanel, + self.overlay, + self.coords) + + rts.colour = (0, 0.6, 0.6) + rts.alpha = self.alpha + rts.label = self.label + rts.lineWidth = self.lineWidth + rts.lineStyle = self.lineStyle + + self.__reducedTs = rts + + def __plotResidualsChanged(self, *a): + if not self.plotResiduals: self.__resTs = None return @@ -225,14 +259,13 @@ class FEATTimeSeries(TimeSeries): def __plotCOPEFitChanged(self, copenum): + if not self.plotCOPEFits[copenum]: self.__copeTs[copenum] = None return - con = self.overlay.contrasts()[copenum] - copets = FEATModelFitTimeSeries( - con, + self.__getContrast('cope', copenum), 'cope', copenum, self.tsPanel, @@ -249,21 +282,19 @@ class FEATTimeSeries(TimeSeries): def __plotPEFitChanged(self, evnum): + if not self.plotPEFits[evnum]: self.__peTs[evnum] = None return - con = [0] * self.overlay.numEVs() - con[evnum] = 1 - pets = FEATModelFitTimeSeries( - con, + self.__getContrast('pe', evnum), 'pe', evnum, self.tsPanel, self.overlay, self.coords) - + pets.colour = (0.7, 0, 0) pets.alpha = self.alpha pets.label = self.label @@ -274,34 +305,55 @@ class FEATTimeSeries(TimeSeries): def __plotFullModelFitChanged(self, *a): + if not self.plotFullModelFit: self.__fullModelTs = None return - self.__fullModelTs = FEATModelFitTimeSeries( - [1] * self.overlay.numEVs(), + fts = FEATModelFitTimeSeries( + self.__getContrast('full', -1), 'full', -1, self.tsPanel, self.overlay, self.coords) - self.__fullModelTs.colour = (0, 0, 1) - self.__fullModelTs.alpha = self.alpha - self.__fullModelTs.label = self.label - self.__fullModelTs.lineWidth = self.lineWidth - self.__fullModelTs.lineStyle = self.lineStyle + + fts.colour = (0, 0, 1) + fts.alpha = self.alpha + fts.label = self.label + fts.lineWidth = self.lineWidth + fts.lineStyle = self.lineStyle + + self.__fullModelTs = fts def update(self, coords): + if not TimeSeries.update(self, coords): return False for modelTs in self.getModelTimeSeries(): + if modelTs is self: + continue modelTs.update(coords) return True +class FEATReducedTimeSeries(TimeSeries): + def __init__(self, contrast, fitType, idx, *args, **kwargs): + TimeSeries.__init__(self, *args, **kwargs) + + self.contrast = contrast + self.fitType = fitType + self.idx = idx + + def getData(self): + + data = self.overlay.reducedData(self.coords, self.contrast, False) + return TimeSeries.getData(self, ydata=data) + + class FEATResidualTimeSeries(TimeSeries): def getData(self): x, y, z = self.coords @@ -311,7 +363,6 @@ class FEATResidualTimeSeries(TimeSeries): class FEATModelFitTimeSeries(TimeSeries): - def __init__(self, contrast, fitType, idx, *args, **kwargs): @@ -348,12 +399,13 @@ class TimeSeriesPanel(plotpanel.PlotPanel): """ - demean = props.Boolean(default=True) usePixdim = props.Boolean(default=False) showCurrent = props.Boolean(default=True) - - # TODO - percentChange = props.Boolean(default=False) + plotMode = props.Choice( + ('normal', 'demean', 'percentChange'), + labels=[strings.choices['TimeSeriesPanel.plotMode.normal'], + strings.choices['TimeSeriesPanel.plotMode.demean'], + strings.choices['TimeSeriesPanel.plotMode.percentChange']]) currentColour = copy.copy(TimeSeries.colour) currentAlpha = copy.copy(TimeSeries.alpha) @@ -392,7 +444,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel): displayCtx .addListener('selectedOverlay', self._name, self.draw) displayCtx .addListener('location', self._name, self.draw) - self.addListener('demean', self._name, self.draw) + self.addListener('plotMode', self._name, self.draw) self.addListener('usePixdim', self._name, self.draw) self.addListener('showCurrent', self._name, self.draw) @@ -413,11 +465,15 @@ class TimeSeriesPanel(plotpanel.PlotPanel): return tss = [self.__currentTs] + if isinstance(self.__currentTs, FEATTimeSeries): - tss.extend(self.__currentTs.getModelTimeSeries()) + tss = self.__currentTs.getModelTimeSeries() for ts in tss: + if ts is self.__currentTs: + continue + # Don't change the colour for associated # time courses (e.g. model fits) if ts is self.__currentTs: @@ -431,7 +487,7 @@ class TimeSeriesPanel(plotpanel.PlotPanel): def destroy(self): plotpanel.PlotPanel.destroy(self) - self.removeListener('demean', self._name) + self.removeListener('plotMode', self._name) self.removeListener('usePixdim', self._name) self.removeListener('showCurrent', self._name) @@ -524,12 +580,12 @@ class TimeSeriesPanel(plotpanel.PlotPanel): if self.showCurrent and \ current is not None: - - extras = [current] - + if isinstance(current, FEATTimeSeries): - extras += current.getModelTimeSeries() - + extras = current.getModelTimeSeries() + else: + extras = [current] + self.drawDataSeries(extras) else: self.drawDataSeries() -- GitLab