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 updateShaderState(self): """Updates the vertex/fragment shader program(s) state, via a call to the GL-version specific ``updateShaderState`` function. """ dopts = self.opts copts = self.canvas.opts lightPos = None flatColour = dopts.getConstantColour() useNegCmap = (not dopts.useLut) and dopts.useNegativeCmap if self.threedee: lightPos = np.array(copts.lightPos) lightPos *= (copts.zoom / 100.0) else: lightPos = None if dopts.useLut: delta = 1.0 / (dopts.lut.max() + 1) cmapXform = transform.scaleOffsetXform(delta, 0.5 * delta) else: cmapXform = self.cmapTexture.getCoordinateTransform() fslgl.glmesh_funcs.updateShaderState(self, useNegCmap=useNegCmap, cmapXform=cmapXform, flatColour=flatColour, lightPos=lightPos)
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 __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)
def __realPrepareTextureData(self, data): """This method prepares and returns the given ``data``, ready to be used as GL texture data. This process potentially involves: - Resampling to a different resolution (see the :func:`.routines.subsample` function). - Pre-filtering (see the ``prefilter`` parameter to :meth:`__init__`). - Normalising (if the ``normalise`` parameter to :meth:`__init__` was ``True``, or if the data type cannot be used as-is). - Casting to a different data type (if the data type cannot be used as-is). :returns: A tuple containing: - A ``numpy`` array containing the image data, ready to be copied to the GPU. - An affine transformation matrix which encodes an offset and a scale, which may be used to transform the texture data from the range ``[0.0, 1.0]`` to its raw data range. - Inverse of ``voxValXform``. """ log.debug('Preparing data for {}({}) - this may take some time ' '...'.format(type(self).__name__, self.getTextureName())) dtype = data.dtype floatTextures = self.canUseFloatTextures() prefilter = self.__prefilter prefilterRange = self.__prefilterRange resolution = self.__resolution scales = self.__scales normalise = self.__normalise normaliseRange = self.__normaliseRange if normalise: dmin, dmax = normaliseRange else: dmin, dmax = 0, 0 if normalise and \ prefilter is not None and \ prefilterRange is not None: dmin, dmax = prefilterRange(dmin, dmax) # Offsets/scales which can be used to transform from # the texture data (which may be offset or normalised) # back to the original voxel data if normalise: offset = dmin elif dtype == np.uint8: offset = 0 elif dtype == np.int8: offset = -128 elif dtype == np.uint16: offset = 0 elif dtype == np.int16: offset = -32768 elif floatTextures: offset = 0 if normalise: scale = dmax - dmin elif dtype == np.uint8: scale = 255 elif dtype == np.int8: scale = 255 elif dtype == np.uint16: scale = 65535 elif dtype == np.int16: scale = 65535 elif floatTextures: scale = 1 # If the data range is 0 (min == max) # we just set an identity xform if scale == 0: voxValXform = np.eye(4) invVoxValXform = np.eye(4) # Otherwise we save a transformation # from the texture values back to the # original data range. Note that if # storing floating point data, this # will be an identity transform. else: invScale = 1.0 / scale voxValXform = transform.scaleOffsetXform(scale, offset) invVoxValXform = transform.scaleOffsetXform( invScale, -offset * invScale) if resolution is not None: data = glroutines.subsample(data, resolution, pixdim=scales)[0] if prefilter is not None: data = prefilter(data) # TODO if FLOAT_TEXTURES, you should # save normalised values as float32 if normalise: log.debug('Normalising to range {} - {}'.format(dmin, dmax)) if dmax != dmin: data = np.clip((data - dmin) / float(dmax - dmin), 0, 1) data = np.round(data * 65535) data = np.array(data, dtype=np.uint16) elif dtype == np.uint8: pass elif dtype == np.int8: data = np.array(data + 128, dtype=np.uint8) elif dtype == np.uint16: pass elif dtype == np.int16: data = np.array(data + 32768, dtype=np.uint16) elif floatTextures and data.dtype != np.float32: data = np.array(data, dtype=np.float32) log.debug('Data preparation for {} complete [dtype={}, ' 'scale={}, offset={}, dmin={}, dmax={}].'.format( self.getTextureName(), data.dtype, scale, offset, dmin, dmax)) return data, voxValXform, invVoxValXform
def __setupTransforms(self): """Calculates transformation matrices between all of the possible spaces in which the overlay may be displayed. These matrices are accessible via the :meth:`getTransform` method. """ image = self.overlay shape = np.array(image.shape[:3]) voxToIdMat = np.eye(4) voxToPixdimMat = np.diag(list(image.pixdim[:3]) + [1.0]) voxToPixFlipMat = image.voxToScaledVoxMat voxToWorldMat = image.voxToWorldMat voxToWorldMat = transform.concat(self.displayXform, voxToWorldMat) ds = self.displayCtx.displaySpace # The reference transforms depend on the value of # displaySpace if ds == 'world': voxToRefMat = voxToWorldMat elif ds is self.overlay: voxToRefMat = voxToPixFlipMat else: voxToRefMat = transform.concat(ds.voxToScaledVoxMat, ds.worldToVoxMat, voxToWorldMat) # When going from voxels to textures, # we add 0.5 to centre the voxel (see # the note on coordinate systems at # the top of this file). voxToTexMat = transform.scaleOffsetXform(tuple(1.0 / shape), tuple(0.5 / shape)) idToVoxMat = transform.invert(voxToIdMat) idToPixdimMat = transform.concat(voxToPixdimMat, idToVoxMat) idToPixFlipMat = transform.concat(voxToPixFlipMat, idToVoxMat) idToWorldMat = transform.concat(voxToWorldMat, idToVoxMat) idToRefMat = transform.concat(voxToRefMat, idToVoxMat) idToTexMat = transform.concat(voxToTexMat, idToVoxMat) pixdimToVoxMat = transform.invert(voxToPixdimMat) pixdimToIdMat = transform.concat(voxToIdMat, pixdimToVoxMat) pixdimToPixFlipMat = transform.concat(voxToPixFlipMat, pixdimToVoxMat) pixdimToWorldMat = transform.concat(voxToWorldMat, pixdimToVoxMat) pixdimToRefMat = transform.concat(voxToRefMat, pixdimToVoxMat) pixdimToTexMat = transform.concat(voxToTexMat, pixdimToVoxMat) pixFlipToVoxMat = transform.invert(voxToPixFlipMat) pixFlipToIdMat = transform.concat(voxToIdMat, pixFlipToVoxMat) pixFlipToPixdimMat = transform.concat(voxToPixdimMat, pixFlipToVoxMat) pixFlipToWorldMat = transform.concat(voxToWorldMat, pixFlipToVoxMat) pixFlipToRefMat = transform.concat(voxToRefMat, pixFlipToVoxMat) pixFlipToTexMat = transform.concat(voxToTexMat, pixFlipToVoxMat) worldToVoxMat = transform.invert(voxToWorldMat) worldToIdMat = transform.concat(voxToIdMat, worldToVoxMat) worldToPixdimMat = transform.concat(voxToPixdimMat, worldToVoxMat) worldToPixFlipMat = transform.concat(voxToPixFlipMat, worldToVoxMat) worldToRefMat = transform.concat(voxToRefMat, worldToVoxMat) worldToTexMat = transform.concat(voxToTexMat, worldToVoxMat) refToVoxMat = transform.invert(voxToRefMat) refToIdMat = transform.concat(voxToIdMat, refToVoxMat) refToPixdimMat = transform.concat(voxToPixdimMat, refToVoxMat) refToPixFlipMat = transform.concat(voxToPixFlipMat, refToVoxMat) refToWorldMat = transform.concat(voxToWorldMat, refToVoxMat) refToTexMat = transform.concat(voxToTexMat, refToVoxMat) texToVoxMat = transform.invert(voxToTexMat) texToIdMat = transform.concat(voxToIdMat, texToVoxMat) texToPixdimMat = transform.concat(voxToPixdimMat, texToVoxMat) texToPixFlipMat = transform.concat(voxToPixFlipMat, texToVoxMat) texToWorldMat = transform.concat(voxToWorldMat, texToVoxMat) texToRefMat = transform.concat(voxToRefMat, texToVoxMat) self.__xforms['id', 'id'] = np.eye(4) self.__xforms['id', 'pixdim'] = idToPixdimMat self.__xforms['id', 'pixdim-flip'] = idToPixFlipMat self.__xforms['id', 'affine'] = idToWorldMat self.__xforms['id', 'reference'] = idToRefMat self.__xforms['id', 'texture'] = idToTexMat self.__xforms['pixdim', 'pixdim'] = np.eye(4) self.__xforms['pixdim', 'id'] = pixdimToIdMat self.__xforms['pixdim', 'pixdim-flip'] = pixdimToPixFlipMat self.__xforms['pixdim', 'affine'] = pixdimToWorldMat self.__xforms['pixdim', 'reference'] = pixdimToRefMat self.__xforms['pixdim', 'texture'] = pixdimToTexMat self.__xforms['pixdim-flip', 'pixdim-flip'] = np.eye(4) self.__xforms['pixdim-flip', 'id'] = pixFlipToIdMat self.__xforms['pixdim-flip', 'pixdim'] = pixFlipToPixdimMat self.__xforms['pixdim-flip', 'affine'] = pixFlipToWorldMat self.__xforms['pixdim-flip', 'reference'] = pixFlipToRefMat self.__xforms['pixdim-flip', 'texture'] = pixFlipToTexMat self.__xforms['affine', 'affine'] = np.eye(4) self.__xforms['affine', 'id'] = worldToIdMat self.__xforms['affine', 'pixdim'] = worldToPixdimMat self.__xforms['affine', 'pixdim-flip'] = worldToPixFlipMat self.__xforms['affine', 'reference'] = worldToRefMat self.__xforms['affine', 'texture'] = worldToTexMat self.__xforms['reference', 'reference'] = np.eye(4) self.__xforms['reference', 'id'] = refToIdMat self.__xforms['reference', 'pixdim'] = refToPixdimMat self.__xforms['reference', 'pixdim-flip'] = refToPixFlipMat self.__xforms['reference', 'affine'] = refToWorldMat self.__xforms['reference', 'texture'] = refToTexMat self.__xforms['texture', 'texture'] = np.eye(4) self.__xforms['texture', 'id'] = texToIdMat self.__xforms['texture', 'pixdim'] = texToPixdimMat self.__xforms['texture', 'pixdim-flip'] = texToPixFlipMat self.__xforms['texture', 'affine'] = texToWorldMat self.__xforms['texture', 'reference'] = texToRefMat