示例#1
0
class MyObj(props.HasProperties):

    myinto  = props.Int()
    myrealo = props.Real()

    myintc  = props.Int( minval=0,   maxval=100, clamped=True)
    myrealc = props.Real(minval=0.0, maxval=1.0, clamped=True)
示例#2
0
class LineVectorOpts(VectorOpts):
    """The ``LineVectorOpts`` class contains settings for displaying vector
    images, using a line to represent the vector value at each voxel.
    """


    lineWidth = props.Real(minval=0.1, maxval=10, default=1, clamped=True)
    """Width of the line in pixels.
    """

    directed = props.Boolean(default=False)
    """If ``True``, the vector data is interpreted as directed. Otherwise,
    the vector data is assumed to be undirected.
    """


    unitLength = props.Boolean(default=True)
    """If ``True``, each vector is scaled so that it has a length of
    ``1 * lengthScale`` (or 0.5 if ``directed`` is ``True``).
    """


    lengthScale = props.Percentage(minval=10, maxval=500, default=100)
    """Length scaling factor. """


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

        kwargs['nounbind'] = ['directed', 'unitLength', 'lengthScale']

        VectorOpts.__init__(self, *args, **kwargs)
示例#3
0
 class MyObj(props.HasProperties):
     unbounded           = props.Real()
     unbounded_default   = props.Real(default=10)
     bounded             = props.Real(minval=0, maxval=10)
     bounded_min         = props.Real(minval=0)
     bounded_max         = props.Real(maxval=10)
     bounded_clamped     = props.Real(clamped=True, minval=0, maxval=10)
     bounded_min_clamped = props.Real(clamped=True, minval=0)
     bounded_max_clamped = props.Real(clamped=True, maxval=10)
    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()
示例#5
0
class LightBoxCanvasOpts(SliceCanvasOpts):
    """The ``LightBoxCanvasOpts`` class defines the display settings
    available on :class:`.LightBoxCanvas` instances.
    """


    sliceSpacing = props.Real(clamped=True,
                              minval=0.1,
                              maxval=30.0,
                              default=1.0)
    """This property controls the spacing between slices in the display
    coordinate system.
    """


    ncols = props.Int(clamped=True, minval=1, maxval=100, default=5)
    """This property controls the number of slices to be displayed on a
    single row.
    """


    nrows = props.Int(clamped=True, minval=1, maxval=100, default=4)
    """This property controls the number of rows to be displayed on the
    canvas.
    """


    topRow = props.Int(clamped=True, minval=0, maxval=20, default=0)
    """This property controls the (0-indexed) row to be displayed at the top
    of the canvas, thus providing the ability to scroll through the slices.
    """


    zrange = props.Bounds(ndims=1)
    """This property controls the range, in display coordinates, of the slices
    to be displayed.
    """


    showGridLines = props.Boolean(default=False)
    """If ``True``, grid lines are drawn between the displayed slices. """


    highlightSlice = props.Boolean(default=False)
    """If ``True``, a box will be drawn around the slice containing the current
示例#6
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
示例#7
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
示例#8
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
示例#9
0
class ColourMapOpts(object):
    """The ``ColourMapOpts`` class is a mixin for use with
    :class:`.DisplayOpts` sub-classes. It provides properties and logic
    for displaying overlays which are coloured according to some data values.
    See the :class:`.MeshOpts` and :class:`.VolumeOpts` classes for examples
    of classes which inherit from this class.


    To use the ``ColourMapOpts`` class, you must:

      1. Define your class to inherit from both :class:`.DisplayOpts` and
         ``ColourMapOpts``::

             class MyOpts(DisplayOpts, ColourMapOpts):
                 ...

      2. Call the ``ColourMapOpts.__init__`` method *after*
         :meth:`.DisplayOpts.__init__`::

             def __init__(self, *args, **kwargs):
                 DisplayOpts.__init__(self, *args, **kwargs)
                 ColourMapOpts.__init__(self)

      3. Implement the :meth:`getDataRange` and (if necessary)
         :meth:`getClippingRange` methods.

      4. Call :meth:`updateDataRange` whenever the data driving the colouring
         changes.


    The ``ColourMapOpts`` class links the :attr:`.Display.brightness` and
    :attr:`.Display.contrast` properties to its own :attr:`displayRange`
    property, so changes in either of the former will result in a change to
    the latter, and vice versa. This relationship is defined by the
    :func:`~.colourmaps.displayRangeToBricon` and
    :func:`~.colourmaps.briconToDisplayRange` functions, in the
    :mod:`.colourmaps` module.


    ``ColourMapOpts`` instances provide the following methods:

    .. autosummary::
       :nosignatures:

       updateDataRange
       getDataRange
       getClippingRange
    """


    displayRange = props.Bounds(ndims=1, clamped=False)
    """Values which map to the minimum and maximum colour map colours.

    .. note:: The values that this property can take are unbound because of
              the interaction between it and the :attr:`.Display.brightness`
              and :attr:`.Display.contrast` properties.  The
              :attr:`displayRange` and :attr:`clippingRange` properties are
              not clamped (they can take values outside of their
              minimum/maximum values) because the data range for large NIFTI
              images may not be known, and may change as more data is read
              from disk.
    """


    clippingRange = props.Bounds(ndims=1, clamped=False)
    """Values outside of this range are not shown.  Clipping works as follows:

     - Values less than or equal to the minimum clipping value are
       clipped.

     - Values greater than or equal to the maximum clipping value are
       clipped.

    Because of this, a small amount of padding is added to the low and high
    clipping range limits, to make it possible for all values to be
    displayed.
    """


    invertClipping = props.Boolean(default=False)
    """If ``True``, the behaviour of :attr:`clippingRange` is inverted, i.e.
    values inside the clipping range are clipped, instead of those outside
    the clipping range.
    """


    cmap = props.ColourMap()
    """The colour map, a :class:`matplotlib.colors.Colourmap` instance."""


    gamma = props.Real(minval=-1, maxval=1, clamped=True, default=0)
    """Gamma correction factor - exponentially weights the :attr:`cmap`
    and :attr:`negCmap` towards the low or high ends.

    This property takes values between -1 and +1. The exponential weight
    that should actually be used to apply gamma correction should be derived
    as follows:

      - -1 corresponds to a gamma of 0.01
      -  0 corresponds to a gamma of 1
      - +1 corresponds to a gamma of 10

    The :meth:`realGamma` method will apply this scaling and return the
    exponent to be used.
    """


    cmapResolution = props.Int(minval=2, maxval=1024, default=256)
    """Resolution for the colour map, i.e. the number of colours to use. """


    interpolateCmaps = props.Boolean(default=False)
    """If ``True``, the colour maps are applied using linear interpolation.
    Otherwise they are applied using nearest neighbour interpolation.
    """


    negativeCmap = props.ColourMap()
    """A second colour map, used if :attr:`useNegativeCmap` is ``True``.
    When active, the :attr:`cmap` is used to colour positive values, and
    the :attr:`negativeCmap` is used to colour negative values.
    """


    useNegativeCmap = props.Boolean(default=False)
    """When ``True``, the :attr:`cmap` is used to colour positive values,
    and the :attr:`negativeCmap` is used to colour negative values.
    When this property is enabled, the minimum value for both the
    :attr:`displayRange` and :attr:`clippingRange` is set to zero. Both
    ranges are applied to positive values, and negated/inverted for negative
    values.

    .. note:: When this property is set to ``True``, the
              :attr:`.Display.brightness` and :attr:`.Display.contrast`
              properties are disabled, as managing the interaction between
              them would be far too complicated.
    """


    invert = props.Boolean(default=False)
    """Use an inverted version of the current colour map (see the :attr:`cmap`
    property).
    """


    linkLowRanges = props.Boolean(default=True)
    """If ``True``, the low bounds on both the :attr:`displayRange` and
    :attr:`clippingRange` ranges will be linked together.
    """


    linkHighRanges = props.Boolean(default=False)
    """If ``True``, the high bounds on both the :attr:`displayRange` and
    :attr:`clippingRange` ranges will be linked together.
    """


    @staticmethod
    def realGamma(gamma):
        """Return the value of ``gamma`` property, scaled appropriately.
        for use as an exponent.
        """

        # a gamma in the range [-1, 0]
        # gets scaled to [0.01, 1]
        if gamma < 0:
            return (gamma + 1.01) * 0.99

        # a gamma in the range [0, 1]
        # gets scaled to [1, 10]
        else:
            return 1 + 9 * gamma


    def __init__(self):
        """Create a ``ColourMapOpts`` instance. This must be called
        *after* the :meth:`.DisplayOpts.__init__` method.
        """

        # The displayRange property of every child ColourMapOpts
        # instance is linked to the corresponding
        # Display.brightness/contrast properties, so changes
        # in one are reflected in the other. This interaction
        # complicates the relationship between parent and child
        # ColourMapOpts instances, so we only implement it on
        # children.
        #
        # NOTE: This means that if we use a parent-less
        #       DisplayContext for display, this bricon-display
        #       range relationship will break.
        #
        self.__registered = self.getParent() is not None

        if self.__registered:

            name    = self.getColourMapOptsListenerName()
            display = self.display

            display    .addListener('brightness',
                                    name,
                                    self.__briconChanged,
                                    immediate=True)
            display    .addListener('contrast',
                                    name,
                                    self.__briconChanged,
                                    immediate=True)
            self       .addListener('displayRange',
                                    name,
                                    self.__displayRangeChanged,
                                    immediate=True)
            self       .addListener('useNegativeCmap',
                                    name,
                                    self.__useNegativeCmapChanged,
                                    immediate=True)
            self       .addListener('linkLowRanges',
                                    name,
                                    self.__linkLowRangesChanged,
                                    immediate=True)
            self       .addListener('linkHighRanges',
                                    name,
                                    self.__linkHighRangesChanged,
                                    immediate=True)

            # Because displayRange and bri/con are intrinsically
            # linked, it makes no sense to let the user sync/unsync
            # them independently. So here we are binding the boolean
            # sync properties which control whether the dRange/bricon
            # properties are synced with their parent. So when one
            # property is synced/unsynced, the other ones are too.
            self.bindProps(self   .getSyncPropertyName('displayRange'),
                           display,
                           display.getSyncPropertyName('brightness'))
            self.bindProps(self   .getSyncPropertyName('displayRange'),
                           display,
                           display.getSyncPropertyName('contrast'))

            # If useNegativeCmap, linkLowRanges or linkHighRanges
            # have been set to True (this will happen if they
            # are true on the parent VolumeOpts instance), make
            # sure the property / listener states are up to date.
            if self.linkLowRanges:   self.__linkLowRangesChanged()
            if self.linkHighRanges:  self.__linkHighRangesChanged()
            if self.useNegativeCmap:
                self.__useNegativeCmapChanged(updateDataRange=False)

        # If this is the parent ColourMapOpts
        # instance, its properties need to be
        # initialised. Child instance properties
        # should inherit the current parent
        # values, unless they are not synced
        # to the parent.
        if (not self.__registered) or \
           (not self.isSyncedToParent('displayRange')):
            self.updateDataRange(False, False)


    def getColourMapOptsListenerName(self):
        """Returns the name used by this ``ColourMapOpts`` instance for
        registering internal property listeners.

        Sibling ``ColourMapOpts``
        instances need to toggle each other's property listeners (see the
        :meth:`__toggleListeners` method), so they use this method to
        retrieve each other's listener names.
        """
        return 'ColourMapOpts_{}'.format(id(self))


    def destroy(self):
        """Must be called when this ``ColourMapOpts`` is no longer needed,
        and before :meth:`.DisplayOpts.destroy` is called. Removes property
        listeners.
        """

        if not self.__registered:
            return

        display = self.display
        name    = self.getColourMapOptsListenerName()

        display.removeListener('brightness',      name)
        display.removeListener('contrast',        name)
        self   .removeListener('displayRange',    name)
        self   .removeListener('useNegativeCmap', name)
        self   .removeListener('linkLowRanges',   name)
        self   .removeListener('linkHighRanges',  name)

        self.unbindProps(self   .getSyncPropertyName('displayRange'),
                         display,
                         display.getSyncPropertyName('brightness'))
        self.unbindProps(self   .getSyncPropertyName('displayRange'),
                         display,
                         display.getSyncPropertyName('contrast'))

        self.__linkRangesChanged(False, 0)
        self.__linkRangesChanged(False, 1)


    def getDataRange(self):
        """Must be overridden by sub-classes. Must return the range of the
        data used for colouring as a ``(min, max)`` tuple.  Note that, even

        if there is no effective data range, you should return two different
        values for ``min`` and ``max`` (e.g. ``(0, 1)``), because otherwise
        the relationship between the :attr:`displayRange` and the
        :attr:`.Display.brightness` and :attr:`.Display.contrast` properties
        will be corrupted.
        """

        raise NotImplementedError('ColourMapOpts.getDataRange must be '
                                  'implemented by sub-classes.')


    def getClippingRange(self):
        """Can be overridden by sub-classes if necessary. If the clipping
        range is always the same as the data range, this method does not
        need to be overridden.

        Otherwise, if the clipping range differs from the data range
        (see e.g. the :attr:`.VolumeOpts.clipImage` property), this method
        must return the clipping range as a ``(min, max)`` tuple.

        When a sub-class implementation wishes to use the default clipping
        range/behaviour, it should return the value returned by this
        base-class implementation.
        """
        return None


    @actions.action
    def resetDisplayRange(self):
        """Resets the :attr:`displayRange` and :attr:`clippingRange` to their
        initial values.
        """
        self.updateDataRange(True, True)


    def updateDataRange(self, resetDR=True, resetCR=True):
        """Must be called by sub-classes whenever the ranges of the underlying
        data or clipping values change.  Configures the minimum/maximum bounds
        of the :attr:`displayRange` and :attr:`clippingRange` properties.

        :arg resetDR: If ``True`` (the default), the :attr:`displayRange`
                      property will be reset to the data range returned
                      by :meth:`getDataRange`. Otherwise the existing
                      value will be preserved.

        :arg resetCR: If ``True`` (the default), the :attr:`clippingRange`
                      property will be reset to the clipping range returned
                      by :meth:`getClippingRange`. Otherwise the existing
                      value will be preserved.

        Note that both of these flags will be ignored if the existing low/high
        :attr:`displayRange`/:attr:`clippingRange` values and limits are equal
        to each other.
        """

        dataMin, dataMax = self.getDataRange()
        clipRange        = self.getClippingRange()

        absolute = self.useNegativeCmap
        drmin    = dataMin
        drmax    = dataMax

        if absolute:
            drmin = min((0,            abs(dataMin)))
            drmax = max((abs(dataMin), abs(dataMax)))

        if clipRange is not None: crmin, crmax = clipRange
        else:                     crmin, crmax = drmin, drmax

        # Clipping works on >= and <=, so we add
        # a small offset to the display range limits
        # (which are equal to the clipping limiits)
        # so the user can configure the scene such
        # that no values are clipped.
        droff  = abs(drmax - drmin) / 100.0
        croff  = abs(crmax - crmin) / 100.0
        crmin -= croff
        crmax += croff
        drmin -= droff
        drmax += droff

        # Execute on the PV call queue,
        # so that property updates occur
        # in the correct order.
        def doUpdate():

            # If display/clipping limit range
            # is 0, we assume that they haven't
            # yet been set
            drUnset = (self.displayRange .xmin == self.displayRange .xmax and
                       self.displayRange .xlo  == self.displayRange .xhi)
            crUnset = (self.clippingRange.xmin == self.clippingRange.xmax and
                       self.clippingRange.xlo  == self.clippingRange.xhi)
            crGrow  =  self.clippingRange.xhi  == self.clippingRange.xmax
            drUnset =  resetDR or drUnset
            crUnset =  resetCR or crUnset

            log.debug('[{}] Updating range limits [dr: {} - {}, ''cr: '
                      '{} - {}]'.format(id(self), drmin, drmax, crmin, crmax))

            self.displayRange .xlim = drmin, drmax
            self.clippingRange.xlim = crmin, crmax

            # If the ranges have not yet been set,
            # initialise them to the min/max.
            # Also, if the high clipping range
            # was previously equal to the max
            # clipping range, keep that relationship,
            # otherwise high values will be clipped.
            if drUnset: self.displayRange .x   = drmin + droff, dataMax
            if crUnset: self.clippingRange.x   = crmin + croff, crmax
            if crGrow:  self.clippingRange.xhi = crmax

            # If using absolute range values, the low
            # display/clipping should be set to 0
            if absolute and self.displayRange .xlo < 0:
                self.displayRange.xlo  = 0
            if absolute and self.clippingRange.xlo < 0:
                self.clippingRange.xlo = 0

        props.safeCall(doUpdate)


    def __toggleListeners(self, enable=True):
        """This method enables/disables the property listeners which
        are registered on the :attr:`displayRange` and
        :attr:`.Display.brightness`/:attr:`.Display.contrast`/properties.

        Because these properties are linked via the
        :meth:`__displayRangeChanged` and :meth:`__briconChanged` methods,
        we need to be careful about avoiding recursive callbacks.

        Furthermore, because the properties of both :class:`ColourMapOpts` and
        :class:`.Display` instances are possibly synchronised to a parent
        instance (which in turn is synchronised to other children), we need to
        make sure that the property listeners on these other sibling instances
        are not called when our own property values change. So this method
        disables/enables the property listeners on all sibling
        ``ColourMapOpts`` and ``Display`` instances.
        """

        parent = self.getParent()

        # this is the parent instance
        if parent is None:
            return

        # The parent.getChildren() method will
        # contain this ColourMapOpts instance,
        # so the below loop toggles listeners
        # for this instance and all of the other
        # children of the parent
        peers  = parent.getChildren()

        for peer in peers:

            name = peer.getColourMapOptsListenerName()
            bri  = peer.display.hasListener('brightness',   name)
            con  = peer.display.hasListener('contrast',     name)
            dr   = peer        .hasListener('displayRange', name)

            if enable:
                if bri: peer.display.enableListener('brightness',   name)
                if con: peer.display.enableListener('contrast',     name)
                if dr:  peer        .enableListener('displayRange', name)
            else:
                if bri: peer.display.disableListener('brightness',   name)
                if con: peer.display.disableListener('contrast',     name)
                if dr:  peer        .disableListener('displayRange', name)


    def __briconChanged(self, *a):
        """Called when the ``brightness``/``contrast`` properties of the
        :class:`.Display` instance change.

        Updates the :attr:`displayRange` property accordingly.

        See :func:`.colourmaps.briconToDisplayRange`.
        """

        dataRange = self.getDataRange()

        dlo, dhi = fslcm.briconToDisplayRange(
            dataRange,
            self.display.brightness / 100.0,
            self.display.contrast   / 100.0)

        self.__toggleListeners(False)
        self.displayRange.x = [dlo, dhi]
        self.__toggleListeners(True)


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

        Updates the :attr:`.Display.brightness` and :attr:`.Display.contrast`
        properties accordingly.

        See :func:`.colourmaps.displayRangeToBricon`.
        """

        if self.useNegativeCmap:
            return

        dataRange = self.getDataRange()

        brightness, contrast = fslcm.displayRangeToBricon(
            dataRange, self.displayRange.x)

        self.__toggleListeners(False)

        # update bricon
        self.display.brightness = brightness * 100
        self.display.contrast   = contrast   * 100

        self.__toggleListeners(True)


    def __useNegativeCmapChanged(self, *a, **kwa):
        """Called when the :attr:`useNegativeCmap` property changes.
        Enables/disables the :attr:`.Display.brightness` and
        :attr:`.Display.contrast` properties, and calls
        :meth:`updateDataRange`.

        :arg updateDatRange: Must be passed as a keyword argument.
                             If ``True`` (the default), calls
                             :meth:`updateDataRange`.
        """

        if self.useNegativeCmap:
            self.display.disableProperty('brightness')
            self.display.disableProperty('contrast')
        else:
            self.display.enableProperty('brightness')
            self.display.enableProperty('contrast')

        if kwa.pop('updateDataRange', True):
            self.updateDataRange(resetDR=False, resetCR=False)


    def __linkLowRangesChanged(self, *a):
        """Called when the :attr:`linkLowRanges` property changes. Calls the
        :meth:`__linkRangesChanged` method.
        """
        self.__linkRangesChanged(self.linkLowRanges, 0)


    def __linkHighRangesChanged(self, *a):
        """Called when the :attr:`linkHighRanges` property changes. Calls the
        :meth:`__linkRangesChanged` method.
        """
        self.__linkRangesChanged(self.linkHighRanges, 1)


    def __linkRangesChanged(self, val, idx):
        """Called when either the :attr:`linkLowRanges` or
        :attr:`linkHighRanges` properties change. Binds/unbinds the specified
        range properties together.

        :arg val: Boolean indicating whether the range values should be
                  linked or unlinked.

        :arg idx: Range value index - 0 corresponds to the low range value,
                  and 1 to the high range value.
        """

        dRangePV = self.displayRange .getPropertyValueList()[idx]
        cRangePV = self.clippingRange.getPropertyValueList()[idx]

        if props.propValsAreBound(dRangePV, cRangePV) == val:
            return

        props.bindPropVals(dRangePV,
                           cRangePV,
                           bindval=True,
                           bindatt=False,
                           unbind=not val)

        if val:
            cRangePV.set(dRangePV.get())
示例#10
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
示例#11
0
class Volume3DOpts(object):
    """The ``Volume3DOpts`` class is a mix-in for use with :class:`.DisplayOpts`
    classes. It defines display properties used for ray-cast based rendering
    of :class:`.Image` overlays.


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        outer = self.getNumOuterSteps()

        return int(outer * self.numInnerSteps)

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

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

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

        return int(outer)

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


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


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

        Returns a tuple containing:

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

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

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

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

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

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

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

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

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

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

        return rayStep, xform

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

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

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

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

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

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

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

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

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

        return origin, normal
示例#12
0
class ComplexPowerSpectrumSeries(VoxelPowerSpectrumSeries):
    """This class is the frequency-spectrum equivalent of the
    :class:`.ComplexTimeSeries` class - see it for more details.
    """

    plotReal = props.Boolean(default=True)
    plotImaginary = props.Boolean(default=False)
    plotMagnitude = props.Boolean(default=False)
    plotPhase = props.Boolean(default=False)

    zeroOrderPhaseCorrection = props.Real(default=0)
    """Apply zero order phase correction to the power spectrum of the complex
    data.
    """

    firstOrderPhaseCorrection = props.Real(default=0)
    """Apply first order phase correction to the power spectrum of the complex
    data.
    """
    def __init__(self, overlay, overlayList, displayCtx, plotPanel):
        """Create a ``ComplexPowerSpectrumSeries``. All arguments are
        passed through to the :class:`VoxelPowerSpectrumSeries` constructor.
        """

        VoxelPowerSpectrumSeries.__init__(self, overlay, overlayList,
                                          displayCtx, plotPanel)

        # Separate DataSeries for the imaginary/
        # magnitude/phase signals, returned by
        # the extraSeries method
        self.__imagps = ImaginaryPowerSpectrumSeries(self, overlay,
                                                     overlayList, displayCtx,
                                                     plotPanel)
        self.__magps = MagnitudePowerSpectrumSeries(self, overlay, overlayList,
                                                    displayCtx, plotPanel)
        self.__phaseps = PhasePowerSpectrumSeries(self, overlay, overlayList,
                                                  displayCtx, plotPanel)

        for ps in (self.__imagps, self.__magps, self.__phaseps):
            ps.colour = fslcm.randomDarkColour()
            ps.bindProps('alpha', self)
            ps.bindProps('lineWidth', self)
            ps.bindProps('lineStyle', self)

    def makeLabelBase(self):
        """Returns a string to be used as the label prefix for this
        ``ComplexPowerSpectrumSeries`` instance, and for the imaginary,
        magnitude, and phase child series.
        """
        return VoxelPowerSpectrumSeries.makeLabel(self)

    def makeLabel(self):
        """Returns a label to use for this data series. """
        return '{} ({})'.format(self.makeLabelBase(), strings.labels[self])

    def getData(self, component='real'):
        """If :attr:`plotReal` is true, returns the real component of the power
        spectrum of the data at the current voxel. Otherwise returns ``(None,
        None)``.

        Every time this method is called, the power spectrum is retrieved (see
        the :class:`VoxelPowerSpectrumSeries` class), phase correction is
        applied if set, andthe data is normalised, if set. A tuple containing
        the ``(xdata, ydata)`` is returned, with ``ydata`` containing the
        requested ``component`` ( ``'real'``, ``'imaginary'``,
        ``'magnitude'``, or ``'phase'``).

        This method is called by the :class:`ImaginarySpectrumPowerSeries`,
        :class:`MagnitudeSpectrumPowerSeries`, and
        :class:`PhasePowerSpectrumPowerSeries` instances that are associated
        with this data series.
        """

        if ((component == 'real')      and (not self.plotReal))      or \
           ((component == 'imaginary') and (not self.plotImaginary)) or \
           ((component == 'magnitude') and (not self.plotMagnitude)) or \
           ((component == 'phase')     and (not self.plotPhase)):
            return None, None

        # See VoxelPowerSpectrumSeries - the data
        # is already fourier-transformed
        ydata = self.dataAtCurrentVoxel()

        if ydata is None:
            return None, None

        # All of the calculations below are repeated
        # for each real/imag/mag/phase series that
        # gets plotted. But keeping the code together
        # and clean is currently more important than
        # performance, as there is not really any
        # performance hit.
        overlay = self.overlay
        xdata = calcFrequencies(overlay.shape[3], self.sampleTime,
                                overlay.dtype)

        if self.zeroOrderPhaseCorrection  != 0 or \
           self.firstOrderPhaseCorrection != 0:
            ydata = phaseCorrection(ydata, xdata,
                                    self.zeroOrderPhaseCorrection,
                                    self.firstOrderPhaseCorrection)

        # Normalise magnitude, real, imaginary
        # components with respect to magnitude.
        # Normalise phase independently.
        if self.varNorm:
            mag = magnitude(ydata)
            mr = mag.min(), mag.max()
            if component == 'phase': ydata = normalise(phase(ydata))
            elif component == 'magnitude': ydata = normalise(mag)
            elif component == 'real': ydata = normalise(ydata.real, *mr)
            elif component == 'imaginary': ydata = normalise(ydata.imag, *mr)

        elif component == 'real': ydata = ydata.real
        elif component == 'imaginary': ydata = ydata.imag
        elif component == 'magnitude': ydata = magnitude(ydata)
        elif component == 'phase': ydata = phase(ydata)

        return xdata, ydata

    def extraSeries(self):
        """Returns a list of additional series to be plotted, based
        on the values of the :attr:`plotImaginary`, :attr:`plotMagnitude`
        and :attr:`plotPhase` properties.
        """

        extras = []
        if self.plotImaginary: extras.append(self.__imagps)
        if self.plotMagnitude: extras.append(self.__magps)
        if self.plotPhase: extras.append(self.__phaseps)
        return extras
示例#13
0
class SHOpts(vectoropts.VectorOpts):
    """The ``SHOpts`` is used for rendering class for rendering :class:`.Image`
    instances which contain fibre orientation distributions (FODs) in the form
    of spherical harmonic (SH) coefficients. A ``SHOpts`` instance will be
    used for ``Image`` overlays with a :attr:`.Displaty.overlayType` set to
    ``'sh'``.


    A collection of pre-calculated SH basis function parameters are stored in
    the ``assets/sh/`` directory. Depending on the SH order that was used in
    the fibre orientation, and the desired display resolution (controlled by
    :attr:`shResolution`), a different set of parameters needs to be used.
    The :meth:`getSHParameters` method will load and return the corrrect
    set of parameters.
    """

    shResolution = props.Int(minval=3, maxval=10, default=5)
    """Resolution of the sphere used to display the FODs at each voxel. The
    value is equal to the number of iterations that an isocahedron, starting
    with 12 vertices, is tessellated. The resulting number of vertices is
    as follows:


    ==================== ==================
    Number of iterations Number of vertices
    3                    92
    4                    162
    5                    252
    6                    362
    7                    492
    8                    642
    9                    812
    10                   1002
    ==================== ==================
    """

    shOrder = props.Choice(allowStr=True)
    """Maximum spherical harmonic order to visualise. This is populated in
    :meth:`__init__`.
    """

    size = props.Percentage(minval=10, maxval=500, default=100)
    """Display size - this is simply a linear scaling factor. """

    lighting = props.Boolean(default=False)
    """Apply a simple directional lighting model to the FODs. """

    radiusThreshold = props.Real(minval=0.0, maxval=1.0, default=0.05)
    """FODs with a maximum radius that is below this threshold are not shown.
    """

    colourMode = props.Choice(('direction', 'radius'))
    """How to colour each FOD. This property is overridden if the
    :attr:`.VectorOpts.colourImage` is set.

      - ``'direction'`` The vertices of an FOD are coloured according to their
                        x/y/z location (see :attr:`xColour`, :attr:`yColour`,
                        and :attr:`zColour`).

      - ``'radius'``    The vertices of an FOD are coloured according to their
                        distance from the FOD centre (see :attr:`colourMap`).
    """
    def __init__(self, *args, **kwargs):

        vectoropts.VectorOpts.__init__(self, *args, **kwargs)

        ncoefs = self.overlay.shape[3]
        shType, maxOrder = SH_COEFFICIENT_TYPE.get(ncoefs)

        if shType is None:
            raise ValueError('{} does not look like a SH '
                             'image'.format(self.overlay.name))

        self.__maxOrder = maxOrder
        self.__shType = shType

        # If this Opts instance has a parent,
        # the shOrder choices will be inherited
        if self.getParent() is None:

            if shType == 'sym': vizOrders = range(0, self.__maxOrder + 1, 2)
            elif shType == 'asym': vizOrders = range(0, self.__maxOrder + 1)

            self.getProp('shOrder').setChoices(list(vizOrders), instance=self)
            self.shOrder = vizOrders[-1]

    @property
    def shType(self):
        """Returns either ``'sym'`` or ``'asym'``, depending on the type
        of the SH coefficients contained in the file.
        """
        return self.__shType

    @property
    def maxOrder(self):
        """Returns the maximum SH order that was used to generate the
        coefficients of the SH image.
        """
        return self.__maxOrder

    def getSHParameters(self):
        """Load and return a ``numpy`` array containing pre-calculated SH
        function parameters for the curert maximum SH order and display
        resolution. The returned array has the shape ``(N, C)``, where ``N``
        is the number of vertices used to represent each FOD, and ``C`` is
        the number of SH coefficients.
        """

        # TODO Adjust matrix if shOrder is
        #      less than its maximum possible
        #      value for this image.
        #
        #      Also, calculate the normal vectors.

        resolution = self.shResolution
        ncoefs = self.overlay.shape[3]
        order = self.shOrder
        ftype, _ = SH_COEFFICIENT_TYPE[ncoefs]
        fname = op.join(fsleyes.assetDir, 'assets', 'sh',
                        '{}_coef_{}_{}.txt'.format(ftype, resolution, order))

        params = np.loadtxt(fname)

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

        return params

    def getVertices(self):
        """Loads and returns a ``numpy`` array of shape ``(N, 3)``, containing
        ``N`` vertices of a tessellated sphere.
        """
        fname = op.join(fsleyes.assetDir, 'assets', 'sh',
                        'vert_{}.txt'.format(self.shResolution))

        return np.loadtxt(fname)

    def getIndices(self):
        """Loads and returns a 1D ``numpy`` array, containing indices into
        the vertex array, specifying the order in which they are to be drawn
        as triangles.
        """
        fname = op.join(fsleyes.assetDir, 'assets', 'sh',
                        'face_{}.txt'.format(self.shResolution))

        return np.loadtxt(fname).flatten()
class ComplexPowerSpectrumSeries(VoxelPowerSpectrumSeries):
    """This class is the frequency-spectrum equivalent of the
    :class:`.ComplexTimeSeries` class - see it for more details.
    """

    plotReal = props.Boolean(default=True)
    plotImaginary = props.Boolean(default=False)
    plotMagnitude = props.Boolean(default=False)
    plotPhase = props.Boolean(default=False)

    zeroOrderPhaseCorrection = props.Real(default=0)
    """Apply zero order phase correction to the power spectrum of the complex
    data.
    """

    firstOrderPhaseCorrection = props.Real(default=0)
    """Apply first order phase correction to the power spectrum of the complex
    data.
    """
    def __init__(self, overlay, overlayList, displayCtx, plotPanel):
        """Create a ``ComplexPowerSpectrumSeries``. All arguments are
        passed through to the :class:`VoxelPowerSpectrumSeries` constructor.
        """

        VoxelPowerSpectrumSeries.__init__(self, overlay, overlayList,
                                          displayCtx, plotPanel)

        self.__cachedData = (None, None)
        self.__imagps = ImaginaryPowerSpectrumSeries(self, overlay,
                                                     overlayList, displayCtx,
                                                     plotPanel)
        self.__magps = MagnitudePowerSpectrumSeries(self, overlay, overlayList,
                                                    displayCtx, plotPanel)
        self.__phaseps = PhasePowerSpectrumSeries(self, overlay, overlayList,
                                                  displayCtx, plotPanel)

        for ps in (self.__imagps, self.__magps, self.__phaseps):
            ps.colour = fslcm.randomDarkColour()
            ps.bindProps('alpha', self)
            ps.bindProps('lineWidth', self)
            ps.bindProps('lineStyle', self)

    def makeLabel(self):
        """Returns a string representation of this
        ``ComplexPowerSpectrumSeries`` instance.
        """
        return '{} ({})'.format(VoxelPowerSpectrumSeries.makeLabel(self),
                                strings.labels[self])

    @property
    def cachedData(self):
        """Returns the currently cached data (see :meth:`getData`). """
        return self.__cachedData

    def getData(self):
        """If :attr:`plotReal` is true, returns the real component of the power
        spectrum of the data at the current voxel. Otherwise returns ``(None,
        None)``.

        Every time this method is called, the power spectrum is calculated,
        phase correction is applied, and a reference to the resulting complex
        power spectrum (and frequencies) is saved; it is accessible via the
        :meth:`cachedData` property, for use by the
        :class:`ImaginaryPowerSpectrumSeries`,
        :class:`MagnitudePowerSpectrumSeries`, and
        :class:`PhasePowerSpectrumSeries`.
        """

        xdata, ydata = VoxelPowerSpectrumSeries.getData(self)

        if self.zeroOrderPhaseCorrection  != 0 or \
           self.firstOrderPhaseCorrection != 0:
            ydata = phaseCorrection(ydata, xdata,
                                    self.zeroOrderPhaseCorrection,
                                    self.firstOrderPhaseCorrection)

        # Note that we're assuming that this
        # ComplexPowerSpectrumSeries.getData
        # method will be called before the
        # corresponding call(s) to the
        # Imaginary/Magnitude/Phase series
        # methods.
        self.__cachedData = xdata, ydata

        if not self.plotReal:
            return None, None

        if ydata is not None:
            ydata = ydata.real

        return xdata, ydata

    def extraSeries(self):
        """Returns a list of additional series to be plotted, based
        on the values of the :attr:`plotImaginary`, :attr:`plotMagnitude`
        and :attr:`plotPhase` properties.
        """

        extras = []
        if self.plotImaginary: extras.append(self.__imagps)
        if self.plotMagnitude: extras.append(self.__magps)
        if self.plotPhase: extras.append(self.__phaseps)
        return extras
示例#15
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
示例#16
0
class LabelOpts(volumeopts.NiftiOpts):
    """The ``LabelOpts`` class defines settings for displaying
    :class:`.Image` overlays as label images., such as anatomical atlas
    images, tissue segmentation images, and so on.
    """

    lut = props.Choice()
    """The :class:`.LookupTable` used to colour each label.
    """

    outline = props.Boolean(default=False)
    """If ``True`` only the outline of contiguous regions with the same label
    value will be shown. If ``False``, contiguous regions will be filled.
    """


    outlineWidth = props.Real(minval=0, maxval=1, default=0.25, clamped=True)
    """Width of labelled region outlines, if :attr:``outline` is ``True``.
    This value is in terms of the image voxels - a value of 1 will result in
    an outline that is one voxel wide.
    """


    showNames = props.Boolean(default=False)
    """If ``True``, region names (as defined by the current
    :class:`.LookupTable`) will be shown alongside each labelled region.

    .. note:: Not implemented yet.
    """


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

        # Some FSL tools will set the nifti aux_file
        # field to the name of a colour map - Check
        # to see if this is the case (again, before
        # calling __init__, so we don't clobber any
        # existing values).
        aux_file = overlay.strval('aux_file').lower()

        if aux_file.startswith('mgh'):
            aux_file = 'mgh-cma-freesurfer'

        # Check to see if any registered lookup table
        # has an ID that starts with the aux_file value.
        # Default to random lut if aux_file is empty,
        # or does not correspond to a registered lut.
        lut = 'random'

        if aux_file != '':
            luts = colourmaps.getLookupTables()
            luts = [l.key for l in luts if l.key.startswith(aux_file)]

            if len(luts) == 1:
                lut = luts[0]

        self.lut = lut

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