diff --git a/fsl/data/featresults.py b/fsl/data/featresults.py index 719092a03771d78867d36c69ae7d241fa2231b59..138c0379e11563504f39f4aac6ece0cb44db0036 100644 --- a/fsl/data/featresults.py +++ b/fsl/data/featresults.py @@ -134,7 +134,7 @@ def loadDesign(featdir): if line.strip() == '/Matrix': break - matrix = np.loadtxt(f) + matrix = np.loadtxt(f, ndmin=2) if matrix is None or matrix.size == 0: raise RuntimeError('{} does not appear to be a ' diff --git a/fsl/data/melodicimage.py b/fsl/data/melodicimage.py index 1456386c9689ed78fe4a14e2ca8294b76fbb637a..3a2087da9a028717e832374be773ff89d3d07dfc 100644 --- a/fsl/data/melodicimage.py +++ b/fsl/data/melodicimage.py @@ -31,6 +31,7 @@ class MelodicImage(fslimage.Image): tr getComponentTimeSeries numComponents + getTopLevelAnalysisDir getDataFile """ @@ -98,6 +99,15 @@ class MelodicImage(fslimage.Image): return self.shape[3] + def getTopLevelAnalysisDir(self): + """Returns the top level analysis, if the melodic analysis for this + ``MelodicImage`` is contained within another analysis. Otherwise, + returnsa ``None``. See the + :func:`.melodicresults.getTopLevelAnalysisDir` function. + """ + return melresults.getTopLevelAnalysisDir(self.__meldir) + + def getDataFile(self): """Returns the file name of the data image from which this ``MelodicImage`` was generated, if possible. See the diff --git a/fsl/data/melodicresults.py b/fsl/data/melodicresults.py index f020003fb06c67c371c91ff0a69f756c35b7ebbf..c31811723bd78e2ce8036072d3c5d3f0e19734d5 100644 --- a/fsl/data/melodicresults.py +++ b/fsl/data/melodicresults.py @@ -142,6 +142,10 @@ def getMixFile(meldir): return op.join(meldir, 'melodic_mix') +def getReportFile(meldir): + pass + + def getNumComponents(meldir): """Returns the number of components generated in the melodic analysis contained in the given directrory. diff --git a/fsl/data/strings.py b/fsl/data/strings.py index 27ec5ed61ebbe336a80ad5fae945d5651d4660c7..ba3d4892673728f3355987b81ad0675045178298 100644 --- a/fsl/data/strings.py +++ b/fsl/data/strings.py @@ -325,13 +325,17 @@ labels = TypeDict({ 'OverlayInfoPanel.Image.transform' : 'Transform/space', 'OverlayInfoPanel.Image.orient' : 'Orientation', - 'OverlayInfoPanel.Image' : 'NIFTI1 image', - 'OverlayInfoPanel.FEATImage' : 'NIFTI1 image (FEAT analysis)', - 'OverlayInfoPanel.FEATImage.featInfo' : 'FEAT information', - 'OverlayInfoPanel.Model' : 'VTK model', - 'OverlayInfoPanel.Model.numVertices' : 'Number of vertices', - 'OverlayInfoPanel.Model.numIndices' : 'Number of indices', - 'OverlayInfoPanel.dataSource' : 'Data source', + 'OverlayInfoPanel.Image' : 'NIFTI1 image', + 'OverlayInfoPanel.FEATImage' : 'NIFTI1 image ' + '(FEAT analysis)', + 'OverlayInfoPanel.FEATImage.featInfo' : 'FEAT information', + 'OverlayInfoPanel.MelodicImage' : 'NIFTI1 image ' + '(MELODIC analysis)', + 'OverlayInfoPanel.MelodicImage.melodicInfo' : 'MELODIC information', + 'OverlayInfoPanel.Model' : 'VTK model', + 'OverlayInfoPanel.Model.numVertices' : 'Number of vertices', + 'OverlayInfoPanel.Model.numIndices' : 'Number of indices', + 'OverlayInfoPanel.dataSource' : 'Data source', }) @@ -708,4 +712,14 @@ feat = TypeDict({ 'numPoints' : 'Number of volumes', 'numEVs' : 'Number of EVs', 'numContrasts' : 'Number of contrasts', + 'report' : 'Link to report', +}) + + +melodic = TypeDict({ + 'dataFile' : 'Data file', + 'partOfAnalysis' : 'Part of analysis', + 'numComponents' : 'Number of ICs', + 'tr' : 'TR time', + 'report' : 'Link to report', }) diff --git a/fsl/fsleyes/colourmaps.py b/fsl/fsleyes/colourmaps.py index 46e1a2f1b07ceb429367d8cfc18b728280ac1d5f..0589151de45b3242394a6b55c768d8af5991bd9c 100644 --- a/fsl/fsleyes/colourmaps.py +++ b/fsl/fsleyes/colourmaps.py @@ -626,7 +626,7 @@ def randomBrightColour(): def randomDarkColour(): """Generates a random saturated and darkened RGB colour.""" - return applyBricon(randomBrightColour(), 0.25, 0.5) + return applyBricon(randomBrightColour(), 0.35, 0.5) def complementaryColour(rgb): diff --git a/fsl/fsleyes/controls/overlayinfopanel.py b/fsl/fsleyes/controls/overlayinfopanel.py index 8f15f85342208b794a0aca449b20e9df794764a2..f44baf5900f0e7b90c77f85db9a6a8b65d74415f 100644 --- a/fsl/fsleyes/controls/overlayinfopanel.py +++ b/fsl/fsleyes/controls/overlayinfopanel.py @@ -30,14 +30,15 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): :scale: 50% :align: center - Slightly different informtion is shown depending on the overlay type, + Slightly different information is shown depending on the overlay type, and is generated by the following methods: - =================== ========================== - :class:`.Image` :meth:`__getImageInfo` - :class:`.FEATImage` :meth:`__getFEATImageInfo` - :class:`.Model` :meth:`__getModelInfo` - =================== ========================== + ====================== ============================= + :class:`.Image` :meth:`__getImageInfo` + :class:`.FEATImage` :meth:`__getFEATImageInfo` + :class:`.MelodicImage` :meth:`__getMelodicImageInfo` + :class:`.Model` :meth:`__getModelInfo` + ====================== ============================= """ @@ -293,6 +294,33 @@ class OverlayInfoPanel(fslpanel.FSLEyesPanel): return info + + def __getMelodicImageInfo(self, overlay, display): + """Creates and returns an :class:`OverlayInfo` object containing + information about the given :class:`.MelodicImage` overlay. + + :arg overlay: A :class:`.MelodicImage` instance. + :arg display: The :class:`.Display` instance assocated with the + ``MelodicImage``. + """ + + info = self.__getImageInfo(overlay, display) + + melInfo = collections.OrderedDict([ + ('tr', overlay.tr), + ('dataFile', overlay.getDataFile()), + ('partOfAnalysis', overlay.getTopLevelAnalysisDir()), + ('numComponents', overlay.numComponents()), + ]) + + secName = strings.labels[self, overlay, 'melodicInfo'] + info.addSection(secName) + + for k, v in melInfo.items(): + info.addInfo(strings.melodic[k], v, section=secName) + + return info + def __getModelInfo(self, overlay, display): """Creates and returns an :class:`OverlayInfo` object containing diff --git a/fsl/fsleyes/controls/timeserieslistpanel.py b/fsl/fsleyes/controls/timeserieslistpanel.py index c3d500fb48be3bc98aa70c8d1b0947d43b7ea45d..c211d35b9c7f7d5c0d98ebf94ddf4c53ac4b15c4 100644 --- a/fsl/fsleyes/controls/timeserieslistpanel.py +++ b/fsl/fsleyes/controls/timeserieslistpanel.py @@ -183,7 +183,7 @@ class TimeSeriesListPanel(fslpanel.FSLEyesPanel): copy.setData(*ts.getData()) # This is hacky, and is here in order to - # make the __onLIstSelect method work. + # make the __onListSelect method work. if isinstance(ts, timeseries.MelodicTimeSeries): copy.tsLoc = 'volume' copy.coord = ts.getComponent() diff --git a/fsl/fsleyes/overlay.py b/fsl/fsleyes/overlay.py index 62d6c1890340963d47df8ff687d053a4a68d9d83..053801d5f8db5d2e1667920a47045449a2a696af 100644 --- a/fsl/fsleyes/overlay.py +++ b/fsl/fsleyes/overlay.py @@ -182,12 +182,12 @@ class OverlayList(props.HasProperties): return self.overlays.insertAll(index, items) -def guessDataSourceType(filename): +def guessDataSourceType(path): """A convenience function which, given the name of a file or directory, figures out a suitable overlay 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 + load the path, and the path itself, possibly adjusted. If the type is unrecognised, the first tuple value will be ``None``. """ @@ -198,30 +198,53 @@ def guessDataSourceType(filename): import fsl.data.melodicresults as melresults import fsl.data.featresults as featresults - filename = op.abspath(filename) - - if filename.endswith('.vtk'): - return fslmodel.Model, filename - - else: - if op.isdir(filename): - if featresults.isFEATDir(filename): - return fslfeatimage.FEATImage, filename - elif melresults.isMelodicDir(filename): - return fslmelimage.MelodicImage, filename - else: - - try: filename = fslimage.addExt(filename, True) - except ValueError: return None, filename + path = op.abspath(path) + + # VTK files are easy + if path.endswith('.vtk'): + return fslmodel.Model, path + + # Now, we check to see if the given + # path is part of a FEAT or MELODIC + # analysis. The way we go about this is + # a bit silly, but is necessary due to + # the fact thet a melodic analysis can + # be contained within a feat analysis + # (or another melodic analysis). So we + # check for all analysis types and, if + # more than one analysis type matches, + # we return the one with the longest + # path name. + analyses = [ + (fslfeatimage.FEATImage, featresults.getFEATDir( path)), + (fslmelimage .MelodicImage, melresults .getMelodicDir(path))] + + # Remove the analysis types that didn't match + # (the get*Dir function returned None) + analyses = [(t, d) for (t, d) in analyses if d is not None] + + # If we have one or more matches for + # an analysis directory, we return + # the one with the longest path + if len(analyses) > 0: + + dirlens = map(len, [d for (t, d) in analyses]) + maxidx = dirlens.index(max(dirlens)) + + return analyses[maxidx] - if featresults.isFEATDir(filename): - return fslfeatimage.FEATImage, filename - elif melresults.isMelodicDir(filename): - return fslmelimage.MelodicImage, filename - else: - return fslimage.Image, filename + # If the path is not an analysis directory, + # see if it is a regular nifti image + try: + path = fslimage.addExt(path, mustExist=True) + return fslimage.Image, path + + except ValueError: + pass - return None, filename + # Otherwise, I don't + # know what to do + return None, path def makeWildcard(): diff --git a/fsl/fsleyes/plotting/timeseries.py b/fsl/fsleyes/plotting/timeseries.py index 094d450c8106f291ed802f94734940c254193a58..64c1631098279f21448f5f4de1bc096b6ba16545 100644 --- a/fsl/fsleyes/plotting/timeseries.py +++ b/fsl/fsleyes/plotting/timeseries.py @@ -26,9 +26,8 @@ import numpy as np import props -import dataseries -import fsl.data.strings as strings -import fsl.data.melodicimage as fslmelimage +import dataseries +import fsl.data.strings as strings class TimeSeries(dataseries.DataSeries): @@ -72,8 +71,7 @@ class TimeSeries(dataseries.DataSeries): def getData(self, xdata=None, ydata=None): """Overrides :meth:`.DataSeries.getData`. Returns the data associated - with this ``TimeSeries`` instance, pre-processed according to the - current :class:`.TimeSeriesPanel` settings. + with this ``TimeSeries`` instance. The ``xdata`` and ``ydata`` arguments may be used by sub-classes to override the x/y data in the event that they have already performed @@ -89,25 +87,7 @@ class TimeSeries(dataseries.DataSeries): xdata = np.array(xdata, dtype=np.float32) ydata = np.array(ydata, dtype=np.float32) - - if self.tsPanel.usePixdim: - if isinstance(self.overlay, fslmelimage.MelodicImage): - xdata *= self.overlay.tr - else: - xdata *= self.overlay.pixdim[3] - if self.tsPanel.plotMode == 'demean': - ydata = ydata - ydata.mean() - - elif self.tsPanel.plotMode == 'normalise': - ymin = ydata.min() - ymax = ydata.max() - ydata = 2 * (ydata - ymin) / (ymax - ymin) - 1 - - elif self.tsPanel.plotMode == 'percentChange': - mean = ydata.mean() - ydata = 100 * (ydata / mean) - 100 - return xdata, ydata @@ -503,7 +483,7 @@ class FEATTimeSeries(VoxelTimeSeries): copenum) - def __plotPEFitChanged(self, evnum): + def __plotPEFitChanged(self, *a): """Called when the :attr:`plotPEFits` setting changes. If necessary, creates and caches one or more diff --git a/fsl/fsleyes/views/plotpanel.py b/fsl/fsleyes/views/plotpanel.py index 0a64d0e59dd5dcafbdf053cb7f0783f3806eb696..e80aa4b782ebd24f5dc04a026b658effb3eac796 100644 --- a/fsl/fsleyes/views/plotpanel.py +++ b/fsl/fsleyes/views/plotpanel.py @@ -355,13 +355,16 @@ class PlotPanel(viewpanel.ViewPanel): self.Refresh() - def drawDataSeries(self, extraSeries=None, **plotArgs): + def drawDataSeries(self, extraSeries=None, preproc=None, **plotArgs): """Plots all of the :class:`.DataSeries` instances in the :attr:`dataSeries` list :arg extraSeries: A sequence of additional ``DataSeries`` to be plotted. + :arg preproc: An optional preprocessing function - passed to the + :meth:`__drawOneDataSeries` method. + :arg plotArgs: Passed through to the :meth:`__drawOneDataSeries` method. """ @@ -397,7 +400,7 @@ class PlotPanel(viewpanel.ViewPanel): ylims = [] for ds in toPlot: - xlim, ylim = self.__drawOneDataSeries(ds, **plotArgs) + xlim, ylim = self.__drawOneDataSeries(ds, preproc, **plotArgs) xlims.append(xlim) ylims.append(ylim) @@ -474,16 +477,26 @@ class PlotPanel(viewpanel.ViewPanel): self.Refresh() - def __drawOneDataSeries(self, ds, **plotArgs): + def __drawOneDataSeries(self, ds, preproc=None, **plotArgs): """Plots a single :class:`.DataSeries` instance. This method is called by the :meth:`drawDataSeries` method. :arg ds: The ``DataSeries`` instance. + :arg preproc: An optional preprocessing function which must accept + the ``DataSeries`` instance as its sole argument, and + must return the ``(xdata, ydata)`` with any required + processing applied. The default preprocessing function + returns the result of a call to + :meth:`.DataSeries.getData`. + :arg plotArgs: May be used to customise the plot - these arguments are all passed through to the ``Axis.plot`` function. """ + + if preproc is None: + preproc = lambda s: s.getData() if ds.alpha == 0: return (0, 0), (0, 0) @@ -491,7 +504,7 @@ class PlotPanel(viewpanel.ViewPanel): log.debug('Drawing {} for {}'.format(type(ds).__name__, ds.overlay)) - xdata, ydata = ds.getData() + xdata, ydata = preproc(ds) if len(xdata) != len(ydata) or len(xdata) == 0: return (0, 0), (0, 0) diff --git a/fsl/fsleyes/views/timeseriespanel.py b/fsl/fsleyes/views/timeseriespanel.py index 5a879e526528b542ece4f59efe961faa81221d1b..d83221c9a1eee94064fc5a21b1cc3f4fa1f6351a 100644 --- a/fsl/fsleyes/views/timeseriespanel.py +++ b/fsl/fsleyes/views/timeseriespanel.py @@ -20,7 +20,7 @@ import fsl.data.featimage as fslfeatimage import fsl.data.melodicimage as fslmelimage import fsl.data.image as fslimage import fsl.fsleyes.colourmaps as fslcmaps -import fsl.fsleyes.plotting.timeseries as timeseries +import fsl.fsleyes.plotting as plotting import fsl.fsleyes.controls.timeseriescontrolpanel as timeseriescontrolpanel import fsl.fsleyes.controls.timeserieslistpanel as timeserieslistpanel @@ -281,14 +281,15 @@ class TimeSeriesPanel(plotpanel.PlotPanel): tss = [ts for ts in tss if ts is not None] for i, ts in enumerate(list(tss)): - if isinstance(ts, timeseries.FEATTimeSeries): + if isinstance(ts, plotting.FEATTimeSeries): tss.pop(i) tss = tss[:i] + ts.getModelTimeSeries() + tss[i:] for ts in tss: ts.label = ts.makeLabel() - self.drawDataSeries(extraSeries=tss) + self.drawDataSeries(extraSeries=tss, + preproc=self.__prepareTimeSeriesData) def getTimeSeries(self, overlay): @@ -298,6 +299,39 @@ class TimeSeriesPanel(plotpanel.PlotPanel): return self.__currentTss.get(overlay) + def __prepareTimeSeriesData(self, ts): + """Given a :class:`.TimeSeries` instance, scales and normalises + the x and y data according to the current values of the + :attr:`usePixdim` and :attr:`plotMode` properties. + + This method is used as a preprocessing function for all + :class:`.TimeSeries` instances that are plotted - see the + :meth:`.PlotPanel.drawDataSeries` method. + """ + + xdata, ydata = ts.getData() + + if self.usePixdim: + if isinstance(ts.overlay, fslmelimage.MelodicImage): + xdata *= ts.overlay.tr + else: + xdata *= ts.overlay.pixdim[3] + + if self.plotMode == 'demean': + ydata = ydata - ydata.mean() + + elif self.plotMode == 'normalise': + ymin = ydata.min() + ymax = ydata.max() + ydata = 2 * (ydata - ymin) / (ymax - ymin) - 1 + + elif self.plotMode == 'percentChange': + mean = ydata.mean() + ydata = 100 * (ydata / mean) - 100 + + return xdata, ydata + + def __overlayListChanged(self, *a): """Called when the :class:`.OverlayList` changes. Makes sure that there are no :class:`.TimeSeries` instances in the @@ -402,18 +436,18 @@ class TimeSeriesPanel(plotpanel.PlotPanel): return None, None, None if isinstance(overlay, fslfeatimage.FEATImage): - ts = timeseries.FEATTimeSeries(self, overlay, self._displayCtx) + ts = plotting.FEATTimeSeries(self, overlay, self._displayCtx) targets = [self._displayCtx] propNames = ['location'] elif isinstance(overlay, fslmelimage.MelodicImage) and \ self.plotMelodicICs: - ts = timeseries.MelodicTimeSeries(self, overlay, self._displayCtx) + ts = plotting.MelodicTimeSeries(self, overlay, self._displayCtx) targets = [self._displayCtx.getOpts(overlay)] propNames = ['volume'] else: - ts = timeseries.VoxelTimeSeries(self, overlay, self._displayCtx) + ts = plotting.VoxelTimeSeries(self, overlay, self._displayCtx) targets = [self._displayCtx] propNames = ['location']