Source code for fsleyes.displaycontext.display

#
# display.py - Definitions of the Display and DisplayOpts classes.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`Display` and :class:`DisplayOpts` classes,
which encapsulate overlay display settings.
"""


import logging
import inspect

import fsl.data.image                 as fslimage
import fsl.data.constants             as constants
import fsleyes_props                  as props
import fsleyes_widgets.utils.typedict as td

import fsleyes.strings                as strings
import fsleyes.actions                as actions


log = logging.getLogger(__name__)


[docs]class Display(props.SyncableHasProperties): """The ``Display`` class contains display settings which are common to all overlay types. A ``Display`` instance is also responsible for managing a single :class:`DisplayOpts` instance, which contains overlay type specific display options. Whenever the :attr:`overlayType` property of a ``Display`` instance changes, the old ``DisplayOpts`` instance (if any) is destroyed, and a new one, of the correct type, created. """ name = props.String() """The overlay name. """ overlayType = props.Choice() """This property defines the overlay type - how the data is to be displayed. The options for this property are populated in the :meth:`__init__` method, from the :attr:`.displaycontext.OVERLAY_TYPES` dictionary. A :class:`DisplayOpts` sub-class exists for every possible value that this property may take. """ enabled = props.Boolean(default=True) """Should this overlay be displayed at all? """ alpha = props.Percentage(default=100.0) """Opacity - 100% is fully opaque, and 0% is fully transparent.""" brightness = props.Percentage() """Brightness - 50% is normal brightness.""" contrast = props.Percentage() """Contrast - 50% is normal contrast."""
[docs] def __init__(self, overlay, overlayList, displayCtx, parent=None, **kwa): """Create a :class:`Display` for the specified overlay. :arg overlay: The overlay object. :arg overlayList: The :class:`.OverlayList` instance which contains all overlays. :arg displayCtx: A :class:`.DisplayContext` instance describing how the overlays are to be displayed. :arg parent: A parent ``Display`` instance - see :mod:`props.syncable`. All other keyword arguments are assumed to be ``name=value`` pairs, containing initial property values, for both this ``Display``, and the initially created :class:`DisplayOpts` instance. For the latter, it is assumed that any properties specified are appropriate for the initial overlay type. """ dispProps = self.getAllProperties()[0] initDispProps = {n : v for n, v in kwa.items() if n in dispProps} initOptProps = {n : v for n, v in kwa.items() if n not in dispProps} self.__overlay = overlay self.__overlayList = overlayList self.__displayCtx = displayCtx self.name = overlay.name # Populate the possible choices # for the overlayType property from . import getOverlayTypes ovlTypes = getOverlayTypes(overlay) ovlTypeProp = self.getProp('overlayType') log.debug('Enabling overlay types for {}: '.format(overlay, ovlTypes)) ovlTypeProp.setChoices(ovlTypes, instance=self) # Call the super constructor after our own # initialisation, in case the provided parent # has different property values to our own, # and our values need to be updated props.SyncableHasProperties.__init__( self, parent=parent, # These properties cannot be unbound, as # they affect the OpenGL representation. # The name can't be unbound either, # because it would be silly to allow # different names for the same overlay. nounbind=['overlayType', 'name'], # Initial sync state between this # Display and the parent Display # (if this Display has a parent) state=displayCtx.syncOverlayDisplay, # set initial display property values **initDispProps) # When the overlay type changes, the property # values of the DisplayOpts instance for the # old overlay type are stored in this dict. # If the overlay is later changed back to the # old type, its previous values are restored. # # The structure of the dictionary is: # # { (type(DisplayOpts), propName) : propValue } # # This also applies to the case where the # overlay type is changed from one type to # a related type (e.g. from VolumeOpts to # LabelOpts) - the values of all common # properties are copied to the new # DisplayOpts instance. self.__oldOptProps = td.TypeDict() # Initial DisplayOpt property values # are used in the first call to # __makeDisplayOpts, and then cleared # afterwards. self.__initOptProps = initOptProps # Set up listeners after caling Syncable.__init__, # so the callbacks don't get called during # synchronisation self.addListener( 'overlayType', 'Display_{}'.format(id(self)), self.__overlayTypeChanged) # The __overlayTypeChanged method creates # a new DisplayOpts instance - for this, # it needs to be able to access this # Display instance's parent (so it can # subsequently access a parent for the # new DisplayOpts instance). Therefore, # we do this after calling Syncable.__init__. self.__displayOpts = None self.__overlayTypeChanged() log.debug('{}.init ({})'.format(type(self).__name__, id(self)))
[docs] def __del__(self): """Prints a log message.""" if log: log.debug('{}.del ({})'.format(type(self).__name__, id(self)))
[docs] def destroy(self): """This method must be called when this ``Display`` instance is no longer needed. When a ``Display`` instance is destroyed, the corresponding :class:`DisplayOpts` instance is also destroyed. """ if self.__displayOpts is not None: self.__displayOpts.destroy() self.removeListener('overlayType', 'Display_{}'.format(id(self))) self.detachAllFromParent() self.__oldOptProps = None self.__initOptProps = None self.__displayOpts = None self.__overlayList = None self.__displayCtx = None self.__overlay = None
@property def overlay(self): """Returns the overlay associated with this ``Display`` instance.""" return self.__overlay @property def opts(self): """Return the :class:`.DisplayOpts` instance associated with this ``Display``, which contains overlay type specific display settings. If a ``DisplayOpts`` instance has not yet been created, or the :attr:`overlayType` property no longer matches the type of the existing ``DisplayOpts`` instance, a new ``DisplayOpts`` instance is created (and the old one destroyed if necessary). See the :meth:`__makeDisplayOpts` method. """ if (self.__displayOpts is None) or \ (self.__displayOpts.overlayType != self.overlayType): if self.__displayOpts is not None: self.__displayOpts.destroy() self.__displayOpts = self.__makeDisplayOpts() return self.__displayOpts def __makeDisplayOpts(self): """Creates a new :class:`DisplayOpts` instance. The specific ``DisplayOpts`` sub-class that is created is dictated by the current value of the :attr:`overlayType` property. The :data:`.displaycontext.DISPLAY_OPTS_MAP` dictionary defines the mapping between overlay types and :attr:`overlayType` values, and ``DisplayOpts`` sub-class types. """ if self.getParent() is None: oParent = None else: oParent = self.getParent().opts initOptProps = self.__initOptProps self.__initOptProps = None if initOptProps is None: initOptProps = {} from . import DISPLAY_OPTS_MAP optType = DISPLAY_OPTS_MAP[self.__overlay, self.overlayType] log.debug('Creating {} instance (synced: {}) for overlay ' '{} ({})'.format(optType.__name__, self.__displayCtx.syncOverlayDisplay, self.__overlay, self.overlayType)) volProps = optType.getVolumeProps() allProps = optType.getAllProperties()[0] initState = {} for p in allProps: if p in volProps: initState[p] = self.__displayCtx.syncOverlayVolume else: initState[p] = self.__displayCtx.syncOverlayDisplay return optType(self.__overlay, self, self.__overlayList, self.__displayCtx, parent=oParent, state=initState, **initOptProps) def __findOptBaseType(self, optType, optName): """Finds the class, in the hierarchy of the given ``optType`` (a :class:`.DisplayOpts` sub-class) in which the given ``optName`` is defined. This method is used by the :meth:`__saveOldDisplayOpts` method, and is an annoying necessity caused by the way that the :class:`.TypeDict` class works. A ``TypeDict`` does not allow types to be used as keys - they must be strings containing the type names. Furthermore, in order for the property values of a common ``DisplayOpts`` base type to be shared across sub types (e.g. copying the :attr:`.NiftiOpts.transform` property between :class:`.VolumeOpts` and :class:`.LabelOpts` instances), we need to store the name of the common base type in the dictionary. """ for base in inspect.getmro(optType): if optName in base.__dict__: return base return None def __saveOldDisplayOpts(self): """Saves the value of every property on the current :class:`DisplayOpts` instance, so they can be restored later if needed. """ opts = self.__displayOpts if opts is None: return for propName in opts.getAllProperties()[0]: base = self.__findOptBaseType(type(opts), propName) base = base.__name__ val = getattr(opts, propName) log.debug('Saving {}.{} = {} [{} {}]'.format( base, propName, val, type(opts).__name__, id(self))) self.__oldOptProps[base, propName] = val def __restoreOldDisplayOpts(self): """Restores any cached values for all of the properties on the current :class:`DisplayOpts` instance. """ opts = self.__displayOpts if opts is None: return for propName in opts.getAllProperties()[0]: try: value = self.__oldOptProps[opts, propName] if not hasattr(opts, propName): continue if not opts.propertyIsEnabled(propName): continue log.debug('Restoring {}.{} = {} [{}]'.format( type(opts).__name__, propName, value, id(self))) setattr(opts, propName, value) except KeyError: pass def __overlayTypeChanged(self, *a): """Called when the :attr:`overlayType` property changes. Makes sure that the :class:`DisplayOpts` instance is of the correct type. """ self.__saveOldDisplayOpts() self.opts self.__restoreOldDisplayOpts()
[docs]class DisplayOpts(props.SyncableHasProperties, actions.ActionProvider): """The ``DisplayOpts`` class contains overlay type specific display settings. ``DisplayOpts`` instances are managed by :class:`Display` instances. The ``DisplayOpts`` class is not meant to be created directly - it is a base class for type specific implementations (e.g. the :class:`.VolumeOpts` class). The following attributes are available on all ``DisplayOpts`` instances: =============== ====================================================== ``overlay`` The overlay object ``display`` The :class:`Display` instance that created this ``DisplayOpts`` instance. ``overlayType`` The value of the :attr:`Display.overlayType` property corresponding to the type of this ``DisplayOpts`` instance. ``overlayList`` The :class:`.OverlayList` instance, which contains all overlays. ``displayCtx`` The :class:`.DisplayContext` instance which is responsible for all ``Display`` and ``DisplayOpts`` instances. ``name`` A unique name for this ``DisplayOpts`` instance. =============== ====================================================== .. warning:: :class:`DisplayOpts` sub-classes must not define any properties with the same name as any of the :class:`Display` properties. """ bounds = props.Bounds(ndims=3) """Specifies a bounding box in the display coordinate system which is big enough to contain the overlay described by this ``DisplayOpts`` instance. The values in this ``bounds`` property must be updated by ``DisplayOpts`` subclasses whenever the spatial representation of their overlay changes. """
[docs] def __init__( self, overlay, display, overlayList, displayCtx, **kwargs): """Create a ``DisplayOpts`` object. :arg overlay: The overlay associated with this ``DisplayOpts`` instance. :arg display: The :class:`Display` instance which owns this ``DisplayOpts`` instance. :arg overlayList: The :class:`.OverlayList` which contains all overlays. :arg displayCtx: A :class:`.DisplayContext` instance describing how the overlays are to be displayed. """ self.__overlay = overlay self.__display = display self.__overlayType = display.overlayType self.__name = '{}_{}'.format(type(self).__name__, id(self)) props.SyncableHasProperties.__init__(self, **kwargs) actions.ActionProvider .__init__(self, overlayList, displayCtx) log.debug('{}.init [DC: {}] ({})'.format( type(self).__name__, id(displayCtx), id(self)))
[docs] def __del__(self): """Prints a log message.""" if log: log.debug('{}.del ({})'.format(type(self).__name__, id(self)))
[docs] def destroy(self): """This method must be called when this ``DisplayOpts`` instance is no longer needed. If a subclass overrides this method, the subclass implementation must call this method, **after** performing its own clean up. """ actions.ActionProvider.destroy(self) self.detachAllFromParent() self.__overlay = None self.__display = None
[docs] @classmethod def getVolumeProps(cls): """Intended to be overridden by sub-classes as needed. Returns a list of property names which control the currently displayed volume/timepoint for 4D overlays. The default implementation returns an empty list. """ return []
@property def overlay(self): """Return the overlay associated with this ``DisplayOpts`` object. """ return self.__overlay @property def display(self): """Return the :class:`.Display` that is managing this ``DisplayOpts`` object. """ return self.__display @property def overlayType(self): """Return the type of this ``DisplayOpts`` object (the value of :attr:`Display.overlayType`). """ return self.__overlayType @property def name(self): """Return the name of this ``DisplayOpts`` object. """ return self.__name @property def referenceImage(self): """Return the reference image associated with this ``DisplayOpts`` instance. Some non-volumetric overlay types (e.g. the :class:`.Mesh` - see :class:`.MeshOpts`) may have a *reference* :class:`.Nifti` instance associated with them, allowing the overlay to be localised in the coordinate space defined by the :class:`.Nifti`. The :class:`.DisplayOpts` sub-class which corresponds to such non-volumetric overlays should override this method to return that reference image. :class:`.DisplayOpts` sub-classes which are associated with volumetric overlays (i.e. :class:`.Nifti` instances) do not need to override this method - in this case, the overlay itself is considered to be its own reference image, and is returned by the base-class implementation of this method. .. note:: The reference :class:`.Nifti` instance returned by sub-class implementations of this method must be in the :class:`.OverlayList`. """ if isinstance(self.overlay, fslimage.Nifti): return self.overlay return None
[docs] def getLabels(self): """Generates some orientation labels for the overlay associated with this ``DisplayOpts`` instance. If the overlay is not a ``Nifti`` instance, or does not have a reference image set, the labels will represent an unknown orientation. Returns a tuple containing: - The ``(xlo, ylo, zlo, xhi, yhi, zhi)`` labels - The ``(xorient, yorient, zorient)`` orientations (see :meth:`.Image.getOrientation`) """ refImage = self.referenceImage if refImage is None: return ('??????', [constants.ORIENT_UNKNOWN] * 3) opts = self.displayCtx.getOpts(refImage) xorient = None yorient = None zorient = None # If we are displaying in voxels/scaled voxels, # and this image is not the current display # image, then we do not show anatomical # orientation labels, as there's no guarantee # that all of the loaded overlays are in the # same orientation, and it can get confusing. if opts.transform in ('id', 'pixdim', 'pixdim-flip') and \ self.displayCtx.displaySpace != refImage: xlo = 'Xmin' xhi = 'Xmax' ylo = 'Ymin' yhi = 'Ymax' zlo = 'Zmin' zhi = 'Zmax' # Otherwise we assume that all images # are aligned to each other, so we # estimate the current image's orientation # in the display coordinate system else: xform = opts.getTransform('display', 'world') xorient = refImage.getOrientation(0, xform) yorient = refImage.getOrientation(1, xform) zorient = refImage.getOrientation(2, xform) xlo = strings.anatomy['Nifti', 'lowshort', xorient] ylo = strings.anatomy['Nifti', 'lowshort', yorient] zlo = strings.anatomy['Nifti', 'lowshort', zorient] xhi = strings.anatomy['Nifti', 'highshort', xorient] yhi = strings.anatomy['Nifti', 'highshort', yorient] zhi = strings.anatomy['Nifti', 'highshort', zorient] return ((xlo, ylo, zlo, xhi, yhi, zhi), (xorient, yorient, zorient))