コード例 #1
0
class LabelOpts(niftiopts.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.Int(minval=0, maxval=10, default=1, clamped=True)
    """Width of labelled region outlines, if :attr:``outline` is ``True``.
    This value is in terms of pixels.
    """

    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 = 'freesurfercolorlut'

        # 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

        niftiopts.NiftiOpts.__init__(self, overlay, *args, **kwargs)
コード例 #2
0
ファイル: volumeopts.py プロジェクト: marcobarilari/fsleyes
class VolumeRGBOpts(niftiopts.NiftiOpts):
    """The ``VolumeRGBOpts`` class is intended for displaying
    :class:`.Image` instances containing RGB(A) data.
    """

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

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

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

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

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

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

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

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

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

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

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

        niftiopts.NiftiOpts.__init__(self, overlay, display, overlayList,
                                     displayCtx, **kwargs)
コード例 #3
0
    class Thing(props.HasProperties):

        myobject     = props.Object()
        mybool       = props.Boolean()
        myint        = props.Int()
        myreal       = props.Real()
        mypercentage = props.Percentage()
        mystring     = props.String()
        mychoice     = props.Choice(('1', '2', '3', '4', '5'))
        myfilepath   = props.FilePath()
        mylist       = props.List()
        mycolour     = props.Colour()
        mycolourmap  = props.ColourMap()
        mybounds     = props.Bounds(ndims=2)
        mypoint      = props.Point(ndims=2)
        myarray      = props.Array()
コード例 #4
0
class OrthoOpts(sceneopts.SceneOpts):
    """The ``OrthoOpts`` class is used by :class:`.OrthoPanel` instances to
    manage their display settings.


    .. note:: While the ``OrthoOpts`` class has :attr:`xzoom`, :attr:`yzoom`,
              and :attr:`zzoom`, properties which control the zoom levels on
              each canvas independently, ``OrthoOpts`` class also inherits a
              ``zoom`` property from the :class:`.SceneOpts` class. This
              *global* zoom property can be used to adjust all canvas zoom
              levels simultaneously.
    """

    cursorGap = copy.copy(canvasopts.SliceCanvasOpts.cursorGap)

    showXCanvas = props.Boolean(default=True)
    """Toggles display of the X canvas."""

    showYCanvas = props.Boolean(default=True)
    """Toggles display of the Y canvas."""

    showZCanvas = props.Boolean(default=True)
    """Toggles display of the Z canvas."""

    showLabels = props.Boolean(default=True)
    """If ``True``, labels showing anatomical orientation are displayed on
    each of the canvases.
    """

    labelSize = props.Int(minval=4, maxval=96, default=14, clamped=True)
    """Label font size."""

    layout = props.Choice(('horizontal', 'vertical', 'grid'))
    """How should we lay out each of the three canvases?"""

    xzoom = copy.copy(sceneopts.SceneOpts.zoom)
    """Controls zoom on the X canvas."""

    yzoom = copy.copy(sceneopts.SceneOpts.zoom)
    """Controls zoom on the Y canvas."""

    zzoom = copy.copy(sceneopts.SceneOpts.zoom)
    """Controls zoom on the Z canvas. """
    def __init__(self, *args, **kwargs):
        """Create an ``OrthoOpts`` instance. All arguments are passed
        through to the :class:`.SceneOpts` constructor.

        This method sets up a binding from the :attr:`.SceneOpts.zoom`
        property to the :attr:`xzoom`, :attr:`yzoom`, and :attr:`zzoom`
        properties - see :meth:`__onZoom`.
        """
        sceneopts.SceneOpts.__init__(self, *args, **kwargs)

        name = '{}_{}'.format(type(self).__name__, id(self))

        self.addListener('zoom', name, self.__onZoom)

    def __onZoom(self, *a):
        """Called when the :attr:`.SceneOpts.zoom` property changes.

        Propagates the change to the :attr:`xzoom`, :attr:`yzoom`, and
        :attr:`zzoom` properties.
        """
        self.xzoom = self.zoom
        self.yzoom = self.zoom
        self.zzoom = self.zoom

    def _onPerformanceChange(self, *a):
        """Overrides :meth:`.SceneOpts._onPerformanceChange`. Changes the
        value of the :attr:`renderMode` property according to the
        performance setting.
        """

        if self.performance == 3: self.renderMode = 'onscreen'
        elif self.performance == 2: self.renderMode = 'offscreen'
        elif self.performance == 1: self.renderMode = 'prerender'

        log.debug('Performance settings changed: '
                  'renderMode={}'.format(self.renderMode))
コード例 #5
0
ファイル: canvasopts.py プロジェクト: laurenpan02/fsleyes
class SliceCanvasOpts(props.HasProperties):
    """The ``SliceCanvasOpts`` class defines all of the display settings
    for a :class:`.SliceCanvas`.
    """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @property
    def yax(self):
        """The display coordinate system axis which maps to the Y (vertical)
        canvas axis.
        """
        return self.__yax
コード例 #6
0
ファイル: volumeopts.py プロジェクト: neurodebian/fsleyes
class VolumeOpts(cmapopts.ColourMapOpts, vol3dopts.Volume3DOpts, NiftiOpts):
    """The ``VolumeOpts`` class defines options for displaying :class:`.Image`
    instances as regular 3D volumes.
    """

    clipImage = props.Choice()
    """Clip voxels according to the values in another image. By default, voxels
    are clipped by the values in the image itself - this property allows the
    user to choose another image by which voxels are to be clipped. Any image
    which is in the :class:`.OverlayList`, and which has the same voxel
    dimensions as the primary image can be selected for clipping. The
    :attr:`.ColourMapOpts.clippingRange` property dictates the values outside
    of which voxels are clipped.
    """

    interpolation = props.Choice(('none', 'linear', 'spline'))
    """How the value shown at a real world location is derived from the
    corresponding data value(s). ``none`` is equivalent to nearest neighbour
    interpolation.
    """
    def __init__(self, overlay, display, overlayList, displayCtx, **kwargs):
        """Create a :class:`VolumeOpts` instance for the specified ``overlay``,
        assumed to be an :class:`.Image` instance.

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

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

        # Interpolation cannot be unbound
        # between VolumeOpts instances. This is
        # primarily to reduce memory requirement
        # - if interpolation were different
        # across different views, we would have
        # to create multiple 3D image textures
        # for the same image.
        nounbind = kwargs.get('nounbind', [])
        nounbind.append('interpolation')
        nounbind.append('clipImage')
        kwargs['nounbind'] = nounbind

        # 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 (do this before
        # calling __init__, so we don't clobber any
        # existing values).
        cmap = str(overlay.header.get('aux_file', 'none')).lower()

        if cmap == 'mgh-subcortical': cmap = 'subcortical'
        if cmap == 'mgh-cortical': cmap = 'cortical'

        if cmap in fslcm.getColourMaps():
            self.cmap = cmap

        NiftiOpts.__init__(self, overlay, display, overlayList, displayCtx,
                           **kwargs)
        cmapopts.ColourMapOpts.__init__(self)
        vol3dopts.Volume3DOpts.__init__(self)

        # Both parent and child VolumeOpts instances
        # listen for Image dataRange changes. The data
        # range for large images may be calculated
        # asynchronously on a separate thread, meaning
        # that data range updates may occur at random
        # times.
        #
        # If parent instances did not listen for data
        # range updates and, at startup, the following
        # sequence of events occurs:
        #
        #   1. Parent VolumeOpts instance created
        #
        #   2. Image.dataRange updated
        #
        #   3. Child VolumeOpts instance created
        #
        # The known parent data range will be 0-0,
        # the child will not receive any notification
        # about the data range change, and the child's
        # data range will be clobbered by the parent's.
        # This ugly situation is avoided simply by
        # having the parent track changes to the data
        # range in addition to all children.
        overlay.register(self.name,
                         self.__dataRangeChanged,
                         'dataRange',
                         runOnIdle=True)

        # We need to listen for changes to clipImage
        # and to [enable]overrideDataRange, as they
        # will change the display data range. These
        # cannot be unbound between parent/children,
        # so only the parent needs to listen.
        self.__registered = self.getParent() is None
        if self.__registered:
            overlayList.addListener('overlays', self.name,
                                    self.__overlayListChanged)
            self.addListener('clipImage', self.name, self.__clipImageChanged)
            self.addListener('enableOverrideDataRange', self.name,
                             self.__enableOverrideDataRangeChanged)
            self.addListener('overrideDataRange', self.name,
                             self.__overrideDataRangeChanged)

            self.__overlayListChanged()
            self.__clipImageChanged(updateDataRange=False)

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

        overlay = self.overlay
        overlayList = self.overlayList

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

        if self.__registered:

            overlayList.removeListener('overlays', self.name)
            self.removeListener('clipImage', self.name)
            self.removeListener('enableOverrideDataRange', self.name)
            self.removeListener('overrideDataRange', self.name)

        cmapopts.ColourMapOpts.destroy(self)
        vol3dopts.Volume3DOpts.destroy(self)
        NiftiOpts.destroy(self)

    def getDataRange(self):
        """Overrides :meth:`.ColourMapOpts.getDataRange`. Returns the
        :attr:`.Image.dataRange` of the image, or the
        :attr:`overrideDataRange` if it is active.
        """
        if self.enableOverrideDataRange: return self.overrideDataRange
        else: return self.overlay.dataRange

    def getClippingRange(self):
        """Overrides :meth:`.ColourMapOpts.getClippingRange`.
        If a :attr:`.clipImage` is set, returns its data range. Otherwise
        returns ``None``.
        """

        if self.clipImage is None:
            return cmapopts.ColourMapOpts.getClippingRange(self)
        else:
            return self.clipImage.dataRange

    def __dataRangeChanged(self, *a):
        """Called when the :attr:`.Image.dataRange` property changes.
        Calls :meth:`.ColourMapOpts.updateDataRange`.
        """
        self.updateDataRange(resetDR=False, resetCR=False)

    def __enableOverrideDataRangeChanged(self, *a):
        """Called when the :attr:`enableOverrideDataRange` property changes.
        Calls :meth:`.ColourMapOpts.updateDataRange`.
        """
        self.updateDataRange()

    def __overrideDataRangeChanged(self, *a):
        """Called when the :attr:`overrideDataRange` property changes.
        Calls :meth:`.ColourMapOpts.updateDataRange`.
        """
        self.updateDataRange()

    def __overlayListChanged(self, *a):
        """Called when the :`class:`.OverlayList` changes. Updates the
        options of the :attr:`clipImage` property.
        """

        clipProp = self.getProp('clipImage')
        clipVal = self.clipImage
        overlays = self.displayCtx.getOrderedOverlays()

        options = [None]

        for overlay in overlays:

            if overlay is self.overlay: continue
            if not isinstance(overlay, fslimage.Image): continue

            options.append(overlay)

        clipProp.setChoices(options, instance=self)

        if clipVal in options: self.clipImage = clipVal
        else: self.clipImage = None

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

        :arg updateDataRange: Defaults to ``True``. If ``False``, the
                              :meth:`.ColourMapOpts.updateDataRange` method
                              is not called.
        """

        updateDR = kwa.get('updateDataRange', True)

        haveClipImage = self.clipImage is not None

        if not haveClipImage:
            self.enableProperty('linkLowRanges')
            self.enableProperty('linkHighRanges')

        # If the clipping range is based on another
        # image, it makes no sense to link the low/
        # high display/clipping ranges, as they are
        # probably different. So if a clip image is
        # selected, we disable the link range
        # properties.
        elif self.propertyIsEnabled('linkLowRanges'):

            self.linkLowRanges = False
            self.linkHighRanges = False

            self.disableProperty('linkLowRanges')
            self.disableProperty('linkHighRanges')

        log.debug('Clip image changed for {}: {}'.format(
            self.overlay, self.clipImage))

        if updateDR:
            self.updateDataRange(resetDR=False)
コード例 #7
0
ファイル: meshopts.py プロジェクト: lachmanfrantisek/fsleyes
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 CanvasPanel(viewpanel.ViewPanel):
    """The ``CanvasPanel`` class is a :class:`.ViewPanel` which is the base
    class for all panels which display overlays using ``OpenGL``
    (e.g. the :class:`.OrthoPanel` and the :class:`.LightBoxPanel`). A
    ``CanvasPanel`` instance uses a :class:`.SceneOpts` instance to control
    much of its functionality. The ``SceneOpts`` instance used by a
    ``CanvasPanel`` can be accessed via the :meth:`sceneOpts` property.


    The ``CanvasPanel`` class contains settings and functionality common to
    all sub-classes, including *movie mode* (see :attr:`movieMode`), the
    ability to show a colour bar (a :class:`.ColourBarPanel`; see
    :attr:`.SceneOpts.showColourBar`), and a number of actions.


    **Sub-class implementations**


    Sub-classes of the ``CanvasPanel`` must do the following:

      1. Add their content to the panel that is accessible via the
         :meth:`contentPanel` property (see the note on
         :ref:`adding content <canvaspanel-adding-content>`).

      2. Override the :meth:`getGLCanvases` method.

      3. Call the :meth:`centrePanelLayout` method in their ``__init__``
         method.

      4. Override the :meth:`centrePanelLayout` method if any custom layout is
         necessary.


    **Actions**


    The following actions are available through a ``CanvasPanel`` (see
    the :mod:`.actions` module):

    .. autosummary::
       :nosignatures:

       screenshot
       movieGif
       showCommandLineArgs
       toggleMovieMode
       toggleDisplaySync
       toggleOverlayList
       toggleOverlayInfo
       toggleAtlasPanel
       toggleDisplayToolBar
       toggleDisplayPanel
       toggleCanvasSettingsPanel
       toggleLocationPanel
       toggleClusterPanel
       toggleLookupTablePanel
       toggleClassificationPanel


    .. _canvaspanel-adding-content:


    **Adding content**


    To support colour bar and screenshot functionality, the ``CanvasPanel``
    uses a hierarchy of ``wx.Panel`` instances, depicted in the following
    containment hierarchy diagram:

    .. graphviz::

       digraph canvasPanel {

         graph [size=""];

         node [style="filled",
               shape="box",
               fillcolor="#ddffdd",
               fontname="sans"];

         rankdir="BT";

         1 [label="CanvasPanel"];
         2 [label="Centre panel"];
         3 [label="Custom content (for complex layouts)"];
         4 [label="Container panel"];
         5 [label="ColourBarPanel"];
         6 [label="Content panel"];
         7 [label="Content added by sub-classes"];

         2 -> 1;
         3 -> 2;
         4 -> 2;
         5 -> 4;
         6 -> 4;
         7 -> 6;
       }


    As depicted in the diagram, sub-classes need to add their content to the
    *content panel*. This panel is accessible via the :meth:`contentPanel`
    property.


    The *centre panel* is the :meth:`.ViewPanel.centrePanel`. The *container
    panel* is also available, via :meth:`containerPanel`. Everything in
    the container panel will appear in screenshots (see the :meth:`screenshot`
    method).


    The :meth:`centrePanelLayout` method lays out the centre panel, using the
    :meth:`layoutContainerPanel` method to lay out the colour bar and the
    content panel. The ``centrePanelLayout`` method simply adds the canvas
    container directly to the centre panel. Sub-classes which have more
    advanced layout requirements (e.g.  the :class:`.LightBoxPanel` needs a
    scrollbar) may override the :meth:`centrePanelLayout` method to implement
    their own layout.  These sub-class implementations must:

      1. Call the :meth:`layoutContainerPanel` method.

      2. Add the container panel (accessed via :meth:`containerPanel`)
         to the centre panel (accessed via :meth:`centrePanel`).

      3. Add any other custom content to the centre panel.
    """

    syncLocation = props.Boolean(default=True)
    """If ``True`` (the default), the :attr:`.DisplayContext.location` for
    this ``CanvasPanel`` is linked to the master ``DisplayContext`` location.
    """

    syncOverlayOrder = props.Boolean(default=True)
    """If ``True`` (the default), the :attr:`.DisplayContext.overlayOrder`
    for this ``CanvasPanel`` is linked to the master ``DisplayContext``
    overlay order.
    """

    syncOverlayDisplay = props.Boolean(default=True)
    """If ``True`` (the default), the properties of the :class:`.Display`
    and :class:`.DisplayOpts` instances for every overlay, as managed
    by the :attr:`.DisplayContext` for this ``CanvasPanel``, are linked to
    the properties of all ``Display`` and ``DisplayOpts`` instances managed
    by the master ``DisplayContext`` instance.
    """

    movieMode = props.Boolean(default=False)
    """If ``True``, and the currently selected overlay (see
    :attr:`.DisplayContext.selectedOverlay`) is a :class:`.Image` instance
    with its display managed by a :class:`.VolumeOpts` instance, the displayed
    volume is changed periodically, according to the :attr:`movieRate`
    property.

    The update is performed on the main application thread via
    ``wx.CallLater``.
    """

    movieRate = props.Int(minval=10, maxval=500, default=400, clamped=True)
    """The movie update rate in milliseconds. The value of this property is
    inverted so that a high value corresponds to a fast rate, which makes
    more sense when displayed as an option to the user.
    """

    movieAxis = props.Choice((0, 1, 2, 3), default=3)
    """Axis along which the movie should be played, relative to the
    currently selected :class:`.Image`.
    """
    def __init__(self, parent, overlayList, displayCtx, frame, sceneOpts):
        """Create a ``CanvasPanel``.

        :arg parent:       The :mod:`wx` parent object.

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

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

        :arg sceneOpts:    A :class:`.SceneOpts` instance for this
                           ``CanvasPanel`` - must be created by
                           sub-classes.
        """

        viewpanel.ViewPanel.__init__(self, parent, overlayList, displayCtx,
                                     frame)

        self.__opts = sceneOpts

        # Use this name for listener registration,
        # in case subclasses use the FSLeyesPanel.name
        self.__name = 'CanvasPanel_{}'.format(self.name)

        # Bind the sync* properties of this
        # CanvasPanel to the corresponding
        # properties on the DisplayContext
        # instance.
        if displayCtx.getParent() is not None:
            self.bindProps('syncLocation', displayCtx,
                           displayCtx.getSyncPropertyName('worldLocation'))
            self.bindProps('syncOverlayOrder', displayCtx,
                           displayCtx.getSyncPropertyName('overlayOrder'))
            self.bindProps('syncOverlayDisplay', displayCtx)

        # If the displayCtx instance does not
        # have a parent, this means that it is
        # a top level instance
        else:
            self.disableProperty('syncLocation')
            self.disableProperty('syncOverlayOrder')
            self.disableProperty('syncOverlayDisplay')

        import fsleyes.actions.moviegif as moviegif

        self.centrePanel = wx.Panel(self)
        self.__containerPanel = wx.Panel(self.centrePanel)
        self.__contentPanel = wx.Panel(self.__containerPanel)
        self.__movieGifAction = moviegif.MovieGifAction(
            overlayList, displayCtx, self)

        self.toggleMovieMode.bindProps('toggled', self, 'movieMode')
        self.toggleDisplaySync.bindProps('toggled', self, 'syncOverlayDisplay')
        self.movieGif.bindProps('enabled', self.__movieGifAction)

        # the __movieModeChanged method is called
        # when movieMode changes, but also when
        # the movie axis, overlay list, or selected
        # overlay changes. This is because, if movie
        # mode is on, but no overlay, or an
        # incompatible overlay, is selected, the
        # movie loop stops. So it needs to be
        # re-started if/when a compatible overlay is
        # selected.
        self.__movieRunning = False
        self.addListener('movieMode', self.__name, self.__movieModeChanged)
        self.addListener('movieAxis', self.__name, self.__movieModeChanged)
        self.overlayList.addListener('overlays', self.__name,
                                     self.__movieModeChanged)
        self.displayCtx.addListener('selectedOverlay', self.__name,
                                    self.__movieModeChanged)

        # Canvas/colour bar layout is managed
        # in the layoutContainerPanel method
        self.__colourBar = None

        self.__opts.addListener('colourBarLocation', self.__name,
                                self.__colourBarPropsChanged)
        self.__opts.addListener('showColourBar', self.__name,
                                self.__colourBarPropsChanged)
        self.__opts.addListener('bgColour', self.__name,
                                self.__bgfgColourChanged)
        self.__opts.addListener('fgColour', self.__name,
                                self.__bgfgColourChanged)

        idle.idle(self.__bgfgColourChanged)

    def destroy(self):
        """Makes sure that any remaining control panels are destroyed
        cleanly, and calls :meth:`.ViewPanel.destroy`.
        """

        if self.__colourBar is not None:
            self.__colourBar.destroy()

        self.removeListener('movieMode', self.__name)
        self.removeListener('movieAxis', self.__name)
        self.overlayList.removeListener('overlays', self.__name)
        self.displayCtx.removeListener('selectedOverlay', self.__name)
        self.sceneOpts.removeListener('colourBarLocation', self.__name)
        self.sceneOpts.removeListener('showColourBar', self.__name)
        self.sceneOpts.removeListener('bgColour', self.__name)
        self.sceneOpts.removeListener('fgColour', self.__name)
        self.__movieGifAction.destroy()

        self.__opts = None
        self.__movieGifAction = None

        viewpanel.ViewPanel.destroy(self)

    @actions.action
    def screenshot(self):
        """Takes a screenshot of the currently displayed scene on this
        ``CanvasPanel``.

        See the :class:`.ScreenshotAction`.
        """
        from fsleyes.actions.screenshot import ScreenshotAction
        ScreenshotAction(self.overlayList, self.displayCtx, self)()

    @actions.action
    def movieGif(self):
        """Generates an animated GIF of the currently displayed scene and
        movie mode settings on this ``CanvasPanel``.

        See the :class:`.MovieGifAction`.
        """
        self.__movieGifAction()

    @actions.action
    def showCommandLineArgs(self):
        """Shows the command line arguments which can be used to re-create
        the currently displayed scene. See the :class:`.ShowCommandLineAction`
        class.
        """
        from fsleyes.actions.showcommandline import ShowCommandLineAction
        ShowCommandLineAction(self.overlayList, self.displayCtx, self)()

    @actions.action
    def applyCommandLineArgs(self):
        """Shows the command line arguments which can be used to re-create
        the currently displayed scene. See the :class:`.ApplyCommandLineAction`
        class.
        """
        from fsleyes.actions.applycommandline import ApplyCommandLineAction
        ApplyCommandLineAction(self.overlayList, self.displayCtx, self)()

    @actions.toggleAction
    def toggleMovieMode(self):
        """Toggles the value of :attr:`movieMode`. """
        # The state of this action gets bound to
        # the movieMode attribute in __init__
        pass

    @actions.toggleAction
    def toggleDisplaySync(self):
        """Toggles the value of :attr:`syncOverlayDisplay`. """
        # The state of this action gets bound to
        # the syncOverlayDisplay attribute in __init__
        pass

    @actions.toggleControlAction(overlaylistpanel.OverlayListPanel)
    def toggleOverlayList(self):
        """Toggles an :class:`.OverlayListPanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(overlaylistpanel.OverlayListPanel, location=wx.BOTTOM)

    @actions.toggleControlAction(overlayinfopanel.OverlayInfoPanel)
    def toggleOverlayInfo(self, floatPane=False):
        """Toggles an :class:`.OverlayInfoPanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(overlayinfopanel.OverlayInfoPanel,
                         location=wx.RIGHT,
                         floatPane=floatPane)

    @actions.toggleControlAction(atlaspanel.AtlasPanel)
    def toggleAtlasPanel(self):
        """Toggles an :class:`.AtlasPanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(atlaspanel.AtlasPanel, location=wx.BOTTOM)

    @actions.toggleControlAction(overlaydisplaytoolbar.OverlayDisplayToolBar)
    def toggleDisplayToolBar(self):
        """Toggles an :class:`.OverlayDisplayToolBar`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(overlaydisplaytoolbar.OverlayDisplayToolBar,
                         viewPanel=self)

    @actions.toggleControlAction(overlaydisplaypanel.OverlayDisplayPanel)
    def toggleDisplayPanel(self, floatPane=False):
        """Toggles an :class:`.OverlayDisplayPanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(overlaydisplaypanel.OverlayDisplayPanel,
                         floatPane=floatPane,
                         location=wx.LEFT)

    @actions.toggleControlAction(canvassettingspanel.CanvasSettingsPanel)
    def toggleCanvasSettingsPanel(self, floatPane=False):
        """Toggles a :class:`.CanvasSettingsPanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(canvassettingspanel.CanvasSettingsPanel,
                         canvasPanel=self,
                         floatPane=floatPane,
                         location=wx.LEFT)

    @actions.toggleControlAction(locationpanel.LocationPanel)
    def toggleLocationPanel(self):
        """Toggles a :class:`.LocationPanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(locationpanel.LocationPanel, location=wx.BOTTOM)

    @actions.toggleControlAction(clusterpanel.ClusterPanel)
    def toggleClusterPanel(self):
        """Toggles a :class:`.ClusterPanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(clusterpanel.ClusterPanel, location=wx.TOP)

    @actions.toggleControlAction(lookuptablepanel.LookupTablePanel)
    def toggleLookupTablePanel(self):
        """Toggles a :class:`.LookupTablePanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(lookuptablepanel.LookupTablePanel, location=wx.RIGHT)

    @actions.toggleControlAction(melclasspanel.MelodicClassificationPanel)
    def toggleClassificationPanel(self):
        """Toggles a :class:`.MelodicClassificationPanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(melclasspanel.MelodicClassificationPanel,
                         location=wx.RIGHT,
                         canvasPanel=self)

    @property
    def sceneOpts(self):
        """Returns the :class:`.SceneOpts` instance used by this
        ``CanvasPanel``.
        """
        return self.__opts

    @property
    def contentPanel(self):
        """Returns the ``wx.Panel`` to which sub-classes must add their content.
        See the note on :ref:`adding content <canvaspanel-adding-content>`.
        """
        return self.__contentPanel

    @property
    def containerPanel(self):
        """Returns the ``wx.Panel`` which contains the
        :class:`.ColourBarPanel` if it is being displayed, and the content
        panel. See the note on
        :ref:`adding content <canvaspanel-adding-content>`.
        """
        return self.__containerPanel

    @property
    def colourBarCanvas(self):
        """If a colour bar is being displayed, this method returns
        the :class:`.ColourBarCanvas` instance which is used by the
        :class:`.ColourBarPanel` to render the colour bar.

        Otherwise, ``None`` is returned.
        """
        if self.__colourBar is not None:
            return self.__colourBar.getCanvas()
        return None

    @deprecation.deprecated(deprecated_in='0.16.0',
                            removed_in='1.0.0',
                            details='Use sceneOpts instead')
    def getSceneOptions(self):
        """Returns the :class:`.SceneOpts` instance used by this
        ``CanvasPanel``.
        """
        return self.__opts

    @deprecation.deprecated(deprecated_in='0.16.0',
                            removed_in='1.0.0',
                            details='Use contentPanel instead')
    def getContentPanel(self):
        """Returns the ``wx.Panel`` to which sub-classes must add their content.
        See the note on :ref:`adding content <canvaspanel-adding-content>`.
        """
        return self.__contentPanel

    @deprecation.deprecated(deprecated_in='0.16.0',
                            removed_in='1.0.0',
                            details='Use containerPanel instead')
    def getContainerPanel(self):
        """Returns the ``wx.Panel`` which contains the
        :class:`.ColourBarPanel` if it is being displayed, and the content
        panel. See the note on
        :ref:`adding content <canvaspanel-adding-content>`.
        """
        return self.__containerPanel

    @deprecation.deprecated(deprecated_in='0.16.0',
                            removed_in='1.0.0',
                            details='Use colourBarCanvas instead')
    def getColourBarCanvas(self):
        """If a colour bar is being displayed, this method returns
        the :class:`.ColourBarCanvas` instance which is used by the
        :class:`.ColourBarPanel` to render the colour bar.

        Otherwise, ``None`` is returned.
        """
        if self.__colourBar is not None:
            return self.__colourBar.getCanvas()
        return None

    def getGLCanvases(self):
        """This method must be overridden by subclasses, and must return a
        list containing all :class:`.SliceCanvas` instances which are being
        displayed.
        """
        raise NotImplementedError('getGLCanvases has not been implemented '
                                  'by {}'.format(type(self).__name__))

    def centrePanelLayout(self):
        """Lays out the centre panel. This method may be overridden by
        sub-classes which need more advanced layout logic. See the note on
        :ref:`adding content <canvaspanel-adding-content>`
        """

        self.layoutContainerPanel()

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.__containerPanel, flag=wx.EXPAND, proportion=1)
        self.centrePanel.SetSizer(sizer)

        self.PostSizeEvent()

    def layoutContainerPanel(self):
        """Creates a ``wx.Sizer``, and uses it to lay out the colour bar panel
        and canvas panel. The sizer object is returned.

        This method is used by the default :meth:`centrePanelLayout` method,
        and is available for custom sub-class implementations to use.
        """

        sopts = self.sceneOpts

        if not sopts.showColourBar:

            if self.__colourBar is not None:
                sopts.unbindProps('colourBarLabelSide', self.__colourBar,
                                  'labelSide')
                self.__colourBar.destroy()
                self.__colourBar.Destroy()
                self.__colourBar = None

            sizer = wx.BoxSizer(wx.HORIZONTAL)
            sizer.Add(self.__contentPanel, flag=wx.EXPAND, proportion=1)
            self.__containerPanel.SetSizer(sizer)
            return

        if self.__colourBar is None:
            self.__colourBar = colourbarpanel.ColourBarPanel(
                self.__containerPanel, self.overlayList, self.displayCtx,
                self.frame)

            bg = sopts.bgColour
            fg = sopts.fgColour
            self.__colourBar.getCanvas().textColour = fg
            self.__colourBar.getCanvas().bgColour = bg

        sopts.bindProps('colourBarLabelSide', self.__colourBar, 'labelSide')

        if sopts.colourBarLocation in ('top', 'bottom'):
            self.__colourBar.orientation = 'horizontal'
        elif sopts.colourBarLocation in ('left', 'right'):
            self.__colourBar.orientation = 'vertical'

        if sopts.colourBarLocation in ('top', 'bottom'):
            sizer = wx.BoxSizer(wx.VERTICAL)
        else:
            sizer = wx.BoxSizer(wx.HORIZONTAL)

        if sopts.colourBarLocation in ('top', 'left'):
            sizer.Add(self.__colourBar, flag=wx.EXPAND)
            sizer.Add(self.__contentPanel, flag=wx.EXPAND, proportion=1)
        else:
            sizer.Add(self.__contentPanel, flag=wx.EXPAND, proportion=1)
            sizer.Add(self.__colourBar, flag=wx.EXPAND)

        self.__containerPanel.SetSizer(sizer)

    def __colourBarPropsChanged(self, *a):
        """Called when any colour bar display properties are changed (see
        :class:`.SceneOpts`). Calls :meth:`canvasPanelLayout`.
        """
        self.centrePanelLayout()

    def __bgfgColourChanged(self, *a, **kwa):
        """Called when the :class:`.SceneOpts.bgColour` or
        :class:`.SceneOpts.fgColour` properties change.  Updates
        background/foreground colours.

        The :attr:`.SliceCanvasOpts.bgColour` properties are bound to
        ``SceneOpts.bgColour``,(see :meth:`.HasProperties.bindProps`), so we
        don't need to manually update them.

        :arg refresh: Must be passed as a keyword argument. If ``True`` (the
                      default), this ``OrthoPanel`` is refreshed.
        """
        refresh = kwa.pop('refresh', True)

        sceneOpts = self.sceneOpts
        cpanel = self.contentPanel
        canvases = self.getGLCanvases()
        bg = sceneOpts.bgColour
        fg = sceneOpts.fgColour

        cpanel.SetBackgroundColour([c * 255 for c in bg])
        cpanel.SetForegroundColour([c * 255 for c in fg])

        if self.__colourBar is not None:
            cbCanvas = self.__colourBar.getCanvas()
            cbCanvas.textColour = fg
            cbCanvas.bgColour = bg
            canvases.append(cbCanvas)

        if refresh:
            self.Refresh()
            self.Update()

    def __movieModeChanged(self, *a):
        """Called when the :attr:`movieMode` property changes. If it has been
        enabled, calls :meth:`__movieUpdate`, to start the movie loop.
        """

        # The fsl.utils.idle idle loop timeout
        # defaults to 200 milliseconds, which can
        # cause delays in frame updates. So when
        # movie mode is on, we bump up the rate.
        def startMovie():
            idle.setIdleTimeout(10)
            if not self.__movieLoop(startLoop=True):
                idle.setIdleTimeout(None)

        # The __movieModeChanged method is called
        # on the props event queue. Here we make
        # sure that __movieLoop() is called *off*
        # the props event queue, by calling it from
        # the idle loop.
        if self.movieMode: idle.idle(startMovie)
        else: idle.setIdleTimeout(None)

    def __movieLoop(self, startLoop=False):
        """Manages the triggering of the next movie frame. This method is
        called by :meth:`__movieModeChanged` when :attr:`movieMode` changes
        and when the selected overlay changes, and also by
        :meth:`__syncMovieRefresh` and :meth:`__unsyncMovieRefresh` while
        the movie loop is running, to trigger the next frame.

        :arg startLoop: This is set to ``True`` when called from
                        :meth:`__movieModeChanged`. If ``True``, and the movie
                        loop is already running, this method does nothing.

        """

        # Movie loop is already running, nothing to do.
        if startLoop and self.__movieRunning:
            return True

        # Attempt to show the next frame -
        # __movieFrame returns True if the
        # movie is continuing, False if it
        # has ended.
        self.__movieRunning = self.__movieFrame()

        return self.__movieRunning

    def canRunMovie(self, overlay, opts):
        """Returns ``True`` or ``False``, depending on whether movie mode
        is possible with the given z`overlay`` and ``opts``.
        """

        import fsl.data.image as fslimage
        import fsl.data.mesh as fslmesh

        axis = self.movieAxis

        # 3D movies are good for all overlays
        if axis < 3:
            return True

        # 4D Nifti images are all good
        if isinstance(overlay, fslimage.Nifti) and \
           len(overlay.shape) > 3              and \
           overlay.shape[3] > 1                and \
           isinstance(opts, displayctx.VolumeOpts):
            return True

        # Mesh surfaces with N-D
        # vertex data are all good
        if isinstance(overlay, fslmesh.TriangleMesh) and \
           opts.vertexDataLen() > 1:
            return True

        return False

    def getMovieFrame(self, overlay, opts):
        """Returns the current movie frame for the given overlay.

        A movie frame is typically a sequentially increasing number in
        some minimum/maximum range, e.g. a voxel or volume index.

        This method may be overridden by sub-classes for custom behaviour
        (e.g. the :class:`.Scene3DPanel`).
        """

        axis = self.movieAxis

        def nifti():
            if axis < 3: return opts.getVoxel(vround=False)[axis]
            else: return opts.volume

        def mesh():
            if axis < 3: return other()
            else: return opts.vertexDataIndex

        def other():
            return self.displayCtx.location.getPos(axis)

        import fsl.data.image as fslimage
        import fsl.data.mesh as fslmesh

        if isinstance(overlay, fslimage.Nifti): return nifti()
        elif isinstance(overlay, fslmesh.TriangleMesh): return mesh()
        else: return other()

    def doMovieUpdate(self, overlay, opts):
        """Called by :meth:`__movieFrame`. Updates the properties on the
        given ``opts`` instance to move forward one frame in the movie.

        This method may be overridden by sub-classes for custom behaviour
        (e.g. the :class:`.Scene3DPanel`).

        :returns:   A value which identifies the current movie frame. This may
                    be a volume or voxel index, or a world coordinate location
                    on one axis.
        """

        axis = self.movieAxis

        def nifti():

            limit = overlay.shape[axis]

            # This method has been called off the props
            # event queue (see __movieModeChanged).
            # Therefore, all listeners on the opts.volume
            # or DisplayContext.location  properties
            # should be called immediately, in these
            # assignments.
            #
            # When the movie axis == 3 (time), this means
            # that image texture refreshes should be
            # triggered and, after the opts.volume
            # assignment, all affected GLObjects should
            # return ready() == False.
            if axis == 3:
                if opts.volume >= limit - 1: opts.volume = 0
                else: opts.volume += 1

                frame = opts.volume

            else:
                voxel = opts.getVoxel()
                if voxel[axis] >= limit - 1: voxel[axis] = 0
                else: voxel[axis] += 1

                self.displayCtx.location = opts.transformCoords(
                    voxel, 'voxel', 'display')

                frame = voxel[axis]
            return frame

        def mesh():

            if axis == 3:
                limit = opts.vertexDataLen()
                val = opts.vertexDataIndex

                if val >= limit - 1: val = 0
                else: val += 1

                opts.vertexDataIndex = val

                return val

            else:
                return other()

        def other():

            bmin, bmax = opts.bounds.getRange(axis)
            delta = (bmax - bmin) / 75.0

            pos = self.displayCtx.location.getPos(axis)

            if pos >= bmax: pos = bmin
            else: pos = pos + delta

            self.displayCtx.location.setPos(axis, pos)
            return pos

        import fsl.data.image as fslimage
        import fsl.data.mesh as fslmesh

        if isinstance(overlay, fslimage.Nifti): frame = nifti()
        elif isinstance(overlay, fslmesh.TriangleMesh): frame = mesh()
        else: frame = other()

        return frame

    def __movieFrame(self):
        """Called by :meth:`__movieLoop`.

        If the currently selected overlay (see
        :attr:`.DisplayContext.selectedOverlay`) is a 4D :class:`.Image` being
        displayed as a ``volume`` (see the :class:`.VolumeOpts` class), the
        :attr:`.NiftiOpts.volume` property is incremented and all
        GL canvases in this ``CanvasPanel`` are refreshed.

        :returns: ``True`` if the movie loop was started, ``False`` otherwise.
        """

        from . import scene3dpanel

        if self.destroyed(): return False
        if not self.movieMode: return False

        overlay = self.displayCtx.getSelectedOverlay()
        canvases = self.getGLCanvases()

        if overlay is None:
            return False

        opts = self.displayCtx.getOpts(overlay)

        if not self.canRunMovie(overlay, opts):
            return False

        # We want the canvas refreshes to be
        # synchronised. So we 'freeze' them
        # while changing the image volume, and
        # then refresh them all afterwards.
        for c in canvases:
            c.FreezeDraw()
            c.FreezeSwapBuffers()

        self.doMovieUpdate(overlay, opts)

        # Now we get refs to *all* GLObjects managed
        # by every canvas - we have to wait until
        # they are all ready to be drawn before we
        # can refresh the canvases.  Note that this
        # is only necessary when the movie axis == 3
        globjs = [c.getGLObject(o) for c in canvases for o in self.overlayList]
        globjs = [g for g in globjs if g is not None]

        def allReady():
            return all([g.ready() for g in globjs])

        # Figure out the movie rate - the
        # number of seconds to wait until
        # triggering the next frame.
        rate = self.movieRate
        rateMin = self.getAttribute('movieRate', 'minval')
        rateMax = self.getAttribute('movieRate', 'maxval')

        # Special case/hack - if this is a Scene3DPanel,
        # and the movie axis is X/Y/Z, we always
        # use a fast rate. Instead, the Scene3dPanel
        # will increase/decrease the rotation angle
        # to speed up/slow down the movie instead.
        if isinstance(self, scene3dpanel.Scene3DPanel) and self.movieAxis < 3:
            rate = rateMax

        rate = (rateMin + (rateMax - rate)) / 1000.0

        # The canvas refreshes are performed by the
        # __syncMovieRefresh or __unsyncMovieRefresh
        # methods. Gallium seems to have a problem
        # with separate renders/buffer swaps, so we
        # have to use a shitty unsynchronised update
        # routine.
        #
        # TODO Ideally, figure out a refresh
        #      regime that works across all
        #      drivers. Failing this, make
        #      this switch user controllable.
        renderer = fslplatform.glRenderer.lower()
        unsyncRenderers = ['gallium', 'mesa dri intel(r)']
        useSync = not any([r in renderer for r in unsyncRenderers])

        if useSync: update = self.__syncMovieRefresh
        else: update = self.__unsyncMovieRefresh

        # Refresh the canvases when all
        # GLObjects are ready to be drawn.
        idle.idleWhen(update, allReady, canvases, rate, pollTime=rate / 10)

        return True

    def __unsyncMovieRefresh(self, canvases, rate):
        """Called by :meth:`__movieUpdate`. Updates all canvases in an
        unsynchronised manner.

        Ideally all canvases should be drawn off-screen (i.e. rendered to the
        back buffer), and then all refreshed together (back and front buffers
        swapped). Unfortunately some OpenGL drivers seem to have trouble with
        this approach, and require drawing and front/back buffer swaps to be
        done at the same time. This method is used for those drivers.

        :arg canvases: List of canvases to update. It is assumed that
                       ``FreezeDraw`` and ``FreezeSwapBuffers`` has been
                       called on every canvas.
        :arg rate:     Delay to trigger the next movie update.
        """

        for c in canvases:
            c.ThawDraw()
            c.ThawSwapBuffers()
            c.Refresh()

        idle.idle(self.__movieLoop, after=rate)

    def __syncMovieRefresh(self, canvases, rate):
        """Updates all canvases in a synchronised manner. All canvases are
        refreshed, and then the front/back buffers are swapped on each of
        them.

        :arg canvases: List of canvases to update. It is assumed that
                       ``FreezeDraw`` and ``FreezeSwapBuffers`` has been
                       called on every canvas.
        :arg rate:     Delay to trigger the next movie update.
        """

        for c in canvases:
            c.ThawDraw()
            c.Refresh()

        for c in canvases:
            c.ThawSwapBuffers()
            c.SwapBuffers()

        idle.idle(self.__movieLoop, after=rate)
コード例 #9
0
ファイル: volumeopts.py プロジェクト: marcobarilari/fsleyes
class VolumeOpts(cmapopts.ColourMapOpts, vol3dopts.Volume3DOpts,
                 niftiopts.NiftiOpts):
    """The ``VolumeOpts`` class defines options for displaying :class:`.Image`
    instances as regular 3D volumes.
    """

    channel = props.Choice(('R', 'G', 'B', 'A'))
    """For images with the NIfTI ``RGB24`` or ``RGBA32`` data type,
    this property controls the channel that gets displayed.
    """

    clipImage = props.Choice()
    """Clip voxels according to the values in another image. By default, voxels
    are clipped by the values in the image itself - this property allows the
    user to choose another image by which voxels are to be clipped. Any image
    which is in the :class:`.OverlayList` can be selected for clipping. The
    :attr:`.ColourMapOpts.clippingRange` property dictates the values outside
    of which voxels are clipped.
    """

    modulateImage = props.Choice()
    """Modulate alapha (opacity) by the intensity of values in the selected
    image, instead of in this image. Only relevant when
    :attr:`.ColourMapOpts.modulateAlpha` is active.
    """

    interpolation = props.Choice(('none', 'linear', 'spline'))
    """How the value shown at a real world location is derived from the
    corresponding data value(s). ``none`` is equivalent to nearest neighbour
    interpolation.
    """
    @classmethod
    def getInitialDisplayRange(cls):
        """This class method returns a tuple containing ``(low, high)``
        percentile values which are used to set the initial values for the
        :attr:`.ColourMapOpts.displayRange` and
        :attr:`.ColourMapOpts.clippingRange` properties. If the initial
        display range has not yet been set (via the
        :meth:`setInitialDisplayRange` method), ``None`` is returned.
        """
        try:
            return cls.__initialDisplayRange
        except AttributeError:
            return None

    @classmethod
    def setInitialDisplayRange(cls, drange):
        """Sets the initial values for the :attr:`.ColourMapOpts.displayRange`
        and :attr:`.ColourMapOpts.clippingRange` to be used for new
        :class:`VolumeOpts` instances.

        :arg drange: A tuple containing ``(low, high)`` display range values
                     as percentiles of the image data range. May be ``None``,
                     in which case the initial display range will be set to the
                     image data range.
        """

        if drange is not None:
            low, high = drange
            if not all(
                (low < high, low >= 0, low <= 100, high >= 0, high <= 100)):
                raise ValueError('Invalid initial display '
                                 'range: {}'.format(drange))

        cls.__initialDisplayRange = drange

    def __init__(self, overlay, display, overlayList, displayCtx, **kwargs):
        """Create a :class:`VolumeOpts` instance for the specified ``overlay``,
        assumed to be an :class:`.Image` instance.

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

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

        # Interpolation cannot be unbound
        # between VolumeOpts instances. This is
        # primarily to reduce memory requirement
        # - if interpolation were different
        # across different views, we would have
        # to create multiple 3D image textures
        # for the same image. Same goes for
        # clip/mod images
        nounbind = kwargs.get('nounbind', [])
        nounbind.append('interpolation')
        nounbind.append('clipImage')
        nounbind.append('modulateImage')
        kwargs['nounbind'] = nounbind

        # 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 (do this before
        # calling __init__, so we don't clobber any
        # existing values).
        cmap = str(overlay.header.get('aux_file', 'none')).lower()

        if cmap == 'mgh-subcortical': cmap = 'subcortical'
        if cmap == 'mgh-cortical': cmap = 'cortical'

        if cmap in fslcm.getColourMaps():
            self.cmap = cmap

        niftiopts.NiftiOpts.__init__(self, overlay, display, overlayList,
                                     displayCtx, **kwargs)

        # Some things only happen
        # on the parent instance
        self.__registered = self.getParent() is None

        # Check whether the data range for this
        # image is silly. If it is, and we are
        # on a platform that cannot use floating
        # point textures, we turn on the override
        # data range option.
        if self.__registered and np.issubdtype(overlay.dtype, np.floating):
            import fsleyes.gl.textures.data as texdata
            if not texdata.canUseFloatTextures()[0]:

                dmin, dmax = overlay.dataRange

                # a range of greater than 10e7
                # is defined as being "silly"
                if abs(dmax - dmin) > 10e7:

                    if overlay.ndim == 3: sample = overlay[:]
                    elif overlay.ndim == 4: sample = overlay[..., 0]

                    drange = np.percentile(sample[sample != 0], [1, 99])

                    self.overrideDataRange = drange
                    self.enableOverrideDataRange = True

        # Configure the initial display
        # range for new images, from the
        # initialDisplayRange percentiles.
        # We do this before ColourMapOpts.init
        drange = VolumeOpts.getInitialDisplayRange()

        if self.__registered and drange is not None:

            if overlay.ndim == 3: sample = overlay[:]
            elif overlay.ndim == 4: sample = overlay[..., 0]

            drange = np.percentile(sample[sample != 0], drange)
            crange = [drange[0], overlay.dataRange[1]]

            self.displayRange = drange
            self.modulateRange = drange
            self.clippingRange = crange

        # If this is not a RGB(A) image, disable
        # the channel property. If it's a RGB
        # image,  remove the "A" option from
        # the channel property.
        if self.__registered:

            nchannels = self.overlay.nvals
            if nchannels == 1:
                self.disableProperty('channel')
            elif nchannels == 3:
                prop = self.getProp('channel')
                prop.removeChoice('A', self)

        cmapopts.ColourMapOpts.__init__(self)
        vol3dopts.Volume3DOpts.__init__(self)

        # Both parent and child VolumeOpts instances
        # listen for Image dataRange changes. The data
        # range for large images may be calculated
        # asynchronously on a separate thread, meaning
        # that data range updates may occur at random
        # times.
        #
        # If parent instances did not listen for data
        # range updates and, at startup, the following
        # sequence of events occurs:
        #
        #   1. Parent VolumeOpts instance created
        #
        #   2. Image.dataRange updated
        #
        #   3. Child VolumeOpts instance created
        #
        # The known parent data range will be 0-0,
        # the child will not receive any notification
        # about the data range change, and the child's
        # data range will be clobbered by the parent's.
        # This ugly situation is avoided simply by
        # having the parent track changes to the data
        # range in addition to all children.
        overlay.register(self.name,
                         self.__dataRangeChanged,
                         'dataRange',
                         runOnIdle=True)

        # We need to listen for changes to clipImage
        # and to [enable]overrideDataRange, as they
        # will change the display data range. These
        # cannot be unbound between parent/children,
        # so only the parent needs to listen.
        if self.__registered:
            overlayList.addListener('overlays', self.name,
                                    self.__overlayListChanged)
            self.addListener('clipImage', self.name, self.__clipImageChanged)
            self.addListener('modulateImage', self.name,
                             self.__modulateImageChanged)
            self.addListener('enableOverrideDataRange', self.name,
                             self.__enableOverrideDataRangeChanged)
            self.addListener('overrideDataRange', self.name,
                             self.__overrideDataRangeChanged)

            self.__overlayListChanged()
            self.__clipImageChanged(updateDataRange=False)
            self.__modulateImageChanged(updateDataRange=False)

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

        overlay = self.overlay
        overlayList = self.overlayList

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

        if self.__registered:

            overlayList.removeListener('overlays', self.name)
            self.removeListener('clipImage', self.name)
            self.removeListener('modulateImage', self.name)
            self.removeListener('enableOverrideDataRange', self.name)
            self.removeListener('overrideDataRange', self.name)

        cmapopts.ColourMapOpts.destroy(self)
        vol3dopts.Volume3DOpts.destroy(self)
        niftiopts.NiftiOpts.destroy(self)

    def getDataRange(self):
        """Overrides :meth:`.ColourMapOpts.getDataRange`. Returns the
        :attr:`.Image.dataRange` of the image, or the
        :attr:`overrideDataRange` if it is active.
        """
        if self.enableOverrideDataRange: return self.overrideDataRange
        else: return self.overlay.dataRange

    def getClippingRange(self):
        """Overrides :meth:`.ColourMapOpts.getClippingRange`.
        If a :attr:`.clipImage` is set, returns its data range. Otherwise
        returns ``None``.
        """

        if self.clipImage is None:
            return cmapopts.ColourMapOpts.getClippingRange(self)
        else:
            return self.clipImage.dataRange

    def getModulateRange(self):
        """Overrides :meth:`.ColourMapOpts.getModulateRange`.
        If a :attr:`.modulateImage` is set, returns its data range. Otherwise
        returns ``None``.
        """

        if self.modulateImage is None:
            return cmapopts.ColourMapOpts.getModulateRange(self)
        else:
            return self.modulateImage.dataRange

    def __dataRangeChanged(self, *a):
        """Called when the :attr:`.Image.dataRange` property changes.
        Calls :meth:`.ColourMapOpts.updateDataRange`.
        """
        self.updateDataRange(False, False, False)

    def __enableOverrideDataRangeChanged(self, *a):
        """Called when the :attr:`enableOverrideDataRange` property changes.
        Calls :meth:`.ColourMapOpts.updateDataRange`.
        """
        self.updateDataRange()

    def __overrideDataRangeChanged(self, *a):
        """Called when the :attr:`overrideDataRange` property changes.
        Calls :meth:`.ColourMapOpts.updateDataRange`.
        """
        self.updateDataRange()

    def __overlayListChanged(self, *a):
        """Called when the :`class:`.OverlayList` changes. Updates the
        options of the :attr:`clipImage` property.
        """

        clipProp = self.getProp('clipImage')
        clipVal = self.clipImage
        modProp = self.getProp('modulateImage')
        modVal = self.modulateImage
        overlays = self.displayCtx.getOrderedOverlays()

        options = [None]

        for overlay in overlays:

            if overlay is self.overlay: continue
            if not isinstance(overlay, fslimage.Image): continue

            options.append(overlay)

        clipProp.setChoices(options, instance=self)
        modProp.setChoices(options, instance=self)

        if clipVal in options: self.clipImage = clipVal
        else: self.clipImage = None
        if modVal in options: self.modulateImage = modVal
        else: self.modulateImage = None

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

        :arg updateDataRange: Defaults to ``True``. If ``False``, the
                              :meth:`.ColourMapOpts.updateDataRange` method
                              is not called.
        """

        updateDR = kwa.get('updateDataRange', True)

        haveClipImage = self.clipImage is not None

        if not haveClipImage:
            self.enableProperty('linkLowRanges')
            self.enableProperty('linkHighRanges')

        # If the clipping range is based on another
        # image, it makes no sense to link the low/
        # high display/clipping ranges, as they are
        # probably different. So if a clip image is
        # selected, we disable the link range
        # properties.
        elif self.propertyIsEnabled('linkLowRanges'):

            self.linkLowRanges = False
            self.linkHighRanges = False

            self.disableProperty('linkLowRanges')
            self.disableProperty('linkHighRanges')

        log.debug('Clip image changed for %s: %s', self.overlay,
                  self.clipImage)

        if updateDR:
            self.updateDataRange(resetDR=False, resetMR=False)

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

        :arg updateDataRange: Defaults to ``True``. If ``False``, the
                              :meth:`.ColourMapOpts.updateDataRange` method
                              is not called.
        """

        updateDR = kwa.get('updateDataRange', True)

        log.debug('Modulate image changed for %s: %s', self.overlay,
                  self.modulateImage)

        if updateDR:
            self.updateDataRange(resetDR=False, resetCR=False)
コード例 #10
0
ファイル: vectoropts.py プロジェクト: marcobarilari/fsleyes
class VectorOpts(niftiopts.NiftiOpts):
    """The ``VectorOpts`` class is the base class for :class:`LineVectorOpts`,
    :class:`RGBVectorOpts`, :class:`.TensorOpts`, and :class:`.SHOpts`. It
    contains display settings which are common to each of them.


    *A note on orientation*


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


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


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


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


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


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


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


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


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


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


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

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


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


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


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


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


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


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


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

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

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

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

        if self.__registered:

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

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

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


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

        niftiopts.NiftiOpts.destroy(self)


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

        image = self.clipImage

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

        minval, maxval = image.dataRange

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

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


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

        image = self.modulateImage

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

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


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

        overlays = self.displayCtx.getOrderedOverlays()

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

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


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

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

        options = [None]

        for overlay in overlays:

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

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

            options.append(overlay)

        prop.setChoices(options, instance=self)

        if val in options: setattr(self, imageName, val)
        else:              setattr(self, imageName, None)
コード例 #11
0
class DisplayContext(props.SyncableHasProperties):
    """A ``DisplayContext`` instance contains a number of properties defining
    how the overlays in an :class:`.OverlayList` are to be displayed, and
    related contextual information.


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


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

    .. autosummary::
       :nosignatures:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    2. **Reference image** space

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

        kwargs = dict(kwargs)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.detachAllFromParent()

        overlayList = self.__overlayList
        displays = self.__displays

        self.__overlayList = None
        self.__displays = None

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

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

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

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

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

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

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

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

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

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

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

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

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

        try:
            display = self.__displays[overlay]

        except KeyError:

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

            from .display import Display

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

        return display

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

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

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

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

        return self.getDisplay(overlay, overlayType).opts

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

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

        return self.getOpts(overlay).referenceImage

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

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

        displaySpace = self.displaySpace

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

        opts = self.getOpts(displaySpace)

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

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

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

        displaySpace = self.displaySpace

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

        opts = self.getOpts(displaySpace)

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

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

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

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

        space = self.displaySpace

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

            return npla.det(xform) > 0

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

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

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

        return self.__overlayList[self.selectedOverlay]

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

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

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

        return self.overlayOrder.index(overlay)

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

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

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

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

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

        try:
            yield

        finally:
            self.thawOverlay(overlay)

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

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

        dctxs = [self] + self.getChildren()

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

            display.disableAllNotification()
            opts.disableAllNotification()

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

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

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

            display.enableAllNotification()
            opts.enableAllNotification()

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

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

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

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

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

        This is an irreversible operation.
        """

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

        for ovl in self.__overlayList:

            opts = self.getOpts(ovl)

            opts.detachFromParent('bounds')

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

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

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

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

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

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

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

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

            ovlType = self.__overlayList.initOverlayType(overlay)

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

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

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

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

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

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

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

            return

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

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

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

            displaySpace = 'world'

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

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

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

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

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

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

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

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

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

        choices.append('world')

        choiceProp.setChoices(choices, instance=self)

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

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

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

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

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

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

        selectedOverlay = self.getSelectedOverlay()

        if selectedOverlay is None:
            return

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

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

            self.__setTransform(overlay)

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

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

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

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

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

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

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

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

            newOrder = []
            newOverlayIdx = len(oldList)

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

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

            self.overlayOrder[:] = newOrder

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

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

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

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

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

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

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

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

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

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

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

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

        for display in self.__displays.values():

            opts = display.opts

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

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

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

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

        for ovl in self.__overlayList:

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

            for ax in range(3):

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

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

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

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

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

        self.__propagateLocation('display')

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

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

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

        if dest == 'world':
            with props.skip(self, 'location', self.__name):
                self.worldLocation = opts.transformCoords(
                    self.location, 'display', 'world')
        else:
            with props.skip(self, 'worldLocation', self.__name):
                self.location = opts.transformCoords(self.worldLocation,
                                                     'world', 'display')
コード例 #12
0
ファイル: shopts.py プロジェクト: neurodebian/fsleyes
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()
コード例 #13
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
コード例 #14
0
class SceneOpts(props.HasProperties):
    """The ``SceneOpts`` class defines settings which are used by
    :class:`.CanvasPanel` instances.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        self.movieSyncRefresh = self.defaultMovieSyncRefresh

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

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

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

        return not unsync

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

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

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

    def __onBgColourChange(self, *a):
        """Called when the background colour changes. Updates the
        :attr:`fgColour` to a complementary colour.
        """
        self.fgColour = fslcm.complementaryColour(self.bgColour)
コード例 #15
0
ファイル: niftiopts.py プロジェクト: marcobarilari/fsleyes
class NiftiOpts(fsldisplay.DisplayOpts):
    """The ``NiftiOpts`` class describes how a :class:`.Nifti` overlay
    should be displayed.


    ``NiftiOpts`` is the base class for a number of :class:`.DisplayOpts`
    sub-classes - it contains display options which are common to all overlay
    types that represent a NIFTI image.
    """

    volume = props.Int(minval=0, maxval=0, default=0, clamped=True)
    """If the ``Image`` has more than 3 dimensions, the current volume to
    display. The volume dimension is controlled by the :attr:`volumeDim`
    property.
    """

    volumeDim = props.Int(minval=0, maxval=5, default=0, clamped=True)
    """For images with more than three dimensions, this property controls
    the dimension that the :attr:`volume` property indexes into. When the
    ``volumeDim`` changes, the ``volume`` for the previous ``volumeDim``
    is fixed at its last value, and used for subsequent lookups.
    """

    transform = props.Choice(
        ('affine', 'pixdim', 'pixdim-flip', 'id', 'reference'),
        default='pixdim-flip')
    """This property defines how the overlay should be transformd into
    the display coordinate system. See the
    :ref:`note on coordinate systems <volumeopts-coordinate-systems>`
    for important information regarding this property.
    """

    displayXform = props.Array(dtype=np.float64,
                               shape=(4, 4),
                               resizable=False,
                               default=[[1, 0, 0, 0], [0, 1, 0, 0],
                                        [0, 0, 1, 0], [0, 0, 0, 1]])
    """A custom transformation matrix which is concatenated on to the voxel ->
    world transformation of the :class:`.Nifti` overlay.

    This transform is intended for temporary changes to the overlay display
    (when :attr:`.DisplayContext.displaySpace` ``== 'world'``) - changes to it
    will *not* result in the ::attr:`.DisplayContext.bounds` being updated.

    If you change the ``displayXform``, make sure to change it back to an
    identity matrix when you are done.
    """

    enableOverrideDataRange = props.Boolean(default=False)
    """By default, the :attr:`.Image.dataRange` property is used to set
    display and clipping ranges. However, if this property is ``True``,
    the :attr:`overrideDataRange` is used instead.

    ..note:: The point of this property is to make it easier to display images
             with a very large data range driven by outliers. On platforms
             which do not support floating point textures, these images are
             impossible to display unless they are normalised according to
             a smaller data range. See the
             :meth:`.Texture3D.__determineTextureType` method for some more
             details.
    """

    overrideDataRange = props.Bounds(ndims=1, clamped=False)
    """Data range used in place of the :attr:`.Image.dataRange` if the
    :attr:`enableOverrideDataRange` property is ``True``.
    """
    def __init__(self, *args, **kwargs):
        """Create a ``NiftiOpts`` instance.

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

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

        nounbind.append('overrideDataRange')
        nounbind.append('enableOverrideDataRange')
        nobind.append('displayXform')

        kwargs['nounbind'] = nounbind
        kwargs['nobind'] = nobind

        fsldisplay.DisplayOpts.__init__(self, *args, **kwargs)

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

        if self.__child:

            # is this a >3D volume?
            ndims = self.overlay.ndim

            # We store indices for every dimension
            # past the XYZ dims. Whenever the volumeDim
            # changes, we cache the index for the old
            # dimensions, and restore the index for the
            # new dimension.
            self.setAttribute('volumeDim', 'maxval', max(0, ndims - 4))
            self.setAttribute('volume', 'cache', [0] * (ndims - 3))

            if ndims <= 3:
                self.setAttribute('volume', 'maxval', 0)

            self.overlay.register(self.name,
                                  self.__overlayTransformChanged,
                                  topic='transform')
            self.addListener('volumeDim',
                             self.name,
                             self.__volumeDimChanged,
                             immediate=True)
            self.addListener('transform',
                             self.name,
                             self.__transformChanged,
                             immediate=True)
            self.addListener('displayXform',
                             self.name,
                             self.__displayXformChanged,
                             immediate=True)
            self.displayCtx.addListener('displaySpace',
                                        self.name,
                                        self.__displaySpaceChanged,
                                        immediate=True)

            # The display<->* transformation matrices
            # are created in the _setupTransforms method.
            # The __displaySpaceChanged method registers
            # a listener with the current display space
            # (if it is an overlay)
            self.__xforms = {}
            self.__dsOverlay = None
            self.__setupTransforms()
            self.__transformChanged()
            self.__volumeDimChanged()

    def destroy(self):
        """Calls the :meth:`.DisplayOpts.destroy` method. """

        if self.__child:
            self.overlay.deregister(self.name, topic='transform')
            self.displayCtx.removeListener('displaySpace', self.name)
            self.removeListener('volumeDim', self.name)
            self.removeListener('transform', self.name)
            self.removeListener('displayXform', self.name)

            if self.__dsOverlay is not None:
                self.__dsOverlay.deregister(self.name, topic='transform')
                self.__dsOverlay = None

        fsldisplay.DisplayOpts.destroy(self)

    def __toggleSiblingListeners(self, enable=True):
        """Enables/disables the ``volumeDim`` listeners of sibling
        ``NiftiOpts`` instances. This is used by the :meth:`__volumeDimChanged`
        method to avoid nastiness.
        """
        for s in self.getParent().getChildren():
            if s is not self:
                if enable: s.enableListener('volumeDim', s.name)
                else: s.disableListener('volumeDim', s.name)

    def __volumeDimChanged(self, *a):
        """Called when the :attr:`volumeDim` changes. Saves the value of
        ``volume`` for the last ``volumeDim``, and restores the previous
        value of ``volume`` for the new ``volumeDim``.
        """

        if self.overlay.ndim <= 3:
            return

        # Here we disable volumeDim listeners on all
        # sibling instances, then save/restore the
        # volume value and properties asynchronously,
        # then re-enable the slblings.  This is a
        # horrible means of ensuring that only the
        # first VolumeOpts instance (out of a set of
        # synchronised instances) updates the volume
        # value and properties. The other instances
        # will be updated through synchronisation.
        # This is necessary because subsequent
        # instances would corrupt the update made by
        # the first instance.
        #
        # A nicer way to do things like this would be
        # nice.
        def update():

            oldVolume = self.volume
            oldVolumeDim = self.getLastValue('volumeDim')

            if oldVolumeDim is None:
                oldVolumeDim = 0

            cache = list(self.getAttribute('volume', 'cache'))
            cache[oldVolumeDim] = oldVolume
            newVolume = cache[self.volumeDim]
            newVolumeLim = self.overlay.shape[self.volumeDim + 3] - 1

            self.setAttribute('volume', 'maxval', newVolumeLim)
            self.setAttribute('volume', 'cache', cache)
            self.volume = newVolume

        self.__toggleSiblingListeners(False)
        props.safeCall(update)
        props.safeCall(self.__toggleSiblingListeners, True)

    def __overlayTransformChanged(self, *a):
        """Called when the :class:`.Nifti` overlay sends a notification
        on the ``'transform'`` topic, indicating that its voxel->world
        transformation matrix has been updated.
        """
        self.__setupTransforms()
        self.__transformChanged()

    def __displaySpaceTransformChanged(self, *a):
        """Called when the :attr:`.DisplayContext.displaySpace` is a
        :class:`.Nifti`  overlay, and its :attr:`.Nifti.voxToWorldMat`
        changes. Updates the transformation matrices for this image.
        """
        self.__setupTransforms()
        self.__transformChanged()

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

        Calculates the min/max values of a 3D bounding box, in the display
        coordinate system, which is big enough to contain the image. Sets the
        :attr:`.DisplayOpts.bounds` property accordingly.
        """

        lo, hi = affine.axisBounds(self.overlay.shape[:3],
                                   self.getTransform('voxel', 'display'))

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

    def __displaySpaceChanged(self, *a):
        """Called when the :attr:`.DisplayContext.displaySpace` property
        changes.  Re-generates transformation matrices, and re-calculates
        the display :attr:`bounds` (via calls to :meth:`__setupTransforms` and
        :meth:`__transformChanged`).
        """

        displaySpace = self.displayCtx.displaySpace

        if self.__dsOverlay is not None:
            self.__dsOverlay.deregister(self.name, topic='transform')
            self.__dsOverlay = None

        # Register a listener on the display space reference
        # image, because when its voxToWorldMat changes, we
        # need to update our *toref and refto* transforms.
        if isinstance(displaySpace, fslimage.Nifti) and \
           displaySpace is not self.overlay:
            self.__dsOverlay = displaySpace
            self.__dsOverlay.register(self.name,
                                      self.__displaySpaceTransformChanged,
                                      topic='transform')

        self.__setupTransforms()
        if self.transform == 'reference':
            self.__transformChanged()

    def __displayXformChanged(self, *a):
        """Called when the :attr:`displayXform` property changes. Updates
        the transformation matrices and :attr:`bounds` accordingly.

        Critically, when the :attr:`displayXform` property changes, the
        :class:`.DisplayContext` is *not* notified. This is because
        the ``displayXform`` is intended for temporary changes.
        """

        # The displayXform is intended as a temporary
        # transformation for display purposes - the
        # DisplayOpts.bounds property gets updated when
        # it changes, but we don't want the
        # DisplayContext.bounds property to be updated.
        # So we suppress all notification while
        # updating the transformation matrices.
        with self.displayCtx.freeze(self.overlay):
            self.__setupTransforms()
            self.__transformChanged()

    def __setupTransforms(self):
        """Calculates transformation matrices between all of the possible
        spaces in which the overlay may be displayed.

        These matrices are accessible via the :meth:`getTransform` method.
        """

        image = self.overlay
        shape = np.array(image.shape[:3])

        voxToIdMat = np.eye(4)
        voxToPixdimMat = np.diag(list(image.pixdim[:3]) + [1.0])
        voxToPixFlipMat = image.voxToScaledVoxMat
        voxToWorldMat = image.voxToWorldMat
        voxToWorldMat = affine.concat(self.displayXform, voxToWorldMat)
        ds = self.displayCtx.displaySpace

        # The reference transforms depend
        # on the value of displaySpace
        if ds == 'world':
            voxToRefMat = voxToWorldMat
        elif ds is self.overlay:
            voxToRefMat = voxToPixFlipMat
        else:
            voxToRefMat = affine.concat(ds.voxToScaledVoxMat, ds.worldToVoxMat,
                                        voxToWorldMat)

        # When going from voxels to textures,
        # we add 0.5 to centre the voxel (see
        # the note on coordinate systems at
        # the top of this file).
        voxToTexMat = affine.scaleOffsetXform(tuple(1.0 / shape),
                                              tuple(0.5 / shape))

        idToVoxMat = affine.invert(voxToIdMat)
        idToPixdimMat = affine.concat(voxToPixdimMat, idToVoxMat)
        idToPixFlipMat = affine.concat(voxToPixFlipMat, idToVoxMat)
        idToWorldMat = affine.concat(voxToWorldMat, idToVoxMat)
        idToRefMat = affine.concat(voxToRefMat, idToVoxMat)
        idToTexMat = affine.concat(voxToTexMat, idToVoxMat)

        pixdimToVoxMat = affine.invert(voxToPixdimMat)
        pixdimToIdMat = affine.concat(voxToIdMat, pixdimToVoxMat)
        pixdimToPixFlipMat = affine.concat(voxToPixFlipMat, pixdimToVoxMat)
        pixdimToWorldMat = affine.concat(voxToWorldMat, pixdimToVoxMat)
        pixdimToRefMat = affine.concat(voxToRefMat, pixdimToVoxMat)
        pixdimToTexMat = affine.concat(voxToTexMat, pixdimToVoxMat)

        pixFlipToVoxMat = affine.invert(voxToPixFlipMat)
        pixFlipToIdMat = affine.concat(voxToIdMat, pixFlipToVoxMat)
        pixFlipToPixdimMat = affine.concat(voxToPixdimMat, pixFlipToVoxMat)
        pixFlipToWorldMat = affine.concat(voxToWorldMat, pixFlipToVoxMat)
        pixFlipToRefMat = affine.concat(voxToRefMat, pixFlipToVoxMat)
        pixFlipToTexMat = affine.concat(voxToTexMat, pixFlipToVoxMat)

        worldToVoxMat = affine.invert(voxToWorldMat)
        worldToIdMat = affine.concat(voxToIdMat, worldToVoxMat)
        worldToPixdimMat = affine.concat(voxToPixdimMat, worldToVoxMat)
        worldToPixFlipMat = affine.concat(voxToPixFlipMat, worldToVoxMat)
        worldToRefMat = affine.concat(voxToRefMat, worldToVoxMat)
        worldToTexMat = affine.concat(voxToTexMat, worldToVoxMat)

        refToVoxMat = affine.invert(voxToRefMat)
        refToIdMat = affine.concat(voxToIdMat, refToVoxMat)
        refToPixdimMat = affine.concat(voxToPixdimMat, refToVoxMat)
        refToPixFlipMat = affine.concat(voxToPixFlipMat, refToVoxMat)
        refToWorldMat = affine.concat(voxToWorldMat, refToVoxMat)
        refToTexMat = affine.concat(voxToTexMat, refToVoxMat)

        texToVoxMat = affine.invert(voxToTexMat)
        texToIdMat = affine.concat(voxToIdMat, texToVoxMat)
        texToPixdimMat = affine.concat(voxToPixdimMat, texToVoxMat)
        texToPixFlipMat = affine.concat(voxToPixFlipMat, texToVoxMat)
        texToWorldMat = affine.concat(voxToWorldMat, texToVoxMat)
        texToRefMat = affine.concat(voxToRefMat, texToVoxMat)

        self.__xforms['id', 'id'] = np.eye(4)
        self.__xforms['id', 'pixdim'] = idToPixdimMat
        self.__xforms['id', 'pixdim-flip'] = idToPixFlipMat
        self.__xforms['id', 'affine'] = idToWorldMat
        self.__xforms['id', 'reference'] = idToRefMat
        self.__xforms['id', 'texture'] = idToTexMat

        self.__xforms['pixdim', 'pixdim'] = np.eye(4)
        self.__xforms['pixdim', 'id'] = pixdimToIdMat
        self.__xforms['pixdim', 'pixdim-flip'] = pixdimToPixFlipMat
        self.__xforms['pixdim', 'affine'] = pixdimToWorldMat
        self.__xforms['pixdim', 'reference'] = pixdimToRefMat
        self.__xforms['pixdim', 'texture'] = pixdimToTexMat

        self.__xforms['pixdim-flip', 'pixdim-flip'] = np.eye(4)
        self.__xforms['pixdim-flip', 'id'] = pixFlipToIdMat
        self.__xforms['pixdim-flip', 'pixdim'] = pixFlipToPixdimMat
        self.__xforms['pixdim-flip', 'affine'] = pixFlipToWorldMat
        self.__xforms['pixdim-flip', 'reference'] = pixFlipToRefMat
        self.__xforms['pixdim-flip', 'texture'] = pixFlipToTexMat

        self.__xforms['affine', 'affine'] = np.eye(4)
        self.__xforms['affine', 'id'] = worldToIdMat
        self.__xforms['affine', 'pixdim'] = worldToPixdimMat
        self.__xforms['affine', 'pixdim-flip'] = worldToPixFlipMat
        self.__xforms['affine', 'reference'] = worldToRefMat
        self.__xforms['affine', 'texture'] = worldToTexMat

        self.__xforms['reference', 'reference'] = np.eye(4)
        self.__xforms['reference', 'id'] = refToIdMat
        self.__xforms['reference', 'pixdim'] = refToPixdimMat
        self.__xforms['reference', 'pixdim-flip'] = refToPixFlipMat
        self.__xforms['reference', 'affine'] = refToWorldMat
        self.__xforms['reference', 'texture'] = refToTexMat

        self.__xforms['texture', 'texture'] = np.eye(4)
        self.__xforms['texture', 'id'] = texToIdMat
        self.__xforms['texture', 'pixdim'] = texToPixdimMat
        self.__xforms['texture', 'pixdim-flip'] = texToPixFlipMat
        self.__xforms['texture', 'affine'] = texToWorldMat
        self.__xforms['texture', 'reference'] = texToRefMat

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

    def getTransform(self, from_, to, xform=None):
        """Return a matrix which may be used to transform coordinates
        from ``from_`` to ``to``. Valid values for ``from_`` and ``to``
        are:


        =============== ======================================================
        ``id``          Voxel coordinates

        ``voxel``       Equivalent to ``id``.

        ``pixdim``      Voxel coordinates, scaled by voxel dimensions

        ``pixdim-flip`` Voxel coordinates, scaled by voxel dimensions, and
                        with the X axis flipped if the affine matrix has
                        a positivie determinant. If the affine matrix does
                        not have a positive determinant, this is equivalent to
                        ``pixdim``.

        ``pixflip``     Equivalent to ``pixdim-flip``.

        ``affine``      World coordinates, as defined by the NIFTI
                        ``qform``/``sform``. See :attr:`.Image.voxToWorldMat`.

        ``world``       Equivalent to ``affine``.

        ``reference``   ``pixdim-flip`` coordinates of the reference image
                        specified by the :attr:`.DisplayContext.displaySpace`
                        attribute. If the ``displaySpace`` is set to
                        ``'world'``, this is equivalent to ``affine``.

        ``ref``         Equivalent to ``reference``.

        ``display``     Equivalent to the current value of :attr:`transform`.

        ``texture``     Voxel coordinates scaled to lie between 0.0 and 1.0,
                        suitable for looking up voxel values when stored as
                        an OpenGL texture.
        =============== ======================================================


        If the ``xform`` parameter is provided, and one of ``from_`` or ``to``
        is ``display``, the value of ``xform`` is used instead of the current
        value of :attr:`transform`.
        """

        if not self.__child:
            raise RuntimeError('getTransform cannot be called on '
                               'a parent NiftiOpts instance')

        if xform is None:
            xform = self.transform

        if from_ == 'display': from_ = xform
        elif from_ == 'world': from_ = 'affine'
        elif from_ == 'voxel': from_ = 'id'
        elif from_ == 'pixflip': from_ = 'pixdim-flip'
        elif from_ == 'ref': from_ = 'reference'

        if to == 'display': to = xform
        elif to == 'world': to = 'affine'
        elif to == 'voxel': to = 'id'
        elif to == 'pixflip': to = 'pixdim-flip'
        elif to == 'ref': to = 'reference'

        return self.__xforms[from_, to]

    def roundVoxels(self, voxels, daxes=None, roundOther=False):
        """Round the given voxel coordinates to integers. This is a
        surprisingly complicated operation.

        FSLeyes and the NIFTI standard map integer voxel coordinates to the
        voxel centre. For example, a voxel [3, 4, 5] fills the space::

            [2.5-3.5, 3.5-4.5, 4.5-5.5].


        So all we need to do is round to the nearest integer. But there are a
        few problems with breaking ties when rounding...


        The numpy.round function breaks ties (e.g. 7.5) by rounding to the
        nearest *even* integer, which can cause funky behaviour.  So instead
        of using numpy.round, we take floor(x+0.5), to force consistent
        behaviour (i.e. always rounding central values up).


        The next problem is that we have to round the voxel coordaintes
        carefully, depending on the orientation of the voxel axis w.r.t. the
        display axis. We want to round in the same direction in the display
        coordinate system, regardless of the voxel orientation. So we need to
        check the orientation of the voxel axis, and round down or up
        accordingly.


        This is to handle scenarios where we have two anatomically aligned
        images, but with opposing storage orders (e.g. one stored
        neurologically, and one stored radiologically). If we have such
        images, and the display location is on a voxel boundary, we want the
        voxel coordinates for one image to be rounded in the same anatomical
        direction (i.e. the same direction in the display coordinate
        system). Otherwise the same display location will map to mis-aligned
        voxels in the two images, because the voxel coordinate rounding will
        move in anatomically opposing directions.


        This method also prevents coordinates that are close to 0 from being
        set to -1, and coordinates that are close to the axis size from being
        set to (size + 1). In other words, voxel coordinates which are on the
        low or high boundaries will be rounded so as to be valid voxel
        coordinates.

        :arg voxels:     A ``(N, 3)`` ``numpy`` array containing the voxel
                         coordinates to be rounded.

        :arg daxes:      Display coordinate system axes along which to round
                         the coordinates (defaults to all axes).

        :arg roundOther: If ``True``, any voxel axes which are not in
                         ``daxes`` will still be rounded, but not with an
                         orientation-specific rounding convention.

        :returns:    The ``voxels``, rounded appropriately.
        """

        if not self.__child:
            raise RuntimeError('roundVoxels cannot be called on '
                               'a parent NiftiOpts instance')

        if daxes is None:
            daxes = list(range(3))

        shape = self.overlay.shape[:3]
        ornts = self.overlay.axisMapping(self.getTransform('display', 'voxel'))

        # We start by truncating the precision
        # of the coordinates, so that values
        # which are very close to voxel midpoints
        # (e.g. 0.49999), get rounded to 0.5.
        voxels = np.round(voxels, decimals=3)

        # Keep track of the voxel axes that
        # have had the rounding treatment
        roundedAxes = []

        for dax in daxes:

            ornt = ornts[dax]
            vax = abs(ornt) - 1
            vals = voxels[:, vax]

            roundedAxes.append(vax)

            # Identify values which are close
            # to the low or high bounds - we
            # will clamp them after rounding.
            #
            # This is a third rounding problem
            # which is not documented above -
            # we clamp low/high values to avoid
            # them under/overflowing in the
            # floor/ceil operations below
            closeLow = np.isclose(vals, -0.5)
            closeHigh = np.isclose(vals, shape[vax] - 0.5)

            # Round in a direction which is
            # dictated by the image orientation
            if ornt < 0: vals = np.floor(vals + 0.5)
            else: vals = np.ceil(vals - 0.5)

            # Clamp low/high voxel coordinates
            vals[closeLow] = 0
            vals[closeHigh] = shape[vax] - 1

            voxels[:, vax] = vals

        # If the roundOther flag is true,
        # we round all other voxel axes
        # in a more conventional manner
        # (but still using floor(v + 0.5)
        # rather than round to avoid
        # annoying numpy even/odd behaviour).
        if roundOther:
            for vax in range(3):
                if vax not in roundedAxes:
                    voxels[:, vax] = np.floor(voxels[:, vax] + 0.5)

        return voxels

    def transformCoords(self,
                        coords,
                        from_,
                        to_,
                        vround=False,
                        vector=False,
                        pre=None,
                        post=None):
        """Transforms the given coordinates from ``from_`` to ``to_``.

        The ``from_`` and ``to_`` parameters must be those accepted by the
        :meth:`getTransform` method.

        :arg coords: Coordinates to transform
        :arg from_:  Space to transform from
        :arg to_:    Space to transform to
        :arg vround: If ``True``, and ``to_ in ('voxel', 'id)``, the
                     transformed coordinates are rounded to the nearest
                     integer.
        :arg vector: Defaults to ``False``. If ``True``, the coordinates
                     are treated as vectors.
        :arg pre:    Transformation to apply before the ``from_``-to-``to``
                     transformation.
        :arg post:   Transformation to apply after the ``from_``-to-``to``
                     transformation.
        """

        if not self.__child:
            raise RuntimeError('transformCoords cannot be called on '
                               'a parent NiftiOpts instance')

        xform = self.getTransform(from_, to_)

        if pre is not None: xform = affine.concat(xform, pre)
        if post is not None: xform = affine.concat(post, xform)

        coords = np.array(coords)
        coords = affine.transform(coords, xform, vector=vector)

        # Round to integer voxel coordinates?
        if to_ in ('voxel', 'id') and vround:
            coords = self.roundVoxels(coords)

        return coords

    def getVoxel(self, xyz=None, clip=True, vround=True):
        """Calculates and returns the voxel coordinates corresponding to the
        given location (assumed to be in the display coordinate system) for
        the :class:`.Nifti` associated with this ``NiftiOpts`` instance..

        :arg xyz:    Display space location to convert to voxels. If not
                     provided, the current :attr:`.DisplayContext.location`
                     is used.

        :arg clip:   If ``False``, and the transformed coordinates are out of
                     the voxel coordinate bounds, the coordinates returned
                     anyway. Defaults to ``True``.


        :arg vround: If ``True``, the returned voxel coordinates are rounded
                     to the nearest integer. Otherwise they may be fractional.


        :returns:    ``None`` if the location is outside of the image bounds,
                     unless ``clip=False``.
        """

        if not self.__child:
            raise RuntimeError('getVoxel cannot be called on '
                               'a parent NiftiOpts instance')

        if xyz is not None: x, y, z = xyz
        else: x, y, z = self.displayCtx.location.xyz

        overlay = self.overlay
        vox = self.transformCoords([[x, y, z]],
                                   'display',
                                   'voxel',
                                   vround=vround)[0]

        if vround:
            vox = [int(v) for v in vox]

        if not clip:
            return vox

        for ax in (0, 1, 2):
            if vox[ax] < 0 or vox[ax] >= overlay.shape[ax]:
                return None

        return vox

    def index(self, slc=None, atVolume=True):
        """Given a slice object ``slc``, which indexes into the X, Y, and Z
        dimensions, fills it to slice every dimension of the image, using
        the current :attr:`volume` and :attr:`volumeDim`, and saved values
        for the other volume dimensions.

        :arg slc:      Something which can slice the first three dimensions
                       of the image. If ``None``, defaults to ``[:, :, :]``.

        :arg atVolume: If ``True``, the returned slice will index the current
                       :attr:`volume` of the current :attr:`volumeDim`.
                       Otherwise the returned slice will index across the whole
                       :attr:`volumeDim`.
        """

        if slc is None:
            slc = [slice(None), slice(None), slice(None)]

        if self.overlay.ndim <= 3:
            return tuple(slc)

        newSlc = [None] * self.overlay.ndim
        newSlc[:3] = slc
        newSlc[3:] = self.getAttribute('volume', 'cache')

        vdim = self.volumeDim + 3

        if atVolume: newSlc[vdim] = self.volume
        else: newSlc[vdim] = slice(None)

        return tuple(newSlc)
コード例 #16
0
ファイル: sceneopts.py プロジェクト: neurodebian/fsleyes
class SceneOpts(props.HasProperties):
    """The ``SceneOpts`` class defines settings which are used by
    :class:`.CanvasPanel` instances.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def __onBgColourChange(self, *a):
        """Called when the background colour changes. Updates the
        :attr:`fgColour` to a complementary colour.
        """
        self.fgColour = fslcm.complementaryColour(self.bgColour)
コード例 #17
0
ファイル: mipopts.py プロジェクト: laurenpan02/fsleyes
class MIPOpts(cmapopts.ColourMapOpts, niftiopts.NiftiOpts):
    """The ``MIPOpts`` class is used for rendering maximum intensity
    projections of .Image overlays.
    """

    window = props.Percentage(minval=1, clamped=True, default=50)
    """Window over which the MIP is calculated, as a proportion of the image
    length. The window is centered at the current display location.
    """

    minimum = props.Boolean(default=False)
    """Display a minimum intensity projection, rather than maximum. """

    absolute = props.Boolean(default=False)
    """Display an absolute maximum intensity projection. This setting
    overrides the :attr:`minimum` setting.
    """

    interpolation = props.Choice(('none', 'linear', 'spline'))
    """How the value shown at a real world location is derived from the
    corresponding data value(s). ``none`` is equivalent to nearest neighbour
    interpolation.
    """
    def __init__(self, *args, **kwargs):
        """Create a ``MIPOpts`` object.

        All arguments are passed through to the :class:`.NiftiOpts` init
        function.
        """

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

        niftiopts.NiftiOpts.__init__(self, *args, **kwargs)
        cmapopts.ColourMapOpts.__init__(self)

        # calculate the approximate number
        # of voxels along the longest diagonal
        # of the image - we use this to calculate
        # the maximum number of samples to take
        x, y, z = self.overlay.shape[:3]
        xy = (x * y, (x, y))
        xz = (x * z, (x, z))
        yz = (y * z, (y, z))
        ax0, ax1 = max((xy, xz, yz))[1]
        self.numSteps = np.ceil(np.sqrt(ax0**2 + ax1**2)) * 2

    def destroy(self):
        """Must be called when this ``MIPOpts`` object is no longer needed. """
        cmapopts.ColourMapOpts.destroy(self)
        niftiopts.NiftiOpts.destroy(self)

    def getDataRange(self):
        """Overrides :meth:`.ColourMapOpts.getDataRange`. Returns the
        :attr:`.Image.dataRange` of the image.
        """
        return self.overlay.dataRange

    def calculateRayCastSettings(self, viewmat):
        """Calculates a camera direction and ray casting step vector, based
        on the given view matrix.
        """

        d2tmat = self.getTransform('display', 'texture')
        xform = affine.concat(d2tmat, viewmat)
        cdir = np.array([0, 0, 1])
        cdir = affine.transform(cdir, xform, vector=True)
        cdir = affine.normalise(cdir)

        # sqrt(3) so the maximum number
        # of samplews is taken along the
        # diagonal of a cube
        rayStep = np.sqrt(3) * cdir / self.numSteps

        return cdir, rayStep
コード例 #18
0
ファイル: display.py プロジェクト: lachmanfrantisek/fsleyes
class Display(props.SyncableHasProperties):
    """The ``Display`` class contains display settings which are common to
    all overlay types.

    A ``Display`` instance is also responsible for managing a single
    :class:`DisplayOpts` instance, which contains overlay type specific
    display options. Whenever the :attr:`overlayType` property of a
    ``Display`` instance changes, the old ``DisplayOpts`` instance (if any)
    is destroyed, and a new one, of the correct type, created.
    """


    name = props.String()
    """The overlay name. """


    overlayType = props.Choice()
    """This property defines the overlay type - how the data is to be
    displayed.

    The options for this property are populated in the :meth:`__init__`
    method, from the :attr:`.displaycontext.OVERLAY_TYPES` dictionary. A
    :class:`DisplayOpts` sub-class exists for every possible value that this
    property may take.

    """

    enabled = props.Boolean(default=True)
    """Should this overlay be displayed at all? """


    alpha = props.Percentage(default=100.0)
    """Opacity - 100% is fully opaque, and 0% is fully transparent."""


    brightness = props.Percentage()
    """Brightness - 50% is normal brightness."""


    contrast   = props.Percentage()
    """Contrast - 50% is normal contrast."""


    def __init__(self,
                 overlay,
                 overlayList,
                 displayCtx,
                 parent=None,
                 overlayType=None):
        """Create a :class:`Display` for the specified overlay.

        :arg overlay:     The overlay object.

        :arg overlayList: The :class:`.OverlayList` instance which contains
                          all overlays.

        :arg displayCtx:  A :class:`.DisplayContext` instance describing how
                          the overlays are to be displayed.

        :arg parent:      A parent ``Display`` instance - see
                          :mod:`props.syncable`.

        :arg overlayType: Initial overlay type - see the :attr:`overlayType`
                          property.
        """

        self.__overlay     = overlay
        self.__overlayList = overlayList
        self.__displayCtx  = displayCtx
        self.name          = overlay.name

        # Populate the possible choices
        # for the overlayType property
        from . import getOverlayTypes

        ovlTypes    = getOverlayTypes(overlay)
        ovlTypeProp = self.getProp('overlayType')

        log.debug('Enabling overlay types for {}: '.format(overlay, ovlTypes))
        ovlTypeProp.setChoices(ovlTypes, instance=self)

        # Override the default overlay
        # type if it has been specified
        if overlayType is not None:
            self.overlayType = overlayType

        # Call the super constructor after our own
        # initialisation, in case the provided parent
        # has different property values to our own,
        # and our values need to be updated
        props.SyncableHasProperties.__init__(
            self,
            parent=parent,

            # These properties cannot be unbound, as
            # they affect the OpenGL representation.
            # The name can't be unbound either,
            # because it would be silly to allow
            # different names for the same overlay.
            nounbind=['overlayType', 'name'],

            # Initial sync state between this
            # Display and the parent Display
            # (if this Display has a parent)
            state=displayCtx.syncOverlayDisplay)

        # When the overlay type changes, the property
        # values of the DisplayOpts instance for the
        # old overlay type are stored in this dict.
        # If the overlay is later changed back to the
        # old type, its previous values are restored.
        #
        # The structure of the dictionary is:
        #
        #   { (type(DisplayOpts), propName) : propValue }
        #
        # This also applies to the case where the
        # overlay type is changed from one type to
        # a related type (e.g. from VolumeOpts to
        # LabelOpts) - the values of all common
        # properties are copied to the new
        # DisplayOpts instance.
        self.__oldOptValues = td.TypeDict()

        # Set up listeners after caling Syncable.__init__,
        # so the callbacks don't get called during
        # synchronisation
        self.addListener(
            'overlayType',
            'Display_{}'.format(id(self)),
            self.__overlayTypeChanged)

        # The __overlayTypeChanged method creates
        # a new DisplayOpts instance - for this,
        # it needs to be able to access this
        # Display instance's parent (so it can
        # subsequently access a parent for the
        # new DisplayOpts instance). Therefore,
        # we do this after calling Syncable.__init__.
        self.__displayOpts = None
        self.__overlayTypeChanged()

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


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


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

        When a ``Display`` instance is destroyed, the corresponding
        :class:`DisplayOpts` instance is also destroyed.
        """

        if self.__displayOpts is not None:
            self.__displayOpts.destroy()

        self.removeListener('overlayType', 'Display_{}'.format(id(self)))

        self.detachAllFromParent()

        self.__displayOpts = None
        self.__overlayList = None
        self.__displayCtx  = None
        self.__overlay     = None


    @deprecated.deprecated('0.14.3', '1.0.0', 'Use overlay instead')
    def getOverlay(self):
        """Deprecated - use :meth:`overlay` instead."""
        return self.__overlay


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


    @property
    def opts(self):
        """Return the :class:`.DisplayOpts` instance associated with this
        ``Display``, which contains overlay type specific display settings.

        If a ``DisplayOpts`` instance has not yet been created, or the
        :attr:`overlayType` property no longer matches the type of the
        existing ``DisplayOpts`` instance, a new ``DisplayOpts`` instance
        is created (and the old one destroyed if necessary).

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

        if (self.__displayOpts             is None) or \
           (self.__displayOpts.overlayType != self.overlayType):

            if self.__displayOpts is not None:
                self.__displayOpts.destroy()

            self.__displayOpts = self.__makeDisplayOpts()

        return self.__displayOpts


    @deprecated.deprecated('0.16.0', '1.0.0', 'Use opts instead')
    def getDisplayOpts(self):
        """Return the :class:`.DisplayOpts` instance associated with this
        ``Display``, which contains overlay type specific display settings.

        If a ``DisplayOpts`` instance has not yet been created, or the
        :attr:`overlayType` property no longer matches the type of the
        existing ``DisplayOpts`` instance, a new ``DisplayOpts`` instance
        is created (and the old one destroyed if necessary).

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

        if (self.__displayOpts             is None) or \
           (self.__displayOpts.overlayType != self.overlayType):

            if self.__displayOpts is not None:
                self.__displayOpts.destroy()

            self.__displayOpts = self.__makeDisplayOpts()

        return self.__displayOpts


    def __makeDisplayOpts(self):
        """Creates a new :class:`DisplayOpts` instance. The specific
        ``DisplayOpts`` sub-class that is created is dictated by the current
        value of the :attr:`overlayType` property.

        The :data:`.displaycontext.DISPLAY_OPTS_MAP` dictionary defines the
        mapping between overlay types and :attr:`overlayType` values, and
        ``DisplayOpts`` sub-class types.
        """

        if self.getParent() is None:
            oParent = None
        else:
            oParent = self.getParent().opts

        from . import DISPLAY_OPTS_MAP

        optType = DISPLAY_OPTS_MAP[self.__overlay, self.overlayType]

        log.debug('Creating {} instance (synced: {}) for overlay '
                  '{} ({})'.format(optType.__name__,
                                   self.__displayCtx.syncOverlayDisplay,
                                   self.__overlay, self.overlayType))

        volProps  = optType.getVolumeProps()
        allProps  = optType.getAllProperties()[0]
        initState = {}

        for p in allProps:
            if p in volProps:
                initState[p] = self.__displayCtx.syncOverlayVolume
            else:
                initState[p] = self.__displayCtx.syncOverlayDisplay

        return optType(self.__overlay,
                       self,
                       self.__overlayList,
                       self.__displayCtx,
                       parent=oParent,
                       state=initState)


    def __findOptBaseType(self, optType, optName):
        """Finds the class, in the hierarchy of the given ``optType`` (a
        :class:`.DisplayOpts` sub-class) in which the given ``optName``
        is defined.

        This method is used by the :meth:`__saveOldDisplayOpts` method, and
        is an annoying necessity caused by the way that the :class:`.TypeDict`
        class works. A ``TypeDict`` does not allow types to be used as keys -
        they must be strings containing the type names.

        Furthermore, in order for the property values of a common
        ``DisplayOpts`` base type to be shared across sub types (e.g. copying
        the :attr:`.NiftiOpts.transform` property between :class:`.VolumeOpts`
        and :class:`.LabelOpts` instances), we need to store the name of the
        common base type in the dictionary.
        """

        for base in inspect.getmro(optType):
            if optName in base.__dict__:
                return base

        return None


    def __saveOldDisplayOpts(self):
        """Saves the value of every property on the current
        :class:`DisplayOpts` instance, so they can be restored later if
        needed.
        """

        opts = self.__displayOpts

        if opts is None:
            return

        for propName in opts.getAllProperties()[0]:
            base = self.__findOptBaseType(type(opts), propName)
            base = base.__name__
            val  = getattr(opts, propName)

            log.debug('Saving {}.{} = {} [{} {}]'.format(
                base, propName, val, type(opts).__name__, id(self)))

            self.__oldOptValues[base, propName] = val


    def __restoreOldDisplayOpts(self):
        """Restores any cached values for all of the properties on the
        current :class:`DisplayOpts` instance.
        """
        opts = self.__displayOpts

        if opts is None:
            return

        for propName in opts.getAllProperties()[0]:

            try:
                value = self.__oldOptValues[opts, propName]

                if not hasattr(opts, propName):
                    continue

                log.debug('Restoring {}.{} = {} [{}]'.format(
                    type(opts).__name__, propName, value, id(self)))

                setattr(opts, propName, value)

            except KeyError:
                pass


    def __overlayTypeChanged(self, *a):
        """Called when the :attr:`overlayType` property changes. Makes sure
        that the :class:`DisplayOpts` instance is of the correct type.
        """
        self.__saveOldDisplayOpts()
        self.opts
        self.__restoreOldDisplayOpts()
コード例 #19
0
class TimeSeriesPanel(plotpanel.OverlayPlotPanel):
    """The ``TimeSeriesPanel`` is an :class:`.OverlayPlotPanel` which plots
    time series data from overlays. A ``TimeSeriesPanel`` looks something like
    the following:


    .. image:: images/timeseriespanel.png
       :scale: 50%
       :align: center


    A ``TimeSeriesPanel`` plots one or more :class:`.TimeSeries` instances,
    which encapsulate time series data from an overlay. All ``TimeSeries``
    classes are defined in the :mod:`.plotting.timeseries` module; these are
    all sub-classes of the :class:`.DataSeries` class - see the
    :class:`.PlotPanel` and :class:`.OverlayPlotPanel` documentation for more
    details:

    .. autosummary::
       :nosignatures:

       ~fsleyes.plotting.timeseries.TimeSeries
       ~fsleyes.plotting.timeseries.FEATTimeSeries
       ~fsleyes.plotting.timeseries.MelodicTimeSeries


    **Control panels**


    Some *FSLeyes control* panels are associated with the
    :class:`.TimeSeriesPanel`:

    .. autosummary::
       :nosignatures:

       ~fsleyes.controls.plotlistpanel.PlotListPanel
       ~fsleyes.controls.timeseriescontrolpanel.TimeSeriesControlPanel


    The ``TimeSeriesPanel`` defines some :mod:`.actions`, allowing the user
    to show/hide these control panels:

    .. autosummary::
       :nosignatures:

       toggleTimeSeriesToolBar
       toggleTimeSeriesControl


    Some tools are also available, to do various things:

    .. autosummary::
       :nosignatures:

       addMaskDataSeries


    **FEATures**


    The ``TimeSeriesPanel`` has some extra functionality for
    :class:`.FEATImage` overlays. For these overlays, a
    :class:`.FEATTimeSeries` instance is plotted, instead of a regular
    :class:`.TimeSeries` instance. The ``FEATTimeSeries`` class, in turn, has
    the ability to generate more ``TimeSeries`` instances which represent
    various aspects of the FEAT model fit. See the :class:`.FEATTimeSeries`
    and the :class:`.TimeSeriesControlPanel` classes for more details.


    **Melodic features**


    The ``TimeSeriesPanel`` also has some functionality for
    :class:`.MelodicImage` overlays - a :class:`.MelodicTimeSeries` instance
    is used to plot the component time courses for the current component (as
    defined by the :attr:`.NiftiOpts.volume` property).
    """

    usePixdim = props.Boolean(default=True)
    """If ``True``, the X axis data is scaled by the pixdim value of the
    selected overlay (which, for FMRI time series data is typically set
    to the TR time).
    """

    plotMode = props.Choice(('normal', 'demean', 'normalise', 'percentChange'))
    """Options to scale/offset the plotted time courses.

    ================= =======================================================
    ``normal``        The data is plotted with no modifications
    ``demean``        The data is demeaned (i.e. plotted with a mean of 0)
    ``normalise``     The data is normalised to lie in the range ``[-1, 1]``.
    ``percentChange`` The data is scaled to percent changed
    ================= =======================================================
    """

    plotMelodicICs = props.Boolean(default=True)
    """If ``True``, the component time courses are plotted for
    :class:`.MelodicImage` overlays (using a :class:`.MelodicTimeSeries`
    instance). Otherwise, ``MelodicImage`` overlays are treated as regular
    4D :class:`.Image` overlays (a :class:`.TimeSeries` instance is used).
    """
    def __init__(self, parent, overlayList, displayCtx, frame):
        """Create a ``TimeSeriesPanel``.

        :arg parent:      A :mod:`wx` parent object.
        :arg overlayList: An :class:`.OverlayList` instance.
        :arg displayCtx:  A :class:`.DisplayContext` instance.
        :arg frame:       The :class:`.FSLeyesFrame` instance.
        """

        # If the currently selected image is from
        # a FEAT analysis, and the corresponding
        # filtered_func_data is loaded, enable the
        # initial state of the time course for
        # that filtered_func_data
        featImage = fsloverlay.findFEATImage(overlayList,
                                             displayCtx.getSelectedOverlay())

        if featImage is None: initialState = None
        else: initialState = {featImage: True}

        plotpanel.OverlayPlotPanel.__init__(self,
                                            parent,
                                            overlayList,
                                            displayCtx,
                                            frame,
                                            initialState=initialState)

        self.addListener('plotMode', self.name, self.draw)
        self.addListener('usePixdim', self.name, self.draw)
        self.addListener('plotMelodicICs', self.name,
                         self.__plotMelodicICsChanged)

        self.__addMaskAction = addmaskdataseries.AddMaskDataSeriesAction(
            overlayList, displayCtx, self)

        self.addMaskDataSeries.bindProps('enabled', self.__addMaskAction)

        self.initProfile()

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

        self.removeListener('plotMode', self.name)
        self.removeListener('usePixdim', self.name)
        self.removeListener('plotMelodicICs', self.name)

        self.__addMaskAction.destroy()
        self.__addMaskAction = None

        plotpanel.OverlayPlotPanel.destroy(self)

    @actions.toggleControlAction(timeseriescontrolpanel.TimeSeriesControlPanel)
    def toggleTimeSeriesControl(self, floatPane=False):
        """Shows/hides a :class:`.TimeSeriesControlPanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(timeseriescontrolpanel.TimeSeriesControlPanel,
                         self,
                         location=wx.RIGHT,
                         floatPane=floatPane)

    @actions.toggleControlAction(timeseriestoolbar.TimeSeriesToolBar)
    def toggleTimeSeriesToolBar(self):
        """Shows/hides a :class:`.TimeSeriesToolBar`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(timeseriestoolbar.TimeSeriesToolBar, tsPanel=self)

    @actions.action
    def addMaskDataSeries(self):
        """Executes the :class:`AddMaskDataSeriesAction`. """
        self.__addMaskAction()

    def getActions(self):
        """Overrides :meth:`.ActionProvider.getActions`. Returns all of the
        :mod:`.actions` that are defined on this ``TimeSeriesPanel``.
        """
        actionz = [
            self.screenshot, self.importDataSeries, self.exportDataSeries,
            None, self.toggleOverlayList, self.togglePlotList,
            self.toggleTimeSeriesToolBar, self.toggleTimeSeriesControl
        ]

        names = [a.__name__ if a is not None else None for a in actionz]

        return list(zip(names, actionz))

    def getTools(self):
        """Returns a list of tools to be added to the ``FSLeyesFrame`` for
        ``TimeSeriesPanel`` views.
        """
        return [self.addMaskDataSeries]

    def draw(self, *a):
        """Overrides :meth:`.PlotPanel.draw`. Passes some :class:`.TimeSeries`
        instances to the :meth:`.PlotPanel.drawDataSeries` method.
        """

        if not self or self.destroyed():
            return

        tss = self.getDataSeriesToPlot()

        # Include all of the extra model series
        # for all FEATTimeSeries instances
        newTss = []
        for ts in tss:
            if isinstance(ts, plotting.FEATTimeSeries):

                mtss = ts.getModelTimeSeries()
                newTss += mtss

                # If the FEATTimeSeries is disabled,
                # disable the associated model time
                # series.
                for mts in mtss:
                    mts.enabled = ts.enabled
            else:
                newTss.append(ts)
        tss = newTss

        for ts in tss:

            # Changing the label might trigger
            # another call to this method, as
            # the PlotPanel might have a listener
            # registered on it. Hence the suppress
            with props.suppress(ts, 'label'):
                ts.label = ts.makeLabel()

        xlabel, ylabel = self.__generateDefaultLabels(tss)

        self.drawDataSeries(extraSeries=tss, xlabel=xlabel, ylabel=ylabel)
        self.drawArtists()

    def createDataSeries(self, overlay):
        """Overrides :meth:`.OverlayPlotPanel.createDataSeries`. Creates and
        returns a :class:`.TimeSeries` instance (or an instance of one of the
        :class:`.TimeSeries` sub-classes) for the specified overlay.

        Returns a tuple containing the following:

          - A :class:`.TimeSeries` instance for the given overlay

          - A list of *targets* - objects which have properties that
            influence the state of the ``TimeSeries`` instance.

          - A list of *property names*, one for each target.

        If the given overlay is not compatible (i.e. it has no time series
        data to be plotted), a tuple of ``None`` values is returned.
        """

        if not isinstance(overlay, fslimage.Image):
            return None, None, None

        # Is this a FEAT filtered_func_data image?
        if isinstance(overlay, fslfeatimage.FEATImage):

            # If the filtered_func for this FEAT analysis
            # has been loaded, we show its time series.
            ts = plotting.FEATTimeSeries(self, overlay, self.displayCtx)
            targets = [self.displayCtx]
            propNames = ['location']

        # If this is a melodic IC image, and we are
        # currently configured to plot component ICs,
        # we use a MelodicTimeSeries object.
        elif isinstance(overlay, fslmelimage.MelodicImage) and \
             self.plotMelodicICs:
            ts = plotting.MelodicTimeSeries(self, overlay, self.displayCtx)
            targets = [self.displayCtx.getOpts(overlay)]
            propNames = ['volume']

        # Otherwise we just plot
        # bog-standard 4D voxel data
        # (listening to volumeDim for
        # images with >4 dimensions)
        elif overlay.ndims > 3:
            ts = plotting.VoxelTimeSeries(self, overlay, self.displayCtx)
            opts = self.displayCtx.getOpts(overlay)
            targets = [self.displayCtx, opts]
            propNames = ['location', 'volumeDim']

        else:
            return None, None, None

        ts.colour = self.getOverlayPlotColour(overlay)
        ts.alpha = 1
        ts.lineWidth = 1
        ts.lineStyle = '-'
        ts.label = ts.makeLabel()

        return ts, targets, propNames

    def prepareDataSeries(self, ts):
        """Overrides :class:`.PlotPanel.prepareDataSeries`. Given a
        :class:`.TimeSeries` instance, scales and normalises the x and y data
        according to the current values of the :attr:`usePixdim` and
        :attr:`plotMode` properties.
        """

        xdata, ydata = ts.getData()

        if len(xdata) == 0:
            return xdata, ydata

        if self.usePixdim:
            if isinstance(ts.overlay, fslmelimage.MelodicImage):
                xdata = xdata * ts.overlay.tr
            else:
                xdata = xdata * ts.overlay.pixdim[3]

            toffset = ts.overlay.nibImage.header.get('toffset', 0)
            xdata += toffset

        if self.plotMode == 'demean':
            ydata = ydata - ydata.mean()

        elif self.plotMode == 'normalise':
            ymin = ydata.min()
            ymax = ydata.max()
            if not np.isclose(ymin, ymax):
                ydata = 2 * (ydata - ymin) / (ymax - ymin) - 1
            else:
                ydata = np.zeros(len(ydata))

        elif self.plotMode == 'percentChange':
            mean = ydata.mean()
            ydata = 100 * (ydata / mean) - 100

        return xdata, ydata

    def __plotMelodicICsChanged(self, *a):
        """Called when the :attr:`plotMelodicICs` property changes. Re-creates
        the internally cached :class:`.TimeSeries` instances for all
        :class:`.MelodicImage` overlays in the :class:`.OverlayList`.
        """

        for overlay in self.overlayList:
            if isinstance(overlay, fslmelimage.MelodicImage):
                self.clearDataSeries(overlay)

        self.updateDataSeries()
        self.draw()

    def __generateDefaultLabels(self, timeSeries):
        """Called by :meth:`draw`. If the :attr:`.PlotPanel.xlabel` or
        :attr:`.PlotPanel.ylabel` properties are unset, an attempt is made
        to generate default labels.
        """

        xlabel = self.xlabel
        ylabel = self.ylabel

        if xlabel is not None:
            return xlabel, ylabel

        if not self.usePixdim:
            xlabel = strings.nifti['t_unit', -1]
            return xlabel, ylabel

        # If all of the overlays related to the data series being
        # plotted:
        #
        #   - are Images
        #   - have the same time unit (as specified in the nifti header)
        #
        # Then a default label is specified
        #
        # n.b. this is not foolproof, as many
        # non-time 4D images will still set
        # the time units to seconds.
        #
        #
        # TODO Non-Image overlays with associated
        #      time series (e.g. MeshOpts)

        # Get all the unique overlays
        overlays = [ts.overlay for ts in it.chain(timeSeries, self.dataSeries)]
        overlays = set(overlays)

        if not all([isinstance(o, fslimage.Image) for o in overlays]):
            return xlabel, ylabel

        # And all of their time units
        units = [o.timeUnits for o in overlays]

        if len(set(units)) == 1:
            xlabel = strings.nifti.get(('t_unit', units[0]),
                                       'INVALID TIME UNITS')

        return xlabel, ylabel
コード例 #20
0
ファイル: colourbar.py プロジェクト: lachmanfrantisek/fsleyes
class ColourBar(props.HasProperties, notifier.Notifier):
    """A ``ColourBar`` is an object which listens to the properties of a
    :class:`.ColourMapOpts` instance, and automatically generates a colour
    bar bitmap representing the current colour map properties.

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


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


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


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


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


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


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


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


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

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


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

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

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

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

        self.__selectedOverlayChanged()


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


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

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


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


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

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

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


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

        if self.__opts is None:
            return

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

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

        except fsldc.InvalidOverlayError:
            pass

        self.__opts    = None
        self.__display = None


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

        overlay = self.__displayCtx.getSelectedOverlay()

        if overlay is None:
            return False

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

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

        self.__opts    = opts
        self.__display = display

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

        return True


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


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

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

        if self.__opts is None:
            return None

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

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

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

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

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

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

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

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

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

        return bitmap
コード例 #21
0
ファイル: volumeopts.py プロジェクト: marcobarilari/fsleyes
class ComplexOpts(VolumeOpts):
    """The ``ComplexOpts`` class is a specialisation of :class:`VolumeOpts` for
    images with a complex data type.
    """

    component = props.Choice(('real', 'imag', 'mag', 'phase'))
    """How to display the complex data:

     - ``'real'``   - display the real component
     - ``'imag'```  - display the imaginary component
     - ``'mag'```   - display the magnitude
     - ``'phase'``` - display the phase
    """
    def __init__(self, *args, **kwargs):
        """Create a ``ComplexOpts``. All arguments are passed through to
        the :class:`VolumeOpts` constructor.
        """
        self.__dataRanges = {}
        VolumeOpts.__init__(self, *args, **kwargs)
        self.addListener('component', self.name, self.__componentChanged)

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

    def getDataRange(self):
        """Overrides :meth:`.ColourMapOpts.getDataRange`.
        Calculates and returns the data range of the current
        :attr:`component`.
        """

        drange = self.__dataRanges.get(self.component, None)
        if drange is None:
            data = self.getComponent(self.overlay[:])
            drange = np.nanmin(data), np.nanmax(data)
            self.__dataRanges[self.component] = drange
        return drange

    def getComponent(self, data):
        """Calculates and returns the current :attr:`component` from the given
        data, assumed to be complex.
        """
        if self.component == 'real': return self.getReal(data)
        elif self.component == 'imag': return self.getImaginary(data)
        elif self.component == 'mag': return self.getMagnitude(data)
        elif self.component == 'phase': return self.getPhase(data)

    @staticmethod
    def getReal(data):
        """Return the real component of the given complex data. """
        return data.real

    @staticmethod
    def getImaginary(data):
        """Return the imaginary component of the given complex data. """
        return data.imag

    @staticmethod
    def getMagnitude(data):
        """Return the magnitude of the given complex data. """
        return (data.real**2 + data.imag**2)**0.5

    @staticmethod
    def getPhase(data):
        """Return the phase of the given complex data. """
        return np.arctan2(data.imag, data.real)

    def __componentChanged(self, *a):
        """Called when the :attr:`component` changes. Calls
        :meth:`.ColourMapOpts.updateDataRange`.
        """
        self.updateDataRange()
コード例 #22
0
class HistogramPanel(plotpanel.OverlayPlotPanel):
    """An :class:`.OverlayPlotPanel` which plots histograms from
    :class:`.Image`     overlay data. A ``HistogramPanel`` looks something
    like this:

    .. image:: images/histogrampanel.png
       :scale: 50%
       :align: center


    A ``HistogramPanel`` plots one or more :class:`HistogramSeries` instances,
    each of which encapsulate histogram data from an :class:`.Image` overlay.


    A couple of control panels may be shown on a ``HistogramPanel``:

    .. autosummary::
       :nosignatures:

       ~fsleyes.controls.plotlistpanel.PlotListPanel
       ~fsleyes.controls.histogramcontrolpanel.HistogramControlPanel


    The following :mod:`actions` are provided, in addition to those already
    provided by the :class:`.PlotPanel`:

    .. autosummary::
       :nosignatures:

       toggleHistogramToolBar
       toggleHistogramControl

    Some tools are also available, to do various things:

    .. autosummary::
       :nosignatures:

       addROIHistogram
    """

    histType = props.Choice(('probability', 'count'))
    """The histogram type:

    =============== ==========================================================
    ``count``       The y axis represents the absolute number of values within
                    each bin
    ``probability`` The y axis represents the number of values within each
                    bin, divided by the total number of values.
    =============== ==========================================================
    """

    plotType = props.Choice(('centre', 'edge'))
    """How histograms are plotted:

    ========== ==========================================================
    ``centre`` Plot one data point at the centre of each bin
    ``edge``   Plot one data point at each bin edge - this produces a
               "stepped" plot.
    ========== ==========================================================
    """
    def __init__(self, parent, overlayList, displayCtx, frame):
        """Create a ``HistogramPanel``.

        :arg parent:      The :mod:`wx` parent.
        :arg overlayList: The :class:`.OverlayList` instance.
        :arg displayCtx:  The :class:`.DisplayContext` instance.
        :arg frame:       The :class:`.FSLeyesFrame` instance.
        """

        plotpanel.OverlayPlotPanel.__init__(self, parent, overlayList,
                                            displayCtx, frame)

        self.__currentHs = None

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

        self.__roiHistAction = roihistogram.AddROIHistogramAction(
            overlayList, displayCtx, self)

        self.addROIHistogram.bindProps('enabled', self.__roiHistAction)

        self.initProfile()
        self.__selectedOverlayChanged()

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

        self.__roiHistAction.destroy()

        self.__currentHs = None
        self.__roiHistAction = None

        self.removeListener('histType', self.name)
        self.removeListener('plotType', self.name)
        self.overlayList.removeListener('overlays', self.name)
        self.displayCtx.removeListener('selectedOverlay', self.name)

        plotpanel.OverlayPlotPanel.destroy(self)

    @actions.toggleControlAction(histogramcontrolpanel.HistogramControlPanel)
    def toggleHistogramControl(self, floatPane=False):
        """Shows/hides a :class:`.HistogramControlPanel`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(histogramcontrolpanel.HistogramControlPanel,
                         self,
                         location=wx.RIGHT,
                         floatPane=floatPane)

    @actions.toggleControlAction(histogramtoolbar.HistogramToolBar)
    def toggleHistogramToolBar(self):
        """Shows/hides a :class:`.HistogramToolBar`. See
        :meth:`.ViewPanel.togglePanel`.
        """
        self.togglePanel(histogramtoolbar.HistogramToolBar, histPanel=self)

    @actions.toggleAction
    def toggleHistogramOverlay(self):
        """Toggles the value of the :attr:`.HistogramSeries.showOverlay`
        for the currently selected overlay (if possible).
        """
        # This action gets configured in the
        # __selectedOverlayChanged method
        pass

    @actions.action
    def addROIHistogram(self):
        """Runs an :class:`.AddROIHistogramAction`. """

        self.__roiHistAction()

    def getActions(self):
        """Overrides :meth:`.ActionProvider.getActions`. Returns all of the
        :mod:`.actions` that are defined on this ``HistogramPanel``.
        """
        actions = [
            self.screenshot, self.importDataSeries, self.exportDataSeries,
            None, self.toggleOverlayList, self.togglePlotList,
            self.toggleHistogramToolBar, self.toggleHistogramControl,
            self.toggleHistogramOverlay
        ]

        names = [a.__name__ if a is not None else None for a in actions]

        return list(zip(names, actions))

    def getTools(self):
        """Returns a list of tools to be added to the ``FSLeyesFrame`` for
        ``HistogramPanel`` views.
        """
        return [self.addROIHistogram]

    def draw(self, *a):
        """Overrides :meth:`.PlotPanel.draw`. Passes some
        :class:`.HistogramSeries` instances to the
        :meth:`.PlotPanel.drawDataSeries` method.
        """

        if not self or self.destroyed():
            return

        hss = self.getDataSeriesToPlot()

        for hs in hss:
            with props.suppress(hs, 'label'):
                hs.label = self.displayCtx.getDisplay(hs.overlay).name

        if self.smooth or self.plotType == 'centre':
            self.drawDataSeries(hss)

        # use a step plot when plotting bin edges
        else:
            self.drawDataSeries(hss, drawstyle='steps-pre')

        self.drawArtists()

    def createDataSeries(self, overlay):
        """Creates a :class:`.HistogramSeries` instance for the specified
        overlay.
        """

        if isinstance(overlay, fsloverlay.ProxyImage):
            overlay = overlay.getBase()

        if isinstance(overlay, fslimage.Image):
            hsType = histogramseries.ImageHistogramSeries
        elif isinstance(overlay, fslmesh.Mesh):
            hsType = histogramseries.MeshHistogramSeries
        else:
            return None, None, None

        hs = hsType(overlay, self.overlayList, self.displayCtx, self)
        hs.colour = self.getOverlayPlotColour(overlay)
        hs.alpha = 1
        hs.lineWidth = 1
        hs.lineStyle = '-'

        return hs, [], []

    def prepareDataSeries(self, hs):
        """Overrides :meth:`.PlotPanel.prepareDataSeries`.

        Performs some pre-processing on the data contained in the given
        :class:`.HistogramSeries` instance.
        """

        xdata, ydata = hs.getData()

        if len(xdata) == 0 or len(ydata) == 0:
            return [], []

        # If smoothing is enabled, we just
        # need to provide the data as-is
        if self.smooth:
            xdata = np.array(xdata[:-1], dtype=np.float32)
            ydata = np.array(ydata, dtype=np.float32)

        # If plotting on bin edges, we need to
        # munge the histogram data a bit so that
        # plt.plot(drawstyle='steps-pre') plots
        # it nicely.
        elif self.plotType == 'edge':
            xpad = np.zeros(len(xdata) + 1, dtype=np.float32)
            ypad = np.zeros(len(ydata) + 2, dtype=np.float32)

            xpad[:-1] = xdata
            xpad[-1] = xdata[-1]
            ypad[1:-1] = ydata
            xdata = xpad
            ydata = ypad

        # otherwise if plotting bin centres,
        # we need to offset the data, as it
        # is on bin edges
        elif self.plotType == 'centre':
            ydata = np.array(ydata, dtype=np.float32)
            xdata = np.array(xdata[:-1], dtype=np.float32)
            xdata = xdata + 0.5 * (xdata[1] - xdata[0])

        # The passed-in series may just
        # be a DataSeries instance.
        if not isinstance(hs, histogramseries.HistogramSeries):
            return xdata, ydata

        # Or a HistogramSeries instance
        nvals = hs.getNumHistogramValues()
        if self.histType == 'count': return xdata, ydata
        elif self.histType == 'probability': return xdata, ydata / nvals

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

        overlay = self.displayCtx.getSelectedOverlay()
        oldHs = self.__currentHs
        newHs = self.getDataSeries(overlay)
        enable  = (overlay is not None) and \
                  (newHs   is not None) and \
                  isinstance(overlay, fslimage.Image)

        self.toggleHistogramOverlay.enabled = enable

        if not enable or (oldHs is newHs):
            return

        self.__currentHs = newHs

        if oldHs is not None:
            self.toggleHistogramOverlay.unbindProps('toggled', oldHs,
                                                    'showOverlay')

        self.toggleHistogramOverlay.bindProps('toggled', newHs, 'showOverlay')
コード例 #23
0
class ColourBarCanvas(props.HasProperties):
    """Contains logic to render a colour bar as an OpenGL texture.
    """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        if not self._setGLContext():
            return

        w, h = self.GetSize()

        if w < 50 or h < 50:
            return

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

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

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

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

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

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

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

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

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

        width, height = self.GetSize()

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

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

        self._tex.drawOnBounds(0, 0, 1, 0, 1, 0, 1)
コード例 #24
0
ファイル: viewpanel.py プロジェクト: lachmanfrantisek/fsleyes
class ViewPanel(fslpanel.FSLeyesPanel):
    """The ``ViewPanel`` class is the base-class for all *FSLeyes views*.

    A ``ViewPanel`` displays some sort of view of the overlays in an
    :class:`.OverlayList`. The settings for a ``ViewPanel`` are defined by a
    :class:`.DisplayContext` instance.


    **Panels and controls**


    A ``ViewPanel`` class uses a ``wx.lib.agw.aui.AuiManager`` to lay out its
    children. A ``ViewPanel`` has one central panel, which contains the
    primary view; and may have one or more secondary panels, which contain
    *controls* - see the :mod:`.controls` package. The centre panel can be set
    via the :meth:`centrePanel` property, and secondary panels can be
    added/removed to/from with the :meth:`togglePanel` method. The current
    state of a secondary panel (i.e. whether one is open or not) can be
    queried with the :meth:`isPanelOpen` method, and existing secondary panels
    can be accessed via the :meth:`getPanel` method.  Secondary panels must be
    derived from either the :class:`.ControlPanel` or :class:`.ControlToolBar`
    base-classes.


    **Profiles**


    Some ``ViewPanel`` classes have relatively complex mouse and keyboard
    interaction behaviour (e.g. the :class:`.OrthoPanel` and
    :class:`.LightBoxPanel`). The logic defines this interaction is provided
    by a :class:`.Profile` instance, and is managed by a
    :class:`.ProfileManager`.  Some ``ViewPanel`` classes have multiple
    interaction profiles - for example, the :class:`.OrthoPanel` has a
    ``view`` profile, and an ``edit`` profile. The current interaction
    profile can be changed with the :attr:`profile` property, and can be
    accessed with the :meth:`getCurrentProfile` method. See the
    :mod:`.profiles` package for more information on interaction profiles.


    **Programming interface**


    The following methods are available on a ``Viewpanel`` for programmatically
    controlling its display and layout:


    .. autosummary::
       :nosignatures:

       togglePanel
       isPanelOpen
       getPanel
       getPanels
       getTools
       removeFromFrame
       removeAllPanels
       getPanelInfo
       auiManager
    """


    profile = props.Choice()
    """The current interaction profile for this ``ViewPanel``. """


    def __init__(self, parent, overlayList, displayCtx, frame):
        """Create a ``ViewPanel``. All arguments are passed through to the
        :class:`.FSLeyesPanel` constructor.
        """

        fslpanel.FSLeyesPanel.__init__(
            self, parent, overlayList, displayCtx, frame)

        self.__profileManager = profiles.ProfileManager(
            self, overlayList, displayCtx)

        # The __centrePanel attribute stores a reference
        # to the main (centre) panel on this ViewPanel.
        # It is set by sub-class implementations via
        # the centrePanel property.
        #
        # The panels dictionary stores a collection
        # of {type : instance} mappings of active
        # FSLeyes control panels that are contained
        # in this view panel.
        self.__centrePanel = None
        self.__panels      = {}

        # See note in FSLeyesFrame about
        # the user of aero docking guides.
        self.__auiMgr = aui.AuiManager(
            self,
            agwFlags=(aui.AUI_MGR_RECTANGLE_HINT          |
                      aui.AUI_MGR_NO_VENETIAN_BLINDS_FADE |
                      aui.AUI_MGR_ALLOW_FLOATING          |
                      aui.AUI_MGR_AERO_DOCKING_GUIDES     |
                      aui.AUI_MGR_LIVE_RESIZE))

        self.__auiMgr.SetDockSizeConstraint(0.5, 0.5)
        self.__auiMgr.Bind(aui.EVT_AUI_PANE_CLOSE, self.__onPaneClose)

        # Use a different listener name so that subclasses
        # can register on the same properties with self.name
        lName = 'ViewPanel_{}'.format(self.name)

        self.addListener('profile', lName, self.__profileChanged)

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

        self.__selectedOverlay = None
        self.__selectedOverlayChanged()

        # A very shitty necessity. When panes are floated,
        # the AuiManager sets the size of the floating frame
        # to the minimum size of the panel, without taking
        # into account the size of its borders/title bar,
        # meaning that the panel size is too small. Here,
        # we're just creating a dummy MiniFrame (from which
        # the AuiFloatingFrame derives), and saving the size
        # of its trimmings for later use in the togglePanel
        # method.
        ff         = wx.MiniFrame(self)
        size       = ff.GetSize().Get()
        clientSize = ff.GetClientSize().Get()

        self.__floatOffset = (size[0] - clientSize[0],
                              size[1] - clientSize[1])

        ff.Destroy()


    def destroy(self):
        """Removes some property listeners, destroys all child panels,
        destroys the :class:`.ProfileManager`, and ``AuiManager``, and
        calls :meth:`.FSLeyesPanel.destroy`.
        """

        # Make sure that any control panels are correctly destroyed
        for panelType, panel in self.__panels.items():
            self.__auiMgr.DetachPane(panel)
            panel.destroy()

        # Remove listeners from the overlay
        # list and display context
        lName = 'ViewPanel_{}'.format(self.name)

        self            .removeListener('profile',         lName)
        self.overlayList.removeListener('overlays',        lName)
        self.displayCtx .removeListener('selectedOverlay', lName)

        # Disable the  ProfileManager
        self.__profileManager.destroy()

        # Un-initialise the AUI manager
        self.__auiMgr.Unbind(aui.EVT_AUI_PANE_CLOSE)
        self.__auiMgr.Update()
        self.__auiMgr.UnInit()

        # The AUI manager does not clear its
        # reference to this panel, so let's
        # do it here.
        self.__auiMgr._frame  = None
        self.__profileManager = None
        self.__auiMgr         = None
        self.__panels         = None
        self.__centrePanel    = None

        fslpanel.FSLeyesPanel.destroy(self)


    def initProfile(self):
        """Must be called by subclasses, after they have initialised all
        of the attributes which may be needed by their associated
        :class:`.Profile` instances.
        """
        self.__profileChanged()


    def getCurrentProfile(self):
        """Returns the :class:`.Profile` instance currently in use. """
        return self.__profileManager.getCurrentProfile()


    @property
    def centrePanel(self):
        """Returns the primary (centre) panel on this ``ViewPanel``.
        """
        return self.__centrePanel


    @deprecated.deprecated('0.16.0', '1.0.0', 'Use centrePanel instead')
    def getCentrePanel(self):
        """Returns the primary (centre) panel on this ``ViewPanel``.
        """
        return self.centrePanel


    @centrePanel.setter
    def centrePanel(self, panel):
        """Set the primary centre panel for this ``ViewPanel``. This method
        is only intended to be called by sub-classes.
        """

        panel.Reparent(self)
        paneInfo = (aui.AuiPaneInfo()
                    .Name(type(panel).__name__)
                    .CentrePane())

        self.__auiMgr.AddPane(panel, paneInfo)
        self.__auiMgrUpdate()
        self.__centrePanel = panel


    @deprecated.deprecated('0.16.0', '1.0.0', 'Use centrePanel instead')
    def setCentrePanel(self, panel):
        """Set the primary centre panel for this ``ViewPanel``. This method
        is only intended to be called by sub-classes.
        """
        self.centrePanel = panel


    def togglePanel(self, panelType, *args, **kwargs):
        """Add/remove the secondary panel of the specified type to/from this
        ``ViewPanel``.

        :arg panelType: Type of the secondary panel.

        :arg args:      All positional arguments are passed to the
                        ``panelType`` constructor.

        :arg floatPane: If ``True``, the secondary panel is initially floated.
                        Defaults to ``False``.

        :arg floatOnly: If ``True``, and ``floatPane=True``, the panel will
                        be permanently floated (i.e. it will not be dockable).

        :arg floatPos:  If provided, and ``floatPane`` is ``True``, specifies
                        the location of the floating panel as ``(w, h)``
                        proportions between 0 and 1, relative to this view
                        panel.

        :arg closeable: If ``False``, and ``floatPane=True``, the panel will
                        not have a close button when it is floated.

        :arg location:  If ``floatPane=False``, the initial dock position of
                        the panel - either ``wx.TOP``, ``wx.BOTTOM``,
                        ``wx.LEFT``, or ``wx.RIGHT. Defaults to ``wx.BOTTOM``.

        :arg title:     Title to give the control. If not provided, it is
                        assumed that a title for ``panelType`` is in
                        :attr:`.strings.titles`.

        :arg kwargs:    All keyword arguments, apart from ``floatPane`` and
                        ``location``, are passed to the ``panelType``
                        constructor.

        .. note::       The ``panelType`` type must be a sub-class of
                        :class:`.ControlPanel` or :class:`.ControlToolBar`,
                        which can be created like so::

                            panel = panelType(parent,
                                              overlayList,
                                              displayCtx,
                                              frame,
                                              *args,
                                              **kwargs)

        .. warning::    Do not define a control (a.k.a. secondary) panel
                        constructor to accept arguments with the names
                        ``floatPane``, ``floatOnly``, ``floatPos``,
                        ``closeable``, or ``location``, as arguments with
                        those names will get eaten by this method before they
                        can be passed to the constructor.
        """

        location  = kwargs.pop('location',  None)
        floatPane = kwargs.pop('floatPane', False)
        floatOnly = kwargs.pop('floatOnly', False)
        closeable = kwargs.pop('closeable', True)
        title     = kwargs.pop('title',     None)
        floatPos  = kwargs.pop('floatPos',  (0.5, 0.5))

        if title is None:
            title = strings.titles.get(panelType, type(panelType).__name__)

        if location not in (None, wx.TOP, wx.BOTTOM, wx.LEFT, wx.RIGHT):
            raise ValueError('Invalid value for location')

        supported = panelType.supportedViews()
        if supported is not None and type(self) not in supported:
            raise ValueError(
                '{} views are not supported by {} controls'.format(
                    type(self).__name__, panelType.__name__))

        window = self.__panels.get(panelType, None)

        # The panel is already open - close it
        if window is not None:
            self.__onPaneClose(None, window)
            return

        # Otherwise, create a new panel of the specified type.
        # The PaneInfo Name is the control panel class name -
        # this is used for saving and restoring layouts.
        paneInfo  = aui.AuiPaneInfo().Name(panelType.__name__)
        window    = panelType(self,
                              self.overlayList,
                              self.displayCtx,
                              self.frame,
                              *args,
                              **kwargs)
        isToolbar = isinstance(window, ctrlpanel.ControlToolBar)

        if isToolbar:

            # ToolbarPane sets the panel layer to 10
            paneInfo.ToolbarPane()

            if window.GetOrient() == wx.VERTICAL:
                paneInfo.GripperTop()

            # We are going to put any new toolbars on
            # the top of the panel, below any existing
            # toolbars. This is annoyingly complicated,
            # because the AUI designer(s) decided to
            # give the innermost layer an index of 0.
            #
            # So in order to put a new toolbar at the
            # innermost layer, we need to adjust the
            # layers of all other existing toolbars

            for p in self.__panels.values():
                if isinstance(p, ctrlpanel.ControlToolBar):
                    info = self.__auiMgr.GetPane(p)

                    # This is nasty - the agw.aui.AuiPaneInfo
                    # class doesn't have any publicly documented
                    # methods of querying its current state.
                    # So I'm accessing its undocumented instance
                    # attributes (determined by browsing the
                    # source code)
                    if info.IsDocked() and \
                       info.dock_direction == aui.AUI_DOCK_TOP:
                        info.Layer(info.dock_layer + 1)

            # When the toolbar contents change,
            # update the layout, so that the
            # toolbar's new size is accommodated
            window.Bind(fsltoolbar.EVT_TOOLBAR_EVENT, self.__auiMgrUpdate)

        paneInfo.Caption(title)

        # Dock the pane at the position specified
        # by the location parameter
        if not floatPane:

            if location is None:
                if isToolbar: location = aui.AUI_DOCK_TOP
                else:         location = aui.AUI_DOCK_BOTTOM

            elif location == wx.TOP:    location = aui.AUI_DOCK_TOP
            elif location == wx.BOTTOM: location = aui.AUI_DOCK_BOTTOM
            elif location == wx.LEFT:   location = aui.AUI_DOCK_LEFT
            elif location == wx.RIGHT:  location = aui.AUI_DOCK_RIGHT

            paneInfo.Direction(location)

        # Or, for floating panes, centre the
        # floating pane on this ViewPanel
        else:

            selfPos    = self.GetScreenPosition().Get()
            selfSize   = self.GetSize().Get()
            selfCentre = (selfPos[0] + selfSize[0] * floatPos[0],
                          selfPos[1] + selfSize[1] * floatPos[1])

            paneSize = window.GetBestSize().Get()
            panePos  = (selfCentre[0] - paneSize[0] * 0.5,
                        selfCentre[1] - paneSize[1] * 0.5)

            paneInfo.Float()                 \
                    .Dockable(not floatOnly) \
                    .CloseButton(closeable)  \
                    .FloatingPosition(panePos)

        self.__auiMgr.AddPane(window, paneInfo)
        self.__panels[panelType] = window
        self.__auiMgrUpdate(newPanel=window)


    def isPanelOpen(self, panelType):
        """Returns ``True`` if a panel of type ``panelType`` is open,
        ``False`` otherwise.
        """
        return self.getPanel(panelType) is not None


    def getPanel(self, panelType):
        """If an instance of ``panelType`` exists, it is returned.
        Otherwise ``None`` is returned.
        """
        if panelType in self.__panels: return self.__panels[panelType]
        else:                          return None


    @actions.action
    def removeAllPanels(self):
        """Remove all control panels from this ``ViewPanel``."""

        for panelType, instance in list(self.__panels.items()):
            self.togglePanel(panelType)


    @actions.action
    def removeFromFrame(self):
        """Remove this ``ViewPanel`` from the :class:`.FSLeyesFrame`.

        Will raise an error if this ``ViewPanel`` is not in a
        ``FSLeyesFrame``.
        """
        self.frame.removeViewPanel(self)


    def getPanels(self):
        """Returns a list containing all control panels currently shown in this
        ``ViewPanel``.
        """
        return list(self.__panels.values())


    def getPanelInfo(self, panel):
        """Returns the ``AuiPaneInfo`` object which contains information about
        the given control panel.
        """
        return self.__auiMgr.GetPane(panel)


    @property
    def auiManager(self):
        """Returns the ``wx.lib.agw.aui.AuiManager`` object which manages the
        layout of this ``ViewPanel``.
        """
        return self.__auiMgr


    @deprecated.deprecated('0.16.0', '1.0.0', 'Use auiManager instead')
    def getAuiManager(self):
        """Returns the ``wx.lib.agw.aui.AuiManager`` object which manages the
        layout of this ``ViewPanel``.
        """
        return self.__auiMgr


    def getTools(self):
        """This method should be overridden by sub-classes (if necessary), and
        should return any ``action`` methods which should be added to the
        :class:`.FSLeyesFrame` *Tools* menu.
        """
        return []


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

        This method is slightly hard-coded and hacky. For the time being,
        profiles called ``edit`` profiles are only supported for ``volume``
        overlay types. This method checks the type of the selected overlay,
        and disables the ``edit`` profile option (if it is an option), so the
        user can only choose an ``edit`` profile on ``volume`` overlay types.
        """

        lName   = 'ViewPanel_{}'.format(self.name)
        overlay = self.displayCtx.getSelectedOverlay()

        if self.__selectedOverlay not in (None, overlay):
            try:
                d = self.displayCtx.getDisplay(self.__selectedOverlay)

                d.removeListener('overlayType', lName)

            # The overlay has been removed
            except fsldisplay.InvalidOverlayError:
                pass

        self.__selectedOverlay = overlay

        if overlay is None:
            return

        # If the overlay is of a compatible type,
        # register for overlay type changes, as
        # these will affect the profile property
        if isinstance(overlay, fslimage.Image):
            display = self.displayCtx.getDisplay(overlay)
            display.addListener('overlayType',
                                lName,
                                self.__configureProfile,
                                overwrite=True)

        self.__configureProfile()


    def __configureProfile(self, *a):
        """Called by the :meth:`__selectedOverlayChanged` method. Implements
        the hacky logic described in the documentation for that method.
        """

        overlay     = self.__selectedOverlay
        display     = self.displayCtx.getDisplay(overlay)
        profileProp = self.getProp('profile')

        # edit profile is not an option -
        # nothing to be done
        if 'edit' not in profileProp.getChoices(self):
            return

        if not isinstance(overlay, fslimage.Image) or \
           display.overlayType not in ('volume', 'label', 'mask'):

            # change profile if needed,
            if self.profile == 'edit':
                self.profile = 'view'

            # and disable edit profile
            log.debug('{}: disabling edit profile for '
                      'selected overlay {}'.format(
                          type(self).__name__, overlay))
            profileProp.disableChoice('edit', self)

        # Otherwise make sure edit
        # is enabled for volume images
        else:
            log.debug('{}: Enabling edit profile for '
                      'selected overlay {}'.format(
                          type(self).__name__, overlay))
            profileProp.enableChoice('edit', self)


    def __profileChanged(self, *a):
        """Called when the current :attr:`profile` property changes. Tells the
        :class:`.ProfileManager` about the change.

        The ``ProfileManager`` will create a new :class:`.Profile` instance of
        the appropriate type.
        """

        self.__profileManager.changeProfile(self.profile)


    def __auiMgrUpdate(self, *args, **kwargs):
        """Called whenever a panel is added/removed to/from this ``ViewPanel``.

        Calls the ``Update`` method on the ``AuiManager`` instance that is
        managing this panel.

        :arg newPanel: Must be passed as a keyword argument. When a new panel
                       is added, it should be passed here.
        """

        newPanel = kwargs.pop('newPanel', None)

        # This method makes sure that size hints
        # for all existing and new panels are
        # set on their AuiPaneInfo objects, and
        # then calls AuiManager.Update.

        # We first loop through all panels, and
        # figure out their best sizes. Each entry
        # in this list is a tuple containing:
        #
        #    - Panel
        #    - AuiPaneInfo instance
        #    - Dock direction (None for floating panels)
        #    - Layer number (None for floating panels)
        #    - Minimum size
        bestSizes = []

        for panel in self.__panels.values():

            if isinstance(panel, ctrlpanel.ControlToolBar):
                continue

            pinfo = self.__auiMgr.GetPane(panel)

            # If the panel is floating, use its
            # current size as its 'best' size,
            # as otherwise the AuiManager will
            # immediately resize the panel to
            # its best size.
            if pinfo.IsFloating():
                dockDir  = None
                layer    = None
                bestSize = panel.GetSize().Get()

                # Unless its current size is tiny
                # (which probably means that it has
                # just been added)
                if bestSize[0] <= 20 or \
                   bestSize[1] <= 20:
                    bestSize = panel.GetBestSize().Get()

            else:
                dockDir  = pinfo.dock_direction
                layer    = pinfo.dock_layer
                bestSize = panel.GetBestSize().Get()

            bestSizes.append((panel, pinfo, dockDir, layer, bestSize))

        # Now we loop through one final time, and
        # set all of the necessary size hints on
        # the AuiPaneInfo instances.
        for panel, pinfo, dockDir, layer, bestSize in bestSizes:

            parent = panel.GetParent()

            # When a panel is added/removed from the AuiManager,
            # the position of floating panels seems to get reset
            # to their original position, when they were created.
            # Here, we explicitly set the position of each
            # floating frame, so the AuiManager doesn't move our
            # windows about the place.
            if pinfo.IsFloating() and \
               isinstance(parent, aui.AuiFloatingFrame):
                pinfo.FloatingPosition(parent.GetScreenPosition())

            # See comments in __init__ about
            # this silly 'float offset' thing
            floatSize = (bestSize[0] + self.__floatOffset[0],
                         bestSize[1] + self.__floatOffset[1])

            log.debug('New size for panel {} - '
                      'best: {}, float: {}'.format(
                          type(panel).__name__, bestSize, floatSize))

            pinfo.MinSize(     (1, 1))  \
                 .BestSize(    bestSize) \
                 .FloatingSize(floatSize)

            # This is a terrible hack which forces
            # the AuiManager to grow a dock when a
            # new panel is added, which is bigger
            # than the existing dock contents.
            if panel is newPanel and not pinfo.IsFloating():
                docks = aui.FindDocks(self.__auiMgr._docks, dockDir, layer)
                for d in docks:
                    d.size = 0

        self.__auiMgr.Update()


    def __onPaneClose(self, ev=None, panel=None):
        """Called when the user closes a control (a.k.a. secondary) panel.
        Calls the
        :class:`.ControlPanel.destroy`/:class:`.ControlToolBar.destroy`
        method on the panel.
        """

        if ev is not None:
            ev.Skip()
            panel = ev.GetPane().window

        # If the user has grouped multiple control panels
        # into a single tabbed notebook, and then closed
        # the entire notebook, the AuiManager will generate
        # a single close event, and will pass us that
        # notebook. So we have to look in the notebook
        # to see which control panels were actually closed.
        if isinstance(panel, wx.lib.agw.aui.AuiNotebook):
            panels = [panel.GetPage(i) for i in range(panel.GetPageCount())]
        else:
            panels = [panel]


        for panel in list(panels):

            if isinstance(panel, (ctrlpanel.ControlPanel,
                                  ctrlpanel.ControlToolBar)):

                # WTF AUI. Sometimes this method gets called
                # twice for a panel, the second time with a
                # reference to a wx._wxpyDeadObject; in such
                # situations, the Destroy method call below
                # would result in an exception being raised.
                if self.__panels.pop(type(panel), None) is None:
                    panels.remove(panel)

                # calling ControlPanel.destroy()
                # here -  wx.Destroy is done below
                else:
                    log.debug('Panel closed: {}'.format(type(panel).__name__))
                    panel.destroy()

        # Destroy all the panels
        for panel in panels:

            # Even when the user closes a pane,
            # AUI does not detach said pane -
            # we have to do it manually
            self.__auiMgr.DetachPane(panel)
            wx.CallAfter(panel.Destroy)

        # Update the view panel layout
        wx.CallAfter(self.__auiMgrUpdate)
コード例 #25
0
class FEATTimeSeries(VoxelTimeSeries):
    """A :class:`VoxelTimeSeries` class for use with :class:`FEATImage`
    instances, containing some extra FEAT specific options.


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


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


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


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

    .. autosummary::
       :nosignatures:

       FEATEVTimeSeries
       FEATResidualTimeSeries
       FEATPartialFitTimeSeries
       FEATModelFitTimeSeries
    """


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


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


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


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


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


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


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


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

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

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

        numEVs    = self.overlay.numEVs()
        numCOPEs  = self.overlay.numContrasts()
        copeNames = self.overlay.contrastNames()

        reduceOpts = ['none'] + \
                     ['PE{}'.format(i + 1) for i in range(numEVs)]

        for i in range(numCOPEs):
            name = 'COPE{} ({})'.format(i + 1, copeNames[i])
            reduceOpts.append(name)

        self.getProp('plotPartial').setChoices(reduceOpts, instance=self)

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

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

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

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

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

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

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


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


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

        modelts = []

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

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

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

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

        return modelts


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


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

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

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

        if fitType == 'full':
            return [1] * self.overlay.numEVs()
        elif fitType == 'pe':
            con      = [0] * self.overlay.numEVs()
            con[idx] = 1
            return con
        elif fitType == 'cope':
            return self.overlay.contrasts()[idx]


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

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

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

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

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

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

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

        return ts


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

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

        partial = self.plotPartial

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

        partial = partial.split()[0]

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

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


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

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

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

        self.__resTs = self.__createModelTs(FEATResidualTimeSeries)


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

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

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

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

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


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

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

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

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

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


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

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

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

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

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


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

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

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

        self.__fullModelTs = self.__createModelTs(
            FEATModelFitTimeSeries, self.__getContrast('full', -1), 'full', -1)
コード例 #26
0
ファイル: dataseries.py プロジェクト: neurodebian/fsleyes
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
コード例 #27
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
コード例 #28
0
ファイル: dataseries.py プロジェクト: marcobarilari/fsleyes
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
コード例 #29
0
class Profile(props.SyncableHasProperties, actions.ActionProvider):
    """A :class:`Profile` class implements keyboard/mouse interaction behaviour
    for a :class:`.ViewPanel` instance.


    Subclasses should specify at least one *mode* of operation, which defines
    a sort of sub-profile. The current mode can be changed with the
    :attr:`mode` property.


    Subclasses must also override the :meth:`getEventTargets` method, to
    return the :mod:`wx` objects that are to be the targets for mouse/keyboard
    interaction.


    The ``Profile`` class currently only supports :class:`.CanvasPanel` and
    :class:`.PlotPanel` views. ``Profile`` instances use a
    :class:`.CanvasPanelEventManager` instance to manage GUI events on
    :class:`.CanvasPanel` instances, or a:class:`.PlotPanelEventManager`
    to manage GUI events on ``matplotlib Canvas`` objects.


    **Receiving events**


    In order to receive mouse or keyboard events, subclasses simply need to
    implement methods which handle the events of interest for the relevant
    mode, and name them appropriately. The name of a method handler must be
    of the form::


        _[modeName]Mode[eventName]


    where ``modeName`` is an identifier for the profile mode (see the
    :meth:`__init__` method), and ``eventName`` is one of the following:

      - ``LeftMouseMove``
      - ``LeftMouseDown``
      - ``LeftMouseDrag``
      - ``LeftMouseUp``
      - ``RightMouseMove``
      - ``RightMouseDown``
      - ``RightMouseDrag``
      - ``RightMouseUp``
      - ``MiddleMouseMove``
      - ``MiddleMouseDown``
      - ``MiddleMouseDrag``
      - ``MiddleMouseUp``
      - ``MouseWheel``
      - ``Char``

    Profiles for :class:`.CanvasPanel` views may also implement handlers
    for these events:

      - ``MouseEnter``
      - ``MouseLeave``

    And :class:`.PlotPanel` views may implement handlers for these events:

      - ``LeftMouseArtistPick``
      - ``RightMouseArtistPick``


    .. note:: The ``MouseEnter`` and ``MouseLeave`` events are not supported
              on :class:`.PlotPanel` views due to bugs in ``matplotlib``.


    For example, if a particular profile has defined a mode called ``nav``,
    and is interested in left clicks, the profile class must provide a method
    called ``_navModeLeftMouseDown``. Then, whenever the profile is in the
    ``nav`` mode, this method will be called on left mouse clicks.


    **Handler methods**


    The parameters that are passed to these methods differs slightly depending
    on the type of event:

     - All mouse events, with the exception of ``MouseWheel`` must have
       the following signature::

        def _[modeName]Mode[eventName](ev, canvas, mouseLoc, canvasLoc)

       where:

         - ``ev`` is the ``wx.Event`` object
         - ``canvas`` is the source canvas,
         - ``mouseLoc`` is the ``(x, y)`` mouse coordinates,
         - ``canvasLoc`` is the coordinates in the display/data coordinate
           system.

     - The ``MouseWheel`` handler must have the following signature::

        def _[modeName]ModeMouseWheel(ev, canvas, wheel, mouseLoc, canvasLoc)

       where ``wheel`` is a positive or negative number indicating how much
       the mouse wheel was moved.

     - ``Char`` events must have the following signature::

        def _[modeName]ModeChar(ev, canvas, key)

       where ``key`` is the key code of the key that was pressed.

     - Pick event handlers (only on :class:`.PlotPanel` views) must have
       the following signature::

        def _[modeName]Mode[eventType](ev, canvas, artist, mouseLoc, canvasLoc)

       where ``artist`` is the ``matplotlib`` artist that was picked.

    All handler methods should return ``True`` to indicate that the event was
    handled, or ``False`` if the event was not handled. This is particularly
    important for ``Char`` event handlers - we don't want ``Profile``
    sub-classes to be eating global keyboard shortcuts. A return value of
    ``None`` is interpreted as ``True``. If a handler returns ``False``, and
    a fallback handler is defined (see below), then that fallback handler will
    be called.


    **Extra handlers**


    Additional handlers can be registered for any event type via the
    :meth:`registerHandler` method. These handlers do not have to be methods
    of the ``Profile`` sub-class, and will be called for every occurrence of
    the event, regardless of the current mode. These handlers will be called
    after the standard handler method.


    When an extra handler is no longer needed, it must be removed via the
    :meth:`deregisterHandler` method.


    **Pre- and post- methods**


    A couple of other methods may be defined which, if they are present, will
    be called on all handled events:

     - ``_preEvent``
     - ``_postEvent``

    The ``_preEvent`` method will get called just before an event is passed
    to the handler. Likewise, the ``_postEvent`` method will get called
    just after the handler has been called. If no handlers for a particular
    event are defined, neither of these methods will be called.


    **Temporary, alternate and fallback handlers**


    The :mod:`.profilemap` module contains a set of dictionaries which define
    temporary, alternate, and fallback handlers.


    The :attr:`.profilemap.tempModeMap` defines, for each profile and each
    mod, a keyboard modifier which may be used to temporarily redirect
    mouse/keyboard events to the handlers for a different mode. For example,
    if while in ``nav`` mode, you would like the user to be able to switch to
    ``zoom`` mode with the control key, you can add a temporary mode map in
    the ``tempModeMap``. Additional temporary modes can be added via the
    :meth:`addTempMode` method.


    The :attr:`.profilemap.altHandlerMap`. dictionary allows you to re-use
    event handlers that have been defined for one mode in another mode. For
    example, if you would like right clicks in ``zoom`` mode to behave like
    left clicks in ``nav`` mode, you can set up such a mapping using the
    ``altHandlerMap`` dictionary. Additional alternate handlers can be added
    via the :meth:`addAltHandler` method.

    The :attr:`.profilemap.fallbackHandlerMap` dictionary allows you to
    define fallback handlers - if the default handler for a specific mode/event
    type returns a value of ``False``, the event will be forwarded to
    the fallback handler instead. Additional fallback handlers can be added
    via the :meth:`addFallbackHandler` method.


    **Actions and attributes**


    As the ``Profile`` class derives from the :class:`.ActionProvider`
    class, ``Profile`` subclasses may define properties and actions for
    the user to configure the profile behaviour, and/or to perform any
    relevant actions.


    The following instance attributes are present on a ``Profile`` instance,
    intended to be accessed by sub-classes:

    =============== =======================================================
    ``viewPanel``   The :class:`ViewPanel` which is using this ``Profile``.
    ``overlayList`` A :class:`.OverlayList` instance.
    ``displayCtx``  A :class:`.DisplayContext` instance.
    ``name``        A unique name for this ``Profile`` instance.
    =============== =======================================================
    """


    mode = props.Choice()
    """The current profile mode - by default this is empty, but subclasses
    may specify the choice options in the :class:`__init__` method.
    """


    def __init__(self,
                 viewPanel,
                 overlayList,
                 displayCtx,
                 modes=None):
        """Create a ``Profile`` instance.

        :arg viewPanel:   The :class:`.ViewPanel` instance for which this
                          ``Profile`` instance defines mouse/keyboard
                          interaction behaviour.

        :arg overlayList: The :class:`.OverlayList` instance which contains
                          the list of overlays being displayed.

        :arg displayCtx:  The :class:`.DisplayContext` instance which defines
                          how the overlays are to be displayed.

        :arg modes:       A sequence of strings, containing the mode
                          identifiers for this profile. These are added as
                          options on the :attr:`mode` property.
        """

        actions.ActionProvider     .__init__(self)
        props.SyncableHasProperties.__init__(self)

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

        import fsleyes.views.canvaspanel as canvaspanel
        import fsleyes.views.plotpanel   as plotpanel

        if isinstance(viewPanel, canvaspanel.CanvasPanel):
            self.__evtManager = CanvasPanelEventManager(self)

        elif isinstance(viewPanel, plotpanel.PlotPanel):
            self.__evtManager = PlotPanelEventManager(self)

        else:
            raise ValueError('Unrecognised view panel type: {}'.format(
                type(viewPanel).__name__))

        # Maps which define temporary modes and
        # alternate/fallback handlers when keyboard
        # modifiers are used, when a handler for
        # a particular event is not defined, or
        # when a handler indicates that the event
        # has not been handled.
        self.__tempModeMap        = {}
        self.__altHandlerMap      = {}
        self.__fallbackHandlerMap = {}

        # Extra handlers - for each event type,
        # a dictionary of { name : handler }
        # mappings which will be called after
        # the profile handler has been called
        # for a given event.
        self.__extraHandlers = collections.defaultdict(collections.OrderedDict)

        # of mouse/canvas event locations
        self.__lastCanvas       = None
        self.__lastMousePos     = None
        self.__lastCanvasPos    = None
        self.__lastMouseUpPos   = None
        self.__lastCanvasUpPos  = None
        self.__mouseDownPos     = None
        self.__canvasDownPos    = None

        # we keep track of the mode we
        # were in on mosue down events,
        # so the correct mode is called
        # on subsequent drag/up events.
        self.__mouseDownMode    = None

        # This field is used to keep
        # track of the last event for
        # which a handler was called.
        # After the first event, it
        # will be a tuple of strings
        # containing the (mode, event),
        # e.g. ('nav', 'LeftMouseMove').
        # This is set in the __getHandler
        # method.
        #
        # The lastMouseUpHandler field
        # is set in __onMouseUp, to
        # keep track of the last mouse
        # handler
        self.__lastHandler        = (None, None)
        self.__lastMouseUpHandler = (None, None)

        # Pre/post event handlers
        self.__preEventHandler  = getattr(self, '_preEvent',  None)
        self.__postEventHandler = getattr(self, '_postEvent', None)

        # Add all of the provided modes
        # as options to the mode property
        if modes is None:
            modes = []

        modeProp = self.getProp('mode')

        for mode in modes:
            modeProp.addChoice(mode, instance=self)

        if len(modes) > 0:
            self.mode = modes[0]

        # Configure temporary modes and alternate
        # event handlers - see the profilemap
        # module
        from . import profilemap

        # We reverse the mro, so that the
        # modes/handlers defined on this
        # class take precedence.
        for cls in reversed(inspect.getmro(self.__class__)):

            tempModes   = profilemap.tempModeMap       .get(cls, {})
            altHandlers = profilemap.altHandlerMap     .get(cls, {})
            fbHandlers  = profilemap.fallbackHandlerMap.get(cls, {})

            for (mode, keymod), tempMode in tempModes.items():
                self.addTempMode(mode, keymod, tempMode)

            for (mode, handler), (altMode, altHandler) in altHandlers.items():
                self.addAltHandler(mode, handler, altMode, altHandler)

            for (mode, handler), (fbMode, fbHandler) in fbHandlers.items():
                self.addFallbackHandler(mode, handler, fbMode, fbHandler)

        # The __onEvent method delegates all
        # events based on this dictionary
        self.__eventMap = {
            wx.EVT_LEFT_DOWN.typeId    : self.__onMouseDown,
            wx.EVT_MIDDLE_DOWN.typeId  : self.__onMouseDown,
            wx.EVT_RIGHT_DOWN.typeId   : self.__onMouseDown,
            wx.EVT_LEFT_UP.typeId      : self.__onMouseUp,
            wx.EVT_MIDDLE_UP.typeId    : self.__onMouseUp,
            wx.EVT_RIGHT_UP.typeId     : self.__onMouseUp,
            wx.EVT_MOTION.typeId       : self.__onMouseMove,
            wx.EVT_MOUSEWHEEL.typeId   : self.__onMouseWheel,
            wx.EVT_ENTER_WINDOW.typeId : self.__onMouseEnter,
            wx.EVT_LEAVE_WINDOW.typeId : self.__onMouseLeave,
            wx.EVT_CHAR.typeId         : self.__onChar,
        }

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


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


    def destroy(self):
        """This method must be called when this ``Profile`` is no longer
        needed - it is typically called by a :class:`ProfileManager`.

        Clears references to the display context, view panel, and overlay
        list, and calls :meth:`.ActionProvider.destroy`.
        """
        actions.ActionProvider.destroy(self)
        self.__lastCanvas    = None
        self.__viewPanel     = None
        self.__overlayList   = None
        self.__displayCtx    = None
        self.__extraHandlers = None


    @property
    def name(self):
        """Returns the name of this ``Profile``. """
        return self.__name


    @property
    def viewPanel(self):
        """Returns the :class:`.ViewPanel` associated with this ``Profile``.
        """
        return self.__viewPanel


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


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


    @property
    @deprecated.deprecated('0.16.0', '1.0.0', 'Use viewPanel instead')
    def _viewPanel(self):
        return self.__viewPanel


    @property
    @deprecated.deprecated('0.16.0', '1.0.0', 'Use name instead')
    def _name(self):
        return self.__name


    @property
    @deprecated.deprecated('0.16.0', '1.0.0', 'Use displayCtx instead')
    def _displayCtx(self):
        return self.__displayCtx


    @property
    @deprecated.deprecated('0.16.0', '1.0.0', 'Use overlayList instead')
    def _overlayList(self):
        return self.__overlayList


    def getEventTargets(self):
        """Must be overridden by subclasses, to return a sequence of
        :mod:`wx` objects that are the targets of mouse/keyboard interaction.

        .. note:: It is currently assumed that all of the objects in the
                  sequence derive from the :class:`.SliceCanvas` class.
        """
        raise NotImplementedError('Profile subclasses must implement '
                                  'the getEventTargets method')


    def getMouseDownLocation(self):
        """If the mouse is currently down, returns a 2-tuple containing the
        x/y mouse coordinates, and the corresponding 3D display space
        coordinates, of the mouse down event. Otherwise, returns
        ``(None, None)``.
        """
        return self.__mouseDownPos, self.__canvasDownPos


    def getLastMouseLocation(self):
        """Returns a 2-tuple containing the most recent x/y mouse coordinates,
        and the corresponding 3D display space coordinates.
        """
        return self.__lastMousePos, self.__lastCanvasPos


    def getLastMouseUpLocation(self):
        """Returns a 2-tuple containing the most recent x/y mouse up event
        coordinates, and the corresponding 3D display space coordinates.
        """
        return self.__lastMouseUpPos, self.__lastCanvasUpPos


    def getLastMouseUpHandler(self):
        """Returns a tuple of two strings specifying the ``(mode, eventType)``
        of the most recent mouse up event that was handled. If no events have
        been handled, returns ``(None, None)``.
        """
        return self.__lastMouseUpHandler


    def getLastHandler(self):
        """Returns a tuple of two strings specifying the ``(mode, eventType)``
        of the most recent event that was handled. If no events have been
        handled, returns ``(None, None)``.
        """
        return self.__lastHandler


    def getLastCanvas(self):
        """Returns a reference to the canvas which most recently generated
        a mouse down or up event.
        """
        return self.__lastCanvas


    def getMplEvent(self):
        """If this ``Profile`` object is associated with a :class:`.PlotPanel`,
        this method will return the last ``matplotlib`` event that was
        generated. Otherwise, this method returns ``None``.

        This method can be called from within an event handler to retrieve
        the current ``matplotlib`` event. See the
        :meth:`PlotPanelEventManager.getMplEvent` method.
        """
        if isinstance(self.__evtManager, PlotPanelEventManager):
            return self.__evtManager.getMplEvent()
        else:
            return None


    def addTempMode(self, mode, modifier, tempMode):
        """Add a temporary mode to this ``Profile``, in addition to those
        defined in the :attr:`.profilemap.tempModeMap` dictionary.

        :arg mode:     The mode to change from.

        :arg modifier: A keyboard modifier which will temporarily
                       change the mode from ``mode`` to ``tempMode``.

        :arg tempMode: The temporary mode which the ``modifier`` key will
                       change into.
        """
        self.__tempModeMap[mode, modifier] = tempMode


    def addAltHandler(self, mode, event, altMode, altEvent):
        """Add an alternate handler to this ``Profile``, in addition to
        those already defined in the :attr:`.profilemap.altHandleMap`
        dictionary.

        :arg mode:     The source mode.

        :arg event:    Name of the event to handle (e.g. ``LeftMouseDown``).

        :arg altMode:  The mode for which the handler is defined.

        :arg altEvent: The event name for which the handler is defined.
        """
        self.__altHandlerMap[mode, event] = (altMode, altEvent)


    def addFallbackHandler(self, mode, event, fbMode, fbEvent):
        """Add a fallback handler to this ``Profile``, in addition to
        those already defined in the :attr:`.profilemap.fallbackHandleMap`
        dictionary.

        :arg mode:    The source mode.

        :arg event:   Name of the event to handle (e.g. ``LeftMouseDown``).

        :arg fbMode:  The mode for which the handler is defined.

        :arg fbEvent: The event name for which the handler is defined.
        """
        self.__fallbackHandlerMap[mode, event] = (fbMode, fbEvent)


    def register(self):
        """This method must be called to register this ``Profile``
        instance as the target for mouse/keyboard events. This method
        is called by the :class:`ProfileManager`.

        Subclasses may override this method to performa any initialisation,
        but must make sure to call this implementation.
        """
        self.__evtManager.register()


    def deregister(self):
        """This method de-registers this :class:`Profile` instance from
        receiving mouse/keybouard events. This method is called by the
        :class:`ProfileManager`.


        Subclasses may override this method to performa any initialisation,
        but must make sure to call this implementation.
        """
        self.__evtManager.deregister()


    def registerHandler(self, event, name, handler):
        """Add an extra handler for the specified event.

        When the event occurs, The ``handler`` function will be called after
        the default handler, provided by the  ``Profie`` sub-class, is called.

        :arg event:   The event type (e.g. ``LeftMouseDown``).

        :arg name:    A unique name for the handler. A ``KeyError`` will be
                      raised if a handler with ``name`` is already registered.

        :arg handler: Function to call when the event occurs. See the class
                      documentation for details on the required signature.
        """

        if name in self.__extraHandlers[event]:
            raise KeyError('A handler with name "{}" is '
                           'already registered'.format(name))

        self.__extraHandlers[event][name] = handler


    def deregisterHandler(self, event, name):
        """Remove an extra handler from the specified event, that was previously
        added via :meth:`registerHandler`

        :arg event:   The event type (e.g. ``LeftMouseDown``).

        :arg name:    A unique name for the handler. A ``KeyError`` will be
                      raised if a handler with ``name`` is already registered.
        """
        if self.__extraHandlers is not None:
            self.__extraHandlers[event].pop(name)


    def handleEvent(self, ev):
        """Called by the event manager when any event occurs on any of
        the :class:`.ViewPanel` targets. Delegates the event to one of
        the handler functions.

        :arg ev: The ``wx.Event`` that occurred.
        """

        evType  = ev.GetEventType()
        source  = ev.GetEventObject()
        handler = self.__eventMap.get(evType, None)

        if source not in self.getEventTargets(): return
        if handler is None:                      return

        if evType in (wx.EVT_LEFT_DOWN  .typeId,
                      wx.EVT_MIDDLE_DOWN.typeId,
                      wx.EVT_RIGHT_DOWN .typeId,
                      wx.EVT_LEFT_UP    .typeId,
                      wx.EVT_MIDDLE_UP  .typeId,
                      wx.EVT_RIGHT_UP   .typeId):
            self.__lastCanvas = source
        handler(ev)


    def handlePickEvent(self, ev):
        """Called by the :class:`PlotPanelEventManager` when a ``matplotlib``
        ``pick_event`` occurs.
        """

        self.__onPick(ev)


    def __getTempMode(self, ev):
        """Checks the temporary mode map to see if a temporary mode should
        be applied. Returns the mode identifier, or ``None`` if no temporary
        mode is applicable.
        """

        mode  = self.mode
        alt   = ev.AltDown()
        ctrl  = ev.ControlDown()
        shift = ev.ShiftDown()

        # Figure out the dictionary key to use,
        # based on the modifier keys that are down
        keys  = {
            (False, False, False) :  None,
            (False, False, True)  :  wx.WXK_SHIFT,
            (False, True,  False) :  wx.WXK_CONTROL,
            (False, True,  True)  : (wx.WXK_CONTROL, wx.WXK_SHIFT),
            (True,  False, False) :  wx.WXK_ALT,
            (True,  False, True)  : (wx.WXK_ALT, wx.WXK_SHIFT),
            (True,  True,  False) : (wx.WXK_ALT, wx.WXK_CONTROL),
            (True,  True,  True)  : (wx.WXK_ALT, wx.WXK_CONTROL, wx.WXK_SHIFT)
        }

        return self.__tempModeMap.get((mode, keys[alt, ctrl, shift]), None)


    def __getMouseLocation(self, ev):
        """Returns two tuples; the first contains the x/y coordinates of the
        given :class:`wx.MouseEvent`, and the second contains the
        corresponding x/y/z display space coordinates (for
        :class:`.CanvasPanel` views), or x/y data coordinates (for
        :class:`.PlotPanel` views).

        See the :meth:`CanvasPanelEventManager.getMouseLocation` and
        :meth:`PlotPanelEventManager.getMouseLocation` methods.
        """
        return self.__evtManager.getMouseLocation(ev)


    def __getMouseButton(self, ev):
        """Returns a string describing the mouse button associated with the
        given :class:`wx.MouseEvent`.
        """

        btn = ev.GetButton()
        if   btn == wx.MOUSE_BTN_LEFT:   return 'Left'
        elif btn == wx.MOUSE_BTN_RIGHT:  return 'Right'
        elif btn == wx.MOUSE_BTN_MIDDLE: return 'Middle'
        elif ev.LeftIsDown():            return 'Left'
        elif ev.RightIsDown():           return 'Right'
        elif ev.MiddleIsDown():          return 'Middle'
        else:                            return  None


    def __getMode(self, ev):
        """Returns the current profile mode - either the value of
        :attr:`mode`, or a temporary mode if one is active.
        """

        # Is a temporary mode active?
        tempMode = self.__getTempMode(ev)

        if tempMode is None: return self.mode
        else:                return tempMode


    def __getHandler(self,
                     ev,
                     evType,
                     mode=None,
                     origEvType=None,
                     direct=False):
        """Returns a function which will handle the given
        :class:`wx.MouseEvent` or :class:`wx.KeyEvent` (the ``ev`` argument),
        or ``None`` if no handlers are found.

        If an alternate handler for the mode/event has been specified, it is
        returned.

        :arg ev:         The event object

        :arg evType:     The event type (e.g. ``'LeftMouseDown'``)

        :arg mode:       Override the default mode with this one. If not
                         provided, the handler for the current mode (or
                         temporary mode, if one is active) will be used.

        :arg origEvType: If the ``evType`` is not the actual event that
                         occurred (e.g. this method has been called to look
                         up an alternate or fallback handler), the original
                         event type must be passed in here.

        :arg direct:     If ``False`` (the default), the returned function will
                         call the standard event handler (a method of the
                         ``Profile`` sub-class), its fallback handler if it
                         returns ``False`` and a fallback has been specfiied,
                         any extra handlers that have been registered for the
                         event type, and will also call the pre- and post-
                         event methods. Otherwise, the returned function will
                         just be the sub-class handler method for the
                         specified for ``evType/ ``mode``.
        """

        if origEvType is None:
            origEvType = evType

        if mode is None:
            mode = self.__getMode(ev)

        # Lookup any alternate/fallback
        # handlers for the event
        alt      = self.__altHandlerMap     .get((mode, evType), None)
        fallback = self.__fallbackHandlerMap.get((mode, evType), None)

        # Is an alternate handler active?
        # Alternate handlers take precedence
        # over default handlers.
        if alt is not None:
            altMode, altEvType = alt
            return self.__getHandler(ev,
                                     altEvType,
                                     mode=altMode,
                                     origEvType=evType)

        # A fallback handler has
        # been specified for this
        # event - get a direct ref
        # to the fallback function
        if fallback is not None:
            fbMode, fbEvType = fallback
            fallback = self.__getHandler(ev,
                                         fbEvType,
                                         mode=fbMode,
                                         origEvType=evType,
                                         direct=True)

        # Search for a default method
        # which can handle the specified
        # mode/evtype.
        if mode is not None:
            handlerName = '_{}Mode{}'.format(mode, evType)
        else:
            handlerName = '_{}{}'.format(evType[0].lower(), evType[1:])

        defHandler = getattr(self, handlerName, None)

        # If direct=True, we just
        # return the handler method,
        # even it there isn't one
        # defined.
        if direct:
            return defHandler

        # Otherwise we return a wrapper
        # which calls the pre- and post-
        # methods, and any extra handlers
        # that have been registered,
        handlers = []

        # Insert a placeholder for the
        # default handler, because we
        # need to check its return value.
        if defHandler is not None:
            handlers.append('defHandler')

        handlers.extend(self.__extraHandlers[origEvType].values())

        def handlerWrapper(*args, **kwargs):

            retval = None

            if self.__preEventHandler is not None:
                self.__preEventHandler(mode, evType)

            for handler in handlers:
                # Get the return value of the
                # default handler, and call its
                # fallback if necessary.
                if handler == 'defHandler':
                    retval = defHandler(*args, **kwargs)

                    if retval is False and fallback is not None:
                        retval = fallback(*args, **kwargs)

                else:
                    handler(*args, **kwargs)

            if self.__postEventHandler is not None:
                self.__postEventHandler(mode, evType)

            # Store the last event
            # that was processed
            self.__lastHandler = (mode, evType)

            return retval

        if len(handlers) > 0:
            log.debug('{} Handler(s) found for mode {}, event {}'.format(
                len(handlers), mode, evType))
            return handlerWrapper

        return None


    def __onMouseWheel(self, ev):
        """Called when the mouse wheel is moved.

        Delegates to a mode specific handler if one is present.
        """

        handler = self.__getHandler(ev, 'MouseWheel')
        if handler is None:
            return

        mouseLoc, canvasLoc = self.__getMouseLocation(ev)
        canvas              = ev.GetEventObject()
        wheel               = ev.GetWheelRotation()

        # wx/osx has this really useful feature
        # whereby if shift is being held down
        # (typically used for horizontal scrolling),
        # a mouse wheel direction which would have
        # produced positive values will now produce
        # negative values.
        if ev.ShiftDown() and \
           fslplatform.wxPlatform in (fslplatform.WX_MAC_COCOA,
                                      fslplatform.WX_MAC_CARBON):
            wheel = -wheel

        log.debug('Mouse wheel event ({}) on {}'.format(
            wheel, type(canvas).__name__))

        handler(ev, canvas, wheel, mouseLoc, canvasLoc)


    def __onMouseEnter(self, ev):
        """Called when the mouse enters a canvas target.

        Delegates to a mode specific handler if one is present.
        """

        handler = self.__getHandler(ev, 'MouseEnter')
        if handler is None:
            return

        canvas              = ev.GetEventObject()
        mouseLoc, canvasLoc = self.__getMouseLocation(ev)

        log.debug('Mouse enter event on {}'.format(
            type(canvas).__name__))

        handler(ev, canvas, mouseLoc, canvasLoc)


    def __onMouseLeave(self, ev):
        """Called when the mouse leaves a canvas target.

        Delegates to a mode specific handler if one is present.
        """

        handler = self.__getHandler(ev, 'MouseLeave')
        if handler is None:
            return

        canvas              = ev.GetEventObject()
        mouseLoc, canvasLoc = self.__getMouseLocation(ev)

        log.debug('Mouse leave event on {}'.format(
            type(canvas).__name__))

        handler(ev, canvas, mouseLoc, canvasLoc)


    def __onMouseDown(self, ev):
        """Called when any mouse button is pushed.

        Delegates to a mode specific handler if one is present.
        """

        mouseLoc, canvasLoc = self.__getMouseLocation(ev)
        canvas              = ev.GetEventObject()

        # On GTK, a GLCanvas won't be given
        # focus when it is clicked on.
        canvas.SetFocus()

        # Save information about this mouse
        # down event, as it may be needed
        # by subesquent drag/up events.
        self.__mouseDownPos  = mouseLoc
        self.__canvasDownPos = canvasLoc
        self.__mouseDownMode = self.__getMode(ev)

        if self.__lastMousePos  is None: self.__lastMousePos  = mouseLoc
        if self.__lastCanvasPos is None: self.__lastCanvasPos = canvasLoc

        evType  = '{}MouseDown'.format(self.__getMouseButton(ev))
        handler = self.__getHandler(ev, evType)
        if handler is None:
            ev.Skip()
            return

        log.debug('{} event ({}, {}) on {}'.format(
            evType, mouseLoc, canvasLoc, type(canvas).__name__))

        # If a handler returns None, we
        # assume that it means True
        if handler(ev, canvas, mouseLoc, canvasLoc) is False:
            ev.Skip()

        self.__lastMousePos  = mouseLoc
        self.__lastCanvasPos = canvasLoc


    def __onMouseUp(self, ev):
        """Called when any mouse button is released.

        Delegates to a mode specific handler if one is present.
        """

        evType  = '{}MouseUp'.format(self.__getMouseButton(ev))
        handler = self.__getHandler(ev, evType, mode=self.__mouseDownMode)

        if handler is None:
            self.__mouseDownPos  = None
            self.__canvasDownPos = None
            self.__mouseDownMode = None
            ev.Skip()
            return

        canvas              = ev.GetEventObject()
        mouseLoc, canvasLoc = self.__getMouseLocation(ev)

        log.debug('{} event ({}, {}) on {}'.format(
            evType, mouseLoc, canvasLoc, type(canvas).__name__))

        if handler(ev, canvas, mouseLoc, canvasLoc) is False:
            ev.Skip()

        self.__lastMouseUpHandler = (self.__mouseDownMode, evType)
        self.__lastMouseUpPos     = mouseLoc
        self.__lastCanvasUpPos    = canvasLoc
        self.__mouseDownPos       = None
        self.__canvasDownPos      = None
        self.__mouseDownMode      = None


    def __onMouseMove(self, ev):
        """Called on mouse motion. If a mouse button is down, delegates to
        :meth:`__onMouseDrag`.

        Otherwise, delegates to a mode specific handler if one is present.
        """

        if ev.Dragging():
            self.__onMouseDrag(ev)
            return

        handler = self.__getHandler(ev, 'MouseMove')

        if handler is None:
            ev.Skip()
            return

        canvas              = ev.GetEventObject()
        mouseLoc, canvasLoc = self.__getMouseLocation(ev)

        log.debug('Mouse move event ({}, {}) on {}'.format(
            mouseLoc, canvasLoc, type(canvas).__name__))

        if handler(ev, canvas, mouseLoc, canvasLoc) is False:
            ev.Skip()

        self.__lastMousePos  = mouseLoc
        self.__lastCanvasPos = canvasLoc


    def __onMouseDrag(self, ev):
        """Called on mouse drags.

        Delegates to a mode specific handler if one is present.
        """
        ev.Skip()

        canvas              = ev.GetEventObject()
        mouseLoc, canvasLoc = self.__getMouseLocation(ev)

        evType  = '{}MouseDrag'.format(self.__getMouseButton(ev))
        handler = self.__getHandler(ev, evType, mode=self.__mouseDownMode)
        if handler is None:
            ev.Skip()
            return

        log.debug('{} event ({}, {}) on {}'.format(
            evType, mouseLoc, canvasLoc, type(canvas).__name__))

        if handler(ev, canvas, mouseLoc, canvasLoc) is False:
            ev.Skip()

        self.__lastMousePos  = mouseLoc
        self.__lastCanvasPos = canvasLoc


    def __onChar(self, ev):
        """Called on keyboard key presses.

        Delegates to a mode specific handler if one is present.
        """

        handler = self.__getHandler(ev, 'Char')
        if handler is None:
            ev.Skip()
            return

        canvas = ev.GetEventObject()
        key    = ev.GetKeyCode()

        log.debug('Keyboard event ({}) on {}'.format(
            key, type(canvas).__name__))

        if handler(ev, canvas, key) is False:
            ev.Skip()


    def __onPick(self, ev):
        """Called by the :meth:`handlePickEvent`. Delegates the event to a
        suitable handler, if one exists.
        """

        evType  = '{}MouseArtistPick'.format(self.__getMouseButton(ev))
        handler = self.__getHandler(ev, evType)
        if handler is None:
            ev.Skip()
            return

        canvas              = ev.GetEventObject()
        artist              = self.__evtManager.getPickedArtist()
        mouseLoc, canvasLoc = self.__getMouseLocation(ev)

        log.debug('Pick event ({}, {}) on {}'.format(
            mouseLoc, canvasLoc, type(canvas).__name__))

        if handler(ev, canvas, artist, mouseLoc, canvasLoc) is False:
            ev.Skip()