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 Scene3DCanvasOpts(props.HasProperties): """The ``Scene3DCanvasOpts`` class defines the display settings available on :class:`.Scene3DCanvas` instances. """ pos = copy.copy(SliceCanvasOpts.pos) """Current cursor position in the display coordinate system. The dimensions are in the same ordering as the display coordinate system, in contrast to the :attr:`SliceCanvasOpts.pos` property. """ showCursor = copy.copy(SliceCanvasOpts.showCursor) cursorColour = copy.copy(SliceCanvasOpts.cursorColour) bgColour = copy.copy(SliceCanvasOpts.bgColour) zoom = copy.copy(SliceCanvasOpts.zoom) showLegend = props.Boolean(default=True) """If ``True``, an orientation guide will be shown on the canvas. """ legendColour = props.Colour(default=(0, 1, 0)) """Colour to use for the legend text.""" occlusion = props.Boolean(default=True) """If ``True``, objects closer to the camera will occlude objects further away. Toggles ``gl.DEPTH_TEST``. """ light = props.Boolean(default=True) """If ``True``, a lighting effect is applied to compatible overlays in the scene. """ lightPos = props.Point(ndims=3) """Light position in the display coordinate system. """ offset = props.Point(ndims=2) """An offset, in X/Y pixels normalised to the range ``[-1, 1]``, from the centre of the ``Scene3DCanvas``. """ rotation = props.Array( dtype=np.float64, shape=(3, 3), resizable=False, default=[[1, 0, 0], [0, 1, 0], [0, 0, 1]]) """A rotation matrix which defines the current ``Scene3DCanvas`` view
class Thing(props.HasProperties): myobject = props.Object() mybool = props.Boolean() myint = props.Int() myreal = props.Real() mypercentage = props.Percentage() mystring = props.String() mychoice = props.Choice(('1', '2', '3', '4', '5')) myfilepath = props.FilePath() mylist = props.List() mycolour = props.Colour() mycolourmap = props.ColourMap() mybounds = props.Bounds(ndims=2) mypoint = props.Point(ndims=2) myarray = props.Array()
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
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
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
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)
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
class ColourBarCanvas(props.HasProperties): """Contains logic to render a colour bar as an OpenGL texture. """ cmap = props.ColourMap() """The :mod:`matplotlib` colour map to use.""" negativeCmap = props.ColourMap() """Negative colour map to use, if :attr:`useNegativeCmap` is ``True``.""" useNegativeCmap = props.Boolean(default=False) """Whether or not to use the :attr:`negativeCmap`. """ cmapResolution = props.Int(minval=2, maxval=1024, default=256) """Number of discrete colours to use in the colour bar. """ invert = props.Boolean(default=False) """Invert the colour map(s). """ vrange = props.Bounds(ndims=1) """The minimum/maximum values to display.""" label = props.String() """A label to display under the centre of the colour bar.""" orientation = props.Choice(('horizontal', 'vertical')) """Whether the colour bar should be vertical or horizontal. """ labelSide = props.Choice(('top-left', 'bottom-right')) """Whether the colour bar labels should be on the top/left, or bottom/right of the colour bar (depending upon whether the colour bar orientation is horizontal/vertical). """ textColour = props.Colour(default=(1, 1, 1, 1)) """Colour to use for the colour bar label. """ bgColour = props.Colour(default=(0, 0, 0, 1)) """Colour to use for the background. """ def __init__(self): """Adds a few listeners to the properties of this object, to update the colour bar when they change. """ self._tex = None self._name = '{}_{}'.format(self.__class__.__name__, id(self)) self.addGlobalListener(self._name, self.__updateTexture) def __updateTexture(self, *a): self._genColourBarTexture() self.Refresh() def _initGL(self): """Called automatically by the OpenGL canvas target superclass (see the :class:`.WXGLCanvasTarget` and :class:`.OSMesaCanvasTarget` for details). Generates the colour bar texture. """ self._genColourBarTexture() def destroy(self): """Should be called when this ``ColourBarCanvas`` is no longer needed. Destroys the :class:`.Texture2D` instance used to render the colour bar. """ self.removeGlobalListener(self._name) self._tex.destroy() self._tex = None def _genColourBarTexture(self): """Generates a texture containing an image of the colour bar, according to the current property values. """ if not self._setGLContext(): return w, h = self.GetSize() if w < 50 or h < 50: return if self.orientation == 'horizontal': if self.labelSide == 'top-left': labelSide = 'top' else: labelSide = 'bottom' else: if self.labelSide == 'top-left': labelSide = 'left' else: labelSide = 'right' if self.cmap is None: bitmap = np.zeros((w, h, 4), dtype=np.uint8) else: if self.useNegativeCmap: negCmap = self.negativeCmap ticks = [0.0, 0.49, 0.51, 1.0] ticklabels = [ '{:0.2f}'.format(-self.vrange.xhi), '{:0.2f}'.format(-self.vrange.xlo), '{:0.2f}'.format(self.vrange.xlo), '{:0.2f}'.format(self.vrange.xhi) ] tickalign = ['left', 'right', 'left', 'right'] else: negCmap = None ticks = [0.0, 1.0] tickalign = ['left', 'right'] ticklabels = [ '{:0.2f}'.format(self.vrange.xlo), '{:0.2f}'.format(self.vrange.xhi) ] bitmap = cbarbmp.colourBarBitmap( cmap=self.cmap, negCmap=negCmap, invert=self.invert, ticks=ticks, ticklabels=ticklabels, tickalign=tickalign, width=w, height=h, label=self.label, orientation=self.orientation, labelside=labelSide, textColour=self.textColour, cmapResolution=self.cmapResolution) if self._tex is None: self._tex = textures.Texture2D( '{}_{}'.format(type(self).__name__, id(self)), gl.GL_LINEAR) # The bitmap has shape W*H*4, but the # Texture2D instance needs it in shape # 4*W*H bitmap = np.fliplr(bitmap).transpose([2, 0, 1]) self._tex.setData(bitmap) self._tex.refresh() def _draw(self): """Renders the colour bar texture using all available canvas space.""" if self._tex is None or not self._setGLContext(): return width, height = self.GetSize() # viewport gl.glViewport(0, 0, width, height) gl.glMatrixMode(gl.GL_PROJECTION) gl.glLoadIdentity() gl.glOrtho(0, 1, 0, 1, -1, 1) gl.glMatrixMode(gl.GL_MODELVIEW) gl.glLoadIdentity() gl.glClearColor(*self.bgColour) gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) gl.glShadeModel(gl.GL_FLAT) self._tex.drawOnBounds(0, 0, 1, 0, 1, 0, 1)
class MaskOpts(volumeopts.NiftiOpts): """The ``MaskOpts`` class defines settings for displaying an :class:`.Image` overlay as a binary mask. """ threshold = props.Bounds(ndims=1) """The mask threshold range - values outside of this range are not displayed. """ invert = props.Boolean(default=False) """If ``True``, the :attr:`threshold` range is inverted - values inside the range are not shown, and values outside of the range are shown. """ colour = props.Colour() """The mask colour.""" def __init__(self, overlay, *args, **kwargs): """Create a ``MaskOpts`` instance for the given overlay. All arguments are passed through to the :class:`.NiftiOpts` constructor. """ ################# # This is a hack. ################# # Mask images are rendered using GLMask, which # inherits from GLVolume. The latter assumes # that the DisplayOpts instance passed to it # has the following attributes (see the # VolumeOpts class). So we're adding dummy # attributes to make the GLVolume rendering # code happy. # # TODO Write independent GLMask rendering routines # instead of using the GLVolume implementations dataMin, dataMax = overlay.dataRange dRangeLen = abs(dataMax - dataMin) dMinDistance = dRangeLen / 100.0 self.clippingRange = (dataMin - 1, dataMax + 1) self.interpolation = 'none' self.invertClipping = False self.useNegativeCmap = False self.clipImage = None self.threshold.xmin = dataMin - dMinDistance self.threshold.xmax = dataMax + dMinDistance self.threshold.xlo = dataMin + dMinDistance self.threshold.xhi = dataMax + dMinDistance volumeopts.NiftiOpts.__init__(self, overlay, *args, **kwargs) overlay.register(self.name, self.__dataRangeChanged, topic='dataRange', runOnIdle=True) # The master MaskOpts instance makes # sure that colour[3] and Display.alpha # are consistent w.r.t. each other. self.__registered = self.getParent() is None if self.__registered: self.display.addListener('alpha', self.name, self.__alphaChanged, immediate=True) self.addListener('colour', self.name, self.__colourChanged, immediate=True) def destroy(self): """Removes some property listeners and calls :meth:`.NitfiOpts.destroy`. """ self.overlay.deregister(self.name, topic='dataRange') if self.__registered: self.display.removeListener('alpha', self.name) self.removeListener('colour', self.name) volumeopts.NiftiOpts.destroy(self) def __dataRangeChanged(self, *a): """Called when the :attr:`~fsl.data.image.Image.dataRange` changes. Updates the :attr:`threshold` limits. """ dmin, dmax = self.overlay.dataRange dRangeLen = abs(dmax - dmin) dminDistance = dRangeLen / 100.0 self.threshold.xmin = dmin - dminDistance self.threshold.xmax = dmax + dminDistance # If the threshold was # previously unset, grow it if self.threshold.x == (0, 0): self.threshold.x = (0, dmax + dminDistance) def __colourChanged(self, *a): """Called when :attr:`.colour` changes. Updates :attr:`.Display.alpha` from the alpha component. """ alpha = self.colour[3] * 100 log.debug('Propagating MaskOpts.colour[3] to ' 'Display.alpha [{}]'.format(alpha)) with props.skip(self.display, 'alpha', self.name): self.display.alpha = alpha def __alphaChanged(self, *a): """Called when :attr:`.Display.alpha` changes. Updates the alpha component of :attr:`.colour`. """ alpha = self.display.alpha / 100.0 r, g, b, _ = self.colour log.debug('Propagating Display.alpha to MaskOpts.' 'colour[3] [{}]'.format(alpha)) with props.skip(self, 'colour', self.name): self.colour = r, g, b, alpha
class MeshOpts(cmapopts.ColourMapOpts, fsldisplay.DisplayOpts): """The ``MeshOpts`` class defines settings for displaying :class:`.Mesh` overlays. See also the :class:`.GiftiOpts` and :class:`.FreesurferOpts` sub-classes. """ colour = props.Colour() """The mesh colour. """ outline = props.Boolean(default=False) """If ``True``, an outline of the mesh is shown. Otherwise a cross- section of the mesh is filled. """ outlineWidth = props.Real(minval=0.1, maxval=10, default=2, clamped=False) """If :attr:`outline` is ``True``, this property defines the width of the outline in pixels. """ showName = props.Boolean(default=False) """If ``True``, the mesh name is shown alongside it. .. note:: Not implemented yet, and maybe never will be. """ discardClipped = props.Boolean(default=False) """Flag which controls clipping. When the mesh is coloured according to some data (the :attr:`vertexData` property), vertices with a data value outside of the clipping range are either discarded (not drawn), or they are still drawn, but not according to the data, rather with the flat :attr:`colour`. """ vertexSet = props.Choice((None, )) """May be populated with the names of files which contain different vertex sets for the :class:`.Mesh` object. """ vertexData = props.Choice((None, )) """May be populated with the names of files which contain data associated with each vertex in the mesh, that can be used to colour the mesh. When some vertex data has been succsessfully loaded, it can be accessed via the :meth:`getVertexData` method. """ vertexDataIndex = props.Int(minval=0, maxval=0, default=0, clamped=True) """If :attr:`vertexData` is loaded, and has multiple data points per vertex (e.g. time series), this property controls the index into the data. """ modulateData = props.Choice((None, )) """Populated with the same files available for the :attr:`vertexData` attribute. Used to apply the :attr:`.ColourMapOpts.modulateAlpha` setting. .. note:: There is currently no support for indexing into multi- dimensional modulate data (e.g. time points). A separate ``modulateDataIndex`` property may be added in the future. """ refImage = props.Choice() """A reference :class:`.Image` instance which the mesh coordinates are in terms of. For example, if this :class:`.Mesh` represents the segmentation of a sub-cortical region from a T1 image, you would set the ``refImage`` to that T1 image. Any :class:`.Image` instance in the :class:`.OverlayList` may be chosen as the reference image. """ useLut = props.Boolean(default=False) """If ``True``, and if some :attr:`vertexData` is loaded, the :attr:`lut` is used to colour vertex values instead of the :attr:`cmap` and :attr:`negativeCmap`. """ lut = props.Choice() """If :attr:`useLut` is ``True``, a :class:`.LookupTable` is used to colour vertex data instead of the :attr:`cmap`/:attr:`negativeCmap`. """ # This property is implicitly tightly-coupled to # the NiftiOpts.getTransform method - the choices # defined in this property are assumed to be valid # inputs to that method (with the exception of # ``'torig'``). coordSpace = props.Choice( ('affine', 'pixdim', 'pixdim-flip', 'id', 'torig'), default='pixdim-flip') """If :attr:`refImage` is not ``None``, this property defines the reference image coordinate space in which the mesh coordinates are defined (i.e. voxels, scaled voxels, or world coordinates). =============== ========================================================= ``affine`` The mesh coordinates are defined in the reference image world coordinate system. ``id`` The mesh coordinates are defined in the reference image voxel coordinate system. ``pixdim`` The mesh coordinates are defined in the reference image voxel coordinate system, scaled by the voxel pixdims. ``pixdim-flip`` The mesh coordinates are defined in the reference image voxel coordinate system, scaled by the voxel pixdims. If the reference image transformation matrix has a positive determinant, the X axis is flipped. ``torig`` The mesh coordinates are defined in the Freesurfer "Torig" / "vox2ras-tkr" coordnie system. =============== ========================================================= The default value is ``pixdim-flip``, as this is the coordinate system used in the VTK sub-cortical segmentation model files output by FIRST. See also the :ref:`note on coordinate systems <volumeopts-coordinate-systems>`, and the :meth:`.NiftiOpts.getTransform` method. """ flatShading = props.Boolean(default=False) """3D only. If ``True``, colours between vertices are not interpolated - each triangle is coloured with the colour assigned to the first vertex. Only has an effect when the mesh is being coloured with vertex data. """ wireframe = props.Boolean(default=False) """3D only. If ``True``, the mesh is rendered as a wireframe. """ def __init__(self, overlay, *args, **kwargs): """Create a ``MeshOpts`` instance. All other arguments are passed through to the :class:`.DisplayOpts` constructor. """ # Set a default colour colour = genMeshColour(overlay) self.colour = np.concatenate((colour, [1.0])) # ColourMapOpts.linkLowRanges defaults to # True, which is annoying for surfaces. self.linkLowRanges = False # A copy of the refImage property # value is kept here so, when it # changes, we can de-register from # the previous one. self.__oldRefImage = None # When the vertexData/modulateData properties # are changed, the data (and its min/max) # is loaded and stored in these # attributes. See the __vdataChanged # method. # # Keys used are 'vertex' and 'modulate' self.__vdata = {} self.__vdataRange = {} nounbind = kwargs.get('nounbind', []) nounbind.extend([ 'refImage', 'coordSpace', 'vertexData', 'vertexSet', 'modulateData' ]) kwargs['nounbind'] = nounbind fsldisplay.DisplayOpts.__init__(self, overlay, *args, **kwargs) cmapopts.ColourMapOpts.__init__(self) self.__registered = self.getParent() is not None # Load all vertex data and vertex # sets on the parent opts instance if not self.__registered: self.addVertexSetOptions(overlay.vertexSets()) self.addVertexDataOptions(overlay.vertexDataSets()) # The master MeshOpts instance is just a # sync-slave, so we only need to register # property listeners on child instances else: self.overlayList.addListener('overlays', self.name, self.__overlayListChanged, immediate=True) self.addListener('refImage', self.name, self.__refImageChanged, immediate=True) self.addListener('coordSpace', self.name, self.__coordSpaceChanged, immediate=True) # We need to keep colour[3] # keeps colour[3] and Display.alpha # consistent w.r.t. each other (see # also MaskOpts) self.display.addListener('alpha', self.name, self.__alphaChanged, immediate=True) self.addListener('colour', self.name, self.__colourChanged, immediate=True) self.addListener('vertexData', self.name, self.__vdataChanged, immediate=True) self.addListener('modulateData', self.name, self.__vdataChanged, immediate=True) self.addListener('vertexSet', self.name, self.__vertexSetChanged, immediate=True) overlay.register(self.name, self.__overlayVerticesChanged, 'vertices') self.__overlayListChanged() self.__updateBounds() self.__refImageChanged() # If we have inherited values from a # parent instance, make sure the vertex/ # modulate data (if set) is initialised if self.vertexData is not None: self.__vdataChanged(self.vertexData, None, None, 'vertexData') if self.modulateData is not None: self.__vdataChanged(self.modulateData, None, None, 'modulateData') # If a reference image has not # been set on the parent MeshOpts # instance, see if there is a # suitable one in the overlay list. if self.refImage is None: self.refImage = fsloverlay.findMeshReferenceImage( self.overlayList, self.overlay) def destroy(self): """Removes some property listeners, and calls the :meth:`.DisplayOpts.destroy` method. """ if self.__registered: self.overlayList.removeListener('overlays', self.name) self.display.removeListener('alpha', self.name) self.removeListener('colour', self.name) self.overlay.deregister(self.name, 'vertices') for overlay in self.overlayList: # An error could be raised if the # DC has been/is being destroyed try: display = self.displayCtx.getDisplay(overlay) opts = self.displayCtx.getOpts(overlay) display.removeListener('name', self.name) if overlay is self.refImage: opts.removeListener('transform', self.name) except Exception: pass self.__oldRefImage = None self.__vdata = None cmapopts.ColourMapOpts.destroy(self) fsldisplay.DisplayOpts.destroy(self) @classmethod def getVolumeProps(cls): """Overrides :meth:`DisplayOpts.getVolumeProps`. Returns a list of property names which control the displayed volume/timepoint. """ return ['vertexDataIndex'] def getDataRange(self): """Overrides the :meth:`.ColourMapOpts.getDisplayRange` method. Returns the display range of the currently selected :attr:`vertexData`, or ``(0, 1)`` if none is selected. """ vdata = self.__vdataRange.get('vertex') if vdata is None: return (0, 1) else: return vdata def getModulateRange(self): """Overrides the :meth:`.ColourMapOpts.getModulateRange` method. Returns the display range of the currently selected :attr:`vertexData`, or ``None`` if none is selected. """ return self.__vdataRange.get('modulate') def getVertexData(self, vdtype='vertex'): """Returns the :attr:`.MeshOpts.vertexData` or :attr:`modulateData` , if some is loaded. Returns ``None`` otherwise. """ return self.__vdata.get(vdtype) def vertexDataLen(self): """Returns the length (number of data points per vertex) of the currently selected :attr:`vertexData`, or ``0`` if no vertex data is selected. """ vdata = self.__vdata.get('vertex') if vdata is None: return 0 elif len(vdata.shape) == 1: return 1 else: return vdata.shape[1] def addVertexData(self, key, data): """Adds the given data as a vertex data set to the :class:`.Mesh` overlay associated with this ``MeshOpts``. :arg key: A unique key to identify the data. If a vertex data set with the key already exists, a unique one is generated and returned. :arg data: ``numpy`` array containing per-vertex data. :returns: The key used to identify the data (typically equal to ``key``) """ count = 1 origKey = key sets = self.overlay.vertexDataSets() # generate a unique key for the # vertex data if one with the # given key already exists while key in sets: key = '{} [{}]'.format(origKey, count) count = count + 1 self.overlay.addVertexData(key, data) self.addVertexDataOptions([key]) return key def addVertexDataOptions(self, paths): """Adds the given sequence of paths as options to the :attr:`vertexData` property. It is assumed that the paths refer to valid vertex data files for the overlay associated with this ``MeshOpts`` instance. """ vdataProp = self.getProp('vertexData') mdataProp = self.getProp('modulateData') newPaths = paths paths = vdataProp.getChoices(instance=self) paths = paths + [p for p in newPaths if p not in paths] vdataProp.setChoices(paths, instance=self) mdataProp.setChoices(paths, instance=self) def addVertexSetOptions(self, paths): """Adds the given sequence of paths as options to the :attr:`vertexSet` property. It is assumed that the paths refer to valid vertex files for the overlay associated with this ``MeshOpts`` instance. """ vsetProp = self.getProp('vertexSet') newPaths = paths paths = vsetProp.getChoices(instance=self) paths = paths + [p for p in newPaths if p not in paths] vsetProp.setChoices(paths, instance=self) def getConstantColour(self): """Returns the current :attr::`colour`, adjusted according to the current :attr:`.Display.brightness`, :attr:`.Display.contrast`, and :attr:`.Display.alpha`. """ display = self.display # Only apply bricon if there is no vertex data assigned if self.vertexData is None: brightness = display.brightness / 100.0 contrast = display.contrast / 100.0 else: brightness = 0.5 contrast = 0.5 colour = list( fslcmaps.applyBricon(self.colour[:3], brightness, contrast)) colour.append(display.alpha / 100.0) return colour @property def referenceImage(self): """Overrides :meth:`.DisplayOpts.referenceImage`. If a :attr:`refImage` is selected, it is returned. Otherwise,``None`` is returned. """ return self.refImage @deprecated.deprecated('0.22.3', '1.0.0', 'Use getTransform instead') def getCoordSpaceTransform(self): """Returns a transformation matrix which can be used to transform the :class:`.Mesh` vertex coordinates into the display coordinate system. If no :attr:`refImage` is selected, this method returns an identity transformation. """ if self.refImage is None or self.refImage not in self.overlayList: return np.eye(4) opts = self.displayCtx.getOpts(self.refImage) return opts.getTransform(self.coordSpace, opts.transform) def getVertex(self, xyz=None, tol=1): """Returns an integer identifying the index of the mesh vertex that coresponds to the given ``xyz`` location, assumed to be specified in the display coordinate system. :arg xyz: Location to convert to a vertex index. If not provided, the current :class:`.DisplayContext.location` is used. :arg tol: Tolerance in mesh coordinate system units - if ``xyz`` is farther than ``tol`` to any vertex, ``None`` is returned. Pass in ``None`` to always return the nearest vertex. """ if xyz is None: xyz = self.displayCtx.location xyz = self.transformCoords(xyz, 'display', 'mesh') xyz = np.asarray(xyz).reshape(1, 3) dist, vidx = self.overlay.trimesh.nearest.vertex(xyz) dist = dist[0] vidx = vidx[0] if tol is not None and dist > tol: return None else: return vidx def normaliseSpace(self, space): """Used by :meth:`transformCoords` and :meth:`getTransform` to normalise their ``from_`` and ``to`` parameters. """ if space not in ('world', 'display', 'mesh', 'voxel', 'id'): raise ValueError('Invalid space: {}'.format(space)) if space == 'mesh': space = self.coordSpace if space == 'torig': space = 'affine' return space def transformCoords(self, coords, from_, to, *args, **kwargs): """Transforms the given ``coords`` from ``from_`` to ``to``. :arg coords: Coordinates to transform. :arg from_: Space that the coordinates are in :arg to: Space to transform the coordinates to All other parameters are passed through to the :meth:`.NiftiOpts.transformCoords` method of the reference image ``DisplayOpts``. The following values are accepted for the ``from_`` and ``to`` parameters: - ``'world'``: World coordinate system - ``'display'`` Display coordinate system - ``'mesh'`` The coordinate system of this mesh. - ``'voxel'``: The voxel coordinate system of the reference image - ``'id'``: Equivalent to ``'voxel'``. """ nfrom_ = self.normaliseSpace(from_) nto = self.normaliseSpace(to) ref = self.refImage pre = None post = None if ref is None: return coords if from_ == 'mesh' and self.coordSpace == 'torig': pre = affine.concat(ref.getAffine('voxel', 'world'), affine.invert(fslmgh.voxToSurfMat(ref))) if to == 'mesh' and self.coordSpace == 'torig': post = affine.concat(fslmgh.voxToSurfMat(ref), ref.getAffine('world', 'voxel')) opts = self.displayCtx.getOpts(ref) return opts.transformCoords(coords, nfrom_, nto, pre=pre, post=post, **kwargs) def getTransform(self, from_, to): """Return a matrix which may be used to transform coordinates from ``from_`` to ``to``. If the :attr:`refImage` property is not set, an identity matrix is returned. The following values are accepted for the ``from_`` and ``to`` parameters: - ``'world'``: World coordinate system - ``'display'``: Display coordinate system - ``'mesh'``: The coordinate system of this mesh. - ``'voxel'``: The voxel coordinate system of the reference image - ``'id'``: Equivalent to ``'voxel'``. """ nfrom_ = self.normaliseSpace(from_) nto = self.normaliseSpace(to) ref = self.refImage if ref is None: return np.eye(4) opts = self.displayCtx.getOpts(ref) xform = opts.getTransform(nfrom_, nto) if from_ == 'mesh' and self.coordSpace == 'torig': surfToVox = affine.invert(fslmgh.voxToSurfMat(ref)) xform = affine.concat(xform, ref.getAffine('voxel', 'world'), surfToVox) if to == 'mesh' and self.coordSpace == 'torig': voxToSurf = fslmgh.voxToSurfMat(ref) xform = affine.concat(voxToSurf, ref.getAffine('world', 'voxel'), xform) return xform def __transformChanged(self, value, valid, ctx, name): """Called when the :attr:`.NiftiOpts.transform` property of the current :attr:`refImage` changes. Calls :meth:`__updateBounds`. """ self.__updateBounds() def __coordSpaceChanged(self, *a): """Called when the :attr:`coordSpace` property changes. Calls :meth:`__updateBounds`. """ self.__updateBounds() def __refImageChanged(self, *a): """Called when the :attr:`refImage` property changes. If a new reference image has been specified, removes listeners from the old one (if necessary), and adds listeners to the :attr:`.NiftiOpts.transform` property associated with the new image. Calls :meth:`__updateBounds`. """ # TODO You are not tracking changes to the # refImage overlay type - if this changes, # you will need to re-bind to the transform # property of the new DisplayOpts instance if self.__oldRefImage is not None and \ self.__oldRefImage in self.overlayList: opts = self.displayCtx.getOpts(self.__oldRefImage) opts.removeListener('transform', self.name) self.__oldRefImage = self.refImage if self.refImage is not None: opts = self.displayCtx.getOpts(self.refImage) opts.addListener('transform', self.name, self.__transformChanged, immediate=True) self.__updateBounds() def __updateBounds(self): """Called whenever any of the :attr:`refImage`, :attr:`coordSpace`, or :attr:`transform` properties change. Updates the :attr:`.DisplayOpts.bounds` property accordingly. """ # create a bounding box for the # overlay vertices in their # native coordinate system lo, hi = self.overlay.bounds xlo, ylo, zlo = lo xhi, yhi, zhi = hi # Transform the bounding box # into display coordinates xform = self.getTransform('mesh', 'display') bbox = list(it.product(*zip(lo, hi))) bbox = affine.transform(bbox, xform) # re-calculate the min/max bounds x = np.sort(bbox[:, 0]) y = np.sort(bbox[:, 1]) z = np.sort(bbox[:, 2]) xlo, xhi = x.min(), x.max() ylo, yhi = y.min(), y.max() zlo, zhi = z.min(), z.max() oldBounds = self.bounds self.bounds = [xlo, xhi, ylo, yhi, zlo, zhi] if np.all(np.isclose(oldBounds, self.bounds)): self.propNotify('bounds') def __overlayListChanged(self, *a): """Called when the overlay list changes. Updates the :attr:`refImage` property so that it contains a list of overlays which can be associated with the mesh. """ imgProp = self.getProp('refImage') imgVal = self.refImage overlays = self.displayCtx.getOrderedOverlays() # the overlay for this MeshOpts # instance has been removed if self.overlay not in overlays: self.overlayList.removeListener('overlays', self.name) return imgOptions = [None] for overlay in overlays: # The overlay must be a Nifti instance. if not isinstance(overlay, fslimage.Nifti): continue imgOptions.append(overlay) display = self.displayCtx.getDisplay(overlay) display.addListener('name', self.name, self.__overlayListChanged, overwrite=True) # The previous refImage may have # been removed from the overlay list if imgVal in imgOptions: self.refImage = imgVal else: self.refImage = None imgProp.setChoices(imgOptions, instance=self) def __overlayVerticesChanged(self, *a): """Called when the :attr:`.Mesh.vertices` change. Makes sure that the :attr:`vertexSet` attribute is synchronised. """ vset = self.overlay.selectedVertices() vsprop = self.getProp('vertexSet') if vset not in vsprop.getChoices(instance=self): self.addVertexSetOptions([vset]) self.vertexSet = vset def __vertexSetChanged(self, *a): """Called when the :attr:`.MeshOpts.vertexSet` property changes. Updates the current vertex set on the :class:`.Mesh` overlay, and the overlay bounds. """ if self.vertexSet not in self.overlay.vertexSets(): self.overlay.loadVertices(self.vertexSet) else: with self.overlay.skip(self.name, 'vertices'): self.overlay.vertices = self.vertexSet self.__updateBounds() def __vdataChanged(self, value, valid, ctx, name): """Called when the :attr:`vertexData` or :attr:`modulateData` properties changes. Attempts to load the data if possible. The data may subsequently be retrieved via the :meth:`getVertexData` method. """ vdata = None vdataRange = None overlay = self.overlay vdfile = value if name == 'vertexData': key = 'vertex' elif name == 'modulateData': key = 'modulate' else: raise RuntimeError() try: if vdfile is not None: if vdfile not in overlay.vertexDataSets(): log.debug('Loading vertex data: {}'.format(vdfile)) vdata = overlay.loadVertexData(vdfile) else: vdata = overlay.getVertexData(vdfile) vdataRange = np.nanmin(vdata), np.nanmax(vdata) if len(vdata.shape) == 1: vdata = vdata.reshape(-1, 1) vdata = dutils.makeWriteable(vdata) except Exception as e: # TODO show a warning log.warning('Unable to load vertex data from {}: {}'.format( vdfile, e, exc_info=True)) vdata = None vdataRange = None self.__vdata[key] = vdata self.__vdataRange[key] = vdataRange if key == 'vertex': if vdata is not None: npoints = vdata.shape[1] else: npoints = 1 self.vertexDataIndex = 0 self.setAttribute('vertexDataIndex', 'maxval', npoints - 1) # if modulate data has changed, # don't update display/clipping # ranges (unless modulateData is # None, meaning that it is using # vertexData) if key == 'vertex': drange = True mrange = self.modulateData is None # and vice versa else: drange = False mrange = True self.updateDataRange(drange, drange, mrange) def __colourChanged(self, *a): """Called when :attr:`.colour` changes. Updates :attr:`.Display.alpha` from the alpha component. """ # modulateAlpha may cause the # alpha property to be disabled if not self.display.propertyIsEnabled('alpha'): return alpha = self.colour[3] * 100 log.debug('Propagating MeshOpts.colour[3] to ' 'Display.alpha [{}]'.format(alpha)) with props.skip(self.display, 'alpha', self.name): self.display.alpha = alpha def __alphaChanged(self, *a): """Called when :attr:`.Display.alpha` changes. Updates the alpha component of :attr:`.colour`. """ alpha = self.display.alpha / 100.0 r, g, b, _ = self.colour log.debug('Propagating Display.alpha to MeshOpts.' 'colour[3] [{}]'.format(alpha)) with props.skip(self, 'colour', self.name): self.colour = r, g, b, alpha
class SceneOpts(props.HasProperties): """The ``SceneOpts`` class defines settings which are used by :class:`.CanvasPanel` instances. Several of the properties of the ``SceneOpts`` class are defined in the :class:`.SliceCanvasOpts` class, so see its documentation for more details. """ showCursor = copy.copy(canvasopts.SliceCanvasOpts.showCursor) zoom = copy.copy(canvasopts.SliceCanvasOpts.zoom) bgColour = copy.copy(canvasopts.SliceCanvasOpts.bgColour) cursorColour = copy.copy(canvasopts.SliceCanvasOpts.cursorColour) renderMode = copy.copy(canvasopts.SliceCanvasOpts.renderMode) highDpi = copy.copy(canvasopts.SliceCanvasOpts.highDpi) fgColour = props.Colour(default=(1, 1, 1)) """Colour to use for foreground items (e.g. labels). .. note:: This colour is automatically updated whenever the :attr:`.bgColour` is changed. But it can be modified independently. """ showColourBar = props.Boolean(default=False) """If ``True``, and it is possible to do so, a colour bar is shown on the scene. """ colourBarLocation = props.Choice(('top', 'bottom', 'left', 'right')) """This property controls the location of the colour bar, if it is being shown. """ colourBarLabelSide = props.Choice(('top-left', 'bottom-right')) """This property controls the location of the colour bar labels, relative to the colour bar, if it is being shown. """ colourBarSize = props.Percentage(default=100) """Size of the major axis of the colour bar, as a proportion of the available space. """ labelSize = props.Int(minval=4, maxval=96, default=12, clamped=True) """Font size used for any labels drawn on the canvas, including orthographic labels, and colour bar labels. """ # NOTE: If you change the maximum performance value, # make sure you update all references to # performance because, for example, the # OrthoEditProfile does numerical comparisons # to it. performance = props.Choice((1, 2, 3), default=3, allowStr=True) """User controllable performance setting. This property is linked to the :attr:`renderMode` property. Setting this property to a low value will result in faster rendering time, at the cost of increased memory usage and poorer rendering quality. See the :meth:`__onPerformanceChange` method. """ movieSyncRefresh = props.Boolean(default=True) """Whether, when in movie mode, to synchronise the refresh for GL canvases. This is not possible in some platforms/environments. See :attr:`.CanvasPanel.movieSyncRefresh`. """ def __init__(self, panel): """Create a ``SceneOpts`` instance. This method simply links the :attr:`performance` property to the :attr:`renderMode` property. """ self.__panel = panel self.__name = '{}_{}'.format(type(self).__name__, id(self)) self.movieSyncRefresh = self.defaultMovieSyncRefresh self.addListener('performance', self.__name, self._onPerformanceChange) self.addListener('bgColour', self.__name, self.__onBgColourChange) self._onPerformanceChange() @property def defaultMovieSyncRefresh(self): """In movie mode, the canvas refreshes are performed by the __syncMovieRefresh or __unsyncMovieRefresh methods of the CanvasPanel class. Some platforms/GL drivers/environments seem to have a problem with separate renders/buffer swaps, so we have to use a shitty unsynchronised update routine. These heuristics are not perfect - the movieSyncRefresh property can therefore be overridden by the user. """ renderer = fslgl.GL_RENDERER.lower() unsyncRenderers = ['gallium', 'mesa dri intel(r)'] unsync = any([r in renderer for r in unsyncRenderers]) return not unsync @property def panel(self): """Return a reference to the ``CanvasPanel`` that owns this ``SceneOpts`` instance. """ return self.__panel def _onPerformanceChange(self, *a): """Called when the :attr:`performance` property changes. This method must be overridden by sub-classes to change the values of the :attr:`renderMode` property according to the new performance setting. """ raise NotImplementedError('The _onPerformanceChange method must' 'be implemented by sub-classes') def __onBgColourChange(self, *a): """Called when the background colour changes. Updates the :attr:`fgColour` to a complementary colour. """ self.fgColour = fslcm.complementaryColour(self.bgColour)
class SceneOpts(props.HasProperties): """The ``SceneOpts`` class defines settings which are used by :class:`.CanvasPanel` instances. Several of the properties of the ``SceneOpts`` class are defined in the :class:`.SliceCanvasOpts` class, so see its documentation for more details. """ showCursor = copy.copy(canvasopts.SliceCanvasOpts.showCursor) zoom = copy.copy(canvasopts.SliceCanvasOpts.zoom) bgColour = copy.copy(canvasopts.SliceCanvasOpts.bgColour) cursorColour = copy.copy(canvasopts.SliceCanvasOpts.cursorColour) renderMode = copy.copy(canvasopts.SliceCanvasOpts.renderMode) fgColour = props.Colour(default=(1, 1, 1)) """Colour to use for foreground items (e.g. labels). .. note:: This colour is automatically updated whenever the :attr:`.bgColour` is changed. But it can be modified independently. """ showColourBar = props.Boolean(default=False) """If ``True``, and it is possible to do so, a colour bar is shown on the scene. """ colourBarLocation = props.Choice(('top', 'bottom', 'left', 'right')) """This property controls the location of the colour bar, if it is being shown. """ colourBarLabelSide = props.Choice(('top-left', 'bottom-right')) """This property controls the location of the colour bar labels, relative to the colour bar, if it is being shown. """ # NOTE: If you change the maximum performance value, # make sure you update all references to # performance because, for example, the # OrthoEditProfile does numerical comparisons # to it. performance = props.Choice((1, 2, 3), default=3, allowStr=True) """User controllable performance setting. This property is linked to the :attr:`renderMode` property. Setting this property to a low value will result in faster rendering time, at the cost of increased memory usage and poorer rendering quality. See the :meth:`__onPerformanceChange` method. """ def __init__(self, panel): """Create a ``SceneOpts`` instance. This method simply links the :attr:`performance` property to the :attr:`renderMode` property. """ self.__panel = panel self.__name = '{}_{}'.format(type(self).__name__, id(self)) self.addListener('performance', self.__name, self._onPerformanceChange) self.addListener('bgColour', self.__name, self.__onBgColourChange) self._onPerformanceChange() @property def panel(self): """Return a reference to the ``CanvasPanel`` that owns this ``SceneOpts`` instance. """ return self.__panel def _onPerformanceChange(self, *a): """Called when the :attr:`performance` property changes. This method must be overridden by sub-classes to change the values of the :attr:`renderMode` property according to the new performance setting. """ raise NotImplementedError('The _onPerformanceChange method must' 'be implemented by sub-classes') def __onBgColourChange(self, *a): """Called when the background colour changes. Updates the :attr:`fgColour` to a complementary colour. """ self.fgColour = fslcm.complementaryColour(self.bgColour)
class MaskOpts(volumeopts.NiftiOpts): """The ``MaskOpts`` class defines settings for displaying an :class:`.Image` overlay as a binary mask. """ threshold = props.Bounds(ndims=1) """The mask threshold range - values outside of this range are not displayed. """ invert = props.Boolean(default=False) """If ``True``, the :attr:`threshold` range is inverted - values inside the range are not shown, and values outside of the range are shown. """ colour = props.Colour() """The mask colour.""" outline = props.Boolean(default=False) """If ``True`` only the outline of the mask will be shown. If ``False``, the filled mask will be displayed. """ outlineWidth = props.Int(minval=0, maxval=10, default=1, clamped=True) """Width of mask outline, if :attr:``outline` is ``True``. This value is in terms of pixels. """ interpolation = copy.copy(volumeopts.VolumeOpts.interpolation) def __init__(self, overlay, *args, **kwargs): """Create a ``MaskOpts`` instance for the given overlay. All arguments are passed through to the :class:`.NiftiOpts` constructor. """ # We need GL >= 2.1 for # spline interpolation if float(fslplatform.glVersion) < 2.1: interp = self.getProp('interpolation') interp.removeChoice('spline', instance=self) interp.updateChoice('linear', instance=self, newAlt=['spline']) kwargs['nounbind'] = ['interpolation'] # Initialise threshold from data reange. Do # this before __init__, in case we need to # inherit settings from the parent instance dmin, dmax = overlay.dataRange dlen = dmax - dmin doff = dlen / 100.0 self.threshold.xmin = dmin - doff self.threshold.xmax = dmax + doff self.threshold.xlo = dmin + doff self.threshold.xhi = dmax + doff volumeopts.NiftiOpts.__init__(self, overlay, *args, **kwargs) overlay.register(self.name, self.__dataRangeChanged, topic='dataRange', runOnIdle=True) # The master MaskOpts instance makes # sure that colour[3] and Display.alpha # are consistent w.r.t. each other. self.__registered = self.getParent() is None if self.__registered: self.display.addListener('alpha', self.name, self.__alphaChanged, immediate=True) self.addListener('colour', self.name, self.__colourChanged, immediate=True) def destroy(self): """Removes some property listeners and calls :meth:`.NitfiOpts.destroy`. """ self.overlay.deregister(self.name, topic='dataRange') if self.__registered: self.display.removeListener('alpha', self.name) self.removeListener('colour', self.name) volumeopts.NiftiOpts.destroy(self) def __dataRangeChanged(self, *a): """Called when the :attr:`~fsl.data.image.Image.dataRange` changes. Updates the :attr:`threshold` limits. """ dmin, dmax = self.overlay.dataRange dRangeLen = abs(dmax - dmin) dminDistance = dRangeLen / 100.0 self.threshold.xmin = dmin - dminDistance self.threshold.xmax = dmax + dminDistance # If the threshold was # previously unset, grow it if self.threshold.x == (0, 0): self.threshold.x = (0, dmax + dminDistance) def __colourChanged(self, *a): """Called when :attr:`.colour` changes. Updates :attr:`.Display.alpha` from the alpha component. """ alpha = self.colour[3] * 100 log.debug('Propagating MaskOpts.colour[3] to ' 'Display.alpha [{}]'.format(alpha)) with props.skip(self.display, 'alpha', self.name): self.display.alpha = alpha def __alphaChanged(self, *a): """Called when :attr:`.Display.alpha` changes. Updates the alpha component of :attr:`.colour`. """ alpha = self.display.alpha / 100.0 r, g, b, _ = self.colour log.debug('Propagating Display.alpha to MaskOpts.' 'colour[3] [{}]'.format(alpha)) with props.skip(self, 'colour', self.name): self.colour = r, g, b, alpha
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
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__()
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