def __xformChanged(self, ev=None): """Called when any of the scale, offset, or rotate widgets are modified. Updates the :attr:`.NiftiOpts.displayXform` for the overlay currently being edited. """ if self.__overlay is None: return overlay = self.__overlay opts = self.displayCtx.getOpts(overlay) if self.__extraXform is None: v2wXform = overlay.voxToWorldMat else: v2wXform = self.__extraXform xform = self.__getCurrentXform() xform = transform.concat(xform, v2wXform) self.__formatXform(xform, self.__newXform) # The NiftiOpts.displayXform is applied on # top of the image voxToWorldMat. But our # xform here has been constructed to replace # the voxToWorldMat entirely. So we include # a worldToVoxMat transform to trick the # NiftiOpts code. opts.displayXform = transform.concat(xform, overlay.worldToVoxMat)
def draw2D(self, zpos, axes, xform=None, bbox=None): """Called by :meth:`.GLSH.draw2D`. Draws the scene. """ opts = self.opts shader = self.shader v2dMat = opts.getTransform('voxel', 'display') if xform is None: xform = v2dMat else: xform = transform.concat(v2dMat, xform) voxels = self.generateVoxelCoordinates2D(zpos, axes, bbox) voxels, radTexShape = self.updateRadTexture(voxels) if len(voxels) == 0: return voxIdxs = np.arange(voxels.shape[0], dtype=np.float32) shader.setAtt('voxel', voxels, divisor=1) shader.setAtt('voxelID', voxIdxs, divisor=1) shader.set('voxToDisplayMat', xform) shader.set('radTexShape', radTexShape) shader.set('radXform', self.radTexture.voxValXform) shader.loadAtts() arbdi.glDrawElementsInstancedARB(gl.GL_TRIANGLES, self.nVertices, gl.GL_UNSIGNED_INT, None, len(voxels))
def updateShaderState(self): """Sets all variables required by the vertex and fragment programs. """ if not self.ready(): return opts = self.opts shader = self.shader # enable the vertex and fragment programs shader.load() # The voxValXform transformation turns # an image texture value into a raw # voxel value. The colourMapXform # transformation turns a raw voxel value # into a value between 0 and 1, suitable # for looking up an appropriate colour # in the 1D colour map texture. voxValXform = transform.concat(self.colourTexture.getCoordinateTransform(), self.imageTexture.voxValXform) voxValXform = [voxValXform[0, 0], voxValXform[0, 3], 0, 0] # And the clipping range, normalised # to the image texture value range invClip = 1 if opts.invertClipping else -1 useNegCmap = 1 if opts.useNegativeCmap else 0 imageIsClip = 1 if opts.clipImage is None else -1 imgXform = self.imageTexture.invVoxValXform if opts.clipImage is None: clipXform = imgXform else: clipXform = self.clipTexture.invVoxValXform clipLo = opts.clippingRange[0] * clipXform[0, 0] + clipXform[0, 3] clipHi = opts.clippingRange[1] * clipXform[0, 0] + clipXform[0, 3] texZero = 0.0 * imgXform[ 0, 0] + imgXform[ 0, 3] clipping = [clipLo, clipHi, invClip, imageIsClip] negCmap = [useNegCmap, texZero, 0, 0] changed = False changed |= shader.setFragParam('voxValXform', voxValXform) changed |= shader.setFragParam('clipping', clipping) changed |= shader.setFragParam('negCmap', negCmap) if self.threedee: clipPlanes = np.zeros((5, 4), dtype=np.float32) d2tmat = opts.getTransform('display', 'texture') for i in range(opts.numClipPlanes): origin, normal = self.get3DClipPlane(i) origin = transform.transform(origin, d2tmat) normal = transform.transformNormal(normal, d2tmat) clipPlanes[i, :] = glroutines.planeEquation2(origin, normal) changed |= shader.setFragParam('clipPlanes', clipPlanes) self.shader.unload() return changed
def draw2D(self, zpos, axes, xform=None, bbox=None): """Draws the line vertices corresponding to a 2D plane located at the specified Z location. """ opts = self.opts vertices, voxCoords = self.lineVertices.getVertices2D(self, zpos, axes, bbox=bbox) if vertices.size == 0: return self.shader.setAtt('voxCoord', voxCoords) self.shader.loadAtts() v2d = opts.getTransform('voxel', 'display') if xform is None: xform = v2d else: xform = transform.concat(xform, v2d) gl.glPushMatrix() gl.glMultMatrixf(np.array(xform, dtype=np.float32).ravel('F')) gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices) gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE) gl.glLineWidth(opts.lineWidth) gl.glDrawArrays(gl.GL_LINES, 0, vertices.size // 3) gl.glPopMatrix()
def lookAt(eye, centre, up): """Replacement for ``gluLookAt`. Creates a transformation matrix which transforms the display coordinate system such that a camera at position (0, 0, 0), and looking towards (0, 0, -1), will see a scene as if from position ``eye``, oriented ``up``, and looking towards ``centre``. See: https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/gluLookAt.xml """ eye = np.array(eye) centre = np.array(centre) up = np.array(up) proj = np.eye(4) forward = centre - eye forward /= np.sqrt(np.dot(forward, forward)) right = np.cross(forward, up) right /= np.sqrt(np.dot(right, right)) up = np.cross(right, forward) up /= np.sqrt(np.dot(up, up)) proj[0, :3] = right proj[1, :3] = up proj[2, :3] = -forward eye = transform.scaleOffsetXform(1, -eye) proj = transform.concat(proj, eye) return proj
def draw2D(self, zpos, axes, xform=None, bbox=None): """Draws the line vectors at a plane at the specified Z location. Voxel coordinates are passed to the vertex shader, which calculates the corresponding line vertex locations. """ opts = self.opts shader = self.shader v2dMat = opts.getTransform('voxel', 'display') voxels = self.generateVoxelCoordinates2D(zpos, axes, bbox=bbox) voxels = np.repeat(voxels, 2, 0) indices = np.arange(voxels.shape[0], dtype=np.uint32) if xform is None: xform = v2dMat else: xform = transform.concat(xform, v2dMat) shader.set( 'voxToDisplayMat', xform) shader.setAtt('vertexID', indices) shader.setAtt('voxel', voxels) shader.loadAtts() gl.glLineWidth(opts.lineWidth) gl.glDrawArrays(gl.GL_LINES, 0, voxels.size // 3) shader.unloadAtts()
def __onApply(self, ev): """Called when the *Apply* button is pushed. Sets the ``voxToWorldMat`` attribute of the :class:`.Image` instance being transformed. """ overlay = self.__overlay if overlay is None: return if self.__extraXform is None: v2wXform = overlay.voxToWorldMat else: v2wXform = self.__extraXform newXform = self.__getCurrentXform() opts = self.displayCtx.getOpts(overlay) xform = transform.concat(newXform, v2wXform) with props.suppress(opts, 'displayXform'): opts.displayXform = np.eye(4) overlay.voxToWorldMat = xform # Reset the interface, and clear any # cached transform for this overlay self.__deregisterOverlay() self.__cachedXforms.pop(overlay, None) self.__selectedOverlayChanged()
def test_prepareMask(): reg = atlases.registry reg.rescanAtlases() probatlas = reg.loadAtlas('harvardoxford-cortical', indexed=True, loadData=False, calcRange=False) probsumatlas = reg.loadAtlas('harvardoxford-cortical', loadSummary=True) lblatlas = reg.loadAtlas('talairach') for atlas in [probatlas, probsumatlas, lblatlas]: ashape = list(atlas.shape[:3]) m2shape = [s * 1.5 for s in ashape] goodmask1 = fslimage.Image( np.array(np.random.random(ashape), dtype=np.float32), xform=atlas.voxToWorldMat) goodmask2, xf = goodmask1.resample(m2shape) goodmask2 = fslimage.Image(goodmask2, xform=xf) wrongdims = fslimage.Image( np.random.random(list(ashape) + [2])) wrongspace = fslimage.Image( np.random.random((20, 20, 20)), xform=transform.concat(atlas.voxToWorldMat, np.diag([2, 2, 2, 1]))) with pytest.raises(atlases.MaskError): atlas.prepareMask(wrongdims) with pytest.raises(atlases.MaskError): atlas.prepareMask(wrongspace) assert list(atlas.prepareMask(goodmask1).shape) == ashape assert list(atlas.prepareMask(goodmask2).shape) == ashape
def _rotateModeLeftMouseDrag(self, ev, canvas, mousePos, canvasPos): """Called on left mouse drag events in ``rotate`` mode. Modifies the canvas rotation matrix according to the X and Y mouse position (relative to the mouse down location). """ if self.__rotateMousePos is None: return w, h = canvas.GetSize() x0, y0 = self.__rotateMousePos x1, y1 = mousePos # Normalise x/y mouse pos to [-fac*pi, +fac*pi] fac = 1 x0 = -1 + 2 * (x0 / float(w)) * fac * np.pi y0 = -1 + 2 * (y0 / float(h)) * fac * np.pi x1 = -1 + 2 * (x1 / float(w)) * fac * np.pi y1 = -1 + 2 * (y1 / float(h)) * fac * np.pi xrot = x1 - x0 yrot = y1 - y0 rot = transform.axisAnglesToRotMat(yrot, 0, xrot) self.__lastRot = rot self.__rotateMousePos = mousePos canvas.opts.rotation = transform.concat(rot, self.__lastRot, self.__baseXform)
def doMovieUpdate(self, overlay, opts): """Overrides :meth:`.CanvasPanel.doMovieUpdate`. For x/y/z axis movies, the scene is rotated. Otherwise (for time) the ``CanvasPanel`` implementation is called. """ if self.movieAxis >= 3: return canvaspanel.CanvasPanel.doMovieUpdate(self, overlay, opts) else: canvas = self.__canvas currot = canvas.opts.rotation rate = float(self.movieRate) rateMin = self.getAttribute('movieRate', 'minval') rateMax = self.getAttribute('movieRate', 'maxval') rate = 0.1 + 0.9 * (rate - rateMin) / (rateMax - rateMin) rate = rate * np.pi / 10 rots = [0, 0, 0] rots[self.movieAxis] = rate xform = transform.axisAnglesToRotMat(*rots) xform = transform.concat(xform, currot) canvas.opts.rotation = xform return np.copy(xform)
def __getMeshInfo(self, overlay, display): """Creates and returns an :class:`OverlayInfo` object containing information about the given :class:`.Mesh` overlay. :arg overlay: A :class:`.Mesh` instance. :arg display: The :class:`.Display` instance assocated with the ``Mesh``. """ opts = display.opts refImg = opts.refImage modelInfo = [ ('numVertices', overlay.vertices.shape[0]), ('numTriangles', overlay.indices.shape[0]), ] if refImg is None: modelInfo.append( ('displaySpace', strings.labels[self, overlay, 'coordSpace', 'display'])) mesh2worldXform = np.eye(4) else: refOpts = self.displayCtx.getOpts(refImg) dsImg = self.displayCtx.displaySpace displaySpace = strings.labels[self, refImg, 'displaySpace', refOpts.transform] coordSpace = strings.labels[self, overlay, 'coordSpace', opts.coordSpace].format(refImg.name) mesh2worldXform = transform.concat( refOpts.getTransform('display', 'world'), opts.getTransform('mesh', 'display')) if refOpts.transform == 'reference': dsDisplay = self.displayCtx.getDisplay(dsImg) displaySpace = displaySpace.format(dsDisplay.name) modelInfo.append(('refImage', refImg.dataSource)) modelInfo.append(('coordSpace', coordSpace)) modelInfo.append(('displaySpace', displaySpace)) bounds = transform.transform(overlay.bounds, mesh2worldXform) lens = bounds[1] - bounds[0] lens = 'X={:0.0f} mm Y={:0.0f} mm Z={:0.0f} mm'.format(*lens) modelInfo.append(('size', lens)) info = OverlayInfo('{} - {}'.format(display.name, strings.labels[self, overlay])) info.addInfo(strings.labels[self, 'dataSource'], overlay.dataSource) for name, value in modelInfo: info.addInfo(strings.labels[self, overlay, name], value) return info
def getTransform(self, from_, to): """Overrides :meth:`.MeshOpts.getTransform`. If the :attr:`.MeshOpts.coordSpace` property is ``'torig'``, and one of ``from_`` or ``to`` is ``'mesh'``, the transform is adjusted to account for the difference between Freesurfer's RAS and RAStkr spaces. """ ref = self.refImage xform = meshopts.MeshOpts.getTransform(self, from_, to) if isinstance(ref, fslmgh.MGHImage) and self.coordSpace == 'torig': surf2world = ref.surfToWorldMat world2surf = ref.worldToSurfMat if from_ == 'mesh': xform = transform.concat(xform, surf2world) elif to == 'mesh': xform = transform.concat(world2surf, xform) return xform
def updateShaderState(self): """Updates the vertex/fragment shader state based on the current state of the :class:`.MIPOpts` instance. """ if not self.ready(): return opts = self.opts shader = self.shader vmin, vmax = self.overlay.dataRange # Convert clipping values from voxel value # range totexture value range (0.0 - 1.0). imgXform = self.imageTexture.invVoxValXform clipLow = opts.clippingRange[0] * imgXform[0, 0] + imgXform[0, 3] clipHigh = opts.clippingRange[1] * imgXform[0, 0] + imgXform[0, 3] textureMin = vmin * imgXform[0, 0] + imgXform[0, 3] textureMax = vmax * imgXform[0, 0] + imgXform[0, 3] imageShape = self.image.shape[:3] # Create a single transformation matrix # which transforms from image texture values # to voxel values, and scales said voxel # values to colour map texture coordinates. img2CmapXform = transform.concat(self.cmapTexture.getCoordinateTransform(), self.imageTexture.voxValXform) # sqrt(3) so the window is 100% # along the diagonal of a cube window = np.sqrt(3) * opts.window / 100.0 shader.load() changed = False changed |= shader.set('imageTexture', 0) changed |= shader.set('cmapTexture', 1) changed |= shader.set('textureMin', textureMin) changed |= shader.set('textureMax', textureMax) changed |= shader.set('img2CmapXform', img2CmapXform) changed |= shader.set('imageShape', imageShape) changed |= shader.set('useSpline', opts.interpolation == 'spline') changed |= shader.set('clipLow', clipLow) changed |= shader.set('clipHigh', clipHigh) changed |= shader.set('invertClip', opts.invertClipping) changed |= shader.set('window', window) changed |= shader.set('useMinimum', opts.minimum) changed |= shader.set('useAbsolute', opts.absolute) shader.unload() return changed
def _draw(self): """Draws the scene to the canvas. """ if not self._setGLContext(): return opts = self.opts glroutines.clear(opts.bgColour) if not self.__setViewport(): return overlays, globjs = self.getGLObjects() if len(overlays) == 0: return # If occlusion is on, we offset the # depth of each overlay so that, where # a depth collision occurs, overlays # which are higher in the list will get # drawn above (closer to the screen) # than lower ones. depthOffset = transform.scaleOffsetXform(1, [0, 0, 0.1]) depthOffset = np.array(depthOffset, dtype=np.float32, copy=False) xform = np.array(self.__viewMat, dtype=np.float32, copy=False) for ovl, globj in zip(overlays, globjs): display = self.__displayCtx.getDisplay(ovl) if not globj.ready(): continue if not display.enabled: continue if opts.occlusion: xform = transform.concat(depthOffset, xform) elif isinstance(ovl, fslimage.Image): gl.glClear(gl.GL_DEPTH_BUFFER_BIT) log.debug('Drawing {} [{}]'.format(ovl, globj)) globj.preDraw(xform=xform) globj.draw3D(xform=xform) globj.postDraw(xform=xform) if opts.showCursor: with glroutines.enabled((gl.GL_DEPTH_TEST)): self.__drawCursor() if opts.showLegend: self.__drawLegend()
def translate(infile, x, y, z): basename = fslimage.removeExt(op.basename(infile)) outfile = '{}_translated_{}_{}_{}.nii.gz'.format(basename, x, y, z) img = fslimage.Image(infile) xform = img.voxToWorldMat shift = transform.scaleOffsetXform(1, (x, y, z)) xform = transform.concat(shift, xform) img.voxToWorldMat = xform img.save(outfile) return outfile
def getAuxTextureXform(self, which): """Generates and returns a transformation matrix which can be used to transform texture coordinates from the vector image to the specified auxillary image (``'clip'``, ``'modulate'`` or ``'colour'``). """ opts = self.opts auxImage = getattr(self, '{}Image'.format(which), None) auxOpts = getattr(self, '{}Opts'.format(which), None) if auxImage is None: return np.eye(4) else: return transform.concat(auxOpts.getTransform('display', 'texture'), opts.getTransform('texture', 'display'))
def drawOutline(self, zpos, axes, xform=None, bbox=None): """Called by :meth:`draw2D` when ``MeshOpts.outline is True or MeshOpts.vertexData is not None``. Calculates the intersection of the mesh with the viewing plane, and renders it as a set of ``GL_LINES``. If ``MeshOpts.vertexData is None``, the draw is performed using immediate mode OpenGL. Otherwise, the :func:`.gl14.glmesh_funcs.draw` or :func:`.gl21.glmesh_funcs.draw` function is used, which performs shader-based rendering. """ opts = self.opts # Makes code below a bit nicer if xform is None: xform = np.eye(4) vertices, faces, dists, vertXform = self.calculateIntersection( zpos, axes, bbox) if vertXform is not None: xform = transform.concat(xform, vertXform) vdata = self.getVertexData(faces, dists) useShader = vdata is not None vertices = vertices.reshape(-1, 3) nvertices = vertices.shape[0] gl.glMatrixMode(gl.GL_MODELVIEW) gl.glPushMatrix() gl.glMultMatrixf(np.array(xform, dtype=np.float32).ravel('F')) gl.glLineWidth(opts.outlineWidth) # Constant colour if not useShader: vertices = vertices.ravel('C') gl.glColor(*opts.getConstantColour()) gl.glEnableClientState(gl.GL_VERTEX_ARRAY) gl.glVertexPointer(3, gl.GL_FLOAT, 0, vertices) gl.glDrawArrays(gl.GL_LINES, 0, nvertices) gl.glDisableClientState(gl.GL_VERTEX_ARRAY) # Coloured from vertex data else: fslgl.glmesh_funcs.draw(self, gl.GL_LINES, vertices, vdata=vdata) gl.glPopMatrix()
def rotate(infile, rx, ry, rz): basename = fslimage.removeExt(op.basename(infile)) outfile = '{}_rotated_{}_{}_{}.nii.gz'.format(basename, rx, ry, rz) img = fslimage.Image(infile) rx = rx * np.pi / 180 ry = ry * np.pi / 180 rz = rz * np.pi / 180 rot = transform.axisAnglesToRotMat(rx, ry, rz) rot = transform.rotMatToAffine(rot) img.voxToWorldMat = transform.concat(rot, img.voxToWorldMat) img.save(outfile) return outfile
def calculateClipCoordTransform(self): """Calculates a transformation matrix which will transform from the image coordinate system into the :attr:`.VolumeOpts.clipImage` coordinate system. If ``clipImage is None``, it will be an identity transform. This transform is used by shader programs to find the clip image coordinates that correspond with specific image coordinates. """ if self.opts.clipImage is None: clipCoordXform = np.eye(4) else: clipCoordXform = transform.concat( self.clipOpts.getTransform('display', 'texture'), self.opts.getTransform('texture', 'display')) return clipCoordXform
def calculateRayCastSettings(self, viewmat): """Calculates a camera direction and ray casting step vector, based on the given view matrix. """ d2tmat = self.getTransform('display', 'texture') xform = transform.concat(d2tmat, viewmat) cdir = np.array([0, 0, 1]) cdir = transform.transform(cdir, xform, vector=True) cdir = transform.normalise(cdir) # sqrt(3) so the maximum number # of samplews is taken along the # diagonal of a cube rayStep = np.sqrt(3) * cdir / self.numSteps return cdir, rayStep
def __prepareSliceTransforms(self, globj, xforms): """Applies the :attr:`.SliceCanvas.invertX` and :attr:`.SliceCanvas.invertY` properties to the given transformation matrices, if necessary. Returns the transformations. """ opts = self.opts if not opts.invertX or opts.invertY: return xforms invXforms = [] lo, hi = globj.getDisplayBounds() xmin = lo[opts.xax] xmax = hi[opts.xax] ymin = lo[opts.yax] ymax = hi[opts.yax] xlen = xmax - xmin ylen = ymax - ymin # We have to translate each slice transformation # to the origin, perform the flip there, then # transform it back to its original location. for xform in xforms: invert = np.eye(4) toOrigin = np.eye(4) fromOrigin = np.eye(4) xoff = xlen / 2.0 + xform[opts.xax, 3] + xmin yoff = ylen / 2.0 + xform[opts.yax, 3] + ymin if opts.invertX: invert[opts.xax, opts.xax] = -1 toOrigin[opts.xax, 3] = -xoff fromOrigin[opts.xax, 3] = xoff if opts.invertY: invert[opts.yax, opts.yax] = -1 toOrigin[opts.yax, 3] = -yoff fromOrigin[opts.yax, 3] = yoff xform = transform.concat(fromOrigin, invert, toOrigin, xform) invXforms.append(xform) return invXforms
def __onSaveFlirt(self, ev): """Called when the user clicks the *Save FLIRT* button. Saves the current transformation to a FLIRT matrix file. """ overlay = self.__overlay if overlay is None: return overlayList = self.overlayList displayCtx = self.displayCtx matFile, refFile = applyflirtxfm.promptForFlirtFiles(self, overlay, overlayList, displayCtx, save=True) if matFile is None or refFile is None: return if self.__extraXform is None: v2wXform = overlay.voxToWorldMat else: v2wXform = self.__extraXform newXform = self.__getCurrentXform() v2wXform = transform.concat(newXform, v2wXform) xform = saveflirtxfm.calculateTransform(overlay, overlayList, displayCtx, refFile, srcXform=v2wXform) try: np.savetxt(matFile, xform, fmt='%0.10f') except Exception as e: log.warn('Error saving FLIRT matrix: {}'.format(e)) wx.MessageDialog(self, strings.messages[self, 'saveFlirt.error'].format( str(e)), style=wx.ICON_ERROR).ShowModal()
def test_bad_mask(seed): fslatlases.rescanAtlases() capture = CaptureStdout() with tempdir() as td: for atlasID, use_label in it.product(atlases, use_labels): atlas = fslatlases.loadAtlas(atlasID, loadSummary=use_label, indexed=True, loadData=False, calcRange=False) ashape = list(atlas.shape[:3]) wrongdims = fslimage.Image( np.array(np.random.random(list(ashape) + [2]), dtype=np.float32)) wrongspace = fslimage.Image(np.random.random((20, 20, 20)), xform=transform.concat( atlas.voxToWorldMat, np.diag([2, 2, 2, 1]))) print(wrongdims.shape) print(wrongspace.shape) wrongdims.save('wrongdims.nii.gz') wrongspace.save('wrongspace.nii.gz') cmd = ['query', atlasID, '-m', 'wrongdims'] expected = 'Mask has wrong number of dimensions' capture.reset() with capture: assert fslatlasq.main(cmd) != 0 assert capture.stdout.strip() == expected cmd = ['query', atlasID, '-m', 'wrongspace'] expected = 'Mask is not in the same space as atlas' capture.reset() with capture: assert fslatlasq.main(cmd) != 0 assert capture.stdout.strip() == expected
def __setViewport(self): """Called by :meth:`_draw`. Configures the viewport and calculates the model-view trasformation matrix. :returns: ``True`` if the viewport was successfully configured, ``False`` otherwise. """ width, height = self.GetScaledSize() b = self.__displayCtx.bounds blo = [b.xlo, b.ylo, b.zlo] bhi = [b.xhi, b.yhi, b.zhi] zoom = self.opts.zoom / 100.0 if width == 0 or height == 0: return False # We allow one dimension to be # flat, so we can display 2D # meshes (e.g. flattened surfaces) if np.sum(np.isclose(blo, bhi)) > 1: return False # Generate the view and projection matrices self.__genViewMatrix(width, height) projmat, viewport = glroutines.ortho(blo, bhi, width, height, zoom) self.__projMat = projmat self.__viewport = viewport self.__invViewProjMat = transform.concat(self.__projMat, self.__viewMat) self.__invViewProjMat = transform.invert(self.__invViewProjMat) gl.glViewport(0, 0, width, height) gl.glMatrixMode(gl.GL_PROJECTION) gl.glLoadMatrixf(self.__projMat.ravel('F')) gl.glMatrixMode(gl.GL_MODELVIEW) gl.glLoadIdentity() return True
def draw2D(self, zpos, axes, xform=None, bbox=None): """Generates voxel coordinates for each tensor to be drawn, does some final shader state configuration, and draws the tensors. """ opts = self.opts shader = self.shader v2dMat = opts.getTransform('voxel', 'display') if xform is None: xform = v2dMat else: xform = transform.concat(v2dMat, xform) voxels = self.generateVoxelCoordinates2D(zpos, axes, bbox) nVoxels = len(voxels) # Set divisor to 1, so we use one set of # voxel coordinates for every sphere drawn shader.setAtt('voxel', voxels, divisor=1) shader.set( 'voxToDisplayMat', xform) shader.loadAtts() arbdi.glDrawElementsInstancedARB( gl.GL_QUADS, self.nVertices, gl.GL_UNSIGNED_INT, None, nVoxels)
def preDraw(self, xform=None, bbox=None): """Called by :meth:`.GLSH.preDraw`. Loads the shader program, and updates some shader attributes. """ shader = self.shader shader.load() # Calculate a transformation matrix for # normal vectors - T(I(MV matrix)) # We transpose mvMat because OpenGL is column-major mvMat = gl.glGetFloatv(gl.GL_MODELVIEW_MATRIX)[:3, :3].T v2dMat = self.opts.getTransform('voxel', 'display')[:3, :3] normalMatrix = transform.concat(mvMat, v2dMat) normalMatrix = npla.inv(normalMatrix).T shader.set('normalMatrix', normalMatrix) gl.glEnable(gl.GL_CULL_FACE) gl.glClear(gl.GL_DEPTH_BUFFER_BIT) gl.glEnable(gl.GL_DEPTH_TEST) gl.glCullFace(gl.GL_BACK)
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
def __genViewMatrix(self, w, h): """Generate and return a transformation matrix to be used as the model-view matrix. This includes applying the current :attr:`zoom`, :attr:`rotation` and :attr:`offset` settings, and configuring the camera. This method is called by :meth:`__setViewport`. :arg w: Canvas width in pixels :arg h: Canvas height in pixels """ opts = self.opts b = self.__displayCtx.bounds centre = [ b.xlo + 0.5 * b.xlen, b.ylo + 0.5 * b.ylen, b.zlo + 0.5 * b.zlen ] # The MV matrix comprises (in this order): # # - A rotation (the rotation property) # # - Camera configuration. With no rotation, the # camera will be looking towards the positive # Y axis (i.e. +y is forwards), and oriented # towards the positive Z axis (i.e. +z is up) # # - A translation (the offset property) # - A scaling (the zoom property) # Scaling and rotation matrices. Rotation # is always around the centre of the # displaycontext bounds (the bounding # box which contains all loaded overlays). scale = opts.zoom / 100.0 scale = transform.scaleOffsetXform([scale] * 3, 0) rotate = transform.rotMatToAffine(opts.rotation, centre) # The offset property is defined in x/y # pixels, normalised to [-1, 1]. We need # to convert them into viewport space, # where the horizontal axis maps to # (-xhalf, xhalf), and the vertical axis # maps to (-yhalf, yhalf). See # gl.routines.ortho. offset = np.array(opts.offset[:] + [0]) xlen, ylen = glroutines.adjust(b.xlen, b.ylen, w, h) offset[0] = xlen * offset[0] / 2 offset[1] = ylen * offset[1] / 2 offset = transform.scaleOffsetXform(1, offset) # And finally the camera. eye = list(centre) eye[1] += 1 up = [0, 0, 1] camera = glroutines.lookAt(eye, centre, up) # Order is very important! xform = transform.concat(offset, scale, camera, rotate) np.array(xform, dtype=np.float32) self.__viewOffset = offset self.__viewScale = scale self.__viewRotate = rotate self.__viewCamera = camera self.__viewMat = xform
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 copyImage(overlayList, displayCtx, overlay, createMask=False, copy4D=True, copyDisplay=True, name=None, roi=None, data=None): """Creates a copy of the given :class:`.Image` overlay, and inserts it into the :class:`.OverlayList`. :arg overlayList: The :class:`.OverlayList`. :arg displayCtx: The :class:`.DisplayContext`. :arg overlay: The :class:`.Image` to be copied. :arg createMask: If ``True``, the copy will be an empty ``Image`` the same shape as the ``overlay``. :arg copy4D: If ``True``, and the ``overlay`` is 4D, the copy will also be 4D. Otherwise, the current 3D voluem is copied. :arg copyDisplay: If ``True``, the copy will inherit the display settings of the ``overlay``. Otherwise, the copy will be initialised with default display settings. :arg name: If provided, will be used as the :attr:`.Display.name` of the copy. Otherwise the copy will be given a name. :arg roi: If provided, the copy will be cropped to the low/high voxel bounds specified in the image. Must be a sequence of tuples, containing the low/high bounds for each voxel dimension. For 4D images, the bounds for the fourth dimension are optional. :arg data: If provided, is used as the image data for the new copy. Must match the shape dictated by the other arguments (i.e. ``copy4D`` and ``roi``). If ``data`` is provided, the ``createMask`` argument is ignored. """ ovlIdx = overlayList.index(overlay) opts = displayCtx.getOpts(overlay) isROI = roi is not None is4D = len(overlay.shape) > 3 and overlay.shape[3] > 1 if name is None: name = '{}_copy'.format(overlay.name) if roi is None: roi = [(0, s) for s in overlay.shape] # If the image is 4D, and an ROI of # length 3 has been given, add some # bounds for the fourth dimension if is4D and copy4D and len(roi) == 3: roi = list(roi) + [(0, overlay.shape[3])] # If we are only supposed to copy # the current 3D volume of a 4D # image, adjust the ROI accordingly. if is4D and not copy4D: roi = list(roi[:3]) + [(opts.volume, opts.volume + 1)] shape = [hi - lo for lo, hi in roi] slc = tuple([slice(lo, hi) for lo, hi in roi]) if data is not None: pass elif createMask: data = np.zeros(shape) else: data = np.copy(overlay[slc]) # If this is an ROI, we need to add # an offset to the image affine if isROI: xform = overlay.voxToWorldMat offset = [lo for lo, hi in roi[:3]] offset = transform.scaleOffsetXform([1, 1, 1], offset) xform = transform.concat(xform, offset) else: xform = None # Create the copy, put it in the list header = overlay.header.copy() copy = fslimage.Image(data, name=name, header=header, xform=xform) overlayList.insert(ovlIdx + 1, copy) # Copy the Display/DisplayOpts settings if copyDisplay: srcDisplay = displayCtx.getDisplay(overlay) destDisplay = displayCtx.getDisplay(copy) for prop in srcDisplay.getAllProperties()[0]: # Don't override the name # that we set above if prop == 'name': continue val = getattr(srcDisplay, prop) setattr(destDisplay, prop, val) # And after the Display has been configured # copy the DisplayOpts settings. srcOpts = displayCtx.getOpts(overlay) destOpts = displayCtx.getOpts(copy) for prop in srcOpts.getAllProperties()[0]: # But don't clobber the transform, and related, # properties, as it is (typically) automatically # controlled via the DisplayContext.displaySpace if prop in ('transform', 'bounds'): continue val = getattr(srcOpts, prop) setattr(destOpts, prop, val)