#!/usr/bin/env python # # volume3dopts.py - The Volume3DOpts class. # # Author: Paul McCarthy # """This module provides the :class:`.Volume3DOpts` class, a mix-in for use with :class:`.DisplayOpts` classes. """ import numpy as np import fsl.transform.affine as affine import fsleyes_props as props import fsleyes_widgets as fwidgets import fsleyes.gl as fslgl 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. """ blendByIntensity = props.Boolean(default=True) """If ``True``, the colours from samples are weighted by voxel intensity as well as the blendFactor. """ 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 fwidgets.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(fslgl.GL_COMPATIBILITY) < 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 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(fslgl.GL_COMPATIBILITY) >= 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 = affine.concat(view, t2dmat) ixform = affine.invert(xform) eye = affine.transform(eye, ixform, vector=True) target = affine.transform(target, ixform, vector=True) # Direction that the 'camera' is # pointing, normalied to unit length cdir = affine.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 = affine.scaleOffsetXform([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) xform = affine.concat(zscale, proj, xform) # The Scene3DViewProfile needs to know the image # texture to screen transformation so it can # transform screen locations into image # coordinates. So we construct an appropriate # transform and cache it in the overlay list # whenever it is recalculated. scr2disp = affine.concat(t2dmat, affine.invert(xform)) self.overlayList.setData( self.overlay, 'screen2DisplayXform_{}'.format(id(self)), scr2disp) 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 = affine.axisAnglesToRotMat(incline, 0, 0) rot2 = affine.axisAnglesToRotMat(0, 0, azimuth) rotation = affine.concat(rot2, rot1) normal = affine.transformNormal(normal, rotation) normal = affine.normalise(normal) offset = (pos - 0.5) * max((b.xlen, b.ylen, b.zlen)) origin = centre + normal * offset return origin, normal