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 OverlayGroup(props.HasProperties): """An ``OverlayGroup`` is a group of overlays for which the corresponding :class:`.Display` and :class:`.DisplayOpts` properties are synchronised. The point of the ``OverlayGroup`` is to allow the user to define groups of overlays, so he/she can change display properties on the entire group, instead of having to change display properties on each overlay one by one. Overlays can be added to an ``OverlayGroup`` with the :meth:`addOverlay`, and removed with the :meth:`removeOverlay`. When an ``OverlayGroup`` is created, it dynamically adds all of the properties which could possibly be linked between overlays to itself, using the :meth:`props.HasProperties.addProperty` method. When the first overlay is added to the group, these group properties are set to the display properties of this overlay. Then, the display properties of overlays which are subsequently added to the group will be set to the group display properties. .. note:: Currently, only a subset of display properties are linked between the overlays in a group. The properties which are linked are hard-coded in the :attr:`_groupBindings` dictionary. A possible future *FSLeyes* enhancement will be to allow the user to specify which display properties within an ``OverlayGroup`` should be linked. """ overlays = props.List() """The list of overlays in this ``OverlayGroup``. .. warning:: Do not add/remove overlays directly to this list - use the :meth:`addOverlay` and :meth:`removeOverlay` methods instead. """ _groupBindings = td.TypeDict({ 'Display': [], 'NiftiOpts': ['volume', 'volumeDim'], 'VolumeOpts': ['interpolation'], 'Volume3DOpts': [ 'numSteps', 'numInnerSteps', 'resolution', 'numClipPlanes', 'clipMode', 'clipPosition', 'clipAzimuth', 'clipInclination' ], 'LabelOpts': [], 'MeshOpts': ['refImage', 'coordSpace'], 'VectorOpts': ['suppressX', 'suppressY', 'suppressZ', 'suppressMode'], 'LineVectorOpts': [], 'RGBVectorOpts': ['interpolation'], 'TensorOpts': ['lighting', 'tensorResolution'], }) """This dictionary defines the properties which are bound across :class:`.Display` instances :class:`.DisplayOpts` sub-class instances, for overlays which are in the same group. """ def __init__(self, displayCtx, overlayList): """Create an ``OverlayGroup``. :arg displayCtx: The :class:`.DisplayContext`. :arg overlayList: The :class:`.OverlayList`. """ self.__displayCtx = displayCtx self.__overlayList = overlayList self.__name = '{}_{}'.format(type(self).__name__, id(self)) # This dict is used by the __bindDisplayOpts # method to keep track of which group properties # have already been given a value self.__hasBeenSet = {} # Import all of the Display/DisplayOpts # classes into the local namespace from fsleyes.displaycontext import ( # noqa Display, NiftiOpts, VolumeOpts, Volume3DOpts, MaskOpts, VectorOpts, RGBVectorOpts, LineVectorOpts, MeshOpts, LabelOpts, TensorOpts) overlayList.addListener('overlays', self.__name, self.__overlayListChanged) # Add all of the properties listed # in the _groupBindings dict as # properties of this OverlayGroup # instance. for clsName, propNames in OverlayGroup._groupBindings.items(): cls = locals()[clsName] for propName in propNames: prop = copy.copy(getattr(cls, propName)) self.addProperty('{}_{}'.format(clsName, propName), prop) self.__hasBeenSet[clsName, propName] = False # Special case - make sure that the NiftiOpts # volume property is not constrained self.setAttribute('NiftiOpts_volume', 'maxval', six.MAXSIZE) def __copy__(self): """Create a copy of this ``OverlayGroup``. A custom copy operator is needed due to the way that the :class:`.props.HasProperties` class works. """ return OverlayGroup(self, self.__displayCtx, self.__overlayList) def __str__(self): """Returns a string representation of this ``OverlayGroup``.""" return str([str(o) for o in self.overlays]) def __repr__(self): """Returns a string representation of this ``OverlayGroup``.""" return '[{}]'.format(', '.join([str(o) for o in self.overlays])) def __contains__(self, ovl): """Returns ``True`` if ``ovl`` is in this ``OverlayGroup``, :attr:`False` otherwise. """ return ovl in self.overlays def __len__(self): """Returns the number of overlays in this ``OverlayGroup``. """ return len(self.overlays) def destroy(self): """Must be called when this ``OverlayGroup`` is no longer needed. Removes all overlays from the group, and removes property listeners. """ for overlay in list(self.overlays): self.removeOverlay(overlay) self.__overlayList.removeListener('overlays', self.__name) self.__overlayList = None self.__displayCtx = None def addOverlay(self, overlay): """Add an overlay to this ``OverlayGroup``. If this is the first overlay to be added, the properties of this ``OverlayGroup`` are set to the overlay display properties. Otherwise, the overlay display properties are set to those of this ``OverlayGroup``. """ if overlay in self.overlays: return self.overlays.append(overlay) display = self.__displayCtx.getDisplay(overlay) opts = display.opts log.debug('Adding overlay {} to group {}'.format( overlay.name, self.__name)) self.__bindDisplayOpts(display) self.__bindDisplayOpts(opts) display.addListener('overlayType', self.__name, self.__overlayTypeChanged) def removeOverlay(self, overlay): """Remove the given overlay from this ``OverlayGroup``. """ if overlay not in self.overlays: return from . import InvalidOverlayError self.overlays.remove(overlay) try: display = self.__displayCtx.getDisplay(overlay) except InvalidOverlayError: return opts = display.opts log.debug('Removing overlay {} from group {}'.format( overlay.name, self.__name)) self.__bindDisplayOpts(display, unbind=True) self.__bindDisplayOpts(opts, unbind=True) display.removeListener('overlayType', self.__name) if len(self.overlays) == 0: for key in self.__hasBeenSet.keys(): self.__hasBeenSet[key] = False def __bindDisplayOpts(self, target, unbind=False): """Binds or unbinds the properties of the given ``target`` to the properties of this ``OverlayGroup``. :arg target: A :class:`.Display` or :class:`.DisplayOpts` instance. :arg unbind: Set to ``True`` to bind the properties, ``False`` to unbind them. """ # This is the first overlay to be added - the # group should inherit its property values if len(self.overlays) == 1: master, slave = target, self # Other overlays are already in the group - the # new overlay should inherit the group properties else: master, slave = self, target bindProps = OverlayGroup._groupBindings.get(target, allhits=True, bykey=True) for clsName, propNames in bindProps.items(): for propName in propNames: groupName = '{}_{}'.format(clsName, propName) # If the group property has not yet # taken on a value, initialise it # to the property value being bound. # # We do this to avoid clobbering # property values with un-initialised # group property values. if not self.__hasBeenSet[clsName, propName]: setattr(self, groupName, getattr(target, propName)) self.__hasBeenSet[clsName, propName] = True if slave is self: otherName = propName propName = groupName else: otherName = groupName slave.bindProps(propName, master, otherName, bindatt=False, unbind=unbind) def __overlayListChanged(self, *a): """Called when overlays are added/removed to/from the :class:`.OverlayList` . Makes sure that the :attr:`overlays` list for this group does not contain any overlays that have been removed. """ for ovl in self.overlays: if ovl not in self.__overlayList: self.removeOverlay(ovl) def __overlayTypeChanged(self, value, valid, display, name): """This method is called when the :attr:`.Display.overlayType` property for an overlay in the group changes. It makes sure that the display properties of the new :class:`.DisplayOpts` instance are bound to the group properties. """ opts = display.opts self.__bindDisplayOpts(opts)
class OverlayList(props.HasProperties): """Class representing a collection of overlays to be displayed together. Contains a :class:`props.properties_types.List` property called :attr:`overlays`, containing overlay objects (e.g. :class:`.Image` or :class:`.TriangleMesh` objects). Listeners can be registered on the ``overlays`` property, so they are notified when the overlay list changes. An :class:`OverlayList` object has a few wrapper methods around the :attr:`overlays` property, allowing the :class:`OverlayList` to be used as if it were a list itself. The :mod:`.loadoverlay` module contains some convenience functions for loading and adding overlays. The :meth:`getData` and :meth:`setData` methods allow arbitrary bits of data associated with an overlay to be stored and retrieved. """ def __validateOverlay(self, atts, overlay): """Makes sure that the given overlay object is valid.""" return (hasattr(overlay, 'name') and hasattr(overlay, 'dataSource')) overlays = props.List(listType=props.Object( allowInvalid=False, validateFunc=__validateOverlay)) """A list of overlay objects to be displayed.""" def __init__(self, overlays=None): """Create an ``OverlayList`` object from the given sequence of overlays.""" if overlays is None: overlays = [] self.overlays.extend(overlays) # The append/insert methods allow an initial # overlay type to be specified for newly # added overlays. This can be queried via # the initOverlayType method (and is done so # by DisplayContext instances). self.__initOverlayType = {} # This dictionary may be used throughout FSLeyes, # via the getData/setData methods, to store # any sort of data associated with an overlay. # It is a dict of dicts: # # { # overlay : { # key : value, # key : value, # }, # overlay : { # key : value, # key : value, # } # } self.__overlayData = weakref.WeakKeyDictionary() def initOverlayType(self, overlay): """Returns the initial type for the given ``overlay``, if it was specified via the :meth:`append` or :meth:`insert` methods. Returns ``None`` otherwise. """ return self.__initOverlayType.get(overlay, None) def getData(self, overlay, key, *args): """Returns any stored value associated with the specified ``overlay`` and ``key``. :arg default: Default value if there is no value associated with the given ``key``. If not specified, and an unknown key is given, a ``KeyError`` is raised. """ if len(args) not in (0, 1): raise RuntimeError('Invalid arguments: {}'.format(args)) ovlDict = self.__overlayData.get(overlay, {}) if len(args) == 1: return ovlDict.get(key, args[0]) else: return ovlDict[key] def setData(self, overlay, key, value): """Stores the given value via the specified ``overlay`` and ``key``. """ ovlDict = self.__overlayData.get(overlay, None) if ovlDict is not None: ovlDict[key] = value else: self.__overlayData[overlay] = {key: value} def find(self, name): """Returns the first overlay with the given ``name`` or ``dataSource``, or ``None`` if there is no overlay with said ``name``/``dataSource``. """ if name is None: return None absname = op.abspath(name) for overlay in self.overlays: if overlay.name == name: return overlay if overlay.dataSource is None: continue # Ignore file extensions for NIFTI images. if isinstance(overlay, fslimage.Image): if fslimage.removeExt(overlay.dataSource) == \ fslimage.removeExt(absname): return overlay else: if overlay.dataSource == absname: return overlay return None def __str__(self): return self.overlays.__str__() def __repr__(self): return self.overlays.__str__() # Wrappers around the overlays list property, allowing this # OverlayList object to be used as if it is actually a list. def __len__(self): return self.overlays.__len__() def __getitem__(self, key): return self.overlays.__getitem__(key) def __iter__(self): return self.overlays.__iter__() def __contains__(self, item): return self.overlays.__contains__(item) def __setitem__(self, key, val): return self.overlays.__setitem__(key, val) def __delitem__(self, key): if isinstance(key, slice): pass elif isinstance(key, int): key = slice(key, key + 1, None) else: raise IndexError('Invalid key type') ovls = self[key] for ovl in ovls: self.__initOverlayType.pop(ovl, None) return self.overlays.__delitem__(key) def index(self, item): return self.overlays.index(item) def count(self, item): return self.overlays.count(item) def append(self, item, overlayType=None): with props.suppress(self, 'overlays', notify=True): self.overlays.append(item) if overlayType is not None: self.__initOverlayType[item] = overlayType def extend(self, iterable, overlayTypes=None): with props.suppress(self, 'overlays', notify=True): result = self.overlays.extend(iterable) if overlayTypes is not None: for overlay, overlayType in overlayTypes.items(): self.__initOverlayType[overlay] = overlayType return result def pop(self, index=-1): ovl = self.overlays.pop(index) self.__initOverlayType.pop(ovl, None) return ovl def move(self, from_, to): return self.overlays.move(from_, to) def remove(self, item): self.__initOverlayType.pop(item, None) self.overlays.remove(item) def insert(self, index, item, overlayType=None): with props.suppress(self, 'overlays', notify=True): self.overlays.insert(index, item) if overlayType is not None: self.__initOverlayType[item] = overlayType def insertAll(self, index, items): return self.overlays.insertAll(index, items)
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 FEATTimeSeries(VoxelTimeSeries): """A :class:`VoxelTimeSeries` class for use with :class:`FEATImage` instances, containing some extra FEAT specific options. The ``FEATTimeSeries`` class acts as a container for several ``TimeSeries`` instances, each of which represent some part of a FEAT analysis. The data returned by a call to :meth:`.getData` on a ``FEATTimeSeries`` instance returns the fMRI time series data (``filtered_func_data`` in the ``.feat`` directory). The :meth:`extraSeries` method may be used to retrieve a list of all the other ``TimeSeries`` instances which are associated with the ``FEATTimeSeries`` instance - all of these ``DataSeries`` instances, in addition to this ``FEATTimeSeries`` instasnce, should be plotted. For example, if the :attr:`plotData` and :attr:`plotFullModelFit` settings are ``True``, the :meth:`extraSeries` method will return a list containing one ``TimeSeries`` instance, containing the full model fit, for the voxel in question. The following classes are used to represent the various parts of a FEAT analysis: .. autosummary:: :nosignatures: FEATEVTimeSeries FEATResidualTimeSeries FEATPartialFitTimeSeries FEATModelFitTimeSeries """ plotData = props.Boolean(default=True) """If ``True``, the FEAT input data is plotted. """ plotFullModelFit = props.Boolean(default=True) """If ``True``, the FEAT full model fit is plotted. """ plotResiduals = props.Boolean(default=False) """If ``True``, the FEAT model residuals are plotted. """ plotEVs = props.List(props.Boolean(default=False)) """A list of ``Boolean`` properties, one for each EV in the FEAT analysis. For elements that are ``True``, the corresponding FEAT EV time course is plotted. """ plotPEFits = props.List(props.Boolean(default=False)) """A list of ``Boolean`` properties, one for each EV in the FEAT analysis. For elements that are ``True``, the model fit for the corresponding FEAT EV is plotted. """ plotCOPEFits = props.List(props.Boolean(default=False)) """A list of ``Boolean`` properties, one for each EV in the FEAT analysis. For elements that are ``True``, the model fit for the corresponding FEAT contrast is plotted. """ plotPartial = props.Choice() """Plot the raw data, after regression against a chosen EV or contrast. The options are populated in the :meth:`__init__` method. """ def __init__(self, *args, **kwargs): """Create a ``FEATTimeSeries``. All arguments are passed through to the :class:`VoxelTimeSeries` constructor. """ VoxelTimeSeries.__init__(self, *args, **kwargs) numEVs = self.overlay.numEVs() numCOPEs = self.overlay.numContrasts() copeNames = self.overlay.contrastNames() reduceOpts = ['none'] + \ ['PE{}'.format(i + 1) for i in range(numEVs)] for i in range(numCOPEs): name = 'COPE{} ({})'.format(i + 1, copeNames[i]) reduceOpts.append(name) self.getProp('plotPartial').setChoices(reduceOpts, instance=self) for i in range(numEVs): self.plotPEFits.append(False) self.plotEVs .append(False) for i in range(numCOPEs): self.plotCOPEFits.append(False) self.__fullModelTs = None self.__partialTs = None self.__resTs = None self.__evTs = [None] * numEVs self.__peTs = [None] * numEVs self.__copeTs = [None] * numCOPEs if not self.overlay.hasStats(): self.plotFullModelFit = False self.addListener('plotFullModelFit', self.name, self.__plotFullModelFitChanged) self.addListener('plotResiduals', self.name, self.__plotResidualsChanged) self.addListener('plotPartial', self.name, self.__plotPartialChanged) self.addListener('plotEVs', self.name, self.__plotEVChanged) self.addListener('plotPEFits', self.name, self.__plotPEFitChanged) self.addListener('plotCOPEFits', self.name, self.__plotCOPEFitChanged) # plotFullModelFit defaults to True, so # force the model fit ts creation here self.__plotFullModelFitChanged() def getData(self): """Returns the fMRI time series data at the current voxel. Or, if :attr:`plotData` is ``False``, returns ``(None, None)``. """ if not self.plotData: return None, None return VoxelTimeSeries.getData(self) def extraSeries(self): """Returns a list containing all of the ``TimeSeries`` instances which should be plotted in place of this ``FEATTimeSeries``. """ modelts = [] if self.plotFullModelFit: modelts.append(self.__fullModelTs) if self.plotResiduals: modelts.append(self.__resTs) if self.plotPartial != 'none': modelts.append(self.__partialTs) for i in range(self.overlay.numEVs()): if self.plotPEFits[i]: modelts.append(self.__peTs[i]) for i in range(self.overlay.numEVs()): if self.plotEVs[i]: modelts.append(self.__evTs[i]) for i in range(self.overlay.numContrasts()): if self.plotCOPEFits[i]: modelts.append(self.__copeTs[i]) return modelts @deprecated.deprecated('0.31.0', '1.0.0', 'Use extraSeries instead') def getModelTimeSeries(self): return self.extraSeries() def __getContrast(self, fitType, idx): """Returns a contrast vector for the given model fit type, and index. :arg fitType: either ``'full'``, ``'pe'``, or ``'cope'``. If ``'full'``, the ``idx`` argument is ignored. :arg idx: The EV or contrast index for ``'pe'`` or ``'cope'`` model fits. """ if fitType == 'full': return [1] * self.overlay.numEVs() elif fitType == 'pe': con = [0] * self.overlay.numEVs() con[idx] = 1 return con elif fitType == 'cope': return self.overlay.contrasts()[idx] def __createModelTs(self, tsType, *args, **kwargs): """Creates a ``TimeSeries`` instance of the given ``tsType``, and sets its display settings according to those of this ``FEATTimeSeries``. :arg tsType: The type to create, e.g. :class:`FEATModelFitTimeSeries`, :class:`FEATEVTimeSeries`, etc. :arg args: Passed to the ``tsType`` constructor. :arg kwargs: Passed to the ``tsType`` constructor. """ ts = tsType(self.overlay, self.overlayList, self.displayCtx, self.plotPanel, self, *args, **kwargs) ts.alpha = self.alpha ts.lineWidth = self.lineWidth ts.lineStyle = self.lineStyle if isinstance(ts, FEATModelFitTimeSeries) and ts.fitType == 'full': ts.colour = (0, 0, 0.8) else: ts.colour = fslcm.randomDarkColour() return ts def __plotPartialChanged(self, *a): """Called when the :attr:`plotPartial` setting changes. If necessary, creates and caches a :class:`FEATPartialFitTimeSeries` instance. """ partial = self.plotPartial if partial == 'none' and self.__partialTs is not None: self.__partialTs = None return partial = partial.split()[0] # fitType is either 'cope' or 'pe' fitType = partial[:-1].lower() idx = int(partial[-1]) - 1 self.__partialTs = self.__createModelTs( FEATPartialFitTimeSeries, self.__getContrast(fitType, idx), fitType, idx) def __plotResidualsChanged(self, *a): """Called when the :attr:`plotResiduals` setting changes. If necessary, creates and caches a :class:`FEATResidualTimeSeries` instance. """ if not self.plotResiduals: self.__resTs = None return self.__resTs = self.__createModelTs(FEATResidualTimeSeries) def __plotEVChanged(self, *a): """Called when the :attr:`plotEVs` setting changes. If necessary, creates and caches one or more :class:`FEATEVTimeSeries` instances. """ for evnum, plotEV in enumerate(self.plotEVs): if not self.plotEVs[evnum]: self.__evTs[evnum] = None elif self.__evTs[evnum] is None: self.__evTs[evnum] = self.__createModelTs( FEATEVTimeSeries, evnum) def __plotCOPEFitChanged(self, *a): """Called when the :attr:`plotCOPEFits` setting changes. If necessary, creates and caches one or more :class:`FEATModelFitTimeSeries` instances. """ for copenum, plotCOPE in enumerate(self.plotCOPEFits): if not self.plotCOPEFits[copenum]: self.__copeTs[copenum] = None elif self.__copeTs[copenum] is None: self.__copeTs[copenum] = self.__createModelTs( FEATModelFitTimeSeries, self.__getContrast('cope', copenum), 'cope', copenum) def __plotPEFitChanged(self, *a): """Called when the :attr:`plotPEFits` setting changes. If necessary, creates and caches one or more :class:`FEATModelFitTimeSeries` instances. """ for evnum, plotPE in enumerate(self.plotPEFits): if not self.plotPEFits[evnum]: self.__peTs[evnum] = None elif self.__peTs[evnum] is None: self.__peTs[evnum] = self.__createModelTs( FEATModelFitTimeSeries, self.__getContrast('pe', evnum), 'pe', evnum) def __plotFullModelFitChanged(self, *a): """Called when the :attr:`plotFullModelFit` setting changes. If necessary, creates and caches a :class:`FEATModelFitTimeSeries` instance. """ if not self.plotFullModelFit: self.__fullModelTs = None return self.__fullModelTs = self.__createModelTs( FEATModelFitTimeSeries, self.__getContrast('full', -1), 'full', -1)
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')