class Scene3DCanvasOpts(props.HasProperties): """The ``Scene3DCanvasOpts`` class defines the display settings available on :class:`.Scene3DCanvas` instances. """ pos = copy.copy(SliceCanvasOpts.pos) """Current cursor position in the display coordinate system. The dimensions are in the same ordering as the display coordinate system, in contrast to the :attr:`SliceCanvasOpts.pos` property. """ showCursor = copy.copy(SliceCanvasOpts.showCursor) cursorColour = copy.copy(SliceCanvasOpts.cursorColour) bgColour = copy.copy(SliceCanvasOpts.bgColour) zoom = copy.copy(SliceCanvasOpts.zoom) showLegend = props.Boolean(default=True) """If ``True``, an orientation guide will be shown on the canvas. """ legendColour = props.Colour(default=(0, 1, 0)) """Colour to use for the legend text.""" occlusion = props.Boolean(default=True) """If ``True``, objects closer to the camera will occlude objects further away. Toggles ``gl.DEPTH_TEST``. """ light = props.Boolean(default=True) """If ``True``, a lighting effect is applied to compatible overlays in the scene. """ lightPos = props.Point(ndims=3) """Light position in the display coordinate system. """ offset = props.Point(ndims=2) """An offset, in X/Y pixels normalised to the range ``[-1, 1]``, from the centre of the ``Scene3DCanvas``. """ rotation = props.Array( dtype=np.float64, shape=(3, 3), resizable=False, default=[[1, 0, 0], [0, 1, 0], [0, 0, 1]]) """A rotation matrix which defines the current ``Scene3DCanvas`` view
class Thing(props.HasProperties): myobject = props.Object() mybool = props.Boolean() myint = props.Int() myreal = props.Real() mypercentage = props.Percentage() mystring = props.String() mychoice = props.Choice(('1', '2', '3', '4', '5')) myfilepath = props.FilePath() mylist = props.List() mycolour = props.Colour() mycolourmap = props.ColourMap() mybounds = props.Bounds(ndims=2) mypoint = props.Point(ndims=2) myarray = props.Array()
class LocationInfoPanel(fslpanel.FSLeyesPanel): """The ``LocationInfoPanel`` is a panel which is embedded in the :class:`LocationPanel`, and which contains controls allowing the user to view and modify the :attr:`.DisplayContext.location` property. The ``LocationInfoPanel`` contains two main sections: - A collection of controls which show the current :attr:`.DisplayContext.location` - A ``wx.html.HtmlWindow`` which displays information about the current :attr:`.DisplayContext.location` for all overlays in the :class:`.OverlayList`. **NIFTI overlays** The ``LocationInfoPanel`` is primarily designed to work with :class:`.Image` overlays. If the :attr:`.DisplayContext.selectedOverlay` is an :class:`.Image`, or has an associated reference image (see :meth:`.DisplayOpts.referenceImage`), the ``LocationInfoPanel`` will display the current :class:`.DisplayContext.location` in both the the voxel coordinates and world coordinates of the ``Image`` instance. **Other overlays** If the :attr:`.DisplayContext.selectedOverlay` is not an :class:`.Image`, or does not have an associated reference image, the ``LocationInfoPanel`` will display the current :attr:`.DisplayContext.location` as-is (i.e. in the display coordinate system); furthermore, the voxel location controls will be disabled. **Location updates** The :data:`DISPLAYOPTS_BOUNDS` and :data:`DISPLAYOPTS_INFO` dictionaries contain lists of property names that the :class:`.LocationInfoPanel` listens on for changes, so it knows when the location widgets, and information about the currenty location, need to be refreshed. For example, when the :attr`.NiftiOpts.volume` property of a :class:`.Nifti` overlay changes, the volume index, and potentially the overlay information, needs to be updated. """ voxelLocation = props.Point(ndims=3, real=False) """If the currently selected overlay is a :class:`.Image` instance , this property tracks the current :attr:`.DisplayContext.location` in voxel coordinates. """ worldLocation = props.Point(ndims=3, real=True) """For :class:`.Image` overlays, this property tracks the current :attr:`.DisplayContext.location` in the image world coordinates. For other overlay types, this property tracks the current location in display coordinates. """ def __init__(self, parent, overlayList, displayCtx, frame): fslpanel.FSLeyesPanel.__init__(self, parent, overlayList, displayCtx, frame, kbFocus=True) # Whenever the selected overlay changes, # a reference to it and its DisplayOpts # instance is stored, as property listeners # are registered on it (and need to be # de-registered later on). self.__registeredOverlay = None self.__registeredDisplay = None self.__registeredOpts = None self.__column1 = wx.Panel(self) self.__column2 = wx.Panel(self) self.__info = wxhtml.HtmlWindow(self) # HTMLWindow does not use # the parent font by default, # so we force it to at least # have the parent font size self.__info.SetStandardFonts(self.GetFont().GetPointSize()) self.__worldLabel = wx.StaticText( self.__column1, label=strings.labels[self, 'worldLocation']) self.__volumeLabel = wx.StaticText( self.__column1, label=strings.labels[self, 'volume']) self.__voxelLabel = wx.StaticText( self.__column2, label=strings.labels[self, 'voxelLocation']) worldX, worldY, worldZ = props.makeListWidgets( self.__column1, self, 'worldLocation', slider=False, spin=True, showLimits=False, mousewheel=True) voxelX, voxelY, voxelZ = props.makeListWidgets( self.__column2, self, 'voxelLocation', slider=False, spin=True, spinWidth=7, showLimits=False, mousewheel=True) self.__worldX = worldX self.__worldY = worldY self.__worldZ = worldZ self.__voxelX = voxelX self.__voxelY = voxelY self.__voxelZ = voxelZ self.__volume = floatspin.FloatSpinCtrl( self.__column2, width=7, style=floatspin.FSC_MOUSEWHEEL | floatspin.FSC_INTEGER) self.__column1Sizer = wx.BoxSizer(wx.VERTICAL) self.__column2Sizer = wx.BoxSizer(wx.VERTICAL) self.__sizer = wx.BoxSizer(wx.HORIZONTAL) self.__column1Sizer.Add(self.__worldLabel, flag=wx.EXPAND) self.__column1Sizer.Add(self.__worldX, flag=wx.EXPAND) self.__column1Sizer.Add(self.__worldY, flag=wx.EXPAND) self.__column1Sizer.Add(self.__worldZ, flag=wx.EXPAND) self.__column1Sizer.Add(self.__volumeLabel, flag=wx.ALIGN_RIGHT) self.__column2Sizer.Add(self.__voxelLabel, flag=wx.EXPAND) self.__column2Sizer.Add(self.__voxelX, flag=wx.EXPAND) self.__column2Sizer.Add(self.__voxelY, flag=wx.EXPAND) self.__column2Sizer.Add(self.__voxelZ, flag=wx.EXPAND) self.__column2Sizer.Add(self.__volume, flag=wx.EXPAND) self.__sizer.Add(self.__column1, flag=wx.EXPAND) self.__sizer.Add((5, -1)) self.__sizer.Add(self.__column2, flag=wx.EXPAND) self.__sizer.Add((5, -1)) self.__sizer.Add(self.__info, flag=wx.EXPAND, proportion=1) self.__column1.SetSizer(self.__column1Sizer) self.__column2.SetSizer(self.__column2Sizer) self .SetSizer(self.__sizer) self.overlayList.addListener('overlays', self.name, self.__selectedOverlayChanged) self.displayCtx .addListener('selectedOverlay', self.name, self.__selectedOverlayChanged) self.displayCtx .addListener('overlayOrder', self.name, self.__overlayOrderChanged) self.displayCtx .addListener('location', self.name, self.__displayLocationChanged) self.addListener( 'voxelLocation', self.name, self.__voxelLocationChanged) self.addListener( 'worldLocation', self.name, self.__worldLocationChanged) self.__selectedOverlayChanged() self.__worldLabel .SetMinSize(self.__calcWorldLabelMinSize()) self.__voxelLabel .SetMinSize(self.__voxelLabel .GetBestSize()) self.__volumeLabel.SetMinSize(self.__volumeLabel.GetBestSize()) self.__column1 .SetMinSize(self.__column1 .GetBestSize()) self.__column2 .SetMinSize(self.__column2 .GetBestSize()) self.__info .SetMinSize((100, 100)) # Keyboard navigation - see FSLeyesPanel self.setNavOrder((self.__worldX, self.__worldY, self.__worldZ, self.__voxelX, self.__voxelY, self.__voxelZ, self.__volume)) self.Layout() self.__minSize = self.__sizer.GetMinSize() self.SetMinSize(self.__minSize) def destroy(self): """Must be called when this ``LocationInfoPanel`` is no longer needed. Removes property listeners and calls :meth:`.FSLeyesPanel.destroy`. """ self.overlayList.removeListener('overlays', self.name) self.displayCtx .removeListener('selectedOverlay', self.name) self.displayCtx .removeListener('location', self.name) self.__deregisterOverlay() fslpanel.FSLeyesPanel.destroy(self) def GetMinSize(self): """Returns the minimum size for this ``LocationInfoPanel``. Under Linux/GTK, the ``wx.agw.lib.aui`` layout manager seems to arbitrarily adjust the minimum sizes of some panels. Therefore, The minimum size of the ``LocationInfoPanel`` is calculated in :meth:`__init__`, and is fixed. """ return self.__minSize def DoGetBestClientSize(self): """Returns the best size for this ``LocationInfoPanel``. """ return self.__minSize def __calcWorldLabelMinSize(self): """Calculates the minimum size that the world label (the label which shows the coordinate space of the currently selected overlay) needs. Called by the :meth:`__init__` method. The world label displays different things depending on the currently selected overlay. But we want it to be a fixed size. So this method calculates the size of all possible values that the world label will display, and returns the maximum size. This is then used as the minimum size for the world label. """ dc = wx.ClientDC(self.__worldLabel) width, height = 0, 0 labelPref = strings.labels[self, 'worldLocation'] labelSufs = [ strings.anatomy['Nifti', 'space', constants.NIFTI_XFORM_UNKNOWN], strings.anatomy['Nifti', 'space', constants.NIFTI_XFORM_SCANNER_ANAT], strings.anatomy['Nifti', 'space', constants.NIFTI_XFORM_ALIGNED_ANAT], strings.anatomy['Nifti', 'space', constants.NIFTI_XFORM_TALAIRACH], strings.anatomy['Nifti', 'space', constants.NIFTI_XFORM_MNI_152], strings.labels[self, 'worldLocation', 'unknown'] ] for labelSuf in labelSufs: w, h = dc.GetTextExtent(labelPref + labelSuf) if w > width: width = w if h > height: height = h return width + 5, height + 5 def __selectedOverlayChanged(self, *a): """Called when the :attr:`.DisplayContext.selectedOverlay` or :class:`.OverlayList` is changed. Registered with the new overlay, and refreshes the ``LocationInfoPanel`` interface accordingly. """ self.__deregisterOverlay() if len(self.overlayList) == 0: self.__updateWidgets() self.__updateLocationInfo() else: self.__registerOverlay() self.__updateWidgets() self.__displayLocationChanged() def __registerOverlay(self): """Registers property listeners with the :class:`.Display` and :class:`.DisplayOpts` instances associated with the currently selected overlay. """ overlay = self.displayCtx.getSelectedOverlay() if overlay is None: return display = self.displayCtx.getDisplay(overlay) opts = display.opts self.__registeredOverlay = overlay self.__registeredDisplay = display self.__registeredOpts = opts # The properties that we need to # listen for are specified in the # DISPLAYOPTS_BOUNDS and # DISPLAYOPTS_INFO dictionaries. boundPropNames = DISPLAYOPTS_BOUNDS.get(opts, [], allhits=True) infoPropNames = DISPLAYOPTS_INFO .get(opts, [], allhits=True) boundPropNames = it.chain(*boundPropNames) infoPropNames = it.chain(*infoPropNames) # DisplayOpts instances get re-created # when an overlay type is changed, so # we need to re-register when this happens. display.addListener('overlayType', self.name, self.__selectedOverlayChanged) for n in boundPropNames: opts.addListener(n, self.name, self.__boundsOptsChanged) for n in infoPropNames: opts.addListener(n, self.name, self.__infoOptsChanged) # Enable the volume widget if the # overlay is a NIFTI image with more # than three dimensions, and bind # the widget to the volume property # of the associated NiftiOpts instance if isinstance(overlay, fslimage.Nifti) and overlay.ndim > 3: props.bindWidget(self.__volume, opts, 'volume', floatspin.EVT_FLOATSPIN) opts.addListener('volumeDim', self.name, self.__volumeDimChanged) self.__volume .Enable() self.__volumeLabel.Enable() self.__volumeDimChanged() # Or, if the overlay is a mesh which # has some time series data associated # with it elif isinstance(overlay, fslmesh.Mesh): props.bindWidget(self.__volume, opts, 'vertexDataIndex', floatspin.EVT_FLOATSPIN) opts.addListener('vertexData', self.name, self.__vertexDataChanged) self.__vertexDataChanged() else: self.__volume.SetRange(0, 0) self.__volume.SetValue(0) self.__volume.Disable() def __deregisterOverlay(self): """De-registers property listeners with the :class:`.Display` and :class:`.DisplayOpts` instances associated with the previously registered overlay. """ opts = self.__registeredOpts display = self.__registeredDisplay overlay = self.__registeredOverlay if overlay is None: return self.__registeredOpts = None self.__registeredDisplay = None self.__registeredOverlay = None boundPropNames = DISPLAYOPTS_BOUNDS.get(opts, [], allhits=True) infoPropNames = DISPLAYOPTS_INFO .get(opts, [], allhits=True) boundPropNames = it.chain(*boundPropNames) infoPropNames = it.chain(*infoPropNames) if display is not None: display.removeListener('overlayType', self.name) for p in boundPropNames: opts.removeListener(p, self.name) for p in infoPropNames: opts.removeListener(p, self.name) if isinstance(overlay, fslimage.Nifti) and overlay.ndim > 3: props.unbindWidget(self.__volume, opts, 'volume', floatspin.EVT_FLOATSPIN) opts.removeListener('volumeDim', self.name) elif isinstance(overlay, fslmesh.Mesh): props.unbindWidget(self.__volume, opts, 'vertexDataIndex', floatspin.EVT_FLOATSPIN) opts.removeListener('vertexData', self.name) def __volumeDimChanged(self, *a): """Called when the selected overlay is a :class:`.Nifti`, and its :attr:`.NiftiOpts.volumeDim` property changes. Updates the volume widget. """ overlay = self.__registeredOverlay opts = self.__registeredOpts volume = opts.volume vdim = opts.volumeDim + 3 self.__volume.SetRange(0, overlay.shape[vdim] - 1) self.__volume.SetValue(volume) self.__infoOptsChanged() def __vertexDataChanged(self, *a): """Called when the selected overlay is a :class:`.Mesh`, and its :attr:`.MeshOpts.vertexData` property changes. Updates the volume widget. """ opts = self.__registeredOpts vd = opts.getVertexData() vdi = opts.vertexDataIndex enabled = vd is not None and vd.shape[1] > 1 self.__volume .Enable(enabled) self.__volumeLabel.Enable(enabled) if enabled: self.__volume.SetRange(0, vd.shape[1] - 1) self.__volume.SetValue(vdi) self.__infoOptsChanged() def __boundsOptsChanged(self, *a): """Called when a :class:`.DisplayOpts` property associated with the currently selected overlay, and listed in the :data:`DISPLAYOPTS_BOUNDS` dictionary, changes. Refreshes the ``LocationInfoPanel`` interface accordingly. """ self.__updateWidgets() self.__displayLocationChanged() def __infoOptsChanged(self, *a): """Called when a :class:`.DisplayOpts` property associated with the currently selected overlay, and listed in the :data:`DISPLAYOPTS_INFO` dictionary, changes. Refreshes the ``LocationInfoPanel`` interface accordingly. """ self.__displayLocationChanged() def __overlayOrderChanged(self, *a): """Called when the :attr:`.DisplayContext.overlayOrder` changes, Refreshes the information panel. """ self.__displayLocationChanged() def __updateWidgets(self): """Called by the :meth:`__selectedOverlayChanged` and :meth:`__displayOptsChanged` methods. Enables/disables the voxel/world location and volume controls depending on the currently selected overlay (or reference image). """ overlay = self.__registeredOverlay opts = self.__registeredOpts if overlay is not None: refImage = opts.referenceImage else: refImage = None haveRef = refImage is not None self.__voxelX .Enable(haveRef) self.__voxelY .Enable(haveRef) self.__voxelZ .Enable(haveRef) self.__voxelLabel .Enable(haveRef) ###################### # World location label ###################### label = strings.labels[self, 'worldLocation'] if haveRef: label += strings.anatomy[refImage, 'space', refImage.getXFormCode()] else: label += strings.labels[ self, 'worldLocation', 'unknown'] self.__worldLabel.SetLabel(label) #################################### # Voxel/world location widget limits #################################### # Figure out the limits for the # voxel/world location widgets if haveRef: opts = self.displayCtx.getOpts(refImage) v2w = opts.getTransform('voxel', 'world') shape = refImage.shape[:3] vlo = [0, 0, 0] vhi = np.array(shape) - 1 wlo, whi = transform.axisBounds(shape, v2w) wstep = refImage.pixdim[:3] else: vlo = [0, 0, 0] vhi = [0, 0, 0] wbounds = self.displayCtx.bounds[:] wlo = wbounds[0::2] whi = wbounds[1::2] wstep = [1, 1, 1] log.debug('Setting voxelLocation limits: {} - {}'.format(vlo, vhi)) log.debug('Setting worldLocation limits: {} - {}'.format(wlo, whi)) # Update the voxel and world location limits, # but don't trigger a listener callback, as # this would change the display location. widgets = [self.__worldX, self.__worldY, self.__worldZ] with props.suppress(self, 'worldLocation'), \ props.suppress(self, 'voxelLocation'): for i in range(3): self.voxelLocation.setLimits(i, vlo[i], vhi[i]) self.worldLocation.setLimits(i, wlo[i], whi[i]) widgets[i].SetIncrement(wstep[i]) def __displayLocationChanged(self, *a): """Called when the :attr:`.DisplayContext.location` changes. Propagates the change on to the :attr:`voxelLocation` and :attr:`worldLocation` properties. .. note:: Because the :attr:`.DisplayContext.location`, :attr:`voxelLocation` and :attr:`worldLocation` properties are all linked through property listeners (see :meth:`props.HasProperties.addListener`), we need to be a bit careful to avoid circular updates. Therefore, each of the :meth:`__displayLocationChanged`, :meth:`__worldLocationChanged` and :meth:`__voxelLocationChanged` methods use the :meth:`__prePropagate`, :meth:`__propagate`, and :meth:`__postPropagate` methods to propagate changes between the three location properties. """ if not self or self.destroyed(): return if len(self.overlayList) == 0: return if self.__registeredOverlay is None: return self.__prePropagate() self.__propagate('display', 'voxel') self.__propagate('display', 'world') self.__postPropagate() self.__updateLocationInfo() def __worldLocationChanged(self, *a): """Called when the :attr:`worldLocation` changes. Propagates the change on to the :attr:`voxelLocation` and :attr:`.DisplayContext.location` properties. """ if len(self.overlayList) == 0: return if self.__registeredOverlay is None: return self.__prePropagate() self.__propagate('world', 'voxel') self.__propagate('world', 'display') self.__postPropagate() self.__updateLocationInfo() def __voxelLocationChanged(self, *a): """Called when the :attr:`voxelLocation` changes. Propagates the change on to the :attr:`worldLocation` and :attr:`.DisplayContext.location` properties. """ if len(self.overlayList) == 0: return if self.__registeredOverlay is None: return self.__prePropagate() self.__propagate('voxel', 'world') self.__propagate('voxel', 'display') self.__postPropagate() self.__updateLocationInfo() def __prePropagate(self): """Called by the :meth:`__displayLocationChanged`, :meth:`__worldLocationChanged` and :meth:`__voxelLocationChanged` methods. Disables notification of all location property listeners, so circular updates do not occur. """ self .disableNotification('voxelLocation') self .disableNotification('worldLocation') self.displayCtx.disableListener( 'location', self.name) self.Freeze() def __propagate(self, source, target): """Called by the :meth:`__displayLocationChanged`, :meth:`__worldLocationChanged` and :meth:`__voxelLocationChanged` methods. Copies the coordinates from the ``source`` location to the ``target`` location. Valid values for the ``source`` and ``target`` are: =========== ============================================== ``display`` The :attr:`.DisplayContext.location` property. ``voxel`` The :attr:`voxelLocation` property. ``world`` The :attr:`worldLocation` property. =========== ============================================== """ if source == 'display': coords = self.displayCtx.location.xyz elif source == 'voxel': coords = self.voxelLocation.xyz elif source == 'world': coords = self.worldLocation.xyz refImage = self.__registeredOpts.referenceImage if refImage is not None: opts = self.displayCtx.getOpts(refImage) xformed = opts.transformCoords([coords], source, target, vround=target == 'voxel')[0] else: xformed = coords log.debug('Updating location ({} {} -> {} {})'.format( source, coords, target, xformed)) if target == 'display': self.displayCtx.location.xyz = xformed elif target == 'voxel': self.voxelLocation .xyz = xformed elif target == 'world': self.worldLocation .xyz = xformed def __postPropagate(self): """Called by the :meth:`__displayLocationChanged`, :meth:`__worldLocationChanged` and :meth:`__voxelLocationChanged` methods. Re-enables the property listeners that were disabled by the :meth:`__postPropagate` method. """ self .enableNotification('voxelLocation') self .enableNotification('worldLocation') self.displayCtx.enableListener( 'location', self.name) self.Thaw() self.Refresh() self.Update() def __updateLocationInfo(self): """Called whenever the :attr:`.DisplayContext.location` changes. Updates the HTML panel which displays information about all overlays in the :class:`.OverlayList`. """ if len(self.overlayList) == 0 or self.__registeredOverlay is None: self.__info.SetPage('') return # Reverse the overlay order so they # are ordered the same on the info # page as in the overlay list panel displayCtx = self.displayCtx overlays = reversed(displayCtx.getOrderedOverlays()) selOvl = displayCtx.getSelectedOverlay() lines = [] for overlay in overlays: display = displayCtx.getDisplay(overlay) opts = display.opts if not display.enabled: continue info = None title = '<b>{}</b>'.format(display.name) # For mesh overlays, if the current location # corresponds to a vertex, show some info # about that vertex if isinstance(overlay, fslmesh.Mesh): vidx = opts.getVertex() vd = opts.getVertexData() vdidx = opts.vertexDataIndex if vidx is None: info = '[no vertex]' else: # some vertex data has been # loaded for this mesh. if vd is not None: # time series/multiple data points per # vertex - display the time/data index # as well if vd.shape[1] > 1: info = '[{}, {}]: {}'.format(vidx, vdidx, vd[vidx, vdidx]) # Only one scalar value per vertex - # don't bother showing the vertex # data index else: info = '[{}]: {}'.format(vidx, vd[vidx, vdidx]) else: info = '[{}]'.format(vidx) elif isinstance(overlay, fslimage.Image): vloc = opts.getVoxel() if vloc is not None: vloc = tuple(int(v) for v in vloc) vloc = opts.index(vloc) vval = overlay[vloc] vloc = ' '.join(map(str, vloc)) if not np.isscalar(vval): vval = np.asscalar(vval) if opts.overlayType == 'label': lbl = opts.lut.get(int(vval)) if lbl is None: lbl = 'no label' else: lbl = lbl.name info = '[{}]: {} / {}'.format(vloc, vval, lbl) else: info = '[{}]: {}'.format(vloc, vval) else: info = strings.labels[self, 'outOfBounds'] else: info = '{}'.format(strings.labels[self, 'noData']) # Indent info for unselected overlays, # to make the info for the selected # overlay a bit more obvious. colourFmt = '<span style="color: #6060ff">{}</span>' if overlay is selOvl: title = colourFmt.format(title) if info is not None: info = colourFmt.format(info) lines.append(title) if info is not None: lines.append(info) self.__info.SetPage('<br>'.join(lines)) self.__info.Refresh()
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 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 DisplayContext(props.SyncableHasProperties): """A ``DisplayContext`` instance contains a number of properties defining how the overlays in an :class:`.OverlayList` are to be displayed, and related contextual information. A ``DisplayContext`` instance is responsible for creating and destroying :class:`.Display` instances for every overlay in the ``OverlayList``. These ``Display`` instances, and the corresponding :class:`.DisplayOpts` instances (which, in turn, are created/destroyed by ``Display`` instances) can be accessed with the :meth:`getDisplay` and :meth:`getOpts` method respectively. A number of other useful methods are provided by a ``DisplayContext`` instance: .. autosummary:: :nosignatures: getDisplay getOpts getReferenceImage displayToWorld worldToDisplay displaySpaceIsRadiological selectOverlay getSelectedOverlay getOverlayOrder getOrderedOverlays freeze freezeOverlay thawOverlay defaultDisplaySpace detachDisplaySpace """ selectedOverlay = props.Int(minval=0, default=0, clamped=True) """Index of the currently 'selected' overlay. .. note:: The value of this index is in relation to the :class:`.OverlayList`, rather than to the :attr:`overlayOrder` list. If you're interested in the currently selected overlay, you must also listen for changes to the :attr:`.OverlayList.images` list as, if the list changes, the :attr:`selectedOverlay` index may not change, but the overlay to which it points may be different. """ location = props.Point(ndims=3) """The location property contains the currently selected 3D location (xyz) in the display coordinate system. Different ``DisplayContext`` instances may be using different display coordinate systems - see the :attr:`displaySpace` property. """ worldLocation = props.Point(ndims=3) """The location property contains the currently selected 3D location (xyz) in the world coordinate system. Whenever the :attr:`location` changes, it gets transformed into the world coordinate system, and propagated to this property. The location of different ``DisplayContext`` instances is synchronised through this property. .. note:: If any :attr:`.NiftiOpts.transform` properties have been modified independently of the :attr:`displaySpace`, this value will be invalid. """ bounds = props.Bounds(ndims=3) """This property contains the min/max values of a bounding box (in display coordinates) which is big enough to contain all of the overlays in the :class:`.OverlayList`. .. warning:: This property shouid be treated as read-only. """ overlayOrder = props.List(props.Int()) """A list of indices into the :attr:`.OverlayList.overlays` list, defining the order in which the overlays are to be displayed. See the :meth:`getOrderedOverlays` method. """ overlayGroups = props.List() """A list of :class:`.OverlayGroup` instances, each of which defines a group of overlays which share display properties. """ syncOverlayDisplay = props.Boolean(default=True) """If this ``DisplayContext`` instance has a parent (see :mod:`props.syncable`), and this property is ``True``, the properties of the :class:`.Display` and :class:`.DisplayOpts` instances for every overlay managed by this ``DisplayContext`` instance will be synchronised to those of the parent instance. Otherwise, the display properties for every overlay will be unsynchronised from the parent. Synchronisation of the following properties between child and parent ``DisplayContext`` instances is also controlled by this flag: - :attr:`displaySpace` - :attr:`bounds` - :attr:`radioOrientation` .. note:: This property is accessed by the :class:`.Display` class, in its constructor, and when it creates new :class:`.DisplayOpts` instances, to set initial sync states. """ displaySpace = props.Choice(('world', )) """The *space* in which overlays are displayed. This property defines the display coordinate system for this ``DisplayContext``. When it is changed, the :attr:`.NiftiOpts.transform` property of all :class:`.Nifti` overlays in the :class:`.OverlayList` is updated. It has two settings, described below. The options for this property are dynamically added by :meth:`__updateDisplaySpaceOptions`. 1. **World** space (a.k.a. ``'world'``) All :class:`.Nifti` overlays are displayed in the space defined by their affine transformation matrix - the :attr:`.NiftiOpts.transform` property for every ``Nifti`` overlay is set to ``affine``. 2. **Reference image** space A single :class:`.Nifti` overlay is selected as a *reference* image, and is displayed in scaled voxel space (with a potential L/R flip for neurological images - its :attr:`.NiftiOpts.transform` is set to ``pixdim-flip``). All other ``Nifti`` overlays are transformed into this reference space - their :attr:`.NiftiOpts.transform` property is set to ``reference``, which results in them being transformed into the scaled voxel space of the reference image. .. note:: The :attr:`.NiftiOpts.transform` property of any :class:`.Nifti` overlay can be set independently of this property. However, whenever *this* property changes, it will change the ``transform`` property for every ``Nifti``, in the manner described above. The :meth:`defaultDisplaySpace` can be used to control how the ``displaySpace`` is initialised. """ radioOrientation = props.Boolean(default=True) """If ``True``, 2D views will show images in radiological convention (i.e.subject left on the right of the display). Otherwise, they will be shown in neurological convention (subject left on the left). .. note:: This setting is not enforced by the ``DisplayContext``. It is the responsibility of the :class:`.OrthoPanel` and :class:`.LightBoxPanel` (and other potential future 2D view panels) to implement the flip. """ autoDisplay = props.Boolean(default=False) """If ``True``, whenever an overlay is added to the :class:`.OverlayList`, the :mod:`autodisplay` module will be used to automatically configure its display settings. Note that the ``DisplayContext`` does not perform this configuration - this flag is used by other modules (e.g. the :class:`.OverlayListPanel` and the :class:`.OpenFileAction`). """ loadInMemory = props.Boolean(default=False) """If ``True``, all :class:`.Image` instances will be loaded into memory, regardless of their size. Otherwise (the default), large compressed ``Image`` overlays may be kept on disk. .. note:: Changing the value of this property will not affect existing ``Image`` overlays. .. note:: This property may end up being used in a more general sense by any code which needs to decide whether to do things in a more or less memory-intensive manner. """ def __init__(self, overlayList, parent=None, defaultDs='ref', **kwargs): """Create a ``DisplayContext``. :arg overlayList: An :class:`.OverlayList` instance. :arg parent: Another ``DisplayContext`` instance to be used as the parent of this instance, passed to the :class:`.SyncableHasProperties` constructor. :arg defaultDs: Initial value for the :meth:`defaultDisplaySpace`. Either ``'ref'`` or ``'world'``. If ``'ref'`` (the default), when overlays are added to an empty list, the :attr:`displaySpace` will be set to the first :class:`.Nifti` overlay. Otherwise (``'world'``), the display space will be set to ``'world'``. All other arguments are passed through to the ``SyncableHasProperties`` constructor, in addition to the following: - The ``syncOverlayDisplay`` and ``location`` properties are added to the ``nobind`` argument - The ``selectedOverlay``, ``overlayGroups``, ``autoDisplay`` and ``loadInMemory`` properties are added to the ``nounbind`` argument. """ kwargs = dict(kwargs) nobind = kwargs.pop('nobind', []) nounbind = kwargs.pop('nounbind', []) nobind.extend(['syncOverlayDisplay', 'location', 'bounds']) nounbind.extend([ 'selectedOverlay', 'overlayGroups', 'autoDisplay', 'loadInMemory' ]) kwargs['parent'] = parent kwargs['nobind'] = nobind kwargs['nounbind'] = nounbind kwargs['state'] = {'overlayOrder': False} props.SyncableHasProperties.__init__(self, **kwargs) self.__overlayList = overlayList self.__name = '{}_{}'.format(self.__class__.__name__, id(self)) self.__child = parent is not None # When the first overlay(s) is/are # added, the display space may get # set either to a reference image, # or to world. The defaultDisplaySpace # controls this behaviour. self.defaultDisplaySpace = defaultDs # The overlayOrder is unsynced by # default, but we will inherit the # current parent value. if self.__child: self.overlayOrder[:] = parent.overlayOrder[:] else: self.overlayOrder[:] = range(len(overlayList)) # If this is the first child DC, we # need to initialise the display # space and location. If there is # already a child DC, then we have # (probably) inherited initial # settings. if self.__child: self.__initDS = (len(parent.getChildren()) - 1) == 0 # This dict contains the Display # objects for every overlay in # the overlay list, as # {Overlay : Display} mappings self.__displays = {} overlayList.addListener('overlays', self.__name, self.__overlayListChanged, immediate=True) if self.__child: self.addListener('syncOverlayDisplay', self.__name, self.__syncOverlayDisplayChanged) self.addListener('displaySpace', self.__name, self.__displaySpaceChanged, immediate=True) self.addListener('location', self.__name, self.__locationChanged, immediate=True) self.addListener('worldLocation', self.__name, self.__worldLocationChanged, immediate=True) # The overlayListChanged method # is important - check it out self.__overlayListChanged() log.debug('{}.init ({})'.format(type(self).__name__, id(self))) def __del__(self): """Prints a log message.""" if log: log.debug('{}.del ({})'.format(type(self).__name__, id(self))) def destroy(self): """This method must be called when this ``DisplayContext`` is no longer needed. When a ``DisplayContext`` is destroyed, all of the :class:`.Display` instances managed by it are destroyed as well. """ self.detachAllFromParent() overlayList = self.__overlayList displays = self.__displays self.__overlayList = None self.__displays = None overlayList.removeListener('overlays', self.__name) if self.__child: self.removeListener('syncOverlayDisplay', self.__name) self.removeListener('displaySpace', self.__name) self.removeListener('location', self.__name) self.removeListener('worldLocation', self.__name) for overlay, display in displays.items(): display.destroy() def destroyed(self): """Returns ``True`` if this ``DisplayContext`` has been, or is being, destroyed, ``False`` otherwise. """ return self.__overlayList is None def getDisplay(self, overlay, overlayType=None): """Returns the :class:`.Display` instance for the specified overlay (or overlay index). If the overlay is not in the ``OverlayList``, an :exc:`InvalidOverlayError` is raised. Otheriwse, if a :class:`Display` object does not exist for the given overlay, one is created. If this ``DisplayContext`` has been destroyed, a ``ValueError`` is raised. :arg overlay: The overlay to retrieve a ``Display`` instance for, or an index into the ``OverlayList``. :arg overlayType: If a ``Display`` instance for the specified ``overlay`` does not exist, one is created - in this case, the specified ``overlayType`` is passed to the :class:`.Display` constructor. """ if overlay is None: raise ValueError('No overlay specified') if self.destroyed(): raise ValueError('DisplayContext has been destroyed') if overlay not in self.__overlayList: raise InvalidOverlayError('Overlay {} is not in ' 'list'.format(overlay.name)) if isinstance(overlay, int): overlay = self.__overlayList[overlay] try: display = self.__displays[overlay] except KeyError: if not self.__child: dParent = None else: dParent = self.getParent().getDisplay(overlay, overlayType) if overlayType is None: overlayType = dParent.overlayType from .display import Display display = Display(overlay, self.__overlayList, self, parent=dParent, overlayType=overlayType) self.__displays[overlay] = display return display def getOpts(self, overlay, overlayType=None): """Returns the :class:`.DisplayOpts` instance associated with the specified overlay. See :meth:`getDisplay` and :meth:`.Display.opts` for more details. """ if overlay is None: raise ValueError('No overlay specified') if self.destroyed(): raise ValueError('DisplayContext has been destroyed') if overlay not in self.__overlayList: raise InvalidOverlayError('Overlay {} is not in ' 'list'.format(overlay.name)) return self.getDisplay(overlay, overlayType).opts def getReferenceImage(self, overlay): """Convenience method which returns the reference image associated with the given overlay, or ``None`` if there is no reference image. See the :class:`.DisplayOpts.referenceImage` method. """ if overlay is None: return None return self.getOpts(overlay).referenceImage def displayToWorld(self, dloc): """Transforms the given coordinates from the display coordinate system into the world coordinate system. .. warning:: If any :attr:`.NiftiOpts.transform` properties have been modified manually, this method will return invalid results. """ displaySpace = self.displaySpace if displaySpace == 'world' or len(self.__overlayList) == 0: return dloc opts = self.getOpts(displaySpace) return opts.transformCoords(dloc, 'display', 'world') def worldToDisplay(self, wloc): """Transforms the given coordinates from the world coordinate system into the display coordinate system. .. warning:: If any :attr:`.NiftiOpts.transform` properties have been modified manually, this method will return invalid results. """ displaySpace = self.displaySpace if displaySpace == 'world' or len(self.__overlayList) == 0: return wloc opts = self.getOpts(displaySpace) return opts.transformCoords(wloc, 'world', 'display') def displaySpaceIsRadiological(self): """Returns ``True`` if the current :attr:`displaySpace` aligns with a radiological orientation. A radiological orientation is one in which anatomical right is shown on the left of the screen, i.e.: - The X axis corresponds to right -> left - The Y axis corresponds to posterior -> anterior - The Z axis corresponds to inferior -> superior """ if len(self.__overlayList) == 0: return True space = self.displaySpace # Display space is either 'world', or an image. # We assume that 'world' is an RAS coordinate # system which, if transferred directly to a # display coordinate system, would result in a # neurological view (left on left, right on # right). if space == 'world': return False else: opts = self.getOpts(space) xform = opts.getTransform('pixdim-flip', 'display') return npla.det(xform) > 0 def selectOverlay(self, overlay): """Selects the specified ``overlay``. Raises an :exc:`IndexError` if the overlay is not in the list. If you want to select an overlay by its index in the ``OverlayList``, you can just assign to the :attr:`selectedOverlay` property directly. """ self.selectedOverlay = self.__overlayList.index(overlay) def getSelectedOverlay(self): """Returns the currently selected overlay object, or ``None`` if there are no overlays. """ if len(self.__overlayList) == 0: return None if self.selectedOverlay >= len(self.__overlayList): return None return self.__overlayList[self.selectedOverlay] def getOverlayOrder(self, overlay): """Returns the order in which the given overlay (or an index into the :class:`.OverlayList` list) should be displayed (see the :attr:`overlayOrder` property). Raises an :exc:`IndexError` if the overlay is not in the list. """ self.__syncOverlayOrder() if not isinstance(overlay, int): overlay = self.__overlayList.index(overlay) return self.overlayOrder.index(overlay) def getOrderedOverlays(self): """Returns a list of overlay objects from the :class:`.OverlayList` list, sorted into the order that they should be displayed, as defined by the :attr:`overlayOrder` property. """ self.__syncOverlayOrder() return [self.__overlayList[idx] for idx in self.overlayOrder] @contextlib.contextmanager def freeze(self, overlay): """This method can be used as a context manager to suppress notification for all :class:`.Display` and :class:`.DisplayOpts` properties related to the given ``overlay``:: with displayCtx.freeze(overlay): # Do stuff which might trigger unwanted # Display/DisplayOpts notifications See :meth:`freezeOverlay` and :meth:`thawOverlay`. """ self.freezeOverlay(overlay) try: yield finally: self.thawOverlay(overlay) def freezeOverlay(self, overlay): """Suppresses notification for all :class:`.Display` and :class:`.DisplayOpts` properties associated with the given ``overlay``. Call :meth:`.thawOverlay` to re-enable notification. See also the :meth:`freeze` method, which can be used as a context manager to automatically call this method and ``thawOverlay``. """ if self.__child: self.getParent().freezeOverlay(overlay) return dctxs = [self] + self.getChildren() for dctx in dctxs: display = dctx.getDisplay(overlay) opts = display.opts display.disableAllNotification() opts.disableAllNotification() def thawOverlay(self, overlay): """Enables notification for all :class:`.Display` and :class:`.DisplayOpts` properties associated with the given ``overlay``. """ if self.__child: self.getParent().thawOverlay(overlay) return dctxs = [self] + self.getChildren() for dctx in dctxs: display = dctx.getDisplay(overlay) opts = display.opts display.enableAllNotification() opts.enableAllNotification() @property def defaultDisplaySpace(self): """This property controls how the :attr:`displaySpace` is initialised when overlays are added to a previously empty :class:`.OverlayList`. If the ``defaultDisplaySpace`` is set to ``'ref'``, the ``displaySpace`` will be initialised to the first :class:`.Nifti` overlay. Otherwise (the ``defaultDisplaySpace`` is set to ``'world'``), the ``displaySpace`` will be set to ``'world'``. """ return self.__defaultDisplaySpace @defaultDisplaySpace.setter def defaultDisplaySpace(self, ds): """Sets the :meth:`defaultDisplaySpace`. :arg ds: Either ``'ref'`` or ``'world'``. """ if ds not in ('world', 'ref'): raise ValueError('Invalid default display space: {}'.format(ds)) self.__defaultDisplaySpace = ds def detachDisplaySpace(self): """Detaches the :attr:`displaySpace` and :attr:`bounds` properties, and all related :class:`.DisplayOpts` properties, from the parent ``DisplayContext``. This allows this ``DisplayContext`` to use a display coordinate system that is completely independent from other instances, and is not affected by changes to the parent properties. This is an irreversible operation. """ self.detachFromParent('displaySpace') self.detachFromParent('bounds') for ovl in self.__overlayList: opts = self.getOpts(ovl) opts.detachFromParent('bounds') if isinstance(ovl, fslimage.Nifti): opts.detachFromParent('transform') def __overlayListChanged(self, *a): """Called when the :attr:`.OverlayList.overlays` property changes. Ensures that a :class:`.Display` and :class:`.DisplayOpts` object exists for every image, updates the :attr:`bounds` property, makes sure that the :attr:`overlayOrder` property is consistent, and updates constraints on the :attr:`selectedOverlay` property. """ # Discard all Display instances # which refer to overlays that # are no longer in the list for overlay in list(self.__displays.keys()): if overlay not in self.__overlayList: display = self.__displays.pop(overlay) opts = display.opts display.removeListener('overlayType', self.__name) opts.removeListener('bounds', self.__name) # The display instance will destroy the # opts instance, so we don't do it here display.destroy() # Ensure that a Display object exists # for every overlay in the list for overlay in self.__overlayList: ovlType = self.__overlayList.initOverlayType(overlay) # The getDisplay method # will create a Display object # if one does not already exist display = self.getDisplay(overlay, ovlType) opts = display.opts # Register a listener on the overlay type, # because when it changes, the DisplayOpts # instance will change, and we will need # to re-register the DisplayOpts.bounds # listener (see the next statement) display.addListener('overlayType', self.__name, self.__overlayListChanged, overwrite=True) # Register a listener on the DisplayOpts.bounds # property for every overlay - if the display # bounds for any overlay changes, we need to # update our own bounds property. This is only # done on child DCs, as the parent DC bounds # only gets used for synchronisation if self.__child: opts.addListener('bounds', self.__name, self.__overlayBoundsChanged, overwrite=True) # If detachDisplaySpace has been called, # make sure the opts bounds (and related) # properties are also detached if not self.canBeSyncedToParent('displaySpace'): opts.detachFromParent('bounds') if isinstance(overlay, fslimage.Nifti): opts.detachFromParent('transform') # Ensure that the displaySpace # property options are in sync # with the overlay list. self.__updateDisplaySpaceOptions() # Stuff which only needs to # be done on the parent DC if not self.__child: # Limit the selectedOverlay property # so it cannot take a value greater # than len(overlayList)-1. selectedOverlay # is always synchronised, so we only # need to do this on the parent DC. nOverlays = len(self.__overlayList) if nOverlays > 0: self.setAttribute('selectedOverlay', 'maxval', nOverlays - 1) else: self.setAttribute('selectedOverlay', 'maxval', 0) return # Ensure that the overlayOrder # property is valid self.__syncOverlayOrder() # If the overlay list was empty, # and is now non-empty, we need # to initialise the display space # and the display location initDS = self.__initDS and \ np.all(np.isclose(self.bounds, 0)) and \ len(self.__overlayList) > 0 self.__initDS = len(self.__overlayList) == 0 # Initialise the display space. We # have to do this before updating # image transforms, and updating # the display bounds if initDS: displaySpace = 'world' if self.defaultDisplaySpace == 'ref': for overlay in self.__overlayList: if isinstance(overlay, fslimage.Nifti): displaySpace = overlay break with props.skip(self, 'displaySpace', self.__name): self.displaySpace = displaySpace # Initialise the transform property # of any Image overlays which have # just been added to the list, oldList = self.__overlayList.getLastValue('overlays')[:] for overlay in self.__overlayList: if isinstance(overlay, fslimage.Nifti) and \ (overlay not in oldList): self.__setTransform(overlay) # Ensure that the bounds # property is accurate self.__updateBounds() # Initialise the display location to # the centre of the display bounds if initDS: b = self.bounds self.location.xyz = [ b.xlo + b.xlen / 2.0, b.ylo + b.ylen / 2.0, b.zlo + b.zlen / 2.0 ] self.__propagateLocation('world') else: self.__propagateLocation('display') def __updateDisplaySpaceOptions(self): """Updates the :attr:`displaySpace` property so it is synchronised with the current contents of the :class:`.OverlayList` This method is called by the :meth:`__overlayListChanged` method. """ choiceProp = self.getProp('displaySpace') choices = [] for overlay in self.__overlayList: if isinstance(overlay, fslimage.Nifti): choices.append(overlay) choices.append('world') choiceProp.setChoices(choices, instance=self) def __setTransform(self, image): """Sets the :attr:`.NiftiOpts.transform` property associated with the given :class:`.Nifti` overlay to a sensible value, given the current value of the :attr:`.displaySpace` property. Called by the :meth:`__displaySpaceChanged` method, and by :meth:`__overlayListChanged` for any :class:`.Image` overlays which have been newly added to the :class:`.OverlayList`. :arg image: An :class:`.Image` overlay. """ space = self.displaySpace opts = self.getOpts(image) # Disable notification of the bounds # property so the __overlayBoundsChanged # method does not get called. Use # ignoreInvalid, because this method might # get called before we have registered a # listener on the bounds property. with props.skip(opts, 'bounds', self.__name, ignoreInvalid=True): if space == 'world': opts.transform = 'affine' elif image is space: opts.transform = 'pixdim-flip' else: opts.transform = 'reference' def __displaySpaceChanged(self, *a): """Called when the :attr:`displaySpace` property changes. Updates the :attr:`.NiftiOpts.transform` property for all :class:`.Nifti` overlays in the :class:`.OverlayList`. """ selectedOverlay = self.getSelectedOverlay() if selectedOverlay is None: return # Update the transform property of all # Image overlays to put them into the # new display space for overlay in self.__overlayList: if not isinstance(overlay, fslimage.Nifti): continue self.__setTransform(overlay) # Update the display world bounds, # and then update the location self.__updateBounds() # Make sure that the location is # kept in the same place, relative # to the world coordinate system self.__propagateLocation('display') def __syncOverlayOrder(self): """Ensures that the :attr:`overlayOrder` property is up to date with respect to the :class:`.OverlayList`. """ if len(self.overlayOrder) == len(self.__overlayList): return # # NOTE: The following logic assumes that operations # which modify the overlay list will only do # one of the following: # # - Adding one or more overlays to the end of the list # - Removing one or more overlays from the list # # More complex overlay list modifications # will cause this code to break. oldList = self.__overlayList.getLastValue('overlays')[:] oldOrder = self.overlayOrder[:] # If the overlay order was just the # list order, preserve that ordering if self.overlayOrder[:] == list(range(len(oldList))): self.overlayOrder[:] = list(range(len(self.__overlayList))) # If overlays have been added to # the overlay list, add indices # for them to the overlayOrder list elif len(self.overlayOrder) < len(self.__overlayList): newOrder = [] newOverlayIdx = len(oldList) # The order of existing overlays is preserved, # and all new overlays added to the end of the # overlay order. for overlay in self.__overlayList: if overlay in oldList: newOrder.append(oldOrder[oldList.index(overlay)]) else: newOrder.append(newOverlayIdx) newOverlayIdx += 1 self.overlayOrder[:] = newOrder # Otherwise, if overlays have been # removed from the overlay list ... elif len(self.overlayOrder) > len(self.__overlayList): # Remove the corresponding indices # from the overlayOrder list for i, overlay in enumerate(oldList): if overlay not in self.__overlayList: oldOrder.remove(i) # Re-generate new indices, # preserving the order of # the remaining overlays newOrder = [sorted(oldOrder).index(idx) for idx in oldOrder] self.overlayOrder[:] = newOrder def __overlayBoundsChanged(self, value, valid, opts, name): """Called when the :attr:`.DisplayOpts.bounds` property of any overlay changes. Updates the :attr:`bounds` property and preserves the display :attr:`location` in terms of the :attr:`worldLocation`. """ # This method might get called # after DisplayOpts instance # has been destroyed if opts.display is None: return # Update the display context bounds # to take into account any changes # to individual overlay bounds. # Inhibit notification on the location # property - it will be updated properly # below self.__updateBounds() # Make sure the display location # is consistent w.r.t. the world # coordinate location self.__propagateLocation('display') def __syncOverlayDisplayChanged(self, *a): """Called when the :attr:`syncOverlayDisplay` property changes. Synchronises or unsychronises the :class:`.Display` and :class:`.DisplayOpts` instances for every overlay to/from their parent instances. """ dcProps = ['displaySpace', 'bounds', 'radioOrientation'] if self.syncOverlayDisplay: for p in dcProps: if self.canBeSyncedToParent(p): self.syncToParent(p) else: for p in dcProps: if self.canBeUnsyncedFromParent(p): self.unsyncFromParent(p) for display in self.__displays.values(): opts = display.opts if self.syncOverlayDisplay: display.syncAllToParent() opts.syncAllToParent() else: display.unsyncAllFromParent() opts.unsyncAllFromParent() def __updateBounds(self, *a): """Called when the overlay list changes, or when any overlay display transform is changed. Updates the :attr:`bounds` property so that it is big enough to contain all of the overlays (as defined by their :attr:`.DisplayOpts.bounds` properties). """ if len(self.__overlayList) == 0: minBounds = [0.0, 0.0, 0.0] maxBounds = [0.0, 0.0, 0.0] else: minBounds = 3 * [sys.float_info.max] maxBounds = 3 * [-sys.float_info.max] for ovl in self.__overlayList: display = self.__displays[ovl] opts = display.opts lo = opts.bounds.getLo() hi = opts.bounds.getHi() for ax in range(3): if lo[ax] < minBounds[ax]: minBounds[ax] = lo[ax] if hi[ax] > maxBounds[ax]: maxBounds[ax] = hi[ax] self.bounds[:] = [ minBounds[0], maxBounds[0], minBounds[1], maxBounds[1], minBounds[2], maxBounds[2] ] # Update the constraints on the location # property to be aligned with the new bounds with props.suppress(self, 'location'): self.location.setLimits(0, self.bounds.xlo, self.bounds.xhi) self.location.setLimits(1, self.bounds.ylo, self.bounds.yhi) self.location.setLimits(2, self.bounds.zlo, self.bounds.zhi) def __locationChanged(self, *a): """Called when the :attr:`location` property changes. Propagates the new location to the :attr:`worldLocation` property. """ self.__propagateLocation('world') def __worldLocationChanged(self, *a): """Called when the :attr:`worldLocation` property changes. Propagates the new location to the :attr:`location` property. """ self.__propagateLocation('display') def __propagateLocation(self, dest): """Called by the :meth:`__locationChanged` and :meth:`__worldLocationChanged` methods. The ``dest`` argument may be either ``'world'`` (the ``worldLocation`` is updated from the ``location``), or ``'display'`` (vice-versa). """ if self.displaySpace == 'world': if dest == 'world': with props.skip(self, 'worldLocation', self.__name): self.worldLocation = self.location else: with props.skip(self, 'location', self.__name): self.location = self.worldLocation return ref = self.displaySpace opts = self.getOpts(ref) if dest == 'world': with props.skip(self, 'location', self.__name): self.worldLocation = opts.transformCoords( self.location, 'display', 'world') else: with props.skip(self, 'worldLocation', self.__name): self.location = opts.transformCoords(self.worldLocation, 'world', 'display')
class MyObj(props.HasProperties): mypointi = props.Point(ndims=2, real=False) mypointf = props.Point(ndims=2)