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)
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)
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)
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 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())
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)
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)
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
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()
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
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
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 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()
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())
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)
class Boo(props.SyncableHasProperties): mybool = props.Boolean()
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
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)
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 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 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
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 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)
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 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 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))
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 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