From cce833cfad230282436d9df371673761e0cc3231 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Mon, 30 Nov 2015 16:29:08 +0000 Subject: [PATCH] New module, displaydefaults, which attempts to configure sensible default display settings for new overlays. Used by the addfile action, the overlay list panel, and at startup via fsleyes_parseargs. --- fsl/fsleyes/actions/openfile.py | 16 ++- fsl/fsleyes/actions/openstandard.py | 4 +- fsl/fsleyes/controls/overlaylistpanel.py | 22 +++- fsl/fsleyes/displaycontext/volumeopts.py | 9 ++ fsl/fsleyes/displaydefaults.py | 154 +++++++++++++++++++++++ fsl/fsleyes/fsleyes_parseargs.py | 14 ++- fsl/fsleyes/overlay.py | 14 +-- 7 files changed, 213 insertions(+), 20 deletions(-) create mode 100644 fsl/fsleyes/displaydefaults.py diff --git a/fsl/fsleyes/actions/openfile.py b/fsl/fsleyes/actions/openfile.py index d74adfec3..dedbf4217 100644 --- a/fsl/fsleyes/actions/openfile.py +++ b/fsl/fsleyes/actions/openfile.py @@ -11,6 +11,8 @@ load overlay files into the :class:`.OverlayList`. import action +import fsl.fsleyes.displaydefaults as displaydefaults + class OpenFileAction(action.Action): """The ``OpenFileAction`` allows the user to add files to the @@ -34,7 +36,15 @@ class OpenFileAction(action.Action): """Calls :meth:`.OverlayList.addOverlays` method. If overlays were added, updates the :attr:`.DisplayContext.selectedOverlay` accordingly. """ + + overlays = self.__overlayList.addOverlays() + + if len(overlays) == 0: + return - if self.__overlayList.addOverlays(): - self.__displayCtx.selectedOverlay = \ - self.__displayCtx.overlayOrder[-1] + self.__displayCtx.selectedOverlay = self.__displayCtx.overlayOrder[-1] + + for overlay in overlays: + displaydefaults.displayDefaults(overlay, + self.__overlayList, + self.__displayCtx) diff --git a/fsl/fsleyes/actions/openstandard.py b/fsl/fsleyes/actions/openstandard.py index 66032d814..5f37bf69b 100644 --- a/fsl/fsleyes/actions/openstandard.py +++ b/fsl/fsleyes/actions/openstandard.py @@ -49,6 +49,8 @@ class OpenStandardAction(action.Action): added some overlays, updates the :attr:`.DisplayContext.selectedOverlay` accordingly. """ - if self.__overlayList.addOverlays(self.__stddir, addToEnd=False): + added = self.__overlayList.addOverlays(self.__stddir, addToEnd=False) + + if len(added) > 0: self.__displayCtx.selectedOverlay = \ self.__displayCtx.overlayOrder[0] diff --git a/fsl/fsleyes/controls/overlaylistpanel.py b/fsl/fsleyes/controls/overlaylistpanel.py index 33ea84a9a..62a4c5b73 100644 --- a/fsl/fsleyes/controls/overlaylistpanel.py +++ b/fsl/fsleyes/controls/overlaylistpanel.py @@ -15,11 +15,12 @@ import wx import props -import pwidgets.elistbox as elistbox +import pwidgets.elistbox as elistbox -import fsl.fsleyes.panel as fslpanel -import fsl.fsleyes.icons as icons -import fsl.data.image as fslimage +import fsl.fsleyes.panel as fslpanel +import fsl.fsleyes.icons as icons +import fsl.fsleyes.displaydefaults as displaydefaults +import fsl.data.image as fslimage log = logging.getLogger(__name__) @@ -207,8 +208,17 @@ class OverlayListPanel(fslpanel.FSLEyesPanel): """Called when the *add* button on the list box is pressed. Calls the :meth:`.OverlayList.addOverlays` method. """ - if self._overlayList.addOverlays(): - self._displayCtx.selectedOverlay = len(self._overlayList) - 1 + overlays = self._overlayList.addOverlays() + + if len(overlays) == 0: + return + + self._displayCtx.selectedOverlay = len(self._overlayList) - 1 + + for overlay in overlays: + displaydefaults.displayDefaults(overlay, + self._overlayList, + self._displayCtx) def __lbRemove(self, ev): diff --git a/fsl/fsleyes/displaycontext/volumeopts.py b/fsl/fsleyes/displaycontext/volumeopts.py index f7199d85b..2ac74501e 100644 --- a/fsl/fsleyes/displaycontext/volumeopts.py +++ b/fsl/fsleyes/displaycontext/volumeopts.py @@ -621,6 +621,15 @@ class VolumeOpts(ImageOpts): display, display.getSyncPropertyName('contrast')) + # If centreRanges, linkLowRanges or linkHighRanges + # have been set to True (this will happen if they + # are true on the parent VolumeOpts instance), make + # sure the property / listener states are up to date. + if self.centreRanges: self.__centreRangesChanged() + else: + if self.linkLowRanges: self.__linkLowRangesChanged() + if self.linkHighRanges: self.__linkHighRangesChanged() + @actions.action def resetDisplayRange(self): diff --git a/fsl/fsleyes/displaydefaults.py b/fsl/fsleyes/displaydefaults.py new file mode 100644 index 000000000..5ce8cff7e --- /dev/null +++ b/fsl/fsleyes/displaydefaults.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# +# displaydefaults.py - Routines for configuring default overlay display +# settings. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""This module provides the :func:`displayDefaults` function, which is used +for configuring default overlay display settings. + +The :displayDefaults` function is called when *FSLeyes* is started, and when +new overlays are loaded. +""" + + +import re +import sys +import logging + +import fsl.data.image as fslimage + + +log = logging.getLogger(__name__) + + +def displayDefaults(overlay, overlayList, displayCtx): + """Configure default display settings for the given overlay. + + :arg overlay: The overlay object (e.g. an :class:`.Image` instance). + :arg overlayList: The :class:`.OverlayList`. + :arg displayCtx: The :class:`.DisplayContext`. + """ + + oType = type(overlay).__name__ + func = getattr(sys.modules[__name__], '_{}Defaults'.format(oType), None) + + if func is None: + log.warn('Unknown overlay type: {}'.format(oType)) + return + + log.debug('Applying default display arguments for {}'.format(overlay)) + func(overlay, overlayList, displayCtx) + + +def _ImageDefaults(overlay, overlayList, displayCtx): + """Configure default display settings for the given :class:`.Image` + overlay. + """ + + if _isStatImage(overlay): + _statImageDefaults(overlay, overlayList, displayCtx) + + +def _isStatImage(overlay): + """Returns ``True`` if the given :class:`.Image` overlay looks like a + statistic image, ``False`` otherwise. + """ + + basename = fslimage.removeExt(overlay.dataSource) + tokens = ['zstat', 'tstat', 'fstat', 'zfstat'] + pattern = '_({})\d+'.format('|'.join(tokens)) + + return re.search(pattern, basename) is not None + + +def _statImageDefaults(overlay, overlayList, displayCtx): + """Configure default display settings for the given statistic + :class:`.Image` overlay. + """ + + opts = displayCtx.getOpts(overlay) + basename = fslimage.removeExt(overlay.dataSource) + cmap = _statImageDefaults.cmaps[_statImageDefaults.currentCmap] + + nameTokens = '_'.split(basename) + + # Give each stat image + # a different colour map + _statImageDefaults.currentCmap += 1 + _statImageDefaults.currentCmap %= len(_statImageDefaults.cmaps) + opts.cmap = cmap + + pTokens = ['p', 'corrp'] + statTokens = ['zstat', 'tstat', 'zfstat'] + fStatTokens = ['fstat'] + + # The order of these tests is + # important, due to name overlap + + # P-value image ? + if any([token in nameTokens for token in pTokens]): + opts.displayRange = [0.95, 1.0] + opts.clippingRange = [0.95, 1.0] + + # T or Z stat image? + elif any([token in nameTokens for token in statTokens]): + + opts.clippingRange = [-0.1, 0.1] + opts.displayRange = [-7.5, 7.5] + opts.centreRanges = True + opts.invertClipping = True + + # F stat image? + elif any([token in nameTokens for token in fStatTokens]): + opts.displayRange = [0, 10] + + +# Colour maps used for statistic images +_statImageDefaults.cmaps = ['red-yellow', + 'blue-lightblue', + 'green', + 'cool', + 'hot', + 'blue', + 'red', + 'yellow', + 'pink', + 'copper'] + + +# Index into the cmaps list, pointing to the +# next colour map to use for statistic images. +_statImageDefaults.currentCmap = 0 + + +def _FEATImageDefaults(overlay, overlayList, displayCtx): + """Configure default display settings for the given :class:`.FEATImage` + overlay. + """ + pass + + +def _MelodicImageDefaults(overlay, overlayList, displayCtx): + """Configure default display settings for the given :class:`.MelodicImage` + overlay. + """ + + opts = displayCtx.getOpts(overlay) + + opts.cmap = 'Render3' + opts.displayRange = [-5.0, 5.0] + opts.clippingRange = [-1.5, 1.5] + + opts.centreRanges = True + opts.invertClipping = True + + +def _ModelDefaults(overlay, display, overlayList, displayCtx): + """Configure default display settings for the given :class:`.Model` + overlay. + """ + + # TODO some nice default colours + pass diff --git a/fsl/fsleyes/fsleyes_parseargs.py b/fsl/fsleyes/fsleyes_parseargs.py index e8775c7bf..835dfacab 100644 --- a/fsl/fsleyes/fsleyes_parseargs.py +++ b/fsl/fsleyes/fsleyes_parseargs.py @@ -1122,8 +1122,18 @@ def applyOverlayArgs(args, overlayList, displayCtx, **kwargs): for i, overlay in enumerate(overlayList): display = displayCtx.getDisplay(overlay) - - _applyArgs(args.overlays[i], display) + + # Figure out how many arguments + # were passed in for this overlay + nArgs = len([v for k, v in vars(args.overlays[i]).items() + if k != 'overlay' and v is not None]) + + # If no arguments were passed, + # apply default display settings + if nArgs == 0 and args.defaultDisplay: + displaydefaults.displayDefaults(overlay, overlayList, displayCtx) + else: + _applyArgs(args.overlays[i], display) # Retrieve the DisplayOpts instance # after applying arguments to the diff --git a/fsl/fsleyes/overlay.py b/fsl/fsleyes/overlay.py index 1ede376e0..0dbe9ef6c 100644 --- a/fsl/fsleyes/overlay.py +++ b/fsl/fsleyes/overlay.py @@ -109,19 +109,17 @@ class OverlayList(props.HasProperties): """Convenience method for interactively adding overlays to this :class:`OverlayList`. - Returns: ``True`` if some overlays were added to the list, ``False`` - otherwise. + Returns: A list containing the overlays that were added - the list + will be empty if no overlays were added. """ overlays = interactiveLoadOverlays(fromDir) - if len(overlays) == 0: - return False - - if addToEnd: self.extend( overlays) - else: self.insertAll(0, overlays) + if len(overlays) > 0: + if addToEnd: self.extend( overlays) + else: self.insertAll(0, overlays) - return True + return overlays def find(self, name): -- GitLab