class Thing(props.HasProperties):

        myobject     = props.Object()
        mybool       = props.Boolean()
        myint        = props.Int()
        myreal       = props.Real()
        mypercentage = props.Percentage()
        mystring     = props.String()
        mychoice     = props.Choice(('1', '2', '3', '4', '5'))
        myfilepath   = props.FilePath()
        mylist       = props.List()
        mycolour     = props.Colour()
        mycolourmap  = props.ColourMap()
        mybounds     = props.Bounds(ndims=2)
        mypoint      = props.Point(ndims=2)
        myarray      = props.Array()
Exemple #2
0
class OverlayGroup(props.HasProperties):
    """An ``OverlayGroup`` is a group of overlays for which the corresponding
    :class:`.Display` and :class:`.DisplayOpts` properties are synchronised.


    The point of the ``OverlayGroup`` is to allow the user to define groups of
    overlays, so he/she can change display properties on the entire group,
    instead of having to change display properties on each overlay one by one.


    Overlays can be added to an ``OverlayGroup`` with the :meth:`addOverlay`,
    and removed with the :meth:`removeOverlay`.


    When an ``OverlayGroup`` is created, it dynamically adds all of the
    properties which could possibly be linked between overlays to itself,
    using the :meth:`props.HasProperties.addProperty` method. When the first
    overlay is added to the group, these group properties are set to the
    display properties of this overlay. Then, the display properties of
    overlays which are subsequently added to the group will be set to the
    group display properties.


    .. note:: Currently, only a subset of display properties are linked
              between the overlays in a group. The properties which are linked
              are hard-coded in the :attr:`_groupBindings` dictionary.

              A possible future *FSLeyes* enhancement will be to allow the
              user to specify which display properties within an
              ``OverlayGroup`` should be linked.
    """

    overlays = props.List()
    """The list of overlays in this ``OverlayGroup``.

    .. warning:: Do not add/remove overlays directly to this list - use the
                :meth:`addOverlay` and :meth:`removeOverlay` methods instead.
    """

    _groupBindings = td.TypeDict({
        'Display': [],
        'NiftiOpts': ['volume', 'volumeDim'],
        'VolumeOpts': ['interpolation'],
        'Volume3DOpts': [
            'numSteps', 'numInnerSteps', 'resolution', 'numClipPlanes',
            'clipMode', 'clipPosition', 'clipAzimuth', 'clipInclination'
        ],
        'LabelOpts': [],
        'MeshOpts': ['refImage', 'coordSpace'],
        'VectorOpts': ['suppressX', 'suppressY', 'suppressZ', 'suppressMode'],
        'LineVectorOpts': [],
        'RGBVectorOpts': ['interpolation'],
        'TensorOpts': ['lighting', 'tensorResolution'],
    })
    """This dictionary defines the properties which are bound across
    :class:`.Display` instances :class:`.DisplayOpts` sub-class instances, for
    overlays which are in the same group.
    """
    def __init__(self, displayCtx, overlayList):
        """Create an ``OverlayGroup``.

        :arg displayCtx:  The :class:`.DisplayContext`.

        :arg overlayList: The :class:`.OverlayList`.
        """

        self.__displayCtx = displayCtx
        self.__overlayList = overlayList
        self.__name = '{}_{}'.format(type(self).__name__, id(self))

        # This dict is used by the __bindDisplayOpts
        # method to keep track of which group properties
        # have already been given a value
        self.__hasBeenSet = {}

        # Import all of the Display/DisplayOpts
        # classes into the local namespace
        from fsleyes.displaycontext import (  # noqa
            Display, NiftiOpts, VolumeOpts, Volume3DOpts, MaskOpts, VectorOpts,
            RGBVectorOpts, LineVectorOpts, MeshOpts, LabelOpts, TensorOpts)

        overlayList.addListener('overlays', self.__name,
                                self.__overlayListChanged)

        # Add all of the properties listed
        # in the _groupBindings dict as
        # properties of this OverlayGroup
        # instance.
        for clsName, propNames in OverlayGroup._groupBindings.items():

            cls = locals()[clsName]

            for propName in propNames:
                prop = copy.copy(getattr(cls, propName))
                self.addProperty('{}_{}'.format(clsName, propName), prop)

                self.__hasBeenSet[clsName, propName] = False

        # Special case - make sure that the NiftiOpts
        # volume property is not constrained
        self.setAttribute('NiftiOpts_volume', 'maxval', six.MAXSIZE)

    def __copy__(self):
        """Create a copy of this ``OverlayGroup``.

        A custom copy operator is needed due to the way that
        the :class:`.props.HasProperties` class works.
        """
        return OverlayGroup(self, self.__displayCtx, self.__overlayList)

    def __str__(self):
        """Returns a string representation of this ``OverlayGroup``."""
        return str([str(o) for o in self.overlays])

    def __repr__(self):
        """Returns a string representation of this ``OverlayGroup``."""
        return '[{}]'.format(', '.join([str(o) for o in self.overlays]))

    def __contains__(self, ovl):
        """Returns ``True`` if ``ovl`` is in this ``OverlayGroup``,
        :attr:`False` otherwise.
        """
        return ovl in self.overlays

    def __len__(self):
        """Returns the number of overlays in this ``OverlayGroup``. """
        return len(self.overlays)

    def destroy(self):
        """Must be called when this ``OverlayGroup`` is no longer needed.
        Removes all overlays from the group, and removes property listeners.
        """
        for overlay in list(self.overlays):
            self.removeOverlay(overlay)

        self.__overlayList.removeListener('overlays', self.__name)

        self.__overlayList = None
        self.__displayCtx = None

    def addOverlay(self, overlay):
        """Add an overlay to this ``OverlayGroup``.

        If this is the first overlay to be added, the properties of this
        ``OverlayGroup`` are set to the overlay display properties. Otherwise,
        the overlay display properties are set to those of this
        ``OverlayGroup``.
        """

        if overlay in self.overlays:
            return

        self.overlays.append(overlay)

        display = self.__displayCtx.getDisplay(overlay)
        opts = display.opts

        log.debug('Adding overlay {} to group {}'.format(
            overlay.name, self.__name))

        self.__bindDisplayOpts(display)
        self.__bindDisplayOpts(opts)

        display.addListener('overlayType', self.__name,
                            self.__overlayTypeChanged)

    def removeOverlay(self, overlay):
        """Remove the given overlay from this ``OverlayGroup``. """

        if overlay not in self.overlays:
            return

        from . import InvalidOverlayError

        self.overlays.remove(overlay)

        try:
            display = self.__displayCtx.getDisplay(overlay)
        except InvalidOverlayError:
            return

        opts = display.opts

        log.debug('Removing overlay {} from group {}'.format(
            overlay.name, self.__name))

        self.__bindDisplayOpts(display, unbind=True)
        self.__bindDisplayOpts(opts, unbind=True)

        display.removeListener('overlayType', self.__name)

        if len(self.overlays) == 0:
            for key in self.__hasBeenSet.keys():
                self.__hasBeenSet[key] = False

    def __bindDisplayOpts(self, target, unbind=False):
        """Binds or unbinds the properties of the given ``target`` to the
        properties of this ``OverlayGroup``.

        :arg target: A :class:`.Display` or :class:`.DisplayOpts` instance.

        :arg unbind: Set to ``True`` to bind the properties, ``False`` to
                     unbind them.
        """

        # This is the first overlay to be added - the
        # group should inherit its property values
        if len(self.overlays) == 1:
            master, slave = target, self

        # Other overlays are already in the group - the
        # new overlay should inherit the group properties
        else:
            master, slave = self, target

        bindProps = OverlayGroup._groupBindings.get(target,
                                                    allhits=True,
                                                    bykey=True)

        for clsName, propNames in bindProps.items():
            for propName in propNames:

                groupName = '{}_{}'.format(clsName, propName)

                # If the group property has not yet
                # taken on a value, initialise it
                # to the property value being bound.
                #
                # We do this to avoid clobbering
                # property values with un-initialised
                # group property values.
                if not self.__hasBeenSet[clsName, propName]:

                    setattr(self, groupName, getattr(target, propName))
                    self.__hasBeenSet[clsName, propName] = True

                if slave is self:
                    otherName = propName
                    propName = groupName
                else:
                    otherName = groupName

                slave.bindProps(propName,
                                master,
                                otherName,
                                bindatt=False,
                                unbind=unbind)

    def __overlayListChanged(self, *a):
        """Called when overlays are added/removed to/from the
        :class:`.OverlayList` . Makes sure that the :attr:`overlays` list for
        this group does not contain any overlays that have been removed.
        """
        for ovl in self.overlays:
            if ovl not in self.__overlayList:
                self.removeOverlay(ovl)

    def __overlayTypeChanged(self, value, valid, display, name):
        """This method is called when the :attr:`.Display.overlayType`
        property for an overlay in the group changes.

        It makes sure that the display properties of the new
        :class:`.DisplayOpts` instance are bound to the group properties.
        """
        opts = display.opts
        self.__bindDisplayOpts(opts)
Exemple #3
0
class OverlayList(props.HasProperties):
    """Class representing a collection of overlays to be displayed together.

    Contains a :class:`props.properties_types.List` property called
    :attr:`overlays`, containing overlay objects (e.g. :class:`.Image` or
    :class:`.TriangleMesh` objects). Listeners can be registered on the
    ``overlays`` property, so they are notified when the overlay list changes.

    An :class:`OverlayList` object has a few wrapper methods around the
    :attr:`overlays` property, allowing the :class:`OverlayList` to be used as
    if it were a list itself.

    The :mod:`.loadoverlay` module contains some convenience functions for
    loading and adding overlays.

    The :meth:`getData` and :meth:`setData` methods allow arbitrary bits
    of data associated with an overlay to be stored and retrieved.
    """
    def __validateOverlay(self, atts, overlay):
        """Makes sure that the given overlay object is valid."""
        return (hasattr(overlay, 'name') and hasattr(overlay, 'dataSource'))

    overlays = props.List(listType=props.Object(
        allowInvalid=False, validateFunc=__validateOverlay))
    """A list of overlay objects to be displayed."""

    def __init__(self, overlays=None):
        """Create an ``OverlayList`` object from the given sequence of
        overlays."""

        if overlays is None: overlays = []
        self.overlays.extend(overlays)

        # The append/insert methods allow an initial
        # overlay type to be specified for newly
        # added overlays. This can be queried via
        # the initOverlayType method (and is done so
        # by DisplayContext instances).
        self.__initOverlayType = {}

        # This dictionary may be used throughout FSLeyes,
        # via the getData/setData methods, to store
        # any sort of data associated with an overlay.
        # It is a dict of dicts:
        #
        #   {
        #      overlay : {
        #        key : value,
        #        key : value,
        #      },
        #      overlay : {
        #        key : value,
        #        key : value,
        #      }
        #   }
        self.__overlayData = weakref.WeakKeyDictionary()

    def initOverlayType(self, overlay):
        """Returns the initial type for the given ``overlay``, if it was
        specified via the :meth:`append` or :meth:`insert` methods. Returns
        ``None`` otherwise.
        """
        return self.__initOverlayType.get(overlay, None)

    def getData(self, overlay, key, *args):
        """Returns any stored value associated with the specified ``overlay``
        and ``key``.

        :arg default: Default value if there is no value associated with the
                      given ``key``. If not specified, and an unknown key is
                      given, a ``KeyError`` is raised.
        """
        if len(args) not in (0, 1):
            raise RuntimeError('Invalid arguments: {}'.format(args))

        ovlDict = self.__overlayData.get(overlay, {})

        if len(args) == 1:
            return ovlDict.get(key, args[0])
        else:
            return ovlDict[key]

    def setData(self, overlay, key, value):
        """Stores the given value via the specified ``overlay`` and ``key``.
        """
        ovlDict = self.__overlayData.get(overlay, None)

        if ovlDict is not None:
            ovlDict[key] = value
        else:
            self.__overlayData[overlay] = {key: value}

    def find(self, name):
        """Returns the first overlay with the given ``name`` or ``dataSource``,
        or ``None`` if there is no overlay with said ``name``/``dataSource``.
        """

        if name is None:
            return None

        absname = op.abspath(name)

        for overlay in self.overlays:

            if overlay.name == name:
                return overlay

            if overlay.dataSource is None:
                continue

            # Ignore file extensions for NIFTI images.
            if isinstance(overlay, fslimage.Image):
                if fslimage.removeExt(overlay.dataSource) == \
                   fslimage.removeExt(absname):
                    return overlay
            else:
                if overlay.dataSource == absname:
                    return overlay

        return None

    def __str__(self):
        return self.overlays.__str__()

    def __repr__(self):
        return self.overlays.__str__()

    # Wrappers around the overlays list property, allowing this
    # OverlayList object to be used as if it is actually a list.
    def __len__(self):
        return self.overlays.__len__()

    def __getitem__(self, key):
        return self.overlays.__getitem__(key)

    def __iter__(self):
        return self.overlays.__iter__()

    def __contains__(self, item):
        return self.overlays.__contains__(item)

    def __setitem__(self, key, val):
        return self.overlays.__setitem__(key, val)

    def __delitem__(self, key):

        if isinstance(key, slice): pass
        elif isinstance(key, int): key = slice(key, key + 1, None)
        else: raise IndexError('Invalid key type')

        ovls = self[key]
        for ovl in ovls:
            self.__initOverlayType.pop(ovl, None)

        return self.overlays.__delitem__(key)

    def index(self, item):
        return self.overlays.index(item)

    def count(self, item):
        return self.overlays.count(item)

    def append(self, item, overlayType=None):

        with props.suppress(self, 'overlays', notify=True):

            self.overlays.append(item)

            if overlayType is not None:
                self.__initOverlayType[item] = overlayType

    def extend(self, iterable, overlayTypes=None):

        with props.suppress(self, 'overlays', notify=True):

            result = self.overlays.extend(iterable)

            if overlayTypes is not None:
                for overlay, overlayType in overlayTypes.items():
                    self.__initOverlayType[overlay] = overlayType

        return result

    def pop(self, index=-1):
        ovl = self.overlays.pop(index)
        self.__initOverlayType.pop(ovl, None)
        return ovl

    def move(self, from_, to):
        return self.overlays.move(from_, to)

    def remove(self, item):
        self.__initOverlayType.pop(item, None)
        self.overlays.remove(item)

    def insert(self, index, item, overlayType=None):

        with props.suppress(self, 'overlays', notify=True):

            self.overlays.insert(index, item)

            if overlayType is not None:
                self.__initOverlayType[item] = overlayType

    def insertAll(self, index, items):
        return self.overlays.insertAll(index, items)
Exemple #4
0
class Volume3DOpts(object):
    """The ``Volume3DOpts`` class is a mix-in for use with :class:`.DisplayOpts`
    classes. It defines display properties used for ray-cast based rendering
    of :class:`.Image` overlays.


    The properties in this class are tightly coupled to the ray-casting
    implementation used by the :class:`.GLVolume` class - see its documentation
    for details.
    """

    blendFactor = props.Real(minval=0.001, maxval=1, default=0.1)
    """Controls how much each sampled point on each ray contributes to the
    final colour.
    """

    numSteps = props.Int(minval=25, maxval=500, default=100, clamped=False)
    """Specifies the maximum number of samples to acquire in the rendering of
    each pixel of the 3D scene. This corresponds to the number of iterations
    of the ray-casting loop.

    .. note:: In a low performance environment, the actual number of steps
              may differ from this value - use the :meth:`getNumSteps` method
              to get the number of steps that are actually executed.
    """

    numInnerSteps = props.Int(minval=1, maxval=100, default=10, clamped=True)
    """Only used in low performance environments. Specifies the number of
    ray-casting steps to execute in a single iteration on the GPU, as part
    of an outer loop which is running on the CPU. See the :class:`.GLVolume`
    class documentation for more details on the rendering process.

    .. warning:: The maximum number of iterations that can be performed within
                 an ARB fragment program is implementation-dependent. Too high
                 a value may result in errors or a corrupted view. See the
                 :class:`.GLVolume` class for details.
    """

    resolution = props.Int(minval=10, maxval=100, default=100, clamped=True)
    """Only used in low performance environments. Specifies the resolution
    of the off-screen buffer to which the volume is rendered, as a percentage
    of the screen resolution.

    See the :class:`.GLVolume` class documentation for more details.
    """

    smoothing = props.Int(minval=0, maxval=10, default=0, clamped=True)
    """Amount of smoothing to apply to the rendered volume - this setting
    controls the smoothing filter radius, in pixels.
    """

    numClipPlanes = props.Int(minval=0, maxval=5, default=0, clamped=True)
    """Number of active clip planes. """

    showClipPlanes = props.Boolean(default=False)
    """If ``True``, wirframes depicting the active clipping planes will
    be drawn.
    """

    clipMode = props.Choice(('intersection', 'union', 'complement'))
    """This setting controls how the active clip planes are combined.

      -  ``intersection`` clips the intersection of all planes
      -  ``union`` clips the union of all planes
      -  ``complement`` clips the complement of all planes
    """

    clipPosition = props.List(props.Percentage(minval=0,
                                               maxval=100,
                                               clamped=True),
                              minlen=10,
                              maxlen=10)
    """Centre of clip-plane rotation, as a distance from the volume centre -
    0.5 is centre.
    """

    clipAzimuth = props.List(props.Real(minval=-180, maxval=180, clamped=True),
                             minlen=10,
                             maxlen=10)
    """Rotation (degrees) of the clip plane about the Z axis, in the display
    coordinate system.
    """

    clipInclination = props.List(props.Real(minval=-180,
                                            maxval=180,
                                            clamped=True),
                                 minlen=10,
                                 maxlen=10)
    """Rotation (degrees) of the clip plane about the Y axis in the display
    coordinate system.
    """
    def __init__(self):
        """Create a :class:`Volume3DOpts` instance.
        """

        # If we're in an X11/SSh session,
        # step down the quality so it's
        # a bit faster.
        if fslplatform.inSSHSession:
            self.numSteps = 60
            self.resolution = 70
            self.blendFactor = 0.3

        # If we're in GL14, restrict the
        # maximum possible amount of
        # smoothing, as GL14 fragment
        # programs cannot be too large.
        if float(fslplatform.glVersion) < 2.1:
            smooth = self.getProp('smoothing')
            smooth.setAttribute(self, 'maxval', 6)

        self.clipPosition[:] = 10 * [50]
        self.clipAzimuth[:] = 10 * [0]
        self.clipInclination[:] = 10 * [0]

        # Give convenient initial values for
        # the first three clipping planes
        self.clipInclination[1] = 90
        self.clipAzimuth[1] = 0
        self.clipInclination[2] = 90
        self.clipAzimuth[2] = 90

    def destroy(self):
        """Does nothing. """
        pass

    @property
    @deprecated.deprecated('0.17.0', '1.0.0',
                           'Dithering is automatically calculated')
    def dithering(self):
        """Deprecated."""
        pass

    def getNumSteps(self):
        """Return the value of the :attr:`numSteps` property, possibly
        adjusted according to the the :attr:`numInnerSteps` property. The
        result of this method should be used instead of the value of
        the :attr:`numSteps` property.

        See the :class:`.GLVolume` class for more details.
        """

        if float(fslplatform.glVersion) >= 2.1:
            return self.numSteps

        outer = self.getNumOuterSteps()

        return int(outer * self.numInnerSteps)

    def getNumOuterSteps(self):
        """Returns the number of iterations for the outer ray-casting loop.

        See the :class:`.GLVolume` class for more details.
        """

        total = self.numSteps
        inner = self.numInnerSteps
        outer = np.ceil(total / float(inner))

        return int(outer)

    def calculateRayCastSettings(self, view=None, proj=None):
        """Calculates various parameters required for 3D ray-cast rendering
        (see the :class:`.GLVolume` class).


        :arg view: Transformation matrix which transforms from model
                   coordinates to view coordinates (i.e. the GL view matrix).


        :arg proj: Transformation matrix which transforms from view coordinates
                   to normalised device coordinates (i.e. the GL projection
                   matrix).

        Returns a tuple containing:

          - A vector defining the amount by which to move along a ray in a
            single iteration of the ray-casting algorithm. This can be added
            directly to the volume texture coordinates.

          - A transformation matrix which transforms from image texture
            coordinates into the display coordinate system.

        .. note:: This method will raise an error if called on a
                  ``GLImageObject`` which is managing an overlay that is not
                  associated with a :class:`.Volume3DOpts` instance.
        """

        if view is None: view = np.eye(4)
        if proj is None: proj = np.eye(4)

        # In GL, the camera position
        # is initially pointing in
        # the -z direction.
        eye = [0, 0, -1]
        target = [0, 0, 1]

        # We take this initial camera
        # configuration, and transform
        # it by the inverse modelview
        # matrix
        t2dmat = self.getTransform('texture', 'display')
        xform = transform.concat(view, t2dmat)
        ixform = transform.invert(xform)

        eye = transform.transform(eye, ixform, vector=True)
        target = transform.transform(target, ixform, vector=True)

        # Direction that the 'camera' is
        # pointing, normalied to unit length
        cdir = transform.normalise(eye - target)

        # Calculate the length of one step
        # along the camera direction in a
        # single iteration of the ray-cast
        # loop. Multiply by sqrt(3) so that
        # the maximum number of steps will
        # be reached across the longest axis
        # of the image texture cube.
        rayStep = np.sqrt(3) * cdir / self.getNumSteps()

        # A transformation matrix which can
        # transform image texture coordinates
        # into the corresponding screen
        # (normalised device) coordinates.
        # This allows the fragment shader to
        # convert an image texture coordinate
        # into a relative depth value.
        #
        # The projection matrix puts depth into
        # [-1, 1], but we want it in [0, 1]
        zscale = transform.scaleOffsetXform([1, 1, 0.5], [0, 0, 0.5])
        xform = transform.concat(zscale, proj, xform)

        return rayStep, xform

    def get3DClipPlane(self, planeIdx):
        """A convenience method which calculates a point-vector description
        of the specified clipping plane. ``planeIdx`` is an index into the
        :attr:`clipPosition`, :attr:`clipAzimuth`, and
        :attr:`clipInclination`, properties.

        Returns the clip plane at the given ``planeIdx`` as an origin and
        normal vector, in the display coordinate system..
        """

        pos = self.clipPosition[planeIdx]
        azimuth = self.clipAzimuth[planeIdx]
        incline = self.clipInclination[planeIdx]

        b = self.bounds
        pos = pos / 100.0
        azimuth = azimuth * np.pi / 180.0
        incline = incline * np.pi / 180.0

        xmid = b.xlo + 0.5 * b.xlen
        ymid = b.ylo + 0.5 * b.ylen
        zmid = b.zlo + 0.5 * b.zlen

        centre = [xmid, ymid, zmid]
        normal = [0, 0, -1]

        rot1 = transform.axisAnglesToRotMat(incline, 0, 0)
        rot2 = transform.axisAnglesToRotMat(0, 0, azimuth)
        rotation = transform.concat(rot2, rot1)

        normal = transform.transformNormal(normal, rotation)
        normal = transform.normalise(normal)

        offset = (pos - 0.5) * max((b.xlen, b.ylen, b.zlen))
        origin = centre + normal * offset

        return origin, normal
Exemple #5
0
class FEATTimeSeries(VoxelTimeSeries):
    """A :class:`VoxelTimeSeries` class for use with :class:`FEATImage`
    instances, containing some extra FEAT specific options.


    The ``FEATTimeSeries`` class acts as a container for several
    ``TimeSeries`` instances, each of which represent some part of a FEAT
    analysis. The data returned by a call to :meth:`.getData` on a
    ``FEATTimeSeries`` instance returns the fMRI time series data
    (``filtered_func_data`` in the ``.feat`` directory).


    The :meth:`extraSeries` method may be used to retrieve a list of all the
    other ``TimeSeries`` instances which are associated with the
    ``FEATTimeSeries`` instance - all of these ``DataSeries`` instances, in
    addition to this ``FEATTimeSeries`` instasnce, should be plotted.


    For example, if the :attr:`plotData` and :attr:`plotFullModelFit` settings
    are ``True``, the :meth:`extraSeries` method will return a list containing
    one ``TimeSeries`` instance, containing the full model fit, for the voxel
    in question.


    The following classes are used to represent the various parts of a FEAT
    analysis:

    .. autosummary::
       :nosignatures:

       FEATEVTimeSeries
       FEATResidualTimeSeries
       FEATPartialFitTimeSeries
       FEATModelFitTimeSeries
    """


    plotData = props.Boolean(default=True)
    """If ``True``, the FEAT input data is plotted. """


    plotFullModelFit = props.Boolean(default=True)
    """If ``True``, the FEAT full model fit is plotted. """


    plotResiduals = props.Boolean(default=False)
    """If ``True``, the FEAT model residuals are plotted. """


    plotEVs = props.List(props.Boolean(default=False))
    """A list of ``Boolean`` properties, one for each EV in the FEAT analysis.
    For elements that are ``True``, the corresponding FEAT EV time course is
    plotted.
    """


    plotPEFits = props.List(props.Boolean(default=False))
    """A list of ``Boolean`` properties, one for each EV in the FEAT analysis.
    For elements that are ``True``, the model fit for the corresponding FEAT
    EV is plotted.
    """


    plotCOPEFits = props.List(props.Boolean(default=False))
    """A list of ``Boolean`` properties, one for each EV in the FEAT analysis.
    For elements that are ``True``, the model fit for the corresponding FEAT
    contrast is plotted.
    """


    plotPartial = props.Choice()
    """Plot the raw data, after regression against a chosen EV or contrast.
    The options are populated in the :meth:`__init__` method.
    """


    def __init__(self, *args, **kwargs):
        """Create a ``FEATTimeSeries``.

        All arguments are passed through to the :class:`VoxelTimeSeries`
        constructor.
        """

        VoxelTimeSeries.__init__(self, *args, **kwargs)

        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('plotPartial').setChoices(reduceOpts, instance=self)

        for i in range(numEVs):
            self.plotPEFits.append(False)
            self.plotEVs   .append(False)

        for i in range(numCOPEs):
            self.plotCOPEFits.append(False)

        self.__fullModelTs =  None
        self.__partialTs   =  None
        self.__resTs       =  None
        self.__evTs        = [None] * numEVs
        self.__peTs        = [None] * numEVs
        self.__copeTs      = [None] * numCOPEs

        if not self.overlay.hasStats():
            self.plotFullModelFit = False

        self.addListener('plotFullModelFit',
                         self.name,
                         self.__plotFullModelFitChanged)
        self.addListener('plotResiduals',
                         self.name,
                         self.__plotResidualsChanged)
        self.addListener('plotPartial',
                         self.name,
                         self.__plotPartialChanged)

        self.addListener('plotEVs',      self.name, self.__plotEVChanged)
        self.addListener('plotPEFits',   self.name, self.__plotPEFitChanged)
        self.addListener('plotCOPEFits', self.name, self.__plotCOPEFitChanged)

        # plotFullModelFit defaults to True, so
        # force the model fit ts creation here
        self.__plotFullModelFitChanged()


    def getData(self):
        """Returns the fMRI time series data at the current voxel. Or,
        if :attr:`plotData` is ``False``, returns ``(None, None)``.
        """
        if not self.plotData:
            return None, None
        return VoxelTimeSeries.getData(self)


    def extraSeries(self):
        """Returns a list containing all of the ``TimeSeries`` instances
        which should be plotted in place of this ``FEATTimeSeries``.
        """

        modelts = []

        if self.plotFullModelFit:      modelts.append(self.__fullModelTs)
        if self.plotResiduals:         modelts.append(self.__resTs)
        if self.plotPartial != 'none': modelts.append(self.__partialTs)

        for i in range(self.overlay.numEVs()):
            if self.plotPEFits[i]:
                modelts.append(self.__peTs[i])

        for i in range(self.overlay.numEVs()):
            if self.plotEVs[i]:
                modelts.append(self.__evTs[i])

        for i in range(self.overlay.numContrasts()):
            if self.plotCOPEFits[i]:
                modelts.append(self.__copeTs[i])

        return modelts


    @deprecated.deprecated('0.31.0', '1.0.0', 'Use extraSeries instead')
    def getModelTimeSeries(self):
        return self.extraSeries()


    def __getContrast(self, fitType, idx):
        """Returns a contrast vector for the given model fit type, and index.

        :arg fitType: either ``'full'``, ``'pe'``, or ``'cope'``. If
                      ``'full'``, the ``idx`` argument is ignored.

        :arg idx:     The EV or contrast index for ``'pe'`` or ``'cope'`` model
                      fits.
        """

        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.contrasts()[idx]


    def __createModelTs(self, tsType, *args, **kwargs):
        """Creates a ``TimeSeries`` instance of the given ``tsType``, and
        sets its display settings  according to those of this
        ``FEATTimeSeries``.

        :arg tsType: The type to create, e.g. :class:`FEATModelFitTimeSeries`,
                     :class:`FEATEVTimeSeries`, etc.

        :arg args:   Passed to the ``tsType`` constructor.

        :arg kwargs: Passed to the ``tsType`` constructor.
        """

        ts = tsType(self.overlay,
                    self.overlayList,
                    self.displayCtx,
                    self.plotPanel,
                    self,
                    *args,
                    **kwargs)

        ts.alpha     = self.alpha
        ts.lineWidth = self.lineWidth
        ts.lineStyle = self.lineStyle

        if isinstance(ts, FEATModelFitTimeSeries) and ts.fitType == 'full':
            ts.colour = (0, 0, 0.8)
        else:
            ts.colour = fslcm.randomDarkColour()

        return ts


    def __plotPartialChanged(self, *a):
        """Called when the :attr:`plotPartial` setting changes.

        If necessary, creates and caches a :class:`FEATPartialFitTimeSeries`
        instance.
        """

        partial = self.plotPartial

        if partial == 'none' and self.__partialTs is not None:
            self.__partialTs = None
            return

        partial = partial.split()[0]

        # fitType is either 'cope' or 'pe'
        fitType = partial[:-1].lower()
        idx     = int(partial[-1]) - 1

        self.__partialTs = self.__createModelTs(
            FEATPartialFitTimeSeries,
            self.__getContrast(fitType, idx),
            fitType,
            idx)


    def __plotResidualsChanged(self, *a):
        """Called when the :attr:`plotResiduals` setting changes.

        If necessary, creates and caches a :class:`FEATResidualTimeSeries`
        instance.
        """

        if not self.plotResiduals:
            self.__resTs = None
            return

        self.__resTs = self.__createModelTs(FEATResidualTimeSeries)


    def __plotEVChanged(self, *a):
        """Called when the :attr:`plotEVs` setting changes.

        If necessary, creates and caches one or more :class:`FEATEVTimeSeries`
        instances.
        """

        for evnum, plotEV in enumerate(self.plotEVs):

            if not self.plotEVs[evnum]:
                self.__evTs[evnum] = None

            elif self.__evTs[evnum] is None:
                self.__evTs[evnum] = self.__createModelTs(
                    FEATEVTimeSeries, evnum)


    def __plotCOPEFitChanged(self, *a):
        """Called when the :attr:`plotCOPEFits` setting changes.

        If necessary, creates and caches one or more
        :class:`FEATModelFitTimeSeries` instances.
        """

        for copenum, plotCOPE in enumerate(self.plotCOPEFits):

            if not self.plotCOPEFits[copenum]:
                self.__copeTs[copenum] = None

            elif self.__copeTs[copenum] is None:
                self.__copeTs[copenum] = self.__createModelTs(
                    FEATModelFitTimeSeries,
                    self.__getContrast('cope', copenum),
                    'cope',
                    copenum)


    def __plotPEFitChanged(self, *a):
        """Called when the :attr:`plotPEFits` setting changes.

        If necessary, creates and caches one or more
        :class:`FEATModelFitTimeSeries` instances.
        """

        for evnum, plotPE in enumerate(self.plotPEFits):

            if not self.plotPEFits[evnum]:
                self.__peTs[evnum] = None

            elif self.__peTs[evnum] is None:
                self.__peTs[evnum] = self.__createModelTs(
                    FEATModelFitTimeSeries,
                    self.__getContrast('pe', evnum),
                    'pe',
                    evnum)


    def __plotFullModelFitChanged(self, *a):
        """Called when the :attr:`plotFullModelFit` setting changes.

        If necessary, creates and caches a
        :class:`FEATModelFitTimeSeries` instance.
        """

        if not self.plotFullModelFit:
            self.__fullModelTs = None
            return

        self.__fullModelTs = self.__createModelTs(
            FEATModelFitTimeSeries, self.__getContrast('full', -1), 'full', -1)
Exemple #6
0
class DisplayContext(props.SyncableHasProperties):
    """A ``DisplayContext`` instance contains a number of properties defining
    how the overlays in an :class:`.OverlayList` are to be displayed, and
    related contextual information.


    A ``DisplayContext`` instance is responsible for creating and destroying
    :class:`.Display` instances for every overlay in the
    ``OverlayList``. These ``Display`` instances, and the corresponding
    :class:`.DisplayOpts` instances (which, in turn, are created/destroyed by
    ``Display`` instances) can be accessed with the :meth:`getDisplay` and
    :meth:`getOpts` method respectively.


    A number of other useful methods are provided by a ``DisplayContext``
    instance:

    .. autosummary::
       :nosignatures:

        getDisplay
        getOpts
        getReferenceImage
        displayToWorld
        worldToDisplay
        displaySpaceIsRadiological
        selectOverlay
        getSelectedOverlay
        getOverlayOrder
        getOrderedOverlays
        freeze
        freezeOverlay
        thawOverlay
        defaultDisplaySpace
        detachDisplaySpace
    """

    selectedOverlay = props.Int(minval=0, default=0, clamped=True)
    """Index of the currently 'selected' overlay.

    .. note:: The value of this index is in relation to the
              :class:`.OverlayList`, rather than to the :attr:`overlayOrder`
              list.

              If you're interested in the currently selected overlay, you must
              also listen for changes to the :attr:`.OverlayList.images` list
              as, if the list changes, the :attr:`selectedOverlay` index may
              not change, but the overlay to which it points may be different.
    """

    location = props.Point(ndims=3)
    """The location property contains the currently selected 3D location (xyz)
    in the display coordinate system. Different ``DisplayContext`` instances
    may be using different display coordinate systems - see the
    :attr:`displaySpace` property.
    """

    worldLocation = props.Point(ndims=3)
    """The location property contains the currently selected 3D location (xyz)
    in the world coordinate system. Whenever the :attr:`location` changes, it
    gets transformed into the world coordinate system, and propagated to this
    property. The location of different ``DisplayContext`` instances is
    synchronised through this property.

    .. note:: If any :attr:`.NiftiOpts.transform` properties have been modified
              independently of the :attr:`displaySpace`, this value will be
              invalid.
    """

    bounds = props.Bounds(ndims=3)
    """This property contains the min/max values of a bounding box (in display
    coordinates) which is big enough to contain all of the overlays in the
    :class:`.OverlayList`.

    .. warning:: This property shouid be treated as read-only.
    """

    overlayOrder = props.List(props.Int())
    """A list of indices into the :attr:`.OverlayList.overlays`
    list, defining the order in which the overlays are to be displayed.

    See the :meth:`getOrderedOverlays` method.
    """

    overlayGroups = props.List()
    """A list of :class:`.OverlayGroup` instances, each of which defines
    a group of overlays which share display properties.
    """

    syncOverlayDisplay = props.Boolean(default=True)
    """If this ``DisplayContext`` instance has a parent (see
    :mod:`props.syncable`), and this property is ``True``, the properties of
    the :class:`.Display` and :class:`.DisplayOpts` instances for every
    overlay managed by this ``DisplayContext`` instance will be synchronised
    to those of the parent instance. Otherwise, the display properties for
    every overlay will be unsynchronised from the parent.

    Synchronisation of the following properties between child and parent
    ``DisplayContext`` instances is also controlled by this flag:

      - :attr:`displaySpace`
      - :attr:`bounds`
      - :attr:`radioOrientation`

    .. note:: This property is accessed by the :class:`.Display` class, in its
              constructor, and when it creates new :class:`.DisplayOpts`
              instances, to set initial sync states.
    """

    displaySpace = props.Choice(('world', ))
    """The *space* in which overlays are displayed. This property defines the
    display coordinate system for this ``DisplayContext``. When it is changed,
    the :attr:`.NiftiOpts.transform` property of all :class:`.Nifti` overlays
    in the :class:`.OverlayList` is updated. It has two settings, described
    below. The options for this property are dynamically added by
    :meth:`__updateDisplaySpaceOptions`.

    1. **World** space (a.k.a. ``'world'``)

       All :class:`.Nifti` overlays are displayed in the space defined by
       their affine transformation matrix - the :attr:`.NiftiOpts.transform`
       property for every ``Nifti`` overlay is set to ``affine``.

    2. **Reference image** space

       A single :class:`.Nifti` overlay is selected as a *reference* image,
       and is displayed in scaled voxel space (with a potential L/R flip for
       neurological images - its :attr:`.NiftiOpts.transform` is set to
       ``pixdim-flip``). All other ``Nifti`` overlays are transformed into
       this reference space - their :attr:`.NiftiOpts.transform` property is
       set to ``reference``, which results in them being transformed into the
       scaled voxel space of the reference image.

    .. note:: The :attr:`.NiftiOpts.transform` property of any
              :class:`.Nifti` overlay can be set independently of this
              property. However, whenever *this* property changes, it will
              change the ``transform`` property for every ``Nifti``, in the
              manner described above.

    The :meth:`defaultDisplaySpace` can be used to control how the
    ``displaySpace`` is initialised.
    """

    radioOrientation = props.Boolean(default=True)
    """If ``True``, 2D views will show images in radiological convention
    (i.e.subject left on the right of the display). Otherwise, they will be
    shown in neurological convention (subject left on the left).

    .. note:: This setting is not enforced by the ``DisplayContext``. It is
              the responsibility of the :class:`.OrthoPanel` and
              :class:`.LightBoxPanel` (and other potential future 2D view
              panels) to implement the flip.
    """

    autoDisplay = props.Boolean(default=False)
    """If ``True``, whenever an overlay is added to the :class:`.OverlayList`,
    the :mod:`autodisplay` module will be used to automatically configure
    its display settings. Note that the ``DisplayContext`` does not perform
    this configuration - this flag is used by other modules (e.g. the
    :class:`.OverlayListPanel` and the :class:`.OpenFileAction`).
    """

    loadInMemory = props.Boolean(default=False)
    """If ``True``, all :class:`.Image` instances will be loaded into memory,
    regardless of their size. Otherwise (the default), large compressed
    ``Image`` overlays may be kept on disk.


    .. note:: Changing the value of this property will not affect existing
              ``Image`` overlays.


    .. note:: This property may end up being used in a more general sense by
              any code which needs to decide whether to do things in a more
              or less memory-intensive manner.
    """
    def __init__(self, overlayList, parent=None, defaultDs='ref', **kwargs):
        """Create a ``DisplayContext``.

        :arg overlayList: An :class:`.OverlayList` instance.

        :arg parent:      Another ``DisplayContext`` instance to be used
                          as the parent of this instance, passed to the
                          :class:`.SyncableHasProperties` constructor.

        :arg defaultDs:   Initial value for the :meth:`defaultDisplaySpace`.
                          Either ``'ref'`` or ``'world'``. If ``'ref'`` (the
                          default), when overlays are added to an empty list,
                          the :attr:`displaySpace` will be set to the first
                          :class:`.Nifti` overlay. Otherwise (``'world'``),
                          the display space will be set to ``'world'``.

        All other arguments are passed through to the ``SyncableHasProperties``
        constructor, in addition to the following:

          - The ``syncOverlayDisplay`` and ``location`` properties
            are added to the ``nobind`` argument

          - The ``selectedOverlay``, ``overlayGroups``,
            ``autoDisplay`` and ``loadInMemory`` properties
            are added to the ``nounbind`` argument.
        """

        kwargs = dict(kwargs)

        nobind = kwargs.pop('nobind', [])
        nounbind = kwargs.pop('nounbind', [])

        nobind.extend(['syncOverlayDisplay', 'location', 'bounds'])
        nounbind.extend([
            'selectedOverlay', 'overlayGroups', 'autoDisplay', 'loadInMemory'
        ])

        kwargs['parent'] = parent
        kwargs['nobind'] = nobind
        kwargs['nounbind'] = nounbind
        kwargs['state'] = {'overlayOrder': False}

        props.SyncableHasProperties.__init__(self, **kwargs)

        self.__overlayList = overlayList
        self.__name = '{}_{}'.format(self.__class__.__name__, id(self))
        self.__child = parent is not None

        # When the first overlay(s) is/are
        # added, the display space may get
        # set either to a reference image,
        # or to world. The defaultDisplaySpace
        # controls this behaviour.
        self.defaultDisplaySpace = defaultDs

        # The overlayOrder is unsynced by
        # default, but we will inherit the
        # current parent value.
        if self.__child: self.overlayOrder[:] = parent.overlayOrder[:]
        else: self.overlayOrder[:] = range(len(overlayList))

        # If this is the first child DC, we
        # need to initialise the display
        # space and location. If there is
        # already a child DC, then we have
        # (probably) inherited initial
        # settings.
        if self.__child:
            self.__initDS = (len(parent.getChildren()) - 1) == 0

        # This dict contains the Display
        # objects for every overlay in
        # the overlay list, as
        # {Overlay : Display} mappings
        self.__displays = {}

        overlayList.addListener('overlays',
                                self.__name,
                                self.__overlayListChanged,
                                immediate=True)

        if self.__child:
            self.addListener('syncOverlayDisplay', self.__name,
                             self.__syncOverlayDisplayChanged)
            self.addListener('displaySpace',
                             self.__name,
                             self.__displaySpaceChanged,
                             immediate=True)
            self.addListener('location',
                             self.__name,
                             self.__locationChanged,
                             immediate=True)
            self.addListener('worldLocation',
                             self.__name,
                             self.__worldLocationChanged,
                             immediate=True)

        # The overlayListChanged method
        # is important - check it out
        self.__overlayListChanged()

        log.debug('{}.init ({})'.format(type(self).__name__, id(self)))

    def __del__(self):
        """Prints a log message."""
        if log:
            log.debug('{}.del ({})'.format(type(self).__name__, id(self)))

    def destroy(self):
        """This method must be called when this ``DisplayContext`` is no
        longer needed.

        When a ``DisplayContext`` is destroyed, all of the :class:`.Display`
        instances managed by it are destroyed as well.
        """

        self.detachAllFromParent()

        overlayList = self.__overlayList
        displays = self.__displays

        self.__overlayList = None
        self.__displays = None

        overlayList.removeListener('overlays', self.__name)

        if self.__child:
            self.removeListener('syncOverlayDisplay', self.__name)
            self.removeListener('displaySpace', self.__name)
            self.removeListener('location', self.__name)
            self.removeListener('worldLocation', self.__name)

        for overlay, display in displays.items():
            display.destroy()

    def destroyed(self):
        """Returns ``True`` if this ``DisplayContext`` has been, or is being,
        destroyed, ``False`` otherwise.
        """
        return self.__overlayList is None

    def getDisplay(self, overlay, overlayType=None):
        """Returns the :class:`.Display` instance for the specified overlay
        (or overlay index).

        If the overlay is not in the ``OverlayList``, an
        :exc:`InvalidOverlayError` is raised.  Otheriwse, if a
        :class:`Display` object does not exist for the given overlay, one is
        created.

        If this ``DisplayContext`` has been destroyed, a ``ValueError`` is
        raised.

        :arg overlay:     The overlay to retrieve a ``Display`` instance for,
                          or an index into the ``OverlayList``.

        :arg overlayType: If a ``Display`` instance for the specified
                          ``overlay`` does not exist, one is created - in
                          this case, the specified ``overlayType`` is passed
                          to the :class:`.Display` constructor.
        """

        if overlay is None:
            raise ValueError('No overlay specified')

        if self.destroyed():
            raise ValueError('DisplayContext has been destroyed')

        if overlay not in self.__overlayList:
            raise InvalidOverlayError('Overlay {} is not in '
                                      'list'.format(overlay.name))

        if isinstance(overlay, int):
            overlay = self.__overlayList[overlay]

        try:
            display = self.__displays[overlay]

        except KeyError:

            if not self.__child:
                dParent = None
            else:
                dParent = self.getParent().getDisplay(overlay, overlayType)
                if overlayType is None:
                    overlayType = dParent.overlayType

            from .display import Display

            display = Display(overlay,
                              self.__overlayList,
                              self,
                              parent=dParent,
                              overlayType=overlayType)
            self.__displays[overlay] = display

        return display

    def getOpts(self, overlay, overlayType=None):
        """Returns the :class:`.DisplayOpts` instance associated with the
        specified overlay.  See :meth:`getDisplay` and :meth:`.Display.opts`
        for more details.
        """

        if overlay is None:
            raise ValueError('No overlay specified')

        if self.destroyed():
            raise ValueError('DisplayContext has been destroyed')

        if overlay not in self.__overlayList:
            raise InvalidOverlayError('Overlay {} is not in '
                                      'list'.format(overlay.name))

        return self.getDisplay(overlay, overlayType).opts

    def getReferenceImage(self, overlay):
        """Convenience method which returns the reference image associated
        with the given overlay, or ``None`` if there is no reference image.

        See the :class:`.DisplayOpts.referenceImage` method.
        """
        if overlay is None:
            return None

        return self.getOpts(overlay).referenceImage

    def displayToWorld(self, dloc):
        """Transforms the given coordinates from the display coordinate
        system into the world coordinate system.

        .. warning:: If any :attr:`.NiftiOpts.transform` properties have
                     been modified manually, this method will return invalid
                     results.
        """

        displaySpace = self.displaySpace

        if displaySpace == 'world' or len(self.__overlayList) == 0:
            return dloc

        opts = self.getOpts(displaySpace)

        return opts.transformCoords(dloc, 'display', 'world')

    def worldToDisplay(self, wloc):
        """Transforms the given coordinates from the world coordinate
        system into the display coordinate system.

        .. warning:: If any :attr:`.NiftiOpts.transform` properties have
                     been modified manually, this method will return invalid
                     results.
        """

        displaySpace = self.displaySpace

        if displaySpace == 'world' or len(self.__overlayList) == 0:
            return wloc

        opts = self.getOpts(displaySpace)

        return opts.transformCoords(wloc, 'world', 'display')

    def displaySpaceIsRadiological(self):
        """Returns ``True`` if the current :attr:`displaySpace` aligns with
        a radiological orientation. A radiological orientation is one in
        which anatomical right is shown on the left of the screen, i.e.:

          - The X axis corresponds to right -> left
          - The Y axis corresponds to posterior -> anterior
          - The Z axis corresponds to inferior -> superior
        """

        if len(self.__overlayList) == 0:
            return True

        space = self.displaySpace

        # Display space is either 'world', or an image.
        # We assume that 'world' is an RAS coordinate
        # system which, if transferred directly to a
        # display coordinate system, would result in a
        # neurological view (left on left, right on
        # right).
        if space == 'world':
            return False
        else:
            opts = self.getOpts(space)
            xform = opts.getTransform('pixdim-flip', 'display')

            return npla.det(xform) > 0

    def selectOverlay(self, overlay):
        """Selects the specified ``overlay``. Raises an :exc:`IndexError` if
        the overlay is not in the list.

        If you want to select an overlay by its index in the ``OverlayList``,
        you can just assign to the :attr:`selectedOverlay` property directly.
        """
        self.selectedOverlay = self.__overlayList.index(overlay)

    def getSelectedOverlay(self):
        """Returns the currently selected overlay object,
        or ``None`` if there are no overlays.
        """
        if len(self.__overlayList) == 0: return None
        if self.selectedOverlay >= len(self.__overlayList): return None

        return self.__overlayList[self.selectedOverlay]

    def getOverlayOrder(self, overlay):
        """Returns the order in which the given overlay (or an index into
        the :class:`.OverlayList` list) should be displayed
        (see the :attr:`overlayOrder` property).

        Raises an :exc:`IndexError` if the overlay is not in the list.
        """
        self.__syncOverlayOrder()

        if not isinstance(overlay, int):
            overlay = self.__overlayList.index(overlay)

        return self.overlayOrder.index(overlay)

    def getOrderedOverlays(self):
        """Returns a list of overlay objects from the :class:`.OverlayList`
        list, sorted into the order that they should be displayed, as defined
        by the :attr:`overlayOrder` property.
        """
        self.__syncOverlayOrder()

        return [self.__overlayList[idx] for idx in self.overlayOrder]

    @contextlib.contextmanager
    def freeze(self, overlay):
        """This method can be used as a context manager to suppress
        notification for all :class:`.Display` and :class:`.DisplayOpts`
        properties related to the given ``overlay``::

            with displayCtx.freeze(overlay):
                # Do stuff which might trigger unwanted
                # Display/DisplayOpts notifications

        See :meth:`freezeOverlay` and :meth:`thawOverlay`.
        """
        self.freezeOverlay(overlay)

        try:
            yield

        finally:
            self.thawOverlay(overlay)

    def freezeOverlay(self, overlay):
        """Suppresses notification for all :class:`.Display` and
        :class:`.DisplayOpts` properties associated with the given ``overlay``.
        Call :meth:`.thawOverlay` to re-enable notification.

        See also the :meth:`freeze` method, which can be used as a context
        manager to automatically call this method and ``thawOverlay``.
        """
        if self.__child:
            self.getParent().freezeOverlay(overlay)
            return

        dctxs = [self] + self.getChildren()

        for dctx in dctxs:
            display = dctx.getDisplay(overlay)
            opts = display.opts

            display.disableAllNotification()
            opts.disableAllNotification()

    def thawOverlay(self, overlay):
        """Enables notification for all :class:`.Display` and
        :class:`.DisplayOpts` properties associated with the given ``overlay``.
        """

        if self.__child:
            self.getParent().thawOverlay(overlay)
            return
        dctxs = [self] + self.getChildren()

        for dctx in dctxs:
            display = dctx.getDisplay(overlay)
            opts = display.opts

            display.enableAllNotification()
            opts.enableAllNotification()

    @property
    def defaultDisplaySpace(self):
        """This property controls how the :attr:`displaySpace` is initialised
        when overlays are added to a previously empty :class:`.OverlayList`.
        If the ``defaultDisplaySpace`` is set to ``'ref'``, the
        ``displaySpace`` will be initialised to the first :class:`.Nifti`
        overlay. Otherwise (the ``defaultDisplaySpace`` is set to ``'world'``),
        the ``displaySpace`` will be set to ``'world'``.
        """
        return self.__defaultDisplaySpace

    @defaultDisplaySpace.setter
    def defaultDisplaySpace(self, ds):
        """Sets the :meth:`defaultDisplaySpace`.

        :arg ds: Either ``'ref'`` or ``'world'``.
        """
        if ds not in ('world', 'ref'):
            raise ValueError('Invalid default display space: {}'.format(ds))
        self.__defaultDisplaySpace = ds

    def detachDisplaySpace(self):
        """Detaches the :attr:`displaySpace` and :attr:`bounds` properties,
        and all related :class:`.DisplayOpts` properties, from the parent
        ``DisplayContext``.

        This allows this ``DisplayContext`` to use a display coordinate
        system that is completely independent from other instances, and is not
        affected by changes to the parent properties.

        This is an irreversible operation.
        """

        self.detachFromParent('displaySpace')
        self.detachFromParent('bounds')

        for ovl in self.__overlayList:

            opts = self.getOpts(ovl)

            opts.detachFromParent('bounds')

            if isinstance(ovl, fslimage.Nifti):
                opts.detachFromParent('transform')

    def __overlayListChanged(self, *a):
        """Called when the :attr:`.OverlayList.overlays` property
        changes.

        Ensures that a :class:`.Display` and :class:`.DisplayOpts` object
        exists for every image, updates the :attr:`bounds` property, makes
        sure that the :attr:`overlayOrder` property is consistent, and updates
        constraints on the :attr:`selectedOverlay` property.
        """

        # Discard all Display instances
        # which refer to overlays that
        # are no longer in the list
        for overlay in list(self.__displays.keys()):
            if overlay not in self.__overlayList:

                display = self.__displays.pop(overlay)
                opts = display.opts

                display.removeListener('overlayType', self.__name)
                opts.removeListener('bounds', self.__name)

                # The display instance will destroy the
                # opts instance, so we don't do it here
                display.destroy()

        # Ensure that a Display object exists
        # for every overlay in the list
        for overlay in self.__overlayList:

            ovlType = self.__overlayList.initOverlayType(overlay)

            # The getDisplay method
            # will create a Display object
            # if one does not already exist
            display = self.getDisplay(overlay, ovlType)
            opts = display.opts

            # Register a listener on the overlay type,
            # because when it changes, the DisplayOpts
            # instance will change, and we will need
            # to re-register the DisplayOpts.bounds
            # listener (see the next statement)
            display.addListener('overlayType',
                                self.__name,
                                self.__overlayListChanged,
                                overwrite=True)

            # Register a listener on the DisplayOpts.bounds
            # property for every overlay - if the display
            # bounds for any overlay changes, we need to
            # update our own bounds property. This is only
            # done on child DCs, as the parent DC bounds
            # only gets used for synchronisation
            if self.__child:
                opts.addListener('bounds',
                                 self.__name,
                                 self.__overlayBoundsChanged,
                                 overwrite=True)

                # If detachDisplaySpace has been called,
                # make sure the opts bounds (and related)
                # properties are also detached
                if not self.canBeSyncedToParent('displaySpace'):
                    opts.detachFromParent('bounds')
                    if isinstance(overlay, fslimage.Nifti):
                        opts.detachFromParent('transform')

        # Ensure that the displaySpace
        # property options are in sync
        # with the overlay list.
        self.__updateDisplaySpaceOptions()

        # Stuff which only needs to
        # be done on the parent DC
        if not self.__child:

            # Limit the selectedOverlay property
            # so it cannot take a value greater
            # than len(overlayList)-1. selectedOverlay
            # is always synchronised, so we only
            # need to do this on the parent DC.
            nOverlays = len(self.__overlayList)
            if nOverlays > 0:
                self.setAttribute('selectedOverlay', 'maxval', nOverlays - 1)
            else:
                self.setAttribute('selectedOverlay', 'maxval', 0)

            return

        # Ensure that the overlayOrder
        # property is valid
        self.__syncOverlayOrder()

        # If the overlay list was empty,
        # and is now non-empty, we need
        # to initialise the display space
        # and the display location
        initDS        = self.__initDS                      and \
                        np.all(np.isclose(self.bounds, 0)) and \
                        len(self.__overlayList) > 0
        self.__initDS = len(self.__overlayList) == 0

        # Initialise the display space. We
        # have to do this before updating
        # image transforms, and updating
        # the display bounds
        if initDS:

            displaySpace = 'world'

            if self.defaultDisplaySpace == 'ref':
                for overlay in self.__overlayList:
                    if isinstance(overlay, fslimage.Nifti):
                        displaySpace = overlay
                        break

            with props.skip(self, 'displaySpace', self.__name):
                self.displaySpace = displaySpace

        # Initialise the transform property
        # of any Image overlays which have
        # just been added to the list,
        oldList = self.__overlayList.getLastValue('overlays')[:]
        for overlay in self.__overlayList:
            if isinstance(overlay, fslimage.Nifti) and \
               (overlay not in oldList):
                self.__setTransform(overlay)

        # Ensure that the bounds
        # property is accurate
        self.__updateBounds()

        # Initialise the display location to
        # the centre of the display bounds
        if initDS:
            b = self.bounds
            self.location.xyz = [
                b.xlo + b.xlen / 2.0, b.ylo + b.ylen / 2.0,
                b.zlo + b.zlen / 2.0
            ]
            self.__propagateLocation('world')
        else:
            self.__propagateLocation('display')

    def __updateDisplaySpaceOptions(self):
        """Updates the :attr:`displaySpace` property so it is synchronised with
        the current contents of the :class:`.OverlayList`

        This method is called by the :meth:`__overlayListChanged` method.
        """

        choiceProp = self.getProp('displaySpace')
        choices = []

        for overlay in self.__overlayList:
            if isinstance(overlay, fslimage.Nifti):
                choices.append(overlay)

        choices.append('world')

        choiceProp.setChoices(choices, instance=self)

    def __setTransform(self, image):
        """Sets the :attr:`.NiftiOpts.transform` property associated with
        the given :class:`.Nifti` overlay to a sensible value, given the
        current value of the :attr:`.displaySpace` property.

        Called by the :meth:`__displaySpaceChanged` method, and by
        :meth:`__overlayListChanged` for any :class:`.Image` overlays which
        have been newly added to the :class:`.OverlayList`.

        :arg image: An :class:`.Image` overlay.
        """

        space = self.displaySpace
        opts = self.getOpts(image)

        # Disable notification of the bounds
        # property so the __overlayBoundsChanged
        # method does not get called. Use
        # ignoreInvalid, because this method might
        # get called before we have registered a
        # listener on the bounds property.
        with props.skip(opts, 'bounds', self.__name, ignoreInvalid=True):
            if space == 'world': opts.transform = 'affine'
            elif image is space: opts.transform = 'pixdim-flip'
            else: opts.transform = 'reference'

    def __displaySpaceChanged(self, *a):
        """Called when the :attr:`displaySpace` property changes. Updates the
        :attr:`.NiftiOpts.transform` property for all :class:`.Nifti`
        overlays in the :class:`.OverlayList`.
        """

        selectedOverlay = self.getSelectedOverlay()

        if selectedOverlay is None:
            return

        # Update the transform property of all
        # Image overlays to put them into the
        # new display space
        for overlay in self.__overlayList:

            if not isinstance(overlay, fslimage.Nifti):
                continue

            self.__setTransform(overlay)

        # Update the display world bounds,
        # and then update the location
        self.__updateBounds()

        # Make sure that the location is
        # kept in the same place, relative
        # to the world coordinate system
        self.__propagateLocation('display')

    def __syncOverlayOrder(self):
        """Ensures that the :attr:`overlayOrder` property is up to date
        with respect to the :class:`.OverlayList`.
        """

        if len(self.overlayOrder) == len(self.__overlayList):
            return

        #
        # NOTE: The following logic assumes that operations
        #       which modify the overlay list will only do
        #       one of the following:
        #
        #        - Adding one or more overlays to the end of the list
        #        - Removing one or more overlays from the list
        #
        # More complex overlay list modifications
        # will cause this code to break.

        oldList = self.__overlayList.getLastValue('overlays')[:]
        oldOrder = self.overlayOrder[:]

        # If the overlay order was just the
        # list order, preserve that ordering
        if self.overlayOrder[:] == list(range(len(oldList))):
            self.overlayOrder[:] = list(range(len(self.__overlayList)))

        # If overlays have been added to
        # the overlay list, add indices
        # for them to the overlayOrder list
        elif len(self.overlayOrder) < len(self.__overlayList):

            newOrder = []
            newOverlayIdx = len(oldList)

            # The order of existing overlays is preserved,
            # and all new overlays added to the end of the
            # overlay order.
            for overlay in self.__overlayList:

                if overlay in oldList:
                    newOrder.append(oldOrder[oldList.index(overlay)])
                else:
                    newOrder.append(newOverlayIdx)
                    newOverlayIdx += 1

            self.overlayOrder[:] = newOrder

        # Otherwise, if overlays have been
        # removed from the overlay list ...
        elif len(self.overlayOrder) > len(self.__overlayList):

            # Remove the corresponding indices
            # from the overlayOrder list
            for i, overlay in enumerate(oldList):
                if overlay not in self.__overlayList:
                    oldOrder.remove(i)

            # Re-generate new indices,
            # preserving the order of
            # the remaining overlays
            newOrder = [sorted(oldOrder).index(idx) for idx in oldOrder]
            self.overlayOrder[:] = newOrder

    def __overlayBoundsChanged(self, value, valid, opts, name):
        """Called when the :attr:`.DisplayOpts.bounds` property of any
        overlay changes. Updates the :attr:`bounds` property and preserves
        the display :attr:`location` in terms of the :attr:`worldLocation`.
        """

        # This method might get called
        # after DisplayOpts instance
        # has been destroyed
        if opts.display is None:
            return

        # Update the display context bounds
        # to take into account any changes
        # to individual overlay bounds.
        # Inhibit notification on the location
        # property - it will be updated properly
        # below
        self.__updateBounds()

        # Make sure the display location
        # is consistent w.r.t. the world
        # coordinate location
        self.__propagateLocation('display')

    def __syncOverlayDisplayChanged(self, *a):
        """Called when the :attr:`syncOverlayDisplay` property
        changes.

        Synchronises or unsychronises the :class:`.Display` and
        :class:`.DisplayOpts` instances for every overlay to/from their
        parent instances.
        """

        dcProps = ['displaySpace', 'bounds', 'radioOrientation']

        if self.syncOverlayDisplay:
            for p in dcProps:
                if self.canBeSyncedToParent(p):
                    self.syncToParent(p)

        else:
            for p in dcProps:
                if self.canBeUnsyncedFromParent(p):
                    self.unsyncFromParent(p)

        for display in self.__displays.values():

            opts = display.opts

            if self.syncOverlayDisplay:
                display.syncAllToParent()
                opts.syncAllToParent()
            else:
                display.unsyncAllFromParent()
                opts.unsyncAllFromParent()

    def __updateBounds(self, *a):
        """Called when the overlay list changes, or when any overlay display
        transform is changed. Updates the :attr:`bounds` property so that it
        is big enough to contain all of the overlays (as defined by their
        :attr:`.DisplayOpts.bounds` properties).
        """

        if len(self.__overlayList) == 0:
            minBounds = [0.0, 0.0, 0.0]
            maxBounds = [0.0, 0.0, 0.0]

        else:
            minBounds = 3 * [sys.float_info.max]
            maxBounds = 3 * [-sys.float_info.max]

        for ovl in self.__overlayList:

            display = self.__displays[ovl]
            opts = display.opts
            lo = opts.bounds.getLo()
            hi = opts.bounds.getHi()

            for ax in range(3):

                if lo[ax] < minBounds[ax]: minBounds[ax] = lo[ax]
                if hi[ax] > maxBounds[ax]: maxBounds[ax] = hi[ax]

        self.bounds[:] = [
            minBounds[0], maxBounds[0], minBounds[1], maxBounds[1],
            minBounds[2], maxBounds[2]
        ]

        # Update the constraints on the location
        # property to be aligned with the new bounds
        with props.suppress(self, 'location'):
            self.location.setLimits(0, self.bounds.xlo, self.bounds.xhi)
            self.location.setLimits(1, self.bounds.ylo, self.bounds.yhi)
            self.location.setLimits(2, self.bounds.zlo, self.bounds.zhi)

    def __locationChanged(self, *a):
        """Called when the :attr:`location` property changes. Propagates
        the new location to the :attr:`worldLocation` property.
        """
        self.__propagateLocation('world')

    def __worldLocationChanged(self, *a):
        """Called when the :attr:`worldLocation` property changes. Propagates
        the new location to the :attr:`location` property.
        """

        self.__propagateLocation('display')

    def __propagateLocation(self, dest):
        """Called by the :meth:`__locationChanged` and
        :meth:`__worldLocationChanged` methods. The ``dest`` argument may be
        either ``'world'`` (the ``worldLocation`` is updated from the
        ``location``), or ``'display'`` (vice-versa).
        """

        if self.displaySpace == 'world':
            if dest == 'world':
                with props.skip(self, 'worldLocation', self.__name):
                    self.worldLocation = self.location
            else:
                with props.skip(self, 'location', self.__name):
                    self.location = self.worldLocation
            return

        ref = self.displaySpace
        opts = self.getOpts(ref)

        if dest == 'world':
            with props.skip(self, 'location', self.__name):
                self.worldLocation = opts.transformCoords(
                    self.location, 'display', 'world')
        else:
            with props.skip(self, 'worldLocation', self.__name):
                self.location = opts.transformCoords(self.worldLocation,
                                                     'world', 'display')