Пример #1
0
class VolumeRGBOpts(niftiopts.NiftiOpts):
    """The ``VolumeRGBOpts`` class is intended for displaying
    :class:`.Image` instances containing RGB(A) data.
    """

    rColour = props.Colour(default=(1, 0, 0))
    """Colour to use for the red channel. """

    gColour = props.Colour(default=(0, 1, 0))
    """Colour to use for the green channel. """

    bColour = props.Colour(default=(0, 0, 1))
    """Colour to use for the blue channel. """

    suppressR = props.Boolean(default=False)
    """Suppress the R channel. """

    suppressG = props.Boolean(default=False)
    """Suppress the G channel. """

    suppressB = props.Boolean(default=False)
    """Suppress the B channel. """

    suppressA = props.Boolean(default=False)
    """Suppress the A channel. """

    suppressMode = props.Choice(('white', 'black', 'transparent'))
    """How colours should be suppressed. """

    interpolation = copy.copy(VolumeOpts.interpolation)
    """See :attr:`VolumeOpts.interpolation`. """
    def __init__(self, overlay, display, overlayList, displayCtx, **kwargs):
        """Create a :class:`VolumeRGBOpts` instance for the specified
        ``overlay``, assumed to be an :class:`.Image` instance with type
        ``NIFTI_TYPE_RGB24`` or ``NIFTI_TYPE_RGBA32``.

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

        # We need GL >= 2.1 for
        # spline interpolation
        if float(fslplatform.glVersion) < 2.1:
            interp = self.getProp('interpolation')
            interp.removeChoice('spline', instance=self)
            interp.updateChoice('linear', instance=self, newAlt=['spline'])

        niftiopts.NiftiOpts.__init__(self, overlay, display, overlayList,
                                     displayCtx, **kwargs)
Пример #2
0
class Scene3DCanvasOpts(props.HasProperties):
    """The ``Scene3DCanvasOpts`` class defines the display settings
    available on :class:`.Scene3DCanvas` instances.
    """

    pos = copy.copy(SliceCanvasOpts.pos)
    """Current cursor position in the display coordinate system. The dimensions
    are in the same ordering as the display coordinate system, in contrast
    to the :attr:`SliceCanvasOpts.pos` property.
    """


    showCursor   = copy.copy(SliceCanvasOpts.showCursor)
    cursorColour = copy.copy(SliceCanvasOpts.cursorColour)
    bgColour     = copy.copy(SliceCanvasOpts.bgColour)
    zoom         = copy.copy(SliceCanvasOpts.zoom)


    showLegend = props.Boolean(default=True)
    """If ``True``, an orientation guide will be shown on the canvas. """


    legendColour = props.Colour(default=(0, 1, 0))
    """Colour to use for the legend text."""


    occlusion = props.Boolean(default=True)
    """If ``True``, objects closer to the camera will occlude objects
    further away. Toggles ``gl.DEPTH_TEST``.
    """


    light = props.Boolean(default=True)
    """If ``True``, a lighting effect is applied to compatible overlays
    in the scene.
    """


    lightPos = props.Point(ndims=3)
    """Light position in the display coordinate system. """


    offset = props.Point(ndims=2)
    """An offset, in X/Y pixels normalised to the range ``[-1, 1]``, from the
    centre of the ``Scene3DCanvas``.
    """


    rotation = props.Array(
        dtype=np.float64,
        shape=(3, 3),
        resizable=False,
        default=[[1, 0, 0], [0, 1, 0], [0, 0, 1]])
    """A rotation matrix which defines the current ``Scene3DCanvas`` view
    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()
Пример #4
0
class MeshOpts(cmapopts.ColourMapOpts, fsldisplay.DisplayOpts):
    """The ``MeshOpts`` class defines settings for displaying :class:`.Mesh`
    overlays. See also the :class:`.GiftiOpts` and :class:`.FreesurferOpts`
    sub-classes.
    """

    colour = props.Colour()
    """The mesh colour. """

    outline = props.Boolean(default=False)
    """If ``True``, an outline of the mesh is shown. Otherwise a
    cross- section of the mesh is filled.
    """

    outlineWidth = props.Real(minval=0.1, maxval=10, default=2, clamped=False)
    """If :attr:`outline` is ``True``, this property defines the width of the
    outline in pixels.
    """

    showName = props.Boolean(default=False)
    """If ``True``, the mesh name is shown alongside it.

    .. note:: Not implemented yet, and maybe never will be.
    """

    discardClipped = props.Boolean(default=False)
    """Flag which controls clipping. When the mesh is coloured according to
    some data (the :attr:`vertexData` property), vertices with a data value
    outside of the clipping range are either discarded (not drawn), or
    they are still drawn, but not according to the data, rather with the
    flat :attr:`colour`.
    """

    vertexSet = props.Choice((None, ))
    """May be populated with the names of files which contain different
    vertex sets for the :class:`.Mesh` object.
    """

    vertexData = props.Choice((None, ))
    """May be populated with the names of files which contain data associated
    with each vertex in the mesh, that can be used to colour the mesh. When
    some vertex data has been succsessfully loaded, it can be accessed via
    the :meth:`getVertexData` method.
    """

    vertexDataIndex = props.Int(minval=0, maxval=0, default=0, clamped=True)
    """If :attr:`vertexData` is loaded, and has multiple data points per
    vertex (e.g. time series), this property controls the index into the
    data.
    """

    refImage = props.Choice()
    """A reference :class:`.Image` instance which the mesh coordinates are
    in terms of.

    For example, if this :class:`.Mesh` represents the segmentation of
    a sub-cortical region from a T1 image, you would set the ``refImage`` to
    that T1 image.

    Any :class:`.Image` instance in the :class:`.OverlayList` may be chosen
    as the reference image.
    """

    useLut = props.Boolean(default=False)
    """If ``True``, and if some :attr:`vertexData` is loaded, the :attr:`lut`
    is used to colour vertex values instead of the :attr:`cmap` and
    :attr:`negativeCmap`.
    """

    lut = props.Choice()
    """If :attr:`useLut` is ``True``, a :class:`.LookupTable` is used to
    colour vertex data instead of the :attr:`cmap`/:attr:`negativeCmap`.
    """

    # This property is implicitly tightly-coupled to
    # the NiftiOpts.getTransform method - the choices
    # defined in this property are assumed to be valid
    # inputs to that method (with the exception of
    # ``'torig'``).
    coordSpace = props.Choice(
        ('torig', 'affine', 'pixdim', 'pixdim-flip', 'id'),
        default='pixdim-flip')
    """If :attr:`refImage` is not ``None``, this property defines the
    reference image coordinate space in which the mesh coordinates are
    defined (i.e. voxels, scaled voxels, or world coordinates).

    =============== =========================================================
    ``affine``      The mesh coordinates are defined in the reference image
                    world coordinate system.

    ``torig``       Equivalent to ``'affine'``, except for
                    :class:`.FreesurferOpts`  sub-classes.

    ``id``          The mesh coordinates are defined in the reference image
                    voxel coordinate system.

    ``pixdim``      The mesh coordinates are defined in the reference image
                    voxel coordinate system, scaled by the voxel pixdims.

    ``pixdim-flip`` The mesh coordinates are defined in the reference image
                    voxel coordinate system, scaled by the voxel pixdims. If
                    the reference image transformation matrix has a positive
                    determinant, the X axis is flipped.
    =============== =========================================================

    The default value is ``pixdim-flip``, as this is the coordinate system
    used in the VTK sub-cortical segmentation model files output by FIRST.
    See also the :ref:`note on coordinate systems
    <volumeopts-coordinate-systems>`, and the :meth:`.NiftiOpts.getTransform`
    method.
    """

    wireframe = props.Boolean(default=False)
    """3D only. If ``True``, the mesh is rendered as a wireframe. """
    def __init__(self, overlay, *args, **kwargs):
        """Create a ``MeshOpts`` instance. All arguments are passed through
        to the :class:`.DisplayOpts` constructor.
        """

        # Set a default colour
        colour = genMeshColour(overlay)
        self.colour = np.concatenate((colour, [1.0]))

        # ColourMapOpts.linkLowRanges defaults to
        # True, which is annoying for surfaces.
        self.linkLowRanges = False

        # A copy of the refImage property
        # value is kept here so, when it
        # changes, we can de-register from
        # the previous one.
        self.__oldRefImage = None

        # When the vertexData property is
        # changed, the data (and its min/max)
        # is loaded and stored in these
        # attributes. See the __vertexDataChanged
        # method.
        self.__vertexData = None
        self.__vertexDataRange = None

        nounbind = kwargs.get('nounbind', [])
        nounbind.extend(['refImage', 'coordSpace', 'vertexData', 'vertexSet'])
        kwargs['nounbind'] = nounbind

        fsldisplay.DisplayOpts.__init__(self, overlay, *args, **kwargs)
        cmapopts.ColourMapOpts.__init__(self)

        self.__registered = self.getParent() is not None

        # Load all vertex data and vertex
        # sets on the parent opts instance
        if not self.__registered:
            self.addVertexSetOptions(overlay.vertexSets())
            self.addVertexDataOptions(overlay.vertexDataSets())

        # The master MeshOpts instance is just a
        # sync-slave, so we only need to register
        # property listeners on child instances
        else:

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

            self.addListener('refImage',
                             self.name,
                             self.__refImageChanged,
                             immediate=True)
            self.addListener('coordSpace',
                             self.name,
                             self.__coordSpaceChanged,
                             immediate=True)

            # We need to keep colour[3]
            # keeps colour[3] and Display.alpha
            # consistent w.r.t. each other (see
            # also MaskOpts)
            self.display.addListener('alpha',
                                     self.name,
                                     self.__alphaChanged,
                                     immediate=True)
            self.addListener('colour',
                             self.name,
                             self.__colourChanged,
                             immediate=True)

            self.addListener('vertexData',
                             self.name,
                             self.__vertexDataChanged,
                             immediate=True)
            self.addListener('vertexSet',
                             self.name,
                             self.__vertexSetChanged,
                             immediate=True)
            overlay.register(self.name, self.__overlayVerticesChanged,
                             'vertices')

            self.__overlayListChanged()
            self.__updateBounds()

        # If we have inherited values from a
        # parent instance, make sure the vertex
        # data (if set) is initialised
        self.__vertexDataChanged()

        # If a reference image has not
        # been set on the parent MeshOpts
        # instance, see  if there is a
        # suitable one in the overlay list.
        if self.refImage is None:
            self.refImage = fsloverlay.findMeshReferenceImage(
                self.overlayList, self.overlay)

    def destroy(self):
        """Removes some property listeners, and calls the
        :meth:`.DisplayOpts.destroy` method.
        """

        if self.__registered:

            self.overlayList.removeListener('overlays', self.name)
            self.display.removeListener('alpha', self.name)
            self.removeListener('colour', self.name)
            self.overlay.deregister(self.name, 'vertices')

            for overlay in self.overlayList:

                # An error could be raised if the
                # DC has been/is being destroyed
                try:

                    display = self.displayCtx.getDisplay(overlay)
                    opts = self.displayCtx.getOpts(overlay)

                    display.removeListener('name', self.name)

                    if overlay is self.refImage:
                        opts.removeListener('transform', self.name)

                except Exception:
                    pass

        self.__oldRefImage = None
        self.__vertexData = None

        cmapopts.ColourMapOpts.destroy(self)
        fsldisplay.DisplayOpts.destroy(self)

    @classmethod
    def getVolumeProps(cls):
        """Overrides :meth:`DisplayOpts.getVolumeProps`. Returns a list
        of property names which control the displayed volume/timepoint.
        """
        return ['vertexDataIndex']

    def getDataRange(self):
        """Overrides the :meth:`.ColourMapOpts.getDisplayRange` method.
        Returns the display range of the currently selected
        :attr:`vertexData`, or ``(0, 1)`` if none is selected.
        """
        if self.__vertexDataRange is None: return (0, 1)
        else: return self.__vertexDataRange

    def getVertexData(self):
        """Returns the :attr:`.MeshOpts.vertexData`, if some is loaded.
        Returns ``None`` otherwise.
        """
        return self.__vertexData

    def vertexDataLen(self):
        """Returns the length (number of data points per vertex) of the
        currently selected :attr:`vertexData`, or ``0`` if no vertex data is
        selected.
        """

        if self.__vertexData is None:
            return 0

        elif len(self.__vertexData.shape) == 1:
            return 1

        else:
            return self.__vertexData.shape[1]

    def addVertexDataOptions(self, paths):
        """Adds the given sequence of paths as options to the
        :attr:`vertexData` property. It is assumed that the paths refer
        to valid vertex data files for the overlay associated with this
        ``MeshOpts`` instance.
        """

        vdataProp = self.getProp('vertexData')
        newPaths = paths
        paths = vdataProp.getChoices(instance=self)
        paths = paths + [p for p in newPaths if p not in paths]

        vdataProp.setChoices(paths, instance=self)

    def addVertexSetOptions(self, paths):
        """Adds the given sequence of paths as options to the
        :attr:`vertexSet` property. It is assumed that the paths refer
        to valid vertex files for the overlay associated with this
        ``MeshOpts`` instance.
        """

        vsetProp = self.getProp('vertexSet')
        newPaths = paths
        paths = vsetProp.getChoices(instance=self)
        paths = paths + [p for p in newPaths if p not in paths]

        vsetProp.setChoices(paths, instance=self)

    def getConstantColour(self):
        """Returns the current :attr::`colour`, adjusted according to the
        current :attr:`.Display.brightness`, :attr:`.Display.contrast`, and
        :attr:`.Display.alpha`.
        """

        display = self.display

        # Only apply bricon if there is no vertex data assigned
        if self.vertexData is None:
            brightness = display.brightness / 100.0
            contrast = display.contrast / 100.0
        else:
            brightness = 0.5
            contrast = 0.5

        colour = list(
            fslcmaps.applyBricon(self.colour[:3], brightness, contrast))

        colour.append(display.alpha / 100.0)

        return colour

    @property
    def referenceImage(self):
        """Overrides :meth:`.DisplayOpts.referenceImage`.

        If a :attr:`refImage` is selected, it is returned. Otherwise,``None``
        is returned.
        """
        return self.refImage

    @deprecated.deprecated('0.22.3', '1.0.0', 'Use getTransform instead')
    def getCoordSpaceTransform(self):
        """Returns a transformation matrix which can be used to transform
        the :class:`.Mesh` vertex coordinates into the display
        coordinate system.

        If no :attr:`refImage` is selected, this method returns an identity
        transformation.
        """

        if self.refImage is None or self.refImage not in self.overlayList:
            return np.eye(4)

        opts = self.displayCtx.getOpts(self.refImage)

        return opts.getTransform(self.coordSpace, opts.transform)

    def getVertex(self, xyz=None):
        """Returns an integer identifying the index of the mesh vertex that
        coresponds to the given ``xyz`` location,

        :arg xyz: Location to convert to a vertex index. If not provided, the
                  current :class:`.DisplayContext.location` is used.
        """

        # TODO return vertex closest to the point,
        #      within some configurabe tolerance?

        if xyz is None:
            xyz = self.displayCtx.location.xyz
            xyz = self.transformCoords(xyz, 'display', 'mesh')

        vert = None
        vidx = self.displayCtx.vertexIndex

        if vidx >= 0 and vidx <= self.overlay.nvertices:
            vert = self.overlay.vertices[vidx, :]

        if vert is not None and np.all(np.isclose(vert, xyz)):
            return vidx
        else:
            return None

    def normaliseSpace(self, space):
        """Used by :meth:`transformCoords` and :meth:`getTransform` to
        normalise their ``from_`` and ``to`` parameters.
        """
        if space not in ('world', 'display', 'mesh'):
            raise ValueError('Invalid space: {}'.format(space))

        if space == 'mesh': space = self.coordSpace
        if space == 'torig': space = 'affine'

        return space

    def transformCoords(self, coords, from_, to, *args, **kwargs):
        """Transforms the given ``coords`` from ``from_`` to ``to``.

        :arg coords: Coordinates to transform.
        :arg from_:  Space that the coordinates are in
        :arg to:     Space to transform the coordinates to

        All other parameters are passed through to the
        :meth:`.NiftiOpts.transformCoords` method of the reference image
        ``DisplayOpts``.

        The following values are accepted for the ``from_`` and ``to``
        parameters:

          - ``'world'``:  World coordinate system
          - ``'display'`` Display coordinate system
          - ``'mesh'``    The coordinate system of this mesh.
        """

        from_ = self.normaliseSpace(from_)
        to = self.normaliseSpace(to)

        if self.refImage is None:
            return coords

        opts = self.displayCtx.getOpts(self.refImage)

        return opts.transformCoords(coords, from_, to, *args, **kwargs)

    def getTransform(self, from_, to):
        """Return a matrix which may be used to transform coordinates from
        ``from_`` to ``to``.

        The following values are accepted for the ``from_`` and ``to``
        parameters:

          - ``'world'``:  World coordinate system
          - ``'display'`` Display coordinate system
          - ``'mesh'``    The coordinate system of this mesh.
        """

        from_ = self.normaliseSpace(from_)
        to = self.normaliseSpace(to)

        if self.refImage is None:
            return np.eye(4)

        opts = self.displayCtx.getOpts(self.refImage)

        return opts.getTransform(from_, to)

    def __transformChanged(self, value, valid, ctx, name):
        """Called when the :attr:`.NiftiOpts.transform` property of the current
        :attr:`refImage` changes. Calls :meth:`__updateBounds`.
        """
        self.__updateBounds()

    def __coordSpaceChanged(self, *a):
        """Called when the :attr:`coordSpace` property changes.
        Calls :meth:`__updateBounds`.
        """
        self.__updateBounds()

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

        If a new reference image has been specified, removes listeners from
        the old one (if necessary), and adds listeners to the
        :attr:`.NiftiOpts.transform` property associated with the new image.
        Calls :meth:`__updateBounds`.
        """

        # TODO You are not tracking changes to the
        # refImage overlay type -  if this changes,
        # you will need to re-bind to the transform
        # property of the new DisplayOpts instance

        if self.__oldRefImage is not None and \
           self.__oldRefImage in self.overlayList:

            opts = self.displayCtx.getOpts(self.__oldRefImage)
            opts.removeListener('transform', self.name)

        self.__oldRefImage = self.refImage

        if self.refImage is not None:
            opts = self.displayCtx.getOpts(self.refImage)
            opts.addListener('transform',
                             self.name,
                             self.__transformChanged,
                             immediate=True)

        self.__updateBounds()

    def __updateBounds(self):
        """Called whenever any of the :attr:`refImage`, :attr:`coordSpace`,
        or :attr:`transform` properties change.

        Updates the :attr:`.DisplayOpts.bounds` property accordingly.
        """

        lo, hi = self.overlay.bounds
        xform = self.getTransform('mesh', 'display')

        lohi = transform.transform([lo, hi], xform)
        lohi.sort(axis=0)
        lo, hi = lohi[0, :], lohi[1, :]

        oldBounds = self.bounds
        self.bounds = [lo[0], hi[0], lo[1], hi[1], lo[2], hi[2]]

        if np.all(np.isclose(oldBounds, self.bounds)):
            self.propNotify('bounds')

    def __overlayListChanged(self, *a):
        """Called when the overlay list changes. Updates the :attr:`refImage`
        property so that it contains a list of overlays which can be
        associated with the mesh.
        """

        imgProp = self.getProp('refImage')
        imgVal = self.refImage
        overlays = self.displayCtx.getOrderedOverlays()

        # the overlay for this MeshOpts
        # instance has been removed
        if self.overlay not in overlays:
            self.overlayList.removeListener('overlays', self.name)
            return

        imgOptions = [None]

        for overlay in overlays:

            # The overlay must be a Nifti instance.
            if not isinstance(overlay, fslimage.Nifti):
                continue

            imgOptions.append(overlay)

            display = self.displayCtx.getDisplay(overlay)
            display.addListener('name',
                                self.name,
                                self.__overlayListChanged,
                                overwrite=True)

        # The previous refImage may have
        # been removed from the overlay list
        if imgVal in imgOptions: self.refImage = imgVal
        else: self.refImage = None

        imgProp.setChoices(imgOptions, instance=self)

    def __overlayVerticesChanged(self, *a):
        """Called when the :attr:`.Mesh.vertices` change. Makes sure that the
        :attr:`vertexSet` attribute is synchronised.
        """

        vset = self.overlay.selectedVertices()
        vsprop = self.getProp('vertexSet')

        if vset not in vsprop.getChoices(instance=self):
            self.addVertexSetOptions([vset])
        self.vertexSet = vset

    def __vertexSetChanged(self, *a):
        """Called when the :attr:`.MeshOpts.vertexSet` property changes.
        Updates the current vertex set on the :class:`.Mesh` overlay, and
        the overlay bounds.
        """

        if self.vertexSet not in self.overlay.vertexSets():
            self.overlay.loadVertices(self.vertexSet)
        else:
            with self.overlay.skip(self.name, 'vertices'):
                self.overlay.vertices = self.vertexSet

        self.__updateBounds()

    def __vertexDataChanged(self, *a):
        """Called when the :attr:`vertexData` property changes. Attempts to
        load the data if possible. The data may subsequently be retrieved
        via the :meth:`getVertexData` method.
        """

        vdata = None
        vdataRange = None
        overlay = self.overlay
        vdfile = self.vertexData

        try:
            if vdfile is not None:

                if vdfile not in overlay.vertexDataSets():
                    log.debug('Loading vertex data: {}'.format(vdfile))
                    vdata = overlay.loadVertexData(vdfile)
                else:
                    vdata = overlay.getVertexData(vdfile)

                vdataRange = np.nanmin(vdata), np.nanmax(vdata)

                if len(vdata.shape) == 1:
                    vdata = vdata.reshape(-1, 1)

        except Exception as e:

            # TODO show a warning
            log.warning('Unable to load vertex data from {}: {}'.format(
                vdfile, e, exc_info=True))

            vdata = None
            vdataRange = None

        self.__vertexData = vdata
        self.__vertexDataRange = vdataRange

        if vdata is not None: npoints = vdata.shape[1]
        else: npoints = 1

        self.vertexDataIndex = 0
        self.setAttribute('vertexDataIndex', 'maxval', npoints - 1)

        self.updateDataRange()

    def __colourChanged(self, *a):
        """Called when :attr:`.colour` changes. Updates :attr:`.Display.alpha`
        from the alpha component.
        """

        alpha = self.colour[3] * 100

        log.debug('Propagating MeshOpts.colour[3] to '
                  'Display.alpha [{}]'.format(alpha))

        with props.skip(self.display, 'alpha', self.name):
            self.display.alpha = alpha

    def __alphaChanged(self, *a):
        """Called when :attr:`.Display.alpha` changes. Updates the alpha
        component of :attr:`.colour`.
        """

        alpha = self.display.alpha / 100.0
        r, g, b, _ = self.colour

        log.debug('Propagating Display.alpha to MeshOpts.'
                  'colour[3] [{}]'.format(alpha))

        with props.skip(self, 'colour', self.name):
            self.colour = r, g, b, alpha
Пример #5
0
class DataSeries(props.HasProperties):
    """A ``DataSeries`` instance encapsulates some data to be plotted by
    a :class:`PlotPanel`, with the data extracted from an overlay in the
    :class:`.OverlayList`.

    Sub-class implementations must:

      - Accept an overlay object in their ``__init__`` method
      - Pass this overlay to meth:`.DataSeries.__init__`
      - Override the :meth:`getData` method
      - Override the :meth:`redrawProperties` method if necessary


    The overlay is accessible as an instance attribute, confusingly called
    ``overlay``.


    .. note:: Some ``DataSeries`` instances may not be associated with
              an overlay (e.g. series imported loaded a text file). In
              this case, the ``overlay`` attribute will be ``None``.


    Each``DataSeries`` instance is plotted as a line, with the line
    style defined by properties on the ``DataSeries`` instance,
    such as :attr:`colour`, :attr:`lineWidth` etc.
    """

    colour = props.Colour()
    """Line colour. """

    enabled = props.Boolean(default=True)
    """Draw or not draw?"""

    alpha = props.Real(minval=0.0, maxval=1.0, default=1.0, clamped=True)
    """Line transparency."""

    label = props.String()
    """Line label (used in the plot legend)."""

    lineWidth = props.Choice((0.5, 1, 2, 3, 4, 5))
    """Line width. """

    lineStyle = props.Choice(('-', '--', '-.', ':'))
    """Line style. """
    def __init__(self, overlay):
        """Create a ``DataSeries``.

        :arg overlay: The overlay from which the data to be plotted is
                      retrieved.  May be ``None``.
        """

        self.__overlay = overlay
        self.setData([], [])

        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 __hash__(self):
        """Returns a hash for this ``DataSeries`` instance."""
        return hash(id(self))

    @property
    def overlay(self):
        """Returns the overlay associated with this ``DataSeries`` instance.
        """
        return self.__overlay

    def destroy(self):
        """This method must be called when this ``DataSeries`` instance is no
        longer needed. This implementation does nothing, but it should be
        overridden by sub-classes which need to perform any clean-up
        operations.
        """
        pass

    def redrawProperties(self):
        """Returns a list of all properties which, when their values change,
        should result in this ``DataSeries`` being re-plotted. This method
        may be overridden by sub-classes.
        """

        return self.getAllProperties()[0]

    def setData(self, xdata, ydata):
        """Set the data to be plotted. This method is irrelevant if a
        ``DataSeries`` sub-class has overridden :meth:`getData`.
        """
        self.__xdata = xdata
        self.__ydata = ydata

    def getData(self):
        """This method should be overridden by sub-classes. It must return
        the data to be plotted, as a tuple of the form:

            ``(xdata, ydata)``

        where ``xdata`` and ``ydata`` are sequences containing the x/y data
        to be plotted.

        The default implementation returns the data that has been set via the
        :meth:`setData` method.
        """
        return self.__xdata, self.__ydata
Пример #6
0
class ColourBar(props.HasProperties, notifier.Notifier):
    """A ``ColourBar`` is an object which listens to the properties of a
    :class:`.ColourMapOpts` instance, and automatically generates a colour
    bar bitmap representing the current colour map properties.

    Whenever the colour bar is refreshed, a notification is emitted via the
    :class:`.Notifier` interface.
    """


    orientation = props.Choice(('horizontal', 'vertical'))
    """Whether the colour bar should be vertical or horizontal. """


    labelSide = props.Choice(('top-left', 'bottom-right'))
    """Whether the colour bar labels should be on the top/left, or bottom/right
    of the colour bar (depending upon whether the colour bar orientation is
    horizontal/vertical).
    """


    textColour = props.Colour(default=(1, 1, 1, 1))
    """Colour to use for the colour bar label. """


    bgColour = props.Colour(default=(0, 0, 0, 1))
    """Colour to use for the background. """


    showLabel = props.Boolean(default=True)
    """Toggle the colour bar label (the :attr:`.Display.name` property). """


    showTicks = props.Boolean(default=True)
    """Toggle the tick labels (the :attr:`.ColourMapOpts.displayRange`). """


    fontSize = props.Int(minval=4, maxval=96, default=12)
    """Size of the font used for the text on the colour bar."""


    def __init__(self, overlayList, displayCtx):
        """Create a ``ColourBar``.

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


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

        overlayList.addListener('overlays',
                                self.name,
                                self.__selectedOverlayChanged)
        displayCtx .addListener('selectedOverlay',
                                self.name,
                                self.__selectedOverlayChanged)

        self.addGlobalListener(self.name, self.__clearColourBar)

        self.__opts      = None
        self.__display   = None
        self.__size      = (None, None, None)
        self.__colourBar = None

        self.__selectedOverlayChanged()


    @property
    def name(self):
        """Return the name of this ColourBar, used internally for registering
        property listeners.
        """
        return self.__name


    def destroy(self):
        """Must be called when this ``ColourBar`` is no longer needed.

        Removes all registered listeners from the :class:`.OverlayList`,
        :class:`.DisplayContext`, and foom individual overlays.
        """


        self.__overlayList.removeListener('overlays',        self.name)
        self.__displayCtx .removeListener('selectedOverlay', self.name)
        self.__deregisterOverlay()


    def __selectedOverlayChanged(self, *a):
        """Called when the :class:`.OverlayList` or the
        :attr:`.DisplayContext.selectedOverlay` changes.

        If the newly selected overlay is being displayed with a
        :class:`.ColourMapOpts` instance, various property listeners are
        registered, and the colour bar is refreshed.
        """

        self.__deregisterOverlay()
        self.__registerOverlay()
        self.__clearColourBar()


    def __deregisterOverlay(self):
        """Called when the selected overlay changes. De-registers property
        listeners from any previously-registered :class:`.ColourMapOpts`
        instance.
        """

        if self.__opts is None:
            return

        try:
            opts    = self.__opts
            display = self.__display

            opts   .removeListener('displayRange',    self.name)
            opts   .removeListener('cmap',            self.name)
            opts   .removeListener('negativeCmap',    self.name)
            opts   .removeListener('useNegativeCmap', self.name)
            opts   .removeListener('invert',          self.name)
            opts   .removeListener('gamma',           self.name)
            opts   .removeListener('cmapResolution',  self.name)
            display.removeListener('name',            self.name)

        except fsldc.InvalidOverlayError:
            pass

        self.__opts    = None
        self.__display = None


    def __registerOverlay(self):
        """Called when the selected overlay changes. Registers property
        listeners with the :class:`.ColourMapOpts` instance associated with
        the newly selected overlay.
        """

        overlay = self.__displayCtx.getSelectedOverlay()

        if overlay is None:
            return False

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

        if not isinstance(opts, cmapopts.ColourMapOpts):
            return False

        self.__opts    = opts
        self.__display = display

        opts   .addListener('displayRange',
                            self.name,
                            self.__clearColourBar)
        opts   .addListener('cmap',
                            self.name,
                            self.__clearColourBar)
        opts   .addListener('negativeCmap',
                            self.name,
                            self.__clearColourBar)
        opts   .addListener('useNegativeCmap',
                            self.name,
                            self.__clearColourBar)
        opts   .addListener('invert',
                            self.name,
                            self.__clearColourBar)
        opts   .addListener('cmapResolution',
                            self.name,
                            self.__clearColourBar)
        opts   .addListener('gamma',
                            self.name,
                            self.__clearColourBar)
        display.addListener('name',
                            self.name,
                            self.__clearColourBar)

        return True


    def __clearColourBar(self, *a):
        """Clears any previously generated colour bar bitmap. """
        self.__colourBar = None
        self.notify()


    def colourBar(self, w, h, scale=1):
        """Returns a bitmap containing the rendered colour bar, rendering it if
        necessary.

        :arg w:     Width in pixels
        :arg h:     Height in pixels
        :arg scale: DPI scaling factor, if applicable.
        """

        if self.__opts is None:
            return None

        if w < 20: w = 20
        if h < 20: h = 20

        if (w, h, scale) == self.__size and self.__colourBar is not None:
            return self.__colourBar

        display        = self.__display
        opts           = self.__opts
        cmap           = opts.cmap
        negCmap        = opts.negativeCmap
        useNegCmap     = opts.useNegativeCmap
        cmapResolution = opts.cmapResolution
        gamma          = opts.realGamma(opts.gamma)
        invert         = opts.invert
        dmin, dmax     = opts.displayRange.x
        label          = display.name

        if self.orientation == 'horizontal':
            if  self.labelSide == 'top-left': labelSide = 'top'
            else:                             labelSide = 'bottom'
        else:
            if  self.labelSide == 'top-left': labelSide = 'left'
            else:                             labelSide = 'right'

        if useNegCmap and dmin == 0.0:
            ticks      = [0.0, 0.5, 1.0]
            ticklabels = ['{:0.3G}'.format(-dmax),
                          '{:0.3G}'.format( dmin),
                          '{:0.3G}'.format( dmax)]
            tickalign  = ['left', 'center', 'right']
        elif useNegCmap:
            ticks      = [0.0, 0.49, 0.51, 1.0]
            ticklabels = ['{:0.3G}'.format(-dmax),
                          '{:0.3G}'.format(-dmin),
                          '{:0.3G}'.format( dmin),
                          '{:0.3G}'.format( dmax)]
            tickalign  = ['left', 'right', 'left', 'right']
        else:
            negCmap    = None
            ticks      = [0.0, 1.0]
            tickalign  = ['left', 'right']
            ticklabels = ['{:0.3G}'.format(dmin),
                          '{:0.3G}'.format(dmax)]

        ticks = np.array(ticks)
        ticks[np.isclose(ticks , 0)] = 0

        if not self.showLabel:
            label = None
        if not self.showTicks:
            ticks      = None
            ticklabels = None

        bitmap = cbarbmp.colourBarBitmap(
            cmap=cmap,
            negCmap=negCmap,
            invert=invert,
            gamma=gamma,
            ticks=ticks,
            ticklabels=ticklabels,
            tickalign=tickalign,
            width=w,
            height=h,
            label=label,
            scale=scale,
            orientation=self.orientation,
            labelside=labelSide,
            textColour=self.textColour,
            fontsize=self.fontSize,
            bgColour=self.bgColour,
            cmapResolution=cmapResolution)

        self.__size      = (w, h, scale)
        self.__colourBar = bitmap

        return bitmap
Пример #7
0
class VectorOpts(niftiopts.NiftiOpts):
    """The ``VectorOpts`` class is the base class for :class:`LineVectorOpts`,
    :class:`RGBVectorOpts`, :class:`.TensorOpts`, and :class:`.SHOpts`. It
    contains display settings which are common to each of them.


    *A note on orientation*


    The :attr:`orientFlip` property allows you to flip the left-right
    orientation of line vectors, tensors, and SH functions. This option is
    necessary, because different tools may output vector data in different
    ways, depending on the image orientation.


    For images which are stored radiologically (with the X axis increasaing
    from right to left), the FSL tools (e.g. `dtifit`) will generate vectors
    which are oriented according to the voxel coordinate system. However, for
    neurologically stored images (X axis increasing from left to right), FSL
    tools generate vectors which are radiologically oriented, and thus are
    inverted with respect to the X axis in the voxel coordinate system.
    Therefore, in order to correctly display vectors from such an image, we
    must flip each vector about the X axis.


    This issue is also applicable to ``tensor`` and ``sh`` overlays.
    """


    xColour = props.Colour(default=(1.0, 0.0, 0.0))
    """Colour used to represent the X vector magnitude."""


    yColour = props.Colour(default=(0.0, 1.0, 0.0))
    """Colour used to represent the Y vector magnitude."""


    zColour = props.Colour(default=(0.0, 0.0, 1.0))
    """Colour used to represent the Z vector magnitude."""


    suppressX = props.Boolean(default=False)
    """Do not use the X vector magnitude to colour vectors."""


    suppressY = props.Boolean(default=False)
    """Do not use the Y vector magnitude to colour vectors."""


    suppressZ = props.Boolean(default=False)
    """Do not use the Z vector magnitude to colour vectors."""


    suppressMode = props.Choice(('white', 'black', 'transparent'))
    """How vector direction colours should be suppressed. """


    orientFlip = props.Boolean(default=True)
    """If ``True``, individual vectors are flipped along the x-axis. This
    property is only applicable to the :class:`.LineVectorOpts`,
    :class:`.TensorOpts`, and :class:`.SHOpts` classes. See the
    :meth:`.NiftiOpts.getTransform` method for more information.

    This value defaults to ``True`` for images which have a neurological
    storage order, and ``False`` for radiological images.
    """


    cmap = props.ColourMap()
    """If an image is selected as the :attr:`colourImage`, this colour map
    is used to colour the vector voxels.
    """


    colourImage = props.Choice()
    """Colour vector voxels by the values contained in this image. Any image which
    is in the :class:`.OverlayList`, and which has the same voxel dimensions as
    the vector image can be selected for modulation. If a ``colourImage`` is
    selected, the :attr:`xColour`, :attr:`yColour`, :attr:`zColour`,
    :attr:`suppressX`, :attr:`suppressY`, and :attr:`suppressZ` properties are
    all ignored.
    """


    modulateImage  = props.Choice()
    """Modulate the vector colour brightness by another image. Any image which
    is in the :class:`.OverlayList`, and which has the same voxel dimensions as
    the vector image can be selected for modulation.
    """


    clipImage = props.Choice()
    """Clip voxels from the vector image according to another image. Any image
    which is in the :class:`.OverlayList`, and which has the same voxel
    dimensions as the vector image can be selected for clipping. The
    :attr:`clippingRange` dictates the value below which vector voxels are
    clipped.
    """


    clippingRange = props.Bounds(ndims=1)
    """Hide voxels for which the :attr:`clipImage` value is outside of this
    range.
    """


    modulateRange = props.Bounds(ndims=1)
    """Data range used in brightness modulation, when a :attr:`modulateImage`
    is in use.
    """


    def __init__(self, image, *args, **kwargs):
        """Create a ``VectorOpts`` instance for the given image.  All
        arguments are passed through to the :class:`.NiftiOpts`
        constructor.
        """

        # The orientFlip property defaults to True
        # for neurologically stored images. We
        # give it this vale before calling __init__,
        # because  if this VectorOptse instance has
        # a parent, we want to inherit the parent's
        # value.
        self.orientFlip = image.isNeurological()

        niftiopts.NiftiOpts.__init__(self, image, *args, **kwargs)

        self.__registered = self.getParent() is not None

        if self.__registered:

            self.overlayList.addListener('overlays',
                                         self.name,
                                         self.__overlayListChanged)
            self            .addListener('clipImage',
                                         self.name,
                                         self.__clipImageChanged)
            self            .addListener('modulateImage',
                                         self.name,
                                         self.__modulateImageChanged)

            if not self.isSyncedToParent('modulateImage'):
                self.__refreshAuxImage('modulateImage')
            if not self.isSyncedToParent('clipImage'):
                self.__refreshAuxImage('clipImage')
            if not self.isSyncedToParent('colourImage'):
                self.__refreshAuxImage('colourImage')

        else:
            self.__overlayListChanged()
            self.__clipImageChanged()
            self.__modulateImageChanged()


    def destroy(self):
        """Removes some property listeners, and calls the
        :meth:`.NiftiOpts.destroy` method.
        """
        if self.__registered:
            self.overlayList.removeListener('overlays',      self.name)
            self            .removeListener('clipImage',     self.name)
            self            .removeListener('modulateImage', self.name)

        niftiopts.NiftiOpts.destroy(self)


    def __clipImageChanged(self, *a):
        """Called when the :attr:`clipImage` property changes. Updates
        the range of the :attr:`clippingRange` property.
        """

        image = self.clipImage

        if image is None:
            self.clippingRange.xmin = 0
            self.clippingRange.xmax = 1
            self.clippingRange.x    = [0, 1]
            return

        minval, maxval = image.dataRange

        # Clipping works with <= and >=, so
        # we add an offset allowing the user
        # to configure the overlay such that
        # no voxels are clipped.
        distance = (maxval - minval) / 100.0

        self.clippingRange.xmin =  minval - distance
        self.clippingRange.xmax =  maxval + distance
        self.clippingRange.x    = [minval, maxval + distance]


    def __modulateImageChanged(self, *a):
        """Called when the :attr:`modulateImage` property changes. Updates
        the range of the :attr:`modulateRange` property.
        """

        image = self.modulateImage

        if image is None: minval, maxval = 0, 1
        else:             minval, maxval = image.dataRange

        self.modulateRange.xmin = minval
        self.modulateRange.xmax = maxval
        self.modulateRange.x    = [minval, maxval]


    def __overlayListChanged(self, *a):
        """Called when the overlay list changes. Updates the :attr:`modulateImage`,
        :attr:`colourImage` and :attr:`clipImage` properties so that they
        contain a list of overlays which could be used to modulate the vector
        image.
        """

        overlays = self.displayCtx.getOrderedOverlays()

        # the image for this VectorOpts
        # instance has been removed
        if self.overlay not in overlays:
            return

        self.__refreshAuxImage('modulateImage')
        self.__refreshAuxImage('clipImage')
        self.__refreshAuxImage('colourImage')


    def __refreshAuxImage(self, imageName):
        """Updates the named image property (:attr:`modulateImage`,
        :attr:`colourImage` or :attr:`clipImage`) so that it contains a list
        of overlays which could be used to modulate the vector image.
        """

        prop     = self.getProp(imageName)
        val      = getattr(self, imageName)
        overlays = self.displayCtx.getOrderedOverlays()

        options = [None]

        for overlay in overlays:

            # It doesn't make sense to
            # modulate/clip/colour the
            # image by itself.
            if overlay is self.overlay:
                continue

            # The modulate/clip/colour
            # images must be images.
            if not isinstance(overlay, fslimage.Image):
                continue

            options.append(overlay)

        prop.setChoices(options, instance=self)

        if val in options: setattr(self, imageName, val)
        else:              setattr(self, imageName, None)
Пример #8
0
class SliceCanvasOpts(props.HasProperties):
    """The ``SliceCanvasOpts`` class defines all of the display settings
    for a :class:`.SliceCanvas`.
    """

    pos = props.Point(ndims=3)
    """The currently displayed position.

    The ``pos.x`` and ``pos.y`` positions denote the position of a *cursor*,
    which is highlighted with crosshairs (see the :attr:`showCursor`
    property). The ``pos.z`` position specifies the currently displayed slice.
    """

    zoom = props.Percentage(minval=100.0,
                            maxval=5000.0,
                            default=100.0,
                            clamped=False)
    """The :attr:`.DisplayContext.bounds` are divided by this zoom
    factor to produce the canvas display bounds.
    """

    displayBounds = props.Bounds(ndims=2, clamped=False)
    """The display bound x/y values specify the horizontal/vertical display
    range of the canvas, in display coordinates. This may be a larger area
    than the size of the displayed overlays, as it is adjusted to preserve
    the aspect ratio.
    """

    showCursor = props.Boolean(default=True)
    """If ``False``, the crosshairs which show the current cursor location
    will not be drawn.
    """

    cursorGap = props.Boolean(default=False)
    """If ``True``, and the currently selected overlay is a :class:`.Nifti`
    instance, a gap will be shown at the cursor centre (i.e. the current
    voxel).
    """

    zax = props.Choice((0, 1, 2),
                       alternates=[['x', 'X'], ['y', 'Y'], ['z', 'Z']],
                       allowStr=True)
    """The display coordinate system axis to be used as the screen *depth*
    axis. The :meth:`xax` and :meth:`yax` attributes are derived from this
    property:

     - If ``zax == 0``, ``xax, yax == 1, 2``
     - If ``zax == 1``, ``xax, yax == 0, 2``
     - If ``zax == 2``, ``xax, yax == 0, 1``
    """

    invertX = props.Boolean(default=False)
    """If ``True``, the display is inverted along the X (horizontal screen)
    axis.
    """

    invertY = props.Boolean(default=False)
    """If ``True``, the display is inverted along the Y (vertical screen)
    axis.
    """

    cursorColour = props.Colour(default=(0, 1, 0))
    """Canvas cursor colour."""

    bgColour = props.Colour(default=(0, 0, 0))
    """Canvas background colour."""

    renderMode = props.Choice(('onscreen', 'offscreen', 'prerender'))
    """How the :class:`.GLObject` instances are rendered to the canvas.

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

    highDpi = props.Boolean(default=False)
    """If FSLeyes is being displayed on a high-DPI screen, try to display
    the scene at full resolution.
    """
    def __init__(self):
        """Create a ``SliceCanvasOpts`` instance. """

        self.__name = '{}_{}'.format(type(self).__name__, id(self))
        self.__xax = 0
        self.__yax = 0

        self.addListener('zax', self.__name, self.__zaxChanged, immediate=True)
        self.__zaxChanged()

    def __zaxChanged(self, *a):
        """Calle when the :attr:`zax` property changes. Derives the
        ``xax`` and ``yax`` values.
        """

        dims = list(range(3))
        dims.pop(self.zax)
        self.__xax = dims[0]
        self.__yax = dims[1]

    @property
    def xax(self):
        """The display coordinate system axis which maps to the X (horizontal)
        canvas axis.
        """
        return self.__xax

    @property
    def yax(self):
        """The display coordinate system axis which maps to the Y (vertical)
        canvas axis.
        """
        return self.__yax
Пример #9
0
class ColourBarCanvas(props.HasProperties):
    """Contains logic to render a colour bar as an OpenGL texture.
    """

    cmap = props.ColourMap()
    """The :mod:`matplotlib` colour map to use."""

    negativeCmap = props.ColourMap()
    """Negative colour map to use, if :attr:`useNegativeCmap` is ``True``."""

    useNegativeCmap = props.Boolean(default=False)
    """Whether or not to use the :attr:`negativeCmap`.
    """

    cmapResolution = props.Int(minval=2, maxval=1024, default=256)
    """Number of discrete colours to use in the colour bar. """

    invert = props.Boolean(default=False)
    """Invert the colour map(s). """

    vrange = props.Bounds(ndims=1)
    """The minimum/maximum values to display."""

    label = props.String()
    """A label to display under the centre of the colour bar."""

    orientation = props.Choice(('horizontal', 'vertical'))
    """Whether the colour bar should be vertical or horizontal. """

    labelSide = props.Choice(('top-left', 'bottom-right'))
    """Whether the colour bar labels should be on the top/left, or bottom/right
    of the colour bar (depending upon whether the colour bar orientation is
    horizontal/vertical).
    """

    textColour = props.Colour(default=(1, 1, 1, 1))
    """Colour to use for the colour bar label. """

    bgColour = props.Colour(default=(0, 0, 0, 1))
    """Colour to use for the background. """
    def __init__(self):
        """Adds a few listeners to the properties of this object, to update
        the colour bar when they change.
        """

        self._tex = None
        self._name = '{}_{}'.format(self.__class__.__name__, id(self))

        self.addGlobalListener(self._name, self.__updateTexture)

    def __updateTexture(self, *a):
        self._genColourBarTexture()
        self.Refresh()

    def _initGL(self):
        """Called automatically by the OpenGL canvas target superclass (see the
        :class:`.WXGLCanvasTarget` and :class:`.OSMesaCanvasTarget` for
        details).

        Generates the colour bar texture.
        """
        self._genColourBarTexture()

    def destroy(self):
        """Should be called when this ``ColourBarCanvas`` is no longer needed.
        Destroys the :class:`.Texture2D` instance used to render the colour
        bar.
        """
        self.removeGlobalListener(self._name)
        self._tex.destroy()
        self._tex = None

    def _genColourBarTexture(self):
        """Generates a texture containing an image of the colour bar,
        according to the current property values.
        """

        if not self._setGLContext():
            return

        w, h = self.GetSize()

        if w < 50 or h < 50:
            return

        if self.orientation == 'horizontal':
            if self.labelSide == 'top-left': labelSide = 'top'
            else: labelSide = 'bottom'
        else:
            if self.labelSide == 'top-left': labelSide = 'left'
            else: labelSide = 'right'

        if self.cmap is None:
            bitmap = np.zeros((w, h, 4), dtype=np.uint8)
        else:

            if self.useNegativeCmap:
                negCmap = self.negativeCmap
                ticks = [0.0, 0.49, 0.51, 1.0]
                ticklabels = [
                    '{:0.2f}'.format(-self.vrange.xhi),
                    '{:0.2f}'.format(-self.vrange.xlo),
                    '{:0.2f}'.format(self.vrange.xlo),
                    '{:0.2f}'.format(self.vrange.xhi)
                ]
                tickalign = ['left', 'right', 'left', 'right']
            else:
                negCmap = None
                ticks = [0.0, 1.0]
                tickalign = ['left', 'right']
                ticklabels = [
                    '{:0.2f}'.format(self.vrange.xlo),
                    '{:0.2f}'.format(self.vrange.xhi)
                ]

            bitmap = cbarbmp.colourBarBitmap(
                cmap=self.cmap,
                negCmap=negCmap,
                invert=self.invert,
                ticks=ticks,
                ticklabels=ticklabels,
                tickalign=tickalign,
                width=w,
                height=h,
                label=self.label,
                orientation=self.orientation,
                labelside=labelSide,
                textColour=self.textColour,
                cmapResolution=self.cmapResolution)

        if self._tex is None:
            self._tex = textures.Texture2D(
                '{}_{}'.format(type(self).__name__, id(self)), gl.GL_LINEAR)

        # The bitmap has shape W*H*4, but the
        # Texture2D instance needs it in shape
        # 4*W*H
        bitmap = np.fliplr(bitmap).transpose([2, 0, 1])

        self._tex.setData(bitmap)
        self._tex.refresh()

    def _draw(self):
        """Renders the colour bar texture using all available canvas space."""

        if self._tex is None or not self._setGLContext():
            return

        width, height = self.GetSize()

        # viewport
        gl.glViewport(0, 0, width, height)
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glLoadIdentity()
        gl.glOrtho(0, 1, 0, 1, -1, 1)
        gl.glMatrixMode(gl.GL_MODELVIEW)
        gl.glLoadIdentity()

        gl.glClearColor(*self.bgColour)
        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
        gl.glEnable(gl.GL_BLEND)
        gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
        gl.glShadeModel(gl.GL_FLAT)

        self._tex.drawOnBounds(0, 0, 1, 0, 1, 0, 1)
Пример #10
0
class MaskOpts(volumeopts.NiftiOpts):
    """The ``MaskOpts`` class defines settings for displaying an
    :class:`.Image` overlay as a binary mask.
    """

    threshold = props.Bounds(ndims=1)
    """The mask threshold range - values outside of this range are not
    displayed.
    """

    invert = props.Boolean(default=False)
    """If ``True``, the :attr:`threshold` range is inverted - values
    inside the range are not shown, and values outside of the range are shown.
    """

    colour = props.Colour()
    """The mask colour."""
    def __init__(self, overlay, *args, **kwargs):
        """Create a ``MaskOpts`` instance for the given overlay. All arguments
        are passed through to the :class:`.NiftiOpts` constructor.
        """

        #################
        # This is a hack.
        #################

        # Mask images are rendered using GLMask, which
        # inherits from GLVolume. The latter assumes
        # that the DisplayOpts instance passed to it
        # has the following attributes (see the
        # VolumeOpts class). So we're adding dummy
        # attributes to make the GLVolume rendering
        # code happy.
        #
        # TODO Write independent GLMask rendering routines
        # instead of using the GLVolume implementations

        dataMin, dataMax = overlay.dataRange
        dRangeLen = abs(dataMax - dataMin)
        dMinDistance = dRangeLen / 100.0

        self.clippingRange = (dataMin - 1, dataMax + 1)
        self.interpolation = 'none'
        self.invertClipping = False
        self.useNegativeCmap = False
        self.clipImage = None

        self.threshold.xmin = dataMin - dMinDistance
        self.threshold.xmax = dataMax + dMinDistance
        self.threshold.xlo = dataMin + dMinDistance
        self.threshold.xhi = dataMax + dMinDistance

        volumeopts.NiftiOpts.__init__(self, overlay, *args, **kwargs)

        overlay.register(self.name,
                         self.__dataRangeChanged,
                         topic='dataRange',
                         runOnIdle=True)

        # The master MaskOpts instance makes
        # sure that colour[3] and Display.alpha
        # are consistent w.r.t. each other.
        self.__registered = self.getParent() is None
        if self.__registered:
            self.display.addListener('alpha',
                                     self.name,
                                     self.__alphaChanged,
                                     immediate=True)
            self.addListener('colour',
                             self.name,
                             self.__colourChanged,
                             immediate=True)

    def destroy(self):
        """Removes some property listeners and calls
        :meth:`.NitfiOpts.destroy`.
        """

        self.overlay.deregister(self.name, topic='dataRange')

        if self.__registered:
            self.display.removeListener('alpha', self.name)
            self.removeListener('colour', self.name)

        volumeopts.NiftiOpts.destroy(self)

    def __dataRangeChanged(self, *a):
        """Called when the :attr:`~fsl.data.image.Image.dataRange` changes.
        Updates the :attr:`threshold` limits.
        """
        dmin, dmax = self.overlay.dataRange
        dRangeLen = abs(dmax - dmin)
        dminDistance = dRangeLen / 100.0

        self.threshold.xmin = dmin - dminDistance
        self.threshold.xmax = dmax + dminDistance

        # If the threshold was
        # previously unset, grow it
        if self.threshold.x == (0, 0):
            self.threshold.x = (0, dmax + dminDistance)

    def __colourChanged(self, *a):
        """Called when :attr:`.colour` changes. Updates :attr:`.Display.alpha`
        from the alpha component.
        """

        alpha = self.colour[3] * 100

        log.debug('Propagating MaskOpts.colour[3] to '
                  'Display.alpha [{}]'.format(alpha))

        with props.skip(self.display, 'alpha', self.name):
            self.display.alpha = alpha

    def __alphaChanged(self, *a):
        """Called when :attr:`.Display.alpha` changes. Updates the alpha
        component of :attr:`.colour`.
        """

        alpha = self.display.alpha / 100.0
        r, g, b, _ = self.colour

        log.debug('Propagating Display.alpha to MaskOpts.'
                  'colour[3] [{}]'.format(alpha))

        with props.skip(self, 'colour', self.name):
            self.colour = r, g, b, alpha
Пример #11
0
class MeshOpts(cmapopts.ColourMapOpts, fsldisplay.DisplayOpts):
    """The ``MeshOpts`` class defines settings for displaying :class:`.Mesh`
    overlays. See also the :class:`.GiftiOpts` and :class:`.FreesurferOpts`
    sub-classes.
    """

    colour = props.Colour()
    """The mesh colour. """

    outline = props.Boolean(default=False)
    """If ``True``, an outline of the mesh is shown. Otherwise a
    cross- section of the mesh is filled.
    """

    outlineWidth = props.Real(minval=0.1, maxval=10, default=2, clamped=False)
    """If :attr:`outline` is ``True``, this property defines the width of the
    outline in pixels.
    """

    showName = props.Boolean(default=False)
    """If ``True``, the mesh name is shown alongside it.

    .. note:: Not implemented yet, and maybe never will be.
    """

    discardClipped = props.Boolean(default=False)
    """Flag which controls clipping. When the mesh is coloured according to
    some data (the :attr:`vertexData` property), vertices with a data value
    outside of the clipping range are either discarded (not drawn), or
    they are still drawn, but not according to the data, rather with the
    flat :attr:`colour`.
    """

    vertexSet = props.Choice((None, ))
    """May be populated with the names of files which contain different
    vertex sets for the :class:`.Mesh` object.
    """

    vertexData = props.Choice((None, ))
    """May be populated with the names of files which contain data associated
    with each vertex in the mesh, that can be used to colour the mesh. When
    some vertex data has been succsessfully loaded, it can be accessed via
    the :meth:`getVertexData` method.
    """

    vertexDataIndex = props.Int(minval=0, maxval=0, default=0, clamped=True)
    """If :attr:`vertexData` is loaded, and has multiple data points per
    vertex (e.g. time series), this property controls the index into the
    data.
    """

    modulateData = props.Choice((None, ))
    """Populated with the same files available for the :attr:`vertexData`
    attribute. Used to apply the :attr:`.ColourMapOpts.modulateAlpha`
    setting.

    .. note:: There is currently no support for indexing into multi-
              dimensional modulate data (e.g. time points). A separate
              ``modulateDataIndex`` property may be added in the future.
    """

    refImage = props.Choice()
    """A reference :class:`.Image` instance which the mesh coordinates are
    in terms of.

    For example, if this :class:`.Mesh` represents the segmentation of
    a sub-cortical region from a T1 image, you would set the ``refImage`` to
    that T1 image.

    Any :class:`.Image` instance in the :class:`.OverlayList` may be chosen
    as the reference image.
    """

    useLut = props.Boolean(default=False)
    """If ``True``, and if some :attr:`vertexData` is loaded, the :attr:`lut`
    is used to colour vertex values instead of the :attr:`cmap` and
    :attr:`negativeCmap`.
    """

    lut = props.Choice()
    """If :attr:`useLut` is ``True``, a :class:`.LookupTable` is used to
    colour vertex data instead of the :attr:`cmap`/:attr:`negativeCmap`.
    """

    # This property is implicitly tightly-coupled to
    # the NiftiOpts.getTransform method - the choices
    # defined in this property are assumed to be valid
    # inputs to that method (with the exception of
    # ``'torig'``).
    coordSpace = props.Choice(
        ('affine', 'pixdim', 'pixdim-flip', 'id', 'torig'),
        default='pixdim-flip')
    """If :attr:`refImage` is not ``None``, this property defines the
    reference image coordinate space in which the mesh coordinates are
    defined (i.e. voxels, scaled voxels, or world coordinates).

    =============== =========================================================
    ``affine``      The mesh coordinates are defined in the reference image
                    world coordinate system.

    ``id``          The mesh coordinates are defined in the reference image
                    voxel coordinate system.

    ``pixdim``      The mesh coordinates are defined in the reference image
                    voxel coordinate system, scaled by the voxel pixdims.

    ``pixdim-flip`` The mesh coordinates are defined in the reference image
                    voxel coordinate system, scaled by the voxel pixdims. If
                    the reference image transformation matrix has a positive
                    determinant, the X axis is flipped.

    ``torig``       The mesh coordinates are defined in the Freesurfer
                    "Torig" / "vox2ras-tkr" coordnie system.
    =============== =========================================================

    The default value is ``pixdim-flip``, as this is the coordinate system
    used in the VTK sub-cortical segmentation model files output by FIRST.
    See also the :ref:`note on coordinate systems
    <volumeopts-coordinate-systems>`, and the :meth:`.NiftiOpts.getTransform`
    method.
    """

    flatShading = props.Boolean(default=False)
    """3D only. If ``True``, colours between vertices are not interpolated -
    each triangle is coloured with the colour assigned to the first vertex.
    Only has an effect when the mesh is being coloured with vertex data.
    """

    wireframe = props.Boolean(default=False)
    """3D only. If ``True``, the mesh is rendered as a wireframe. """
    def __init__(self, overlay, *args, **kwargs):
        """Create a ``MeshOpts`` instance.

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

        # Set a default colour
        colour = genMeshColour(overlay)
        self.colour = np.concatenate((colour, [1.0]))

        # ColourMapOpts.linkLowRanges defaults to
        # True, which is annoying for surfaces.
        self.linkLowRanges = False

        # A copy of the refImage property
        # value is kept here so, when it
        # changes, we can de-register from
        # the previous one.
        self.__oldRefImage = None

        # When the vertexData/modulateData properties
        # are changed, the data (and its min/max)
        # is loaded and stored in these
        # attributes. See the __vdataChanged
        # method.
        #
        # Keys used are 'vertex' and 'modulate'
        self.__vdata = {}
        self.__vdataRange = {}

        nounbind = kwargs.get('nounbind', [])
        nounbind.extend([
            'refImage', 'coordSpace', 'vertexData', 'vertexSet', 'modulateData'
        ])
        kwargs['nounbind'] = nounbind

        fsldisplay.DisplayOpts.__init__(self, overlay, *args, **kwargs)
        cmapopts.ColourMapOpts.__init__(self)

        self.__registered = self.getParent() is not None

        # Load all vertex data and vertex
        # sets on the parent opts instance
        if not self.__registered:
            self.addVertexSetOptions(overlay.vertexSets())
            self.addVertexDataOptions(overlay.vertexDataSets())

        # The master MeshOpts instance is just a
        # sync-slave, so we only need to register
        # property listeners on child instances
        else:

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

            self.addListener('refImage',
                             self.name,
                             self.__refImageChanged,
                             immediate=True)
            self.addListener('coordSpace',
                             self.name,
                             self.__coordSpaceChanged,
                             immediate=True)

            # We need to keep colour[3]
            # keeps colour[3] and Display.alpha
            # consistent w.r.t. each other (see
            # also MaskOpts)
            self.display.addListener('alpha',
                                     self.name,
                                     self.__alphaChanged,
                                     immediate=True)
            self.addListener('colour',
                             self.name,
                             self.__colourChanged,
                             immediate=True)

            self.addListener('vertexData',
                             self.name,
                             self.__vdataChanged,
                             immediate=True)
            self.addListener('modulateData',
                             self.name,
                             self.__vdataChanged,
                             immediate=True)
            self.addListener('vertexSet',
                             self.name,
                             self.__vertexSetChanged,
                             immediate=True)
            overlay.register(self.name, self.__overlayVerticesChanged,
                             'vertices')

            self.__overlayListChanged()
            self.__updateBounds()
            self.__refImageChanged()

        # If we have inherited values from a
        # parent instance, make sure the vertex/
        # modulate data (if set) is initialised
        if self.vertexData is not None:
            self.__vdataChanged(self.vertexData, None, None, 'vertexData')
        if self.modulateData is not None:
            self.__vdataChanged(self.modulateData, None, None, 'modulateData')

        # If a reference image has not
        # been set on the parent MeshOpts
        # instance, see  if there is a
        # suitable one in the overlay list.
        if self.refImage is None:
            self.refImage = fsloverlay.findMeshReferenceImage(
                self.overlayList, self.overlay)

    def destroy(self):
        """Removes some property listeners, and calls the
        :meth:`.DisplayOpts.destroy` method.
        """

        if self.__registered:

            self.overlayList.removeListener('overlays', self.name)
            self.display.removeListener('alpha', self.name)
            self.removeListener('colour', self.name)
            self.overlay.deregister(self.name, 'vertices')

            for overlay in self.overlayList:

                # An error could be raised if the
                # DC has been/is being destroyed
                try:

                    display = self.displayCtx.getDisplay(overlay)
                    opts = self.displayCtx.getOpts(overlay)

                    display.removeListener('name', self.name)

                    if overlay is self.refImage:
                        opts.removeListener('transform', self.name)

                except Exception:
                    pass

        self.__oldRefImage = None
        self.__vdata = None

        cmapopts.ColourMapOpts.destroy(self)
        fsldisplay.DisplayOpts.destroy(self)

    @classmethod
    def getVolumeProps(cls):
        """Overrides :meth:`DisplayOpts.getVolumeProps`. Returns a list
        of property names which control the displayed volume/timepoint.
        """
        return ['vertexDataIndex']

    def getDataRange(self):
        """Overrides the :meth:`.ColourMapOpts.getDisplayRange` method.
        Returns the display range of the currently selected
        :attr:`vertexData`, or ``(0, 1)`` if none is selected.
        """
        vdata = self.__vdataRange.get('vertex')
        if vdata is None: return (0, 1)
        else: return vdata

    def getModulateRange(self):
        """Overrides the :meth:`.ColourMapOpts.getModulateRange` method.
        Returns the display range of the currently selected
        :attr:`vertexData`, or ``None`` if none is selected.
        """
        return self.__vdataRange.get('modulate')

    def getVertexData(self, vdtype='vertex'):
        """Returns the :attr:`.MeshOpts.vertexData` or :attr:`modulateData` ,
        if some is loaded.  Returns ``None`` otherwise.
        """
        return self.__vdata.get(vdtype)

    def vertexDataLen(self):
        """Returns the length (number of data points per vertex) of the
        currently selected :attr:`vertexData`, or ``0`` if no vertex data is
        selected.
        """

        vdata = self.__vdata.get('vertex')

        if vdata is None:
            return 0

        elif len(vdata.shape) == 1:
            return 1

        else:
            return vdata.shape[1]

    def addVertexData(self, key, data):
        """Adds the given data as a  vertex data set to the :class:`.Mesh`
        overlay associated with this ``MeshOpts``.

        :arg key:  A unique key to identify the data. If a vertex data set
                   with the key already exists, a unique one is generated
                   and returned.

        :arg data: ``numpy`` array containing per-vertex data.

        :returns:  The key used to identify the data (typically equal to
                   ``key``)
        """
        count = 1
        origKey = key
        sets = self.overlay.vertexDataSets()

        # generate a unique key for the
        # vertex data if one with the
        # given key already exists
        while key in sets:
            key = '{} [{}]'.format(origKey, count)
            count = count + 1

        self.overlay.addVertexData(key, data)
        self.addVertexDataOptions([key])

        return key

    def addVertexDataOptions(self, paths):
        """Adds the given sequence of paths as options to the
        :attr:`vertexData` property. It is assumed that the paths refer
        to valid vertex data files for the overlay associated with this
        ``MeshOpts`` instance.
        """

        vdataProp = self.getProp('vertexData')
        mdataProp = self.getProp('modulateData')
        newPaths = paths
        paths = vdataProp.getChoices(instance=self)
        paths = paths + [p for p in newPaths if p not in paths]

        vdataProp.setChoices(paths, instance=self)
        mdataProp.setChoices(paths, instance=self)

    def addVertexSetOptions(self, paths):
        """Adds the given sequence of paths as options to the
        :attr:`vertexSet` property. It is assumed that the paths refer
        to valid vertex files for the overlay associated with this
        ``MeshOpts`` instance.
        """

        vsetProp = self.getProp('vertexSet')
        newPaths = paths
        paths = vsetProp.getChoices(instance=self)
        paths = paths + [p for p in newPaths if p not in paths]

        vsetProp.setChoices(paths, instance=self)

    def getConstantColour(self):
        """Returns the current :attr::`colour`, adjusted according to the
        current :attr:`.Display.brightness`, :attr:`.Display.contrast`, and
        :attr:`.Display.alpha`.
        """

        display = self.display

        # Only apply bricon if there is no vertex data assigned
        if self.vertexData is None:
            brightness = display.brightness / 100.0
            contrast = display.contrast / 100.0
        else:
            brightness = 0.5
            contrast = 0.5

        colour = list(
            fslcmaps.applyBricon(self.colour[:3], brightness, contrast))

        colour.append(display.alpha / 100.0)

        return colour

    @property
    def referenceImage(self):
        """Overrides :meth:`.DisplayOpts.referenceImage`.

        If a :attr:`refImage` is selected, it is returned. Otherwise,``None``
        is returned.
        """
        return self.refImage

    @deprecated.deprecated('0.22.3', '1.0.0', 'Use getTransform instead')
    def getCoordSpaceTransform(self):
        """Returns a transformation matrix which can be used to transform
        the :class:`.Mesh` vertex coordinates into the display
        coordinate system.

        If no :attr:`refImage` is selected, this method returns an identity
        transformation.
        """

        if self.refImage is None or self.refImage not in self.overlayList:
            return np.eye(4)

        opts = self.displayCtx.getOpts(self.refImage)

        return opts.getTransform(self.coordSpace, opts.transform)

    def getVertex(self, xyz=None, tol=1):
        """Returns an integer identifying the index of the mesh vertex that
        coresponds to the given ``xyz`` location, assumed to be specified
        in the display coordinate system.

        :arg xyz: Location to convert to a vertex index. If not provided, the
                  current :class:`.DisplayContext.location` is used.

        :arg tol: Tolerance in mesh coordinate system units - if ``xyz`` is
                  farther than ``tol`` to any vertex, ``None`` is returned.
                  Pass in ``None`` to always return the nearest vertex.
        """

        if xyz is None:
            xyz = self.displayCtx.location

        xyz = self.transformCoords(xyz, 'display', 'mesh')
        xyz = np.asarray(xyz).reshape(1, 3)
        dist, vidx = self.overlay.trimesh.nearest.vertex(xyz)
        dist = dist[0]
        vidx = vidx[0]

        if tol is not None and dist > tol: return None
        else: return vidx

    def normaliseSpace(self, space):
        """Used by :meth:`transformCoords` and :meth:`getTransform` to
        normalise their ``from_`` and ``to`` parameters.
        """
        if space not in ('world', 'display', 'mesh', 'voxel', 'id'):
            raise ValueError('Invalid space: {}'.format(space))

        if space == 'mesh': space = self.coordSpace
        if space == 'torig': space = 'affine'

        return space

    def transformCoords(self, coords, from_, to, *args, **kwargs):
        """Transforms the given ``coords`` from ``from_`` to ``to``.

        :arg coords: Coordinates to transform.
        :arg from_:  Space that the coordinates are in
        :arg to:     Space to transform the coordinates to

        All other parameters are passed through to the
        :meth:`.NiftiOpts.transformCoords` method of the reference image
        ``DisplayOpts``.

        The following values are accepted for the ``from_`` and ``to``
        parameters:

          - ``'world'``:  World coordinate system
          - ``'display'`` Display coordinate system
          - ``'mesh'``    The coordinate system of this mesh.
          - ``'voxel'``:   The voxel coordinate system of the reference
                           image
          - ``'id'``:      Equivalent to ``'voxel'``.
        """

        nfrom_ = self.normaliseSpace(from_)
        nto = self.normaliseSpace(to)
        ref = self.refImage
        pre = None
        post = None

        if ref is None:
            return coords

        if from_ == 'mesh' and self.coordSpace == 'torig':
            pre = affine.concat(ref.getAffine('voxel', 'world'),
                                affine.invert(fslmgh.voxToSurfMat(ref)))

        if to == 'mesh' and self.coordSpace == 'torig':
            post = affine.concat(fslmgh.voxToSurfMat(ref),
                                 ref.getAffine('world', 'voxel'))

        opts = self.displayCtx.getOpts(ref)

        return opts.transformCoords(coords,
                                    nfrom_,
                                    nto,
                                    pre=pre,
                                    post=post,
                                    **kwargs)

    def getTransform(self, from_, to):
        """Return a matrix which may be used to transform coordinates from
        ``from_`` to ``to``.

        If the :attr:`refImage` property is not set, an identity matrix is
        returned.

        The following values are accepted for the ``from_`` and ``to``
        parameters:

          - ``'world'``:   World coordinate system
          - ``'display'``: Display coordinate system
          - ``'mesh'``:    The coordinate system of this mesh.
          - ``'voxel'``:   The voxel coordinate system of the reference
                           image
          - ``'id'``:      Equivalent to ``'voxel'``.
        """

        nfrom_ = self.normaliseSpace(from_)
        nto = self.normaliseSpace(to)
        ref = self.refImage

        if ref is None:
            return np.eye(4)

        opts = self.displayCtx.getOpts(ref)
        xform = opts.getTransform(nfrom_, nto)

        if from_ == 'mesh' and self.coordSpace == 'torig':
            surfToVox = affine.invert(fslmgh.voxToSurfMat(ref))
            xform = affine.concat(xform, ref.getAffine('voxel', 'world'),
                                  surfToVox)
        if to == 'mesh' and self.coordSpace == 'torig':
            voxToSurf = fslmgh.voxToSurfMat(ref)
            xform = affine.concat(voxToSurf, ref.getAffine('world', 'voxel'),
                                  xform)

        return xform

    def __transformChanged(self, value, valid, ctx, name):
        """Called when the :attr:`.NiftiOpts.transform` property of the current
        :attr:`refImage` changes. Calls :meth:`__updateBounds`.
        """
        self.__updateBounds()

    def __coordSpaceChanged(self, *a):
        """Called when the :attr:`coordSpace` property changes.
        Calls :meth:`__updateBounds`.
        """
        self.__updateBounds()

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

        If a new reference image has been specified, removes listeners from
        the old one (if necessary), and adds listeners to the
        :attr:`.NiftiOpts.transform` property associated with the new image.
        Calls :meth:`__updateBounds`.
        """

        # TODO You are not tracking changes to the
        # refImage overlay type -  if this changes,
        # you will need to re-bind to the transform
        # property of the new DisplayOpts instance

        if self.__oldRefImage is not None and \
           self.__oldRefImage in self.overlayList:

            opts = self.displayCtx.getOpts(self.__oldRefImage)
            opts.removeListener('transform', self.name)

        self.__oldRefImage = self.refImage

        if self.refImage is not None:
            opts = self.displayCtx.getOpts(self.refImage)
            opts.addListener('transform',
                             self.name,
                             self.__transformChanged,
                             immediate=True)

        self.__updateBounds()

    def __updateBounds(self):
        """Called whenever any of the :attr:`refImage`, :attr:`coordSpace`,
        or :attr:`transform` properties change.

        Updates the :attr:`.DisplayOpts.bounds` property accordingly.
        """

        # create a bounding box for the
        # overlay vertices in their
        # native coordinate system
        lo, hi = self.overlay.bounds
        xlo, ylo, zlo = lo
        xhi, yhi, zhi = hi

        # Transform the bounding box
        # into display coordinates
        xform = self.getTransform('mesh', 'display')
        bbox = list(it.product(*zip(lo, hi)))
        bbox = affine.transform(bbox, xform)

        # re-calculate the min/max bounds
        x = np.sort(bbox[:, 0])
        y = np.sort(bbox[:, 1])
        z = np.sort(bbox[:, 2])
        xlo, xhi = x.min(), x.max()
        ylo, yhi = y.min(), y.max()
        zlo, zhi = z.min(), z.max()

        oldBounds = self.bounds
        self.bounds = [xlo, xhi, ylo, yhi, zlo, zhi]

        if np.all(np.isclose(oldBounds, self.bounds)):
            self.propNotify('bounds')

    def __overlayListChanged(self, *a):
        """Called when the overlay list changes. Updates the :attr:`refImage`
        property so that it contains a list of overlays which can be
        associated with the mesh.
        """

        imgProp = self.getProp('refImage')
        imgVal = self.refImage
        overlays = self.displayCtx.getOrderedOverlays()

        # the overlay for this MeshOpts
        # instance has been removed
        if self.overlay not in overlays:
            self.overlayList.removeListener('overlays', self.name)
            return

        imgOptions = [None]

        for overlay in overlays:

            # The overlay must be a Nifti instance.
            if not isinstance(overlay, fslimage.Nifti):
                continue

            imgOptions.append(overlay)

            display = self.displayCtx.getDisplay(overlay)
            display.addListener('name',
                                self.name,
                                self.__overlayListChanged,
                                overwrite=True)

        # The previous refImage may have
        # been removed from the overlay list
        if imgVal in imgOptions: self.refImage = imgVal
        else: self.refImage = None

        imgProp.setChoices(imgOptions, instance=self)

    def __overlayVerticesChanged(self, *a):
        """Called when the :attr:`.Mesh.vertices` change. Makes sure that the
        :attr:`vertexSet` attribute is synchronised.
        """

        vset = self.overlay.selectedVertices()
        vsprop = self.getProp('vertexSet')

        if vset not in vsprop.getChoices(instance=self):
            self.addVertexSetOptions([vset])
        self.vertexSet = vset

    def __vertexSetChanged(self, *a):
        """Called when the :attr:`.MeshOpts.vertexSet` property changes.
        Updates the current vertex set on the :class:`.Mesh` overlay, and
        the overlay bounds.
        """

        if self.vertexSet not in self.overlay.vertexSets():
            self.overlay.loadVertices(self.vertexSet)
        else:
            with self.overlay.skip(self.name, 'vertices'):
                self.overlay.vertices = self.vertexSet

        self.__updateBounds()

    def __vdataChanged(self, value, valid, ctx, name):
        """Called when the :attr:`vertexData` or :attr:`modulateData`
        properties changes. Attempts to load the data if possible. The data may
        subsequently be retrieved via the :meth:`getVertexData` method.
        """

        vdata = None
        vdataRange = None
        overlay = self.overlay
        vdfile = value

        if name == 'vertexData': key = 'vertex'
        elif name == 'modulateData': key = 'modulate'
        else: raise RuntimeError()

        try:
            if vdfile is not None:

                if vdfile not in overlay.vertexDataSets():
                    log.debug('Loading vertex data: {}'.format(vdfile))
                    vdata = overlay.loadVertexData(vdfile)
                else:
                    vdata = overlay.getVertexData(vdfile)

                vdataRange = np.nanmin(vdata), np.nanmax(vdata)

                if len(vdata.shape) == 1:
                    vdata = vdata.reshape(-1, 1)

                vdata = dutils.makeWriteable(vdata)

        except Exception as e:

            # TODO show a warning
            log.warning('Unable to load vertex data from {}: {}'.format(
                vdfile, e, exc_info=True))

            vdata = None
            vdataRange = None

        self.__vdata[key] = vdata
        self.__vdataRange[key] = vdataRange

        if key == 'vertex':
            if vdata is not None: npoints = vdata.shape[1]
            else: npoints = 1
            self.vertexDataIndex = 0
            self.setAttribute('vertexDataIndex', 'maxval', npoints - 1)

        # if modulate data has changed,
        # don't update display/clipping
        # ranges (unless modulateData is
        # None, meaning that it is using
        # vertexData)
        if key == 'vertex':
            drange = True
            mrange = self.modulateData is None
        # and vice versa
        else:
            drange = False
            mrange = True

        self.updateDataRange(drange, drange, mrange)

    def __colourChanged(self, *a):
        """Called when :attr:`.colour` changes. Updates :attr:`.Display.alpha`
        from the alpha component.
        """

        # modulateAlpha may cause the
        # alpha property to be disabled
        if not self.display.propertyIsEnabled('alpha'):
            return

        alpha = self.colour[3] * 100

        log.debug('Propagating MeshOpts.colour[3] to '
                  'Display.alpha [{}]'.format(alpha))

        with props.skip(self.display, 'alpha', self.name):
            self.display.alpha = alpha

    def __alphaChanged(self, *a):
        """Called when :attr:`.Display.alpha` changes. Updates the alpha
        component of :attr:`.colour`.
        """

        alpha = self.display.alpha / 100.0
        r, g, b, _ = self.colour

        log.debug('Propagating Display.alpha to MeshOpts.'
                  'colour[3] [{}]'.format(alpha))

        with props.skip(self, 'colour', self.name):
            self.colour = r, g, b, alpha
Пример #12
0
class SceneOpts(props.HasProperties):
    """The ``SceneOpts`` class defines settings which are used by
    :class:`.CanvasPanel` instances.

    Several of the properties of the ``SceneOpts`` class are defined in the
    :class:`.SliceCanvasOpts` class, so see its documentation for more
    details.
    """

    showCursor = copy.copy(canvasopts.SliceCanvasOpts.showCursor)
    zoom = copy.copy(canvasopts.SliceCanvasOpts.zoom)
    bgColour = copy.copy(canvasopts.SliceCanvasOpts.bgColour)
    cursorColour = copy.copy(canvasopts.SliceCanvasOpts.cursorColour)
    renderMode = copy.copy(canvasopts.SliceCanvasOpts.renderMode)
    highDpi = copy.copy(canvasopts.SliceCanvasOpts.highDpi)

    fgColour = props.Colour(default=(1, 1, 1))
    """Colour to use for foreground items (e.g. labels).

    .. note:: This colour is automatically updated whenever the
              :attr:`.bgColour` is changed. But it can be modified
              independently.
    """

    showColourBar = props.Boolean(default=False)
    """If ``True``, and it is possible to do so, a colour bar is shown on
    the scene.
    """

    colourBarLocation = props.Choice(('top', 'bottom', 'left', 'right'))
    """This property controls the location of the colour bar, if it is being
    shown.
    """

    colourBarLabelSide = props.Choice(('top-left', 'bottom-right'))
    """This property controls the location of the colour bar labels, relative
    to the colour bar, if it is being shown.
    """

    colourBarSize = props.Percentage(default=100)
    """Size of the major axis of the colour bar, as a proportion of the
    available space.
    """

    labelSize = props.Int(minval=4, maxval=96, default=12, clamped=True)
    """Font size used for any labels drawn on the canvas, including
    orthographic labels, and colour bar labels.
    """

    # NOTE: If you change the maximum performance value,
    #       make sure you update all references to
    #       performance because, for example, the
    #       OrthoEditProfile does numerical comparisons
    #       to it.
    performance = props.Choice((1, 2, 3), default=3, allowStr=True)
    """User controllable performance setting.

    This property is linked to the :attr:`renderMode` property. Setting this
    property to a low value will result in faster rendering time, at the cost
    of increased memory usage and poorer rendering quality.

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

    movieSyncRefresh = props.Boolean(default=True)
    """Whether, when in movie mode, to synchronise the refresh for GL
    canvases. This is not possible in some platforms/environments. See
    :attr:`.CanvasPanel.movieSyncRefresh`.
    """
    def __init__(self, panel):
        """Create a ``SceneOpts`` instance.

        This method simply links the :attr:`performance` property to the
        :attr:`renderMode` property.
        """

        self.__panel = panel
        self.__name = '{}_{}'.format(type(self).__name__, id(self))

        self.movieSyncRefresh = self.defaultMovieSyncRefresh

        self.addListener('performance', self.__name, self._onPerformanceChange)
        self.addListener('bgColour', self.__name, self.__onBgColourChange)
        self._onPerformanceChange()

    @property
    def defaultMovieSyncRefresh(self):
        """In movie mode, the canvas refreshes are performed by the
        __syncMovieRefresh or __unsyncMovieRefresh methods of the CanvasPanel
         class. Some platforms/GL drivers/environments seem to have a problem
        with separate renders/buffer swaps, so we have to use a shitty
        unsynchronised update routine.

        These heuristics are not perfect - the movieSyncRefresh property can
        therefore be overridden by the user.
        """
        renderer = fslgl.GL_RENDERER.lower()
        unsyncRenderers = ['gallium', 'mesa dri intel(r)']
        unsync = any([r in renderer for r in unsyncRenderers])

        return not unsync

    @property
    def panel(self):
        """Return a reference to the ``CanvasPanel`` that owns this
        ``SceneOpts`` instance.
        """
        return self.__panel

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

        This method must be overridden by sub-classes to change the values of
        the :attr:`renderMode` property according to the new performance
        setting.
        """
        raise NotImplementedError('The _onPerformanceChange method must'
                                  'be implemented by sub-classes')

    def __onBgColourChange(self, *a):
        """Called when the background colour changes. Updates the
        :attr:`fgColour` to a complementary colour.
        """
        self.fgColour = fslcm.complementaryColour(self.bgColour)
Пример #13
0
class SceneOpts(props.HasProperties):
    """The ``SceneOpts`` class defines settings which are used by
    :class:`.CanvasPanel` instances.

    Several of the properties of the ``SceneOpts`` class are defined in the
    :class:`.SliceCanvasOpts` class, so see its documentation for more
    details.
    """

    showCursor = copy.copy(canvasopts.SliceCanvasOpts.showCursor)
    zoom = copy.copy(canvasopts.SliceCanvasOpts.zoom)
    bgColour = copy.copy(canvasopts.SliceCanvasOpts.bgColour)
    cursorColour = copy.copy(canvasopts.SliceCanvasOpts.cursorColour)
    renderMode = copy.copy(canvasopts.SliceCanvasOpts.renderMode)

    fgColour = props.Colour(default=(1, 1, 1))
    """Colour to use for foreground items (e.g. labels).

    .. note:: This colour is automatically updated whenever the
              :attr:`.bgColour` is changed. But it can be modified
              independently.
    """

    showColourBar = props.Boolean(default=False)
    """If ``True``, and it is possible to do so, a colour bar is shown on
    the scene.
    """

    colourBarLocation = props.Choice(('top', 'bottom', 'left', 'right'))
    """This property controls the location of the colour bar, if it is being
    shown.
    """

    colourBarLabelSide = props.Choice(('top-left', 'bottom-right'))
    """This property controls the location of the colour bar labels, relative
    to the colour bar, if it is being shown.
    """

    # NOTE: If you change the maximum performance value,
    #       make sure you update all references to
    #       performance because, for example, the
    #       OrthoEditProfile does numerical comparisons
    #       to it.
    performance = props.Choice((1, 2, 3), default=3, allowStr=True)
    """User controllable performance setting.

    This property is linked to the :attr:`renderMode` property. Setting this
    property to a low value will result in faster rendering time, at the cost
    of increased memory usage and poorer rendering quality.

    See the :meth:`__onPerformanceChange` method.
    """
    def __init__(self, panel):
        """Create a ``SceneOpts`` instance.

        This method simply links the :attr:`performance` property to the
        :attr:`renderMode` property.
        """

        self.__panel = panel
        self.__name = '{}_{}'.format(type(self).__name__, id(self))
        self.addListener('performance', self.__name, self._onPerformanceChange)
        self.addListener('bgColour', self.__name, self.__onBgColourChange)
        self._onPerformanceChange()

    @property
    def panel(self):
        """Return a reference to the ``CanvasPanel`` that owns this
        ``SceneOpts`` instance.
        """
        return self.__panel

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

        This method must be overridden by sub-classes to change the values of
        the :attr:`renderMode` property according to the new performance
        setting.
        """
        raise NotImplementedError('The _onPerformanceChange method must'
                                  'be implemented by sub-classes')

    def __onBgColourChange(self, *a):
        """Called when the background colour changes. Updates the
        :attr:`fgColour` to a complementary colour.
        """
        self.fgColour = fslcm.complementaryColour(self.bgColour)
Пример #14
0
class MaskOpts(volumeopts.NiftiOpts):
    """The ``MaskOpts`` class defines settings for displaying an
    :class:`.Image` overlay as a binary mask.
    """

    threshold = props.Bounds(ndims=1)
    """The mask threshold range - values outside of this range are not
    displayed.
    """

    invert = props.Boolean(default=False)
    """If ``True``, the :attr:`threshold` range is inverted - values
    inside the range are not shown, and values outside of the range are shown.
    """

    colour = props.Colour()
    """The mask colour."""

    outline = props.Boolean(default=False)
    """If ``True`` only the outline of the mask will be shown.  If ``False``,
    the filled mask will be displayed.
    """

    outlineWidth = props.Int(minval=0, maxval=10, default=1, clamped=True)
    """Width of mask outline, if :attr:``outline` is ``True``.  This value is
    in terms of pixels.
    """

    interpolation = copy.copy(volumeopts.VolumeOpts.interpolation)

    def __init__(self, overlay, *args, **kwargs):
        """Create a ``MaskOpts`` instance for the given overlay. All arguments
        are passed through to the :class:`.NiftiOpts` constructor.
        """

        # We need GL >= 2.1 for
        # spline interpolation
        if float(fslplatform.glVersion) < 2.1:
            interp = self.getProp('interpolation')
            interp.removeChoice('spline', instance=self)
            interp.updateChoice('linear', instance=self, newAlt=['spline'])

        kwargs['nounbind'] = ['interpolation']

        # Initialise threshold from data reange. Do
        # this before __init__, in case we need to
        # inherit settings from the parent instance
        dmin, dmax = overlay.dataRange
        dlen = dmax - dmin
        doff = dlen / 100.0

        self.threshold.xmin = dmin - doff
        self.threshold.xmax = dmax + doff
        self.threshold.xlo = dmin + doff
        self.threshold.xhi = dmax + doff

        volumeopts.NiftiOpts.__init__(self, overlay, *args, **kwargs)

        overlay.register(self.name,
                         self.__dataRangeChanged,
                         topic='dataRange',
                         runOnIdle=True)

        # The master MaskOpts instance makes
        # sure that colour[3] and Display.alpha
        # are consistent w.r.t. each other.
        self.__registered = self.getParent() is None
        if self.__registered:
            self.display.addListener('alpha',
                                     self.name,
                                     self.__alphaChanged,
                                     immediate=True)
            self.addListener('colour',
                             self.name,
                             self.__colourChanged,
                             immediate=True)

    def destroy(self):
        """Removes some property listeners and calls
        :meth:`.NitfiOpts.destroy`.
        """

        self.overlay.deregister(self.name, topic='dataRange')

        if self.__registered:
            self.display.removeListener('alpha', self.name)
            self.removeListener('colour', self.name)

        volumeopts.NiftiOpts.destroy(self)

    def __dataRangeChanged(self, *a):
        """Called when the :attr:`~fsl.data.image.Image.dataRange` changes.
        Updates the :attr:`threshold` limits.
        """
        dmin, dmax = self.overlay.dataRange
        dRangeLen = abs(dmax - dmin)
        dminDistance = dRangeLen / 100.0

        self.threshold.xmin = dmin - dminDistance
        self.threshold.xmax = dmax + dminDistance

        # If the threshold was
        # previously unset, grow it
        if self.threshold.x == (0, 0):
            self.threshold.x = (0, dmax + dminDistance)

    def __colourChanged(self, *a):
        """Called when :attr:`.colour` changes. Updates :attr:`.Display.alpha`
        from the alpha component.
        """

        alpha = self.colour[3] * 100

        log.debug('Propagating MaskOpts.colour[3] to '
                  'Display.alpha [{}]'.format(alpha))

        with props.skip(self.display, 'alpha', self.name):
            self.display.alpha = alpha

    def __alphaChanged(self, *a):
        """Called when :attr:`.Display.alpha` changes. Updates the alpha
        component of :attr:`.colour`.
        """

        alpha = self.display.alpha / 100.0
        r, g, b, _ = self.colour

        log.debug('Propagating Display.alpha to MaskOpts.'
                  'colour[3] [{}]'.format(alpha))

        with props.skip(self, 'colour', self.name):
            self.colour = r, g, b, alpha
Пример #15
0
class DataSeries(props.HasProperties):
    """A ``DataSeries`` instance encapsulates some data to be plotted by
    a :class:`PlotPanel`, with the data extracted from an overlay in the
    :class:`.OverlayList`.

    Sub-class implementations must:

      - Accept an overlay object, :class:`.OverlayList`,
        :class:`.DisplayContext`, and :class:`.PlotPanel` in their
        ``__init__`` method, and pass these through to
        :meth:`.DataSeries.__init__`.
      - Override the :meth:`getData` method
      - Override the :meth:`redrawProperties` method if necessary


    The overlay is accessible as an instance attribute, confusingly called
    ``overlay``.


    .. note:: Some ``DataSeries`` instances may not be associated with
              an overlay (e.g. series imported loaded a text file). In
              this case, the ``overlay`` attribute will be ``None``.


    Each``DataSeries`` instance is plotted as a line, with the line
    style defined by properties on the ``DataSeries`` instance,
    such as :attr:`colour`, :attr:`lineWidth` etc.
    """

    colour = props.Colour()
    """Line colour. """

    enabled = props.Boolean(default=True)
    """Draw or not draw?"""

    alpha = props.Real(minval=0.0, maxval=1.0, default=1.0, clamped=True)
    """Line transparency."""

    label = props.String()
    """Line label (used in the plot legend)."""

    lineWidth = props.Choice((0.5, 1, 2, 3, 4, 5))
    """Line width. """

    lineStyle = props.Choice(
        ('-', '--', '-.', ':', (0, (5, 7)), (0, (1, 7)), (0, (4, 10, 1, 10)),
         (0, (4, 1, 1, 1, 1, 1)), (0, (4, 1, 4, 1, 1, 1))))
    """Line style. See
    https://matplotlib.org/gallery/lines_bars_and_markers/linestyles.html
    """
    def __init__(self, overlay, overlayList, displayCtx, plotPanel):
        """Create a ``DataSeries``.

        :arg overlay:     The overlay from which the data to be plotted is
                          retrieved.  May be ``None``.
        :arg overlayList: The :class:`.OverlayList` instance.
        :arg displayCtx:  The :class:`.DisplayContext` instance.
        :arg plotPanel:   The :class:`.PlotPanel` that owns this
                          ``DataSeries``.
        """

        self.__name = '{}_{}'.format(type(self).__name__, id(self))
        self.__overlay = overlay
        self.__overlayList = overlayList
        self.__displayCtx = displayCtx
        self.__plotPanel = plotPanel
        self.setData([], [])

        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 __hash__(self):
        """Returns a hash for this ``DataSeries`` instance."""
        return hash(id(self))

    @property
    def name(self):
        """Returns a unique name for this ``DataSeries`` instance. """
        return self.__name

    @property
    def overlay(self):
        """Returns the overlay associated with this ``DataSeries`` instance.
        """
        return self.__overlay

    @property
    def overlayList(self):
        """Returns the :class:`.OverlayList`.
        """
        return self.__overlayList

    @property
    def displayCtx(self):
        """Returns the :class:`.DisplayContext`.
        """
        return self.__displayCtx

    @property
    def plotPanel(self):
        """Returns the :class:`.PlotPanel` that owns this ``DataSeries``
        instance.
        """
        return self.__plotPanel

    def destroy(self):
        """This method must be called when this ``DataSeries`` instance is no
        longer needed. This implementation may be overridden by sub-classes
        which need to perform any clean-up operations. Sub-class
        implementations should call this implementation.
        """
        self.__overlay = None
        self.__overlayList = None
        self.__displayCtx = None
        self.__plotPanel = None

    def redrawProperties(self):
        """Returns a list of all properties which, when their values change,
        should result in this ``DataSeries`` being re-plotted. This method
        may be overridden by sub-classes.
        """

        return self.getAllProperties()[0]

    def extraSeries(self):
        """Some ``DataSeries`` types have additional ``DataSeries`` associated
        with them (see e.g. the :class:`.FEATTimeSeries` class). This method
        can be overridden to return a list of these extra ``DataSeries``
        instances. The default implementation returns an empty list.
        """
        return []

    def setData(self, xdata, ydata):
        """Set the data to be plotted. This method is irrelevant if a
        ``DataSeries`` sub-class has overridden :meth:`getData`.
        """
        self.__xdata = xdata
        self.__ydata = ydata

    def getData(self):
        """This method should be overridden by sub-classes. It must return
        the data to be plotted, as a tuple of the form:

            ``(xdata, ydata)``

        where ``xdata`` and ``ydata`` are sequences containing the x/y data
        to be plotted.

        The default implementation returns the data that has been set via the
        :meth:`setData` method.
        """
        return self.__xdata, self.__ydata
Пример #16
0
class LutLabel(props.HasProperties):
    """This class represents a mapping from a value to a colour and name.
    ``LutLabel`` instances are created and managed by :class:`LookupTable`
    instances.

    Listeners may be registered on the :attr:`name`, :attr:`colour`, and
    :attr:`enabled` properties to be notified when they change.
    """

    name = props.String(default='Label')
    """The display name for this label. Internally (for comparison), the
    :meth:`internalName` is used, which is simply this name, converted to
    lower case.
    """

    colour = props.Colour(default=(0, 0, 0))
    """The colour for this label. """

    enabled = props.Boolean(default=True)
    """Whether this label is currently enabled or disabled. """
    def __init__(self, value, name=None, colour=None, enabled=None):
        """Create a ``LutLabel``.

        :arg value:   The label value.
        :arg name:    The label name.
        :arg colour:  The label colour.
        :arg enabled: Whether the label is enabled/disabled.
        """

        if value is None:
            raise ValueError('LutLabel value cannot be None')

        if name is None:
            name = LutLabel.getProp('name').getAttribute(None, 'default')

        if colour is None:
            colour = LutLabel.getProp('colour').getAttribute(None, 'default')

        if enabled is None:
            enabled = LutLabel.getProp('enabled').getAttribute(None, 'default')

        self.__value = value
        self.name = name
        self.colour = colour
        self.enabled = enabled

    @property
    def value(self):
        """Returns the value of this ``LutLabel``. """
        return self.__value

    @property
    def internalName(self):
        """Returns the *internal* name of this ``LutLabel``, which is just
        its :attr:`name`, converted to lower-case. This is used by
        :meth:`__eq__` and :meth:`__hash__`, and by the
        :class:`LookupTable` class.
        """
        return self.name.lower()

    def __eq__(self, other):
        """Equality operator - returns ``True`` if this ``LutLabel``
        has the same  value as the given one.
        """

        return self.value == other.value

    def __lt__(self, other):
        """Less-than operator - compares two ``LutLabel`` instances
        based on their value.
        """
        return self.value < other.value

    def __hash__(self):
        """The hash of a ``LutLabel`` is a combination of its
        value, name, and colour, but not its enabled state.
        """
        return (hash(self.value) ^ hash(self.internalName) ^ hash(self.colour))

    def __str__(self):
        """Returns a string representation of this ``LutLabel``."""
        return '{}: {} / {} ({})'.format(self.value, self.internalName,
                                         self.colour, self.enabled)

    def __repr__(self):
        """Returns a string representation of this ``LutLabel``."""
        return self.__str__()
Пример #17
0
class Scene3DCanvasOpts(props.HasProperties):
    """The ``Scene3DCanvasOpts`` class defines the display settings
    available on :class:`.Scene3DCanvas` instances.
    """

    pos = copy.copy(SliceCanvasOpts.pos)
    """Current cursor position in the display coordinate system. The dimensions
    are in the same ordering as the display coordinate system, in contrast
    to the :attr:`SliceCanvasOpts.pos` property.
    """

    showCursor = copy.copy(SliceCanvasOpts.showCursor)
    cursorColour = copy.copy(SliceCanvasOpts.cursorColour)
    bgColour = copy.copy(SliceCanvasOpts.bgColour)
    zoom = copy.copy(SliceCanvasOpts.zoom)
    highDpi = copy.copy(SliceCanvasOpts.highDpi)

    showLegend = props.Boolean(default=True)
    """If ``True``, an orientation guide will be shown on the canvas. """

    legendColour = props.Colour(default=(0, 1, 0))
    """Colour to use for the legend text."""

    labelSize = props.Int(minval=4, maxval=96, default=12, clamped=True)
    """Font size used for the legend labels. """

    occlusion = props.Boolean(default=True)
    """If ``True``, objects closer to the camera will occlude objects
    further away. Toggles ``gl.DEPTH_TEST``.
    """

    light = props.Boolean(default=True)
    """If ``True``, a lighting effect is applied to compatible overlays
    in the scene.
    """

    showLight = props.Boolean(default=False)
    """If ``True``, a point is drawn at the current light position. """

    lightPos = props.Point(ndims=3)
    """Defines the light position in the display coordinate system. This
    property contains a set of three rotation values, in degrees.

    The lighting model uses a point source which is located a fixed distance
    away from the display coordinate system centre - the distance is set
    by the :attr:`lightDistance` property.

    The lightPos property defines how the light is rotated with respect to
    the centre of the display coordinate system.

    The :meth:`.Scene3DCanvas.lightPos` method can be used to calculate the
    actual position of the light in the display coordinate system.
    """

    lightDistance = props.Real(minval=0.5, maxval=10, default=2)
    """Distance of the light source from the centre of the display coordinate
    system. This is used as a multiplicative factor - a value of 2 set the
    light source a distance of twice the length of the display bounding box
    from the bounding box centre.
    """

    offset = props.Point(ndims=2)
    """An offset, in X/Y pixels normalised to the range ``[-1, 1]``, from the
    centre of the ``Scene3DCanvas``.
    """

    rotation = props.Array(dtype=np.float64,
                           shape=(3, 3),
                           resizable=False,
                           default=[[1, 0, 0], [0, 1, 0], [0, 0, 1]])
    """A rotation matrix which defines the current ``Scene3DCanvas`` view