Esempio n. 1
0
class LineVectorOpts(VectorOpts):
    """The ``LineVectorOpts`` class contains settings for displaying vector
    images, using a line to represent the vector value at each voxel.
    """


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

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


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


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


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

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

        VectorOpts.__init__(self, *args, **kwargs)
Esempio n. 2
0
class ComplexHistogramSeries(ImageHistogramSeries):
    """Thre ``ComplexHistogramSeries`` class is a specialisation of the
    :class:`ImageHistogramSeries` for images with a complex data type.

    See also the :class:`.ComplexTimeSeries` and
    :class:`.ComplexPowerSpectrumSeries` classes.
    """

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

    def __init__(self, *args, **kwargs):
        """Create a ``ComplexHistogramSeries``. All arguments are passed
        through to the ``ImageHistogramSeries`` constructor.
        """
        ImageHistogramSeries.__init__(self, *args, **kwargs)

        self.__imaghs = ImaginaryHistogramSeries(*args, **kwargs)
        self.__maghs = MagnitudeHistogramSeries(*args, **kwargs)
        self.__phasehs = PhaseHistogramSeries(*args, **kwargs)

        for hs in (self.__imaghs, self.__maghs, self.__phasehs):
            hs.colour = fslcm.randomDarkColour()
            hs.bindProps('alpha', self)
            hs.bindProps('lineWidth', self)
            hs.bindProps('lineStyle', self)
            hs.bindProps('autoBin', self)
            hs.bindProps('ignoreZeros', self)
            hs.bindProps('includeOutliers', self)

    def extraSeries(self):
        """Returns a list containing an :class:`ImaginaryHistogramSeries`,
        :class:`MagnitudeHistogramSeries`, and/or
        :class:`PhaseHistogramSeries`, depending on the values of the
        :attr:`plotImaginary`, :attr:`plotMagnitude`, and :attr:`plotPhase`
        properties.
        """
        extras = []
        if self.plotImaginary: extras.append(self.__imaghs)
        if self.plotMagnitude: extras.append(self.__maghs)
        if self.plotPhase: extras.append(self.__phasehs)
        return extras

    def getData(self):
        """Overrides :meth:`HistogramSeries.setHistogramData`. If
        :attr:`plotReal` is ``False``, returns ``(None, None)``. Otherwise
        returns the parent class implementation.
        """
        if self.plotReal: return ImageHistogramSeries.getData(self)
        else: return None, None

    def setHistogramData(self, data, key):
        """Overrides :meth:`HistogramSeries.setHistogramData`.  The real
        component of the data is passed to the parent class implementation.
        """
        data = data.real
        ImageHistogramSeries.setHistogramData(self, data, key)
Esempio n. 3
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)
Esempio n. 4
0
class Scene3DCanvasOpts(props.HasProperties):
    """The ``Scene3DCanvasOpts`` class defines the display settings
    available on :class:`.Scene3DCanvas` instances.
    """

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


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


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


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


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


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


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


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


    rotation = props.Array(
        dtype=np.float64,
        shape=(3, 3),
        resizable=False,
        default=[[1, 0, 0], [0, 1, 0], [0, 0, 1]])
    """A rotation matrix which defines the current ``Scene3DCanvas`` view
Esempio n. 5
0
class VolumeRGBOpts(niftiopts.NiftiOpts):
    """The ``VolumeRGBOpts`` class is intended for displaying
    :class:`.Image` instances containing RGB(A) data.
    """

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

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

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

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

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

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

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

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

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

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

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

        niftiopts.NiftiOpts.__init__(self, overlay, display, overlayList,
                                     displayCtx, **kwargs)
    class Thing(props.SyncableHasProperties):
        crange = props.Bounds(ndims=1, clamped=False)
        drange = props.Bounds(ndims=1, clamped=False)
        linklo = props.Boolean(default=True)

        def __init__(self, *args, **kwargs):
            props.SyncableHasProperties.__init__(self, *args, **kwargs)
            parent = self.getParent()

            if parent is not None:
                self.addListener('linklo', str(id(self)), self.__linkloChanged)
                if self.linklo: self.__linkloChanged()

        def __linkloChanged(self, *a):
            self.__updateLink(self.linklo, 0)

        def __updateLink(self, val, idx):

            drangePV = self.drange.getPropertyValueList()[idx]
            crangePV = self.crange.getPropertyValueList()[idx]

            if props.propValsAreBound(drangePV, crangePV) == val:
                return

            props.bindPropVals(drangePV,
                               crangePV,
                               bindval=True,
                               bindatt=False,
                               unbind=not val)

            if val:
                crangePV.set(drangePV.get())
Esempio n. 7
0
class TensorOpts(vectoropts.VectorOpts):
    """The ``TensorOpts`` class defines options for displaying :class:`.GLTensor`
    instances.
    """

    lighting = props.Boolean(default=True)
    """Enables a basic lighting model on the tensor ellipsoids. """

    tensorResolution = props.Int(minval=4, maxval=20, default=10)
    """Tensor ellipsoid resolution - this value controls the number of vertices
    used to represent each tensor. It is ultimately passed to the
    :func:`.routines.unitSphere` function.
    """

    tensorScale = props.Percentage(minval=50, maxval=600, default=100)
    """Scaling factor - by default, the tensor ellipsoids are scaled such that
    the biggest tensor (as defined by the first principal eigenvalue) fits
    inside a voxel. This parameter can be used to adjust this scale.
    """
    def __init__(self, *args, **kwargs):
        """Create a ``TensorOpts`` instance. All arguments are passed through
        to :meth:`.VectorOpts.__init__`.
        """

        vectoropts.VectorOpts.__init__(self, *args, **kwargs)
Esempio n. 8
0
class RGBVectorOpts(VectorOpts):
    """The ``RGBVectorOpts`` class contains settings for displaying vector
    images, using a combination of three colours to represent the vector value
    at each voxel.
    """


    interpolation = copy.copy(volumeopts.VolumeOpts.interpolation)
    """Apply interpolation to the image data. """


    unitLength = props.Boolean(default=False)
    """If ``True``, the vector data is scaled so it has length 1. """


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

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

        kwargs['nounbind'] = ['interpolation']
        VectorOpts.__init__(self, *args, **kwargs)
Esempio n. 9
0
class LightBoxCanvasOpts(SliceCanvasOpts):
    """The ``LightBoxCanvasOpts`` class defines the display settings
    available on :class:`.LightBoxCanvas` instances.
    """


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


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


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


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


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


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


    highlightSlice = props.Boolean(default=False)
    """If ``True``, a box will be drawn around the slice containing the current
Esempio n. 10
0
class PowerSpectrumSeries(dataseries.DataSeries):
    """The ``PowerSpectrumSeries`` encapsulates a power spectrum data series
    from an overlay. The ``PowerSpectrumSeries`` class is the base class for
    all other classes in this module. It provides the :meth:`calcPowerSpectrum`
    method which (surprisingly) calculates the power spectrum of a data
    series.
    """

    varNorm = props.Boolean(default=True)
    """If ``True``, the data is normalised to unit variance before the fourier
    transformation.
    """
    def __init__(self, overlay, overlayList, displayCtx, plotPanel):
        """Create a ``PowerSpectrumSeries``.

        :arg overlay:     The overlay from which the data to be plotted is
                          retrieved.
        :arg overlayList: The :class:`.OverlayList` instance.
        :arg displayCtx:  The :class:`.DisplayContext` instance.
        :arg plotPanel:   The :class:`.PlotPanel` that owns this
                          ``PowerSpectrumSeries``.
        """
        dataseries.DataSeries.__init__(self, overlay, overlayList, displayCtx,
                                       plotPanel)

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

    def makeLabel(self):
        """Returns a label that can be used for this ``PowerSpectrumSeries``.
        """
        display = self.displayCtx.getDisplay(self.overlay)
        return display.name

    def calcPowerSpectrum(self, data):
        """Calculates a power spectrum for the given one-dimensional data
        array. If the :attr:`varNorm` property is ``True``, the data is
        de-meaned and normalised by its standard deviation before the fourier
        transformation.
        """
        if self.varNorm:
            mean = data.mean()
            std = data.std()

            if not np.isclose(std, 0):
                data = data - mean
                data = data / std
            else:
                data = np.zeros(data.shape)

        data = fft.rfft(data)[1:]
        data = np.power(data.real, 2) + np.power(data.imag, 2)

        return data
    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()
Esempio n. 12
0
class PowerSpectrumSeries(object):
    """The ``PowerSpectrumSeries`` encapsulates a power spectrum data series
    from an overlay. The ``PowerSpectrumSeries`` class is a base mixin class
    for all other classes in this module.
    """

    varNorm = props.Boolean(default=True)
    """If ``True``, the data is normalised to unit variance before the fourier
    transformation.
    """
    @property
    def sampleTime(self):
        """Returns the time between time series samples for the overlay
        data. """
        if isinstance(self.overlay, fslmelimage.MelodicImage):
            return self.overlay.tr
        elif isinstance(self.overlay, fslimage.Image):
            return self.overlay.pixdim[3]
        else:
            return 1
Esempio n. 13
0
class PowerSpectrumSeries:
    """The ``PowerSpectrumSeries`` encapsulates a power spectrum data series
    from an overlay. The ``PowerSpectrumSeries`` class is a base mixin class
    for all other classes in this module.
    """

    varNorm = props.Boolean(default=False)
    """If ``True``, the fourier-transformed data is normalised to the range
    [0, 1] before plotting.

    .. note:: The :class:`ComplexPowerSpectrumSeries` applies normalisation
              differently.
    """
    @property
    def sampleTime(self):
        """Returns the time between time series samples for the overlay
        data. """
        if isinstance(self.overlay, fslmelimage.MelodicImage):
            return self.overlay.tr
        elif isinstance(self.overlay, fslimage.Image):
            return self.overlay.pixdim[3]
        else:
            return 1
Esempio n. 14
0
class VectorOpts(niftiopts.NiftiOpts):
    """The ``VectorOpts`` class is the base class for :class:`LineVectorOpts`,
    :class:`RGBVectorOpts`, :class:`.TensorOpts`, and :class:`.SHOpts`. It
    contains display settings which are common to each of them.


    *A note on orientation*


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


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


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


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


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


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


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


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


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


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


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

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


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


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


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


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


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


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


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

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

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

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

        if self.__registered:

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

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

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


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

        niftiopts.NiftiOpts.destroy(self)


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

        image = self.clipImage

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

        minval, maxval = image.dataRange

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

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


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

        image = self.modulateImage

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

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


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

        overlays = self.displayCtx.getOrderedOverlays()

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

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


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

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

        options = [None]

        for overlay in overlays:

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

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

            options.append(overlay)

        prop.setChoices(options, instance=self)

        if val in options: setattr(self, imageName, val)
        else:              setattr(self, imageName, None)
Esempio n. 15
0
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()
Esempio n. 16
0
class ColourMapOpts(object):
    """The ``ColourMapOpts`` class is a mixin for use with
    :class:`.DisplayOpts` sub-classes. It provides properties and logic
    for displaying overlays which are coloured according to some data values.
    See the :class:`.MeshOpts` and :class:`.VolumeOpts` classes for examples
    of classes which inherit from this class.


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

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

             class MyOpts(DisplayOpts, ColourMapOpts):
                 ...

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

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

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

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


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


    ``ColourMapOpts`` instances provide the following methods:

    .. autosummary::
       :nosignatures:

       updateDataRange
       getDataRange
       getClippingRange
    """


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

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


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

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

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

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


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


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


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

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

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

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


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


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


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


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

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


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


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


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


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

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

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


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

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

        if self.__registered:

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

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

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

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

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


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

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


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

        if not self.__registered:
            return

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

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

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

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


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

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

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


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

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

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


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


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

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

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

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

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

        absolute = self.useNegativeCmap
        drmin    = dataMin
        drmax    = dataMax

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

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

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

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

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

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

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

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

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

        props.safeCall(doUpdate)


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

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

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

        parent = self.getParent()

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

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

        for peer in peers:

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

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


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

        Updates the :attr:`displayRange` property accordingly.

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

        dataRange = self.getDataRange()

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

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


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

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

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

        if self.useNegativeCmap:
            return

        dataRange = self.getDataRange()

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

        self.__toggleListeners(False)

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

        self.__toggleListeners(True)


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

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

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

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


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


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


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

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

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

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

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

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

        if val:
            cRangePV.set(dRangePV.get())
Esempio n. 17
0
class ToggleAction(Action):
    """A ``ToggleAction`` an ``Action`` which is intended to encapsulate
    actions that toggle some sort of state. For example, a ``ToggleAction``
    could be used to encapsulate an action which opens and/or closes a dialog
    window.
    """

    toggled = props.Boolean(default=False)
    """Boolean which tracks the current state of the ``ToggleAction``. """
    def __init__(self, *args, **kwargs):
        """Create a ``ToggleAction``. All arguments are passed to
        :meth:`Action.__init__`.
        """

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

        self.addListener('toggled',
                         'ToggleAction_{}_internal'.format(id(self)),
                         self.__toggledChanged)

    def __call__(self, *args, **kwargs):
        """Call this ``ToggleAction``. The value of the :attr:`toggled` property
        is flipped.
        """

        # Copy the toggled value before running
        # the action, in case it gets inadvertently
        # changed
        toggled = self.toggled
        result = Action.__call__(self, *args, **kwargs)
        self.toggled = not toggled

        return result

    def bindToWidget(self, parent, evType, widget, wrapper=None):
        """Bind this ``ToggleAction`` to a widget. If the widget is a
        ``wx.MenuItem``, its ``Check`` is called whenever the :attr:`toggled`
        state changes.
        """

        Action.bindToWidget(self, parent, evType, widget, wrapper)
        self.__setState(widget)

    def __setState(self, widget):
        """Sets the toggled state of the given widget to the current value of
        :attr:`toggled`.
        """

        import wx
        import fsleyes_widgets.bitmaptoggle as bmptoggle

        if isinstance(widget, wx.MenuItem):
            widget.Check(self.toggled)
        elif isinstance(
                widget,
            (wx.CheckBox, wx.ToggleButton, bmptoggle.BitmapToggleButton)):
            widget.SetValue(self.toggled)

    def __toggledChanged(self, *a):
        """Internal method called when :attr:`toggled` changes. Updates the
        state of any bound widgets.
        """

        for bw in list(self.getBoundWidgets()):

            # An error will be raised if a widget
            # has been destroyed, so we'll unbind
            # any widgets which no longer exist.
            try:

                if not bw.isAlive():
                    raise Exception()

                self.__setState(bw.widget)

            except:
                self.unbindWidget(bw.widget)
Esempio n. 18
0
 class Boo(props.SyncableHasProperties):
     mybool = props.Boolean()
Esempio n. 19
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
Esempio n. 20
0
class OrthoEditToolBar(ctrlpanel.ControlToolBar):
    """The ``OrthoEditToolBar`` is a :class:`.ControlToolBar` which displays
    controls for editing :class:`.Image` instances in an :class:`.OrthoPanel`.

    An ``OrthoEditToolBar`` looks something like this:


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


    The ``OrthoEditToolBar`` exposes properties and actions which are defined
    on the :class:`.OrthoEditProfile` class, and allows the user to:

     - Change the :class:`.OrthoPanel` profile  between ``view`` and ``edit``
       mode (see the :attr:`.ViewPanel.profile` property). When in ``view``
       mode, all of the other controls are hidden.

     - Undo/redo changes to the selection and to :class:`.Image` instances.

     - Clear and fill the current selection.

     - Switch between a 2D and 3D selection cursor.

     - Change the selection cursor size.

     - Create a new mask/ROI :class:`.Image` from the current selection.

     - Switch between regular *select* mode, and *select by intensity* mode,
       and adjust the select by intensity mode settings.


    All of the controls shown on an ``OrthoEditToolBar`` instance are defined
    in the :attr:`_TOOLBAR_SPECS` dictionary.
    """

    selint = props.Boolean(default=False)
    """This property allows the user to change the :class:`.OrthoEditProfile`
    between ``sel`` mode, and ``selint`` mode.
    """
    def __init__(self, parent, overlayList, displayCtx, frame, ortho):
        """Create an ``OrthoEditToolBar``.

        :arg parent:      The :mod:`wx` parent object.
        :arg overlayList: The :class:`.OverlayList` instance.
        :arg displayCtx:  The :class:`.DisplayContext` instance.
        :arg frame:       The :class:`.FSLeyesFrame` instance.
        :arg ortho:       The :class:`.OrthoPanel` instance.
        """
        ctrlpanel.ControlToolBar.__init__(self,
                                          parent,
                                          overlayList,
                                          displayCtx,
                                          frame,
                                          height=24,
                                          kbFocus=True)

        self.__orthoPanel = ortho
        self.__dsWarning = dswarning.DisplaySpaceWarning(
            self, self.overlayList, self.displayCtx, self.frame,
            strings.messages[self, 'dsWarning'], 'not like overlay', 'overlay')

        ortho.addListener('profile', self.name, self.__profileChanged)

        self.__profileChanged()

    def destroy(self):
        """Must be called when this ``OrthoEditToolBar`` is no longer
        needed. Removes property listeners, and calls the
        :meth:`.ControlToolBar.destroy` method.
        """
        self.__orthoPanel.removeListener('profile', self.name)
        self.__dsWarning.destroy()

        self.__orthoPanel = None
        self.__dsWarning = None

        ctrlpanel.ControlToolBar.destroy(self)

    @staticmethod
    def supportedViews():
        """Overrides :meth:`.ControlMixin.supportedViews`. The
        ``OrthoEditToolBar`` is only intended to be added to
        :class:`.OrthoPanel` views.
        """
        from fsleyes.views.orthopanel import OrthoPanel
        return [OrthoPanel]

    def __profileChanged(self, *a):
        """Called when the :attr:`.ViewPanel.profile` property of the
        :class:`.OrthoPanel` changes. Shows/hides edit controls accordingly.
        """

        self.ClearTools(destroy=True, postevent=False)

        ortho = self.__orthoPanel
        profile = ortho.profile
        profileObj = ortho.getCurrentProfile()

        if profile != 'edit':
            self.__dsWarning.Show(False)
            return

        allTools = []
        allWidgets = []

        for specGroup in _TOOLBAR_SPECS:

            if specGroup == 'div':
                allTools.append(
                    fsltoolbar.ToolBarDivider(self,
                                              height=24,
                                              orient=wx.VERTICAL))
                continue

            groupWidgets = []
            isGroup = isinstance(specGroup, list)

            if isGroup:
                parent = wx.Panel(self)

            else:
                parent = self
                specGroup = [specGroup]

            for spec in specGroup:

                widget = props.buildGUI(parent, profileObj, spec)

                if not isGroup and spec.label is not None:
                    widget = self.MakeLabelledTool(widget, spec.label)

                allWidgets.append(widget)
                groupWidgets.append(widget)

            # Assuming here that all
            # widgets have labels
            if isGroup:

                sizer = wx.FlexGridSizer(2, 2, 0, 0)
                parent.SetSizer(sizer)

                labels = [s.label for s in specGroup]
                labels = [wx.StaticText(parent, label=l) for l in labels]

                for w, l in zip(groupWidgets, labels):
                    sizer.Add(l,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT)
                    sizer.Add(w,
                              flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT)

                allTools.append(parent)

            else:
                allTools.append(groupWidgets[0])

        self.SetTools([self.__dsWarning] + allTools)
        self.setNavOrder(allWidgets)
Esempio n. 21
0
class ColourBar(props.HasProperties, notifier.Notifier):
    """A ``ColourBar`` is an object which listens to the properties of a
    :class:`.ColourMapOpts` instance, and automatically generates a colour
    bar bitmap representing the current colour map properties.

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


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


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


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


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


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


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


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


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

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


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

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

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

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

        self.__selectedOverlayChanged()


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


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

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


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


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

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

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


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

        if self.__opts is None:
            return

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

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

        except fsldc.InvalidOverlayError:
            pass

        self.__opts    = None
        self.__display = None


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

        overlay = self.__displayCtx.getSelectedOverlay()

        if overlay is None:
            return False

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

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

        self.__opts    = opts
        self.__display = display

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

        return True


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


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

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

        if self.__opts is None:
            return None

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

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

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

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

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

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

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

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

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

        return bitmap
Esempio n. 22
0
class DataSeries(props.HasProperties):
    """A ``DataSeries`` instance encapsulates some data to be plotted by
    a :class:`PlotPanel`, with the data extracted from an overlay in the
    :class:`.OverlayList`.

    Sub-class implementations must:

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


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


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


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

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

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

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

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

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

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

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

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

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

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

    def __hash__(self):
        """Returns a hash for this ``DataSeries`` instance."""
        return hash(id(self))

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

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

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

        return self.getAllProperties()[0]

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

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

            ``(xdata, ydata)``

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

        The default implementation returns the data that has been set via the
        :meth:`setData` method.
        """
        return self.__xdata, self.__ydata
Esempio n. 23
0
class Scene3DCanvasOpts(props.HasProperties):
    """The ``Scene3DCanvasOpts`` class defines the display settings
    available on :class:`.Scene3DCanvas` instances.
    """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    rotation = props.Array(dtype=np.float64,
                           shape=(3, 3),
                           resizable=False,
                           default=[[1, 0, 0], [0, 1, 0], [0, 0, 1]])
    """A rotation matrix which defines the current ``Scene3DCanvas`` view
Esempio n. 24
0
class DataSeries(props.HasProperties):
    """A ``DataSeries`` instance encapsulates some data to be plotted by
    a :class:`PlotPanel`, with the data extracted from an overlay in the
    :class:`.OverlayList`.

    Sub-class implementations must:

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


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


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


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

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

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

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

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

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

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

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

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

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

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

    def __hash__(self):
        """Returns a hash for this ``DataSeries`` instance."""
        return hash(id(self))

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

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

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

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

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

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

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

        return self.getAllProperties()[0]

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

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

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

            ``(xdata, ydata)``

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

        The default implementation returns the data that has been set via the
        :meth:`setData` method.
        """
        return self.__xdata, self.__ydata
Esempio n. 25
0
class Action(props.HasProperties):
    """Represents an action of some sort. """

    enabled = props.Boolean(default=True)
    """Controls whether the action is currently enabled or disabled.
    When this property is ``False`` calls to the action will
    result in a :exc:`ActionDisabledError`.
    """
    def __init__(self, func, instance=None):
        """Create an ``Action``.

        :arg func:     The action function.

        :arg instance: Object associated with the function, if this ``Action``
                       is encapsulating an instance method.
        """
        self.__instance = instance
        self.__func = func
        self.__name = func.__name__
        self.__boundWidgets = []

        self.addListener('enabled', 'Action_{}_internal'.format(id(self)),
                         self.__enabledChanged)

    def __str__(self):
        """Returns a string representation of this ``Action``. """
        return '{}({})'.format(type(self).__name__, self.__name)

    def __repr__(self):
        """Returns a string representation of this ``Action``. """
        return self.__str__()

    def name(self):
        """Returns the name of this ``Action``. """
        return self.__name

    def __call__(self, *args, **kwargs):
        """Calls this action. An :exc:`ActionDisabledError` will be raised
        if :attr:`enabled` is ``False``.
        """

        if not self.enabled:
            raise ActionDisabledError('Action {} is disabled'.format(
                self.__name))

        log.debug('Action {}.{} called'.format(
            type(self.__instance).__name__, self.__name))

        if self.__instance is not None:
            args = [self.__instance] + list(args)

        return self.__func(*args, **kwargs)

    def destroy(self):
        """Must be called when this ``Action`` is no longer needed. """
        self.unbindAllWidgets()
        self.__func = None
        self.__instance = None

    def bindToWidget(self, parent, evType, widget, wrapper=None):
        """Binds this action to the given :mod:`wx` widget.

        :arg parent:  The :mod:`wx` object on which the event should be bound.
        :arg evType:  The :mod:`wx` event type.
        :arg widget:  The :mod:`wx` widget.
        :arg wrapper: Optional custom wrapper function used to execute the
                      action.
        """

        if wrapper is None:

            def wrapper(ev):
                self()

        parent.Bind(evType, wrapper, widget)
        widget.Enable(self.enabled)
        self.__boundWidgets.append(BoundWidget(parent, evType, widget))

    def unbindWidget(self, widget):
        """Unbinds the given widget from this ``Action``. """

        # Figure out the index into __boundWidgets,
        # as we need this to pass to __unbindWidget,
        # which does the real work.
        index = -1

        for i, bw in enumerate(self.__boundWidgets):
            if bw.widget == widget:
                index = i
                break

        if index == -1:
            raise ValueError('Widget {} [{}] is not bound'.format(
                type(widget).__name__, id(widget)))

        self.__unbindWidget(index)
        self.__boundWidgets.pop(index)

    def __unbindWidget(self, index):
        """Unbinds the widget at the specified index into the
        ``__boundWidgets`` list. Does not remove it from the list.
        """

        bw = self.__boundWidgets[index]

        # Only attempt to unbind if the parent
        # and widget have not been destroyed
        if bw.isAlive():
            bw.parent.Unbind(bw.evType, source=bw.widget)

    def unbindAllWidgets(self):
        """Unbinds all widgets which have been bound via :meth:`bindToWidget`.
        """

        for i in range(len(self.__boundWidgets)):
            self.__unbindWidget(i)

        self.__boundWidgets = []

    def getBoundWidgets(self):
        """Returns a list of :class:`BoundWidget` instances, containing all
        widgets which have been bound to this ``Action``.
        """
        return list(self.__boundWidgets)

    def __enabledChanged(self, *args):
        """Internal method which is called when the :attr:`enabled` property
        changes. Enables/disables any bound widgets.
        """

        for bw in self.__boundWidgets:

            # The widget may have been destroyed,
            # so check before trying to access it
            if bw.isAlive(): bw.widget.Enable(self.enabled)
            else: self.unbindWidget(bw.widget)
Esempio n. 26
0
class SliceCanvasOpts(props.HasProperties):
    """The ``SliceCanvasOpts`` class defines all of the display settings
    for a :class:`.SliceCanvas`.
    """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @property
    def yax(self):
        """The display coordinate system axis which maps to the Y (vertical)
        canvas axis.
        """
        return self.__yax
Esempio n. 27
0
class MeshOpts(cmapopts.ColourMapOpts, fsldisplay.DisplayOpts):
    """The ``MeshOpts`` class defines settings for displaying :class:`.Mesh`
    overlays. See also the :class:`.GiftiOpts` and :class:`.FreesurferOpts`
    sub-classes.
    """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            self.__overlayListChanged()
            self.__updateBounds()

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

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

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

        if self.__registered:

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

            for overlay in self.overlayList:

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

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

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

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

                except Exception:
                    pass

        self.__oldRefImage = None
        self.__vertexData = None

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

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

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

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

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

        if self.__vertexData is None:
            return 0

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

        else:
            return self.__vertexData.shape[1]

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

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

        vdataProp.setChoices(paths, instance=self)

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

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

        vsetProp.setChoices(paths, instance=self)

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

        display = self.display

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

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

        colour.append(display.alpha / 100.0)

        return colour

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

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

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

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

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

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

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

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

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

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

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

        vert = None
        vidx = self.displayCtx.vertexIndex

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

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

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

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

        return space

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

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

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

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

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

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

        if self.refImage is None:
            return coords

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

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

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

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

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

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

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

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

        return opts.getTransform(from_, to)

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

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

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

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

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

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

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

        self.__oldRefImage = self.refImage

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

        self.__updateBounds()

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

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

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

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

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

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

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

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

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

        imgOptions = [None]

        for overlay in overlays:

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

            imgOptions.append(overlay)

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

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

        imgProp.setChoices(imgOptions, instance=self)

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

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

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

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

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

        self.__updateBounds()

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

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

        try:
            if vdfile is not None:

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

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

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

        except Exception as e:

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

            vdata = None
            vdataRange = None

        self.__vertexData = vdata
        self.__vertexDataRange = vdataRange

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

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

        self.updateDataRange()

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

        alpha = self.colour[3] * 100

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

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

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

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

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

        with props.skip(self, 'colour', self.name):
            self.colour = r, g, b, alpha
Esempio n. 28
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))
Esempio n. 29
0
class LutLabel(props.HasProperties):
    """This class represents a mapping from a value to a colour and name.
    ``LutLabel`` instances are created and managed by :class:`LookupTable`
    instances.

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

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

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

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

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

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

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

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

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

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

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

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

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

        return self.value == other.value

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

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

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

    def __repr__(self):
        """Returns a string representation of this ``LutLabel``."""
        return self.__str__()
Esempio n. 30
0
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