Source code for fsleyes.gl.lightboxcanvas

#
# lightboxcanvas.py - The LightBoxCanvas class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`LightBoxCanvas` class, which is a
:class:`.SliceCanvas` that displays multiple 2D slices along a single axis
from a collection of 3D overlays.
"""

import sys
import logging

import numpy     as np
import OpenGL.GL as gl

import fsl.data.image                    as fslimage
import fsl.transform.affine              as affine

import fsleyes.displaycontext.canvasopts as canvasopts
import fsleyes.gl.slicecanvas            as slicecanvas
import fsleyes.gl.resources              as glresources
import fsleyes.gl.routines               as glroutines
import fsleyes.gl.textures               as textures


log = logging.getLogger(__name__)


[docs]class LightBoxCanvas(slicecanvas.SliceCanvas): """The ``LightBoxCanvas`` represents an OpenGL canvas which displays multiple slices from a collection of 3D overlays. The slices are laid out on the same canvas along rows and columns, with the slice at the minimum Z position translated to the top left of the canvas, and the slice with the maximum Z value translated to the bottom right. .. note:: The :class:`LightBoxCanvas` class is not intended to be instantiated directly - use one of these subclasses, depending on your use-case: - :class:`.OSMesaLightBoxCanvas` for static off-screen rendering of a scene using OSMesa. - :class:`.WXGLLightBoxCanvas` for interactive rendering on a :class:`wx.glcanvas.GLCanvas` canvas. The ``LightBoxCanvas`` class derives from the :class:`.SliceCanvas` class, and is tightly coupled to the ``SliceCanvas`` implementation. Various settings, and the current scene displayed on a ``LightBoxCanvas`` instance, can be changed through the properties of the ``LightBoxCanvasOpts`` instance, available via the :attr:`opts` attribute. Performance of a ``LightBoxCanvas`` instance may be controlled through the :attr:`.SliceCanvasOpts.renderMode` property, in the same way as for the :class:`.SliceCanvas`. However, the ``LightBoxCanvas`` handles the ``offscreen`` render mode differently to the ``SliceCanvas. Where the ``SliceCanvas`` uses a separate :class:`.RenderTexture` for every overlay in the :class:`.OverlayList`, the ``LightBoxCanvas`` uses a single ``RenderTexture`` to render all overlays off-screen. The ``LightBoxCanvas`` class defines the following convenience methods (in addition to those defined in the ``SliceCanvas`` class): .. autosummary:: :nosignatures: canvasToWorld worldToCanvas getTotalRows calcSliceSpacing """
[docs] def __init__(self, overlayList, displayCtx, zax=0): """Create a ``LightBoxCanvas`` object. :arg overlayList: An :class:`.OverlayList` object which contains a list of overlays to be displayed. :arg displayCtx: A :class:`.DisplayContext` object which defines how that overlay list is to be displayed. :arg zax: Display coordinate system axis to be used as the 'depth' axis. Can be changed via the :attr:`.SliceCanvas.zax` property. """ # These attributes are used to keep track of # the total number of slices to be displayed, # and the total number of rows to be displayed self._nslices = 0 self._totalRows = 0 # The final display bounds calculated by # SliceCanvas._updateDisplayBounds is not # necessarily the same as the actual bounds, # as they are adjusted to preserve the # image aspect ratio. But the real bounds # are of use in the _zPosChanged method, so # we save them here as an attribute - # see _updateDisplayBounds. self._realBounds = None # This will point to a RenderTexture if # the offscreen render mode is enabled self._offscreenRenderTexture = None opts = canvasopts.LightBoxCanvasOpts() slicecanvas.SliceCanvas.__init__(self, overlayList, displayCtx, zax, opts) opts.addListener('sliceSpacing', self.name, self._slicePropsChanged) opts.addListener('ncols', self.name, self._slicePropsChanged) opts.addListener('nrows', self.name, self._slicePropsChanged) opts.addListener('zrange', self.name, self._slicePropsChanged) opts.addListener('showGridLines', self.name, self.Refresh) opts.addListener('highlightSlice', self.name, self.Refresh) opts.addListener('topRow', self.name, self._topRowChanged) # Add a listener to the position so when it # changes we can adjust the display range (via # the topRow property) to ensure the slice # corresponding to the current z position is # visible. SliceCanvas.__init__ has already # registered a listener, on pos, with # self.name - so we use a different name # here opts.addListener('pos', '{}_zPosChanged'.format(self.name), self._zPosChanged)
[docs] def destroy(self): """Overrides :meth:`.SliceCanvas.destroy`. Must be called when this ``LightBoxCanvas`` is no longer needed. Removes some property listeners, makes sure that the off screen :class:`.RenderTexture` (if one exists) is destroyed, and then calls the :meth:`.SliceCanvas.destroy` method. """ opts = self.opts opts.removeListener('pos', '{}_zPosChanged'.format(self.name)) opts.removeListener('sliceSpacing', self.name) opts.removeListener('ncols', self.name) opts.removeListener('nrows', self.name) opts.removeListener('zrange', self.name) opts.removeListener('showGridLines', self.name) opts.removeListener('highlightSlice', self.name) opts.removeListener('topRow', self.name) if self._offscreenRenderTexture is not None: self._offscreenRenderTexture.destroy() slicecanvas.SliceCanvas.destroy(self)
[docs] def worldToCanvas(self, pos): """Given an x/y/z location in the display coordinate system, converts it into an x/y position, in the coordinate system of this ``LightBoxCanvas``. """ opts = self.opts xpos = pos[opts.xax] ypos = pos[opts.yax] zpos = pos[opts.zax] sliceno = int(np.floor((zpos - opts.zrange.xlo) / opts.sliceSpacing)) xlen = self.displayCtx.bounds.getLen(opts.xax) ylen = self.displayCtx.bounds.getLen(opts.yax) xmin = self.displayCtx.bounds.getLo( opts.xax) ymin = self.displayCtx.bounds.getLo( opts.yax) row = self._totalRows - int(np.floor(sliceno / opts.ncols)) - 1 col = int(np.floor(sliceno % opts.ncols)) if opts.invertX: xpos = xmin + xlen - (xpos - xmin) if opts.invertY: ypos = ymin + ylen - (ypos - ymin) xpos = xpos + xlen * col ypos = ypos + ylen * row return xpos, ypos
[docs] def canvasToWorld(self, xpos, ypos): """Overrides :meth:.SliceCanvas.canvasToWorld`. Given pixel x/y coordinates on this canvas, translates them into the corresponding display space x/y/z coordinates. Returns a 3-tuple containing the (x, y, z) display system coordinates. If the given canvas position is out of the :attr:`.SliceCanvas.displayBounds`, ``None`` is returned. """ opts = self.opts nrows = self._totalRows ncols = opts.ncols screenPos = slicecanvas.SliceCanvas.canvasToWorld( self, xpos, ypos, invertX=False, invertY=False) if screenPos is None: return None screenx = screenPos[opts.xax] screeny = screenPos[opts.yax] xmin = self.displayCtx.bounds.getLo( opts.xax) ymin = self.displayCtx.bounds.getLo( opts.yax) xlen = self.displayCtx.bounds.getLen(opts.xax) ylen = self.displayCtx.bounds.getLen(opts.yax) xmax = xmin + ncols * xlen ymax = ymin + nrows * ylen col = int(np.floor((screenx - xmin) / xlen)) row = nrows - int(np.floor((screeny - ymin) / ylen)) - 1 sliceno = row * ncols + col if screenx < xmin or \ screenx > xmax or \ screeny < ymin or \ screeny > ymax or \ sliceno < 0 or \ sliceno >= self._nslices: return None xpos = screenx - col * xlen ypos = screeny - (nrows - row - 1) * ylen if opts.invertX: xpos = xlen - (xpos - xmin) + xmin if opts.invertY: ypos = ylen - (ypos - ymin) + ymin zpos = opts.zrange.xlo + (sliceno + 0.5) * opts.sliceSpacing pos = [0, 0, 0] pos[opts.xax] = xpos pos[opts.yax] = ypos pos[opts.zax] = zpos return tuple(pos)
[docs] def getTotalRows(self): """Returns the total number of rows that may be displayed. """ return self._totalRows
[docs] def calcSliceSpacing(self, overlay): """Calculates and returns a Z-axis slice spacing value suitable for the given overlay. """ copts = self.opts displayCtx = self.displayCtx dopts = displayCtx.getOpts(overlay) zmin, zmax = dopts.bounds.getRange(copts.zax) overlay = displayCtx.getReferenceImage(overlay) # If the overlay does not have a # reference NIFTI image, choose # an arbitrary slice spacing. if overlay is None: return (zmax - zmin) / 50.0 # Get the DisplayOpts instance # for the reference image dopts = displayCtx.getOpts(overlay) # Otherwise return a spacing # appropriate for the current # display space if dopts.transform == 'id': return 1 elif dopts.transform == 'pixdim': return overlay.pixdim[copts.zax] elif dopts.transform == 'pixdim-flip': return overlay.pixdim[copts.zax] elif dopts.transform == 'affine': return min(overlay.pixdim[:3]) # This overlay is being displayed with a # custrom transformation matrix - check # to see what display space we're in displaySpace = displayCtx.displaySpace if isinstance(displaySpace, fslimage.Nifti) and \ overlay is not displaySpace: return self.calcSliceSpacing(displaySpace) else: return min(overlay.pixdim[:3])
[docs] def _zAxisChanged(self, *a): """Overrides :meth:`.SliceCanvas._zAxisChanged`. Calls that method, and then resets the :attr:`sliceSpacing` and :attr:`zrange` properties to sensible values. """ slicecanvas.SliceCanvas._zAxisChanged(self, *a) self._updateZAxisProperties() self._slicePropsChanged()
[docs] def _topRowChanged(self, *a): """Called when the :attr:`topRow` property changes. Adjusts display range and refreshes the canvas. """ self._updateDisplayBounds() self.Refresh()
[docs] def _slicePropsChanged(self, *a): """Called when any of the slice properties change. Regenerates slice locations and display bounds, and refreshes the canvas. """ self._calcNumSlices() self._genSliceLocations() self._zPosChanged() self._updateDisplayBounds() if log.getEffectiveLevel() == logging.DEBUG: opts = self.opts props = [('nrows', opts.nrows), ('ncols', opts.ncols), ('sliceSpacing', opts.sliceSpacing), ('zrange', opts.zrange)] props = '; '.join(['{}={}'.format(k, v) for k, v in props]) log.debug('Lightbox properties changed: [{}]'.format(props)) self.Refresh()
[docs] def _renderModeChange(self, *a): """Overrides :meth:`.SliceCanvas._renderModeChange`. Makes sure that any off-screen :class:`.RenderTexture` is destroyed, and calls the :meth:`.SliceCanvas._renderModeChange` method. """ if self._offscreenRenderTexture is not None: self._offscreenRenderTexture.destroy() self._offscreenRenderTexture = None slicecanvas.SliceCanvas._renderModeChange(self, *a)
[docs] def _updateRenderTextures(self): """Overrides :meth:`.SliceCanvas._updateRenderTextures`. Destroys/creates :class:`.RenderTexture` and :class:`.RenderTextureStack` instances as needed. """ renderMode = self.opts.renderMode if renderMode == 'onscreen': return # The LightBoxCanvas does offscreen rendering # a bit different to the SliceCanvas. The latter # uses a separate render texture for each overlay # whereas here we're going to use a single # render texture for all overlays. elif renderMode == 'offscreen': if self._offscreenRenderTexture is not None: self._offscreenRenderTexture.destroy() self._offscreenRenderTexture = textures.RenderTexture( '{}_{}'.format(type(self).__name__, id(self)), interp=gl.GL_LINEAR) self._offscreenRenderTexture.shape = 768, 768 # The LightBoxCanvas handles pre-render mode # the same way as the SliceCanvas - a separate # RenderTextureStack for eacn globject elif renderMode == 'prerender': # Delete any RenderTextureStack instances for # overlays which have been removed from the list for overlay, (tex, name) in list(self._prerenderTextures.items()): if overlay not in self.overlayList: self._prerenderTextures.pop(overlay) glresources.delete(name) # Create a RendeTextureStack for overlays # which have been added to the list for overlay in self.overlayList: if overlay in self._prerenderTextures: continue globj = self._glObjects.get(overlay, None) if (globj is None) or (not globj): continue rt, name = self._getPreRenderTexture(globj, overlay) self._prerenderTextures[overlay] = rt, name self.Refresh()
[docs] def _calcNumSlices(self, *a): """Calculates the total number of slices to be displayed and the total number of rows. """ opts = self.opts xlen = self.displayCtx.bounds.getLen(opts.xax) ylen = self.displayCtx.bounds.getLen(opts.yax) zlen = opts.zrange.xlen width, height = self.GetSize() if xlen == 0 or \ ylen == 0 or \ width == 0 or \ height == 0: return self._nslices = int(np.ceil(zlen / opts.sliceSpacing)) self._totalRows = int(np.ceil(self._nslices / float(opts.ncols))) if self._nslices == 0 or self._totalRows == 0: return # All slices are going to be displayed, so # we'll 'disable' the topRow property if self._totalRows < opts.nrows: opts.setAttribute('topRow', 'minval', 0) opts.setAttribute('topRow', 'maxval', 0) # nrows slices are going to be displayed, # and the topRow property can be used to # scroll through all available rows. else: opts.setAttribute('topRow', 'maxval', self._totalRows - opts.nrows)
[docs] def _zPosChanged(self, *a): """Called when the :attr:`.SliceCanvas.pos` ``z`` value changes. Makes sure that the corresponding slice is visible. """ # figure out where we are in the canvas world opts = self.opts canvasX, canvasY = self.worldToCanvas(opts.pos.xyz) # Get the actual canvas bounds xlo, xhi, ylo, yhi = self._realBounds # already in bounds if canvasX >= xlo and \ canvasX <= xhi and \ canvasY >= ylo and \ canvasY <= yhi: return # figure out what row we're on zpos = opts.pos[opts.zax] sliceno = int(np.floor((zpos - opts.zrange.xlo) / opts.sliceSpacing)) row = int(np.floor(sliceno / opts.ncols)) # and make sure that row is visible opts.topRow = row
[docs] def _overlayListChanged(self, *a): """Overrides :meth:`.SliceCanvas._overlayListChanged`. Regenerates slice locations for all overlays, and calls the :meth:`.SliceCanvas._overlayListChanged` method. """ self._updateZAxisProperties() self._genSliceLocations() slicecanvas.SliceCanvas._overlayListChanged(self, *a)
[docs] def _updateZAxisProperties(self): """Called by the :meth:`_overlayListChanged` and :meth:`_overlayBoundsChanged` methods. Updates the constraints (minimum/maximum values) of the :attr:`sliceSpacing` and :attr:`zrange` properties. """ opts = self.opts if len(self.overlayList) == 0: opts.setAttribute('zrange', 'minDistance', 0) opts.zrange.x = (0, 0) opts.sliceSpacing = 0 return # Get the new Z range from the # display context bounding box. # # And calculate the minimum possible # slice spacing - the smallest pixdim # across all overlays in the list. newZRange = self.displayCtx.bounds.getRange(opts.zax) newZGap = sys.float_info.max for overlay in self.overlayList: zgap = self.calcSliceSpacing(overlay) if zgap < newZGap: newZGap = zgap # Update the zrange and slice # spacing constraints opts.zrange.setLimits(0, *newZRange) opts.setAttribute('zrange', 'minDistance', newZGap) opts.setAttribute('sliceSpacing', 'minval', newZGap)
[docs] def _overlayBoundsChanged(self, *a): """Overrides :meth:`.SliceCanvas._overlayBoundsChanged`. Called when the :attr:`.DisplayContext.bounds` change. Updates the :attr:`zrange` min/max values. """ slicecanvas.SliceCanvas._overlayBoundsChanged(self, preserveZoom=False) self._updateZAxisProperties() self._calcNumSlices() self._genSliceLocations()
[docs] def _updateDisplayBounds(self, *args, **kwargs): """Overrides :meth:`.SliceCanvas._updateDisplayBounds`. Called on canvas resizes, display bound changes and lightbox slice property changes. Calculates the required bounding box that is to be displayed, in display coordinates. """ if self.destroyed: return opts = self.opts xmin = self.displayCtx.bounds.getLo( opts.xax) ymin = self.displayCtx.bounds.getLo( opts.yax) xlen = self.displayCtx.bounds.getLen(opts.xax) ylen = self.displayCtx.bounds.getLen(opts.yax) # Calculate the vertical offset required to # ensure that the current 'topRow' is the first # row, and the correct number of rows ('nrows') # are displayed # if the number of rows to be displayed (nrows) # is more than the number of rows that exist # (totalRows), calculate an offset to vertically # centre the existing row space in the display # row space if self._totalRows < opts.nrows: off = (self._totalRows - opts.nrows) / 2.0 # otherwise calculate the offset so that the # top of the display space lines up with the # current topRow else: off = self._totalRows - opts.nrows - opts.topRow ymin = ymin + ylen * off xmax = xmin + xlen * opts.ncols ymax = ymin + ylen * opts.nrows # Save the real canvas bounds self._realBounds = (xmin, xmax, ymin, ymax) log.debug('Required lightbox bounds: X: ({}, {}) Y: ({}, {})'.format( xmin, xmax, ymin, ymax)) slicecanvas.SliceCanvas._updateDisplayBounds( self, (xmin, xmax, ymin, ymax))
[docs] def _genSliceLocations(self): """Called when any of the slice display properties change. For every overlay in the overlay list, generates a list of transformation matrices, and a list of slice indices. The latter specifies the slice indices from the overlay to be displayed, and the former specifies the transformation matrix to be used to position the slice on the canvas. """ # calculate the locations, in display coordinates, # of all slices to be displayed on the canvas opts = self.opts sliceLocs = np.arange( opts.zrange.xlo + opts.sliceSpacing * 0.5, opts.zrange.xhi, opts.sliceSpacing) self._sliceLocs = {} self._transforms = {} # calculate the transformation for each # slice in each overlay, and the index of # each slice to be displayed for i, overlay in enumerate(self.overlayList): iSliceLocs = [] iTransforms = [] for zi, zpos in enumerate(sliceLocs): xform = self._calculateSliceTransform(overlay, zi) iTransforms.append(xform) iSliceLocs .append(zpos) self._transforms[overlay] = iTransforms self._sliceLocs[ overlay] = iSliceLocs
[docs] def _calculateSliceTransform(self, overlay, sliceno): """Calculates a transformation matrix for the given slice number in the given overlay. Each slice is displayed on the same canvas, but is translated to a specific row/column. So translation matrix is created, to position the slice in the correct location on the canvas. """ opts = self.opts nrows = self._totalRows ncols = opts.ncols row = int(np.floor(sliceno / ncols)) col = int(np.floor(sliceno % ncols)) xlen = self.displayCtx.bounds.getLen(opts.xax) ylen = self.displayCtx.bounds.getLen(opts.yax) translate = np.identity(4, dtype=np.float32) translate[opts.xax, 3] = xlen * col translate[opts.yax, 3] = ylen * (nrows - row - 1) translate[opts.zax, 3] = 0 return translate
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 = affine.concat(fromOrigin, invert, toOrigin, xform) invXforms.append(xform) return invXforms
[docs] def _drawGridLines(self): """Draws grid lines between all the displayed slices.""" opts = self.opts if self._totalRows == 0 or opts.ncols == 0: return xlen = self.displayCtx.bounds.getLen(opts.xax) ylen = self.displayCtx.bounds.getLen(opts.yax) xmin = self.displayCtx.bounds.getLo( opts.xax) ymin = self.displayCtx.bounds.getLo( opts.yax) rowLines = np.zeros(((self._totalRows - 1) * 2, 2), dtype=np.float32) colLines = np.zeros(((opts.ncols - 1) * 2, 2), dtype=np.float32) topRow = self._totalRows - opts.topRow btmRow = topRow - self._totalRows rowLines[:, 1] = np.arange( ymin + (btmRow + 1) * ylen, ymin + topRow * ylen, ylen).repeat(2) rowLines[:, 0] = np.tile( np.array([xmin, xmin + opts.ncols * xlen]), self._totalRows - 1) colLines[:, 0] = np.arange( xmin + xlen, xmin + xlen * opts.ncols, xlen).repeat(2) colLines[:, 1] = np.tile(np.array([ ymin + btmRow * ylen, ymin + topRow * ylen]), opts.ncols - 1) colour = (0.3, 0.9, 1.0, 0.8) for lines in (rowLines, colLines): for i in range(0, len(lines), 2): self.getAnnotations().line(*lines[i], *lines[i + 1], colour=colour, width=2)
[docs] def _drawSliceHighlight(self): """Draws a box around the slice which contains the current cursor location. """ opts = self.opts zpos = opts.pos[opts.zax] sliceno = int(np.floor((zpos - opts.zrange.xlo) / opts.sliceSpacing)) xlen = self.displayCtx.bounds.getLen(opts.xax) ylen = self.displayCtx.bounds.getLen(opts.yax) xmin = self.displayCtx.bounds.getLo( opts.xax) ymin = self.displayCtx.bounds.getLo( opts.yax) row = int(np.floor(sliceno / opts.ncols)) col = int(np.floor(sliceno % opts.ncols)) # don't draw the cursor if it is on a # non-existent or non-displayed slice if sliceno > self._nslices: return if row < opts.topRow: return if row >= opts.topRow + opts.nrows: return # in GL space, the top row is actually the bottom row row = self._totalRows - row - 1 self.getAnnotations().rect(xmin + xlen * col, ymin + ylen * row, xlen, ylen, filled=False, colour=(1, 0, 0), width=2)
[docs] def _drawCursor(self): """Draws a cursor at the current canvas position (the :attr:`.SliceCanvas.pos` property). """ opts = self.opts zpos = opts.pos[opts.zax] sliceno = int(np.floor((zpos - opts.zrange.xlo) / opts.sliceSpacing)) xlen = self.displayCtx.bounds.getLen(opts.xax) ylen = self.displayCtx.bounds.getLen(opts.yax) xmin = self.displayCtx.bounds.getLo( opts.xax) ymin = self.displayCtx.bounds.getLo( opts.yax) row = int(np.floor(sliceno / opts.ncols)) col = int(np.floor(sliceno % opts.ncols)) # don't draw the cursor if it is on a # non-existent or non-displayed slice if sliceno > self._nslices: return if row < opts.topRow: return if row >= opts.topRow + opts.nrows: return # in GL space, the top row is actually the bottom row row = self._totalRows - row - 1 xpos, ypos = self.worldToCanvas(opts.pos.xyz) xverts = np.zeros((2, 2)) yverts = np.zeros((2, 2)) xverts[:, 0] = xpos xverts[0, 1] = ymin + (row) * ylen xverts[1, 1] = ymin + (row + 1) * ylen yverts[:, 1] = ypos yverts[0, 0] = xmin + (col) * xlen yverts[1, 0] = xmin + (col + 1) * xlen annot = self.getAnnotations() kwargs = { 'colour' : opts.cursorColour, 'width' : 1 } annot.line(*xverts[0], *xverts[1], **kwargs) annot.line(*yverts[0], *yverts[1], **kwargs)
[docs] def _draw(self, *a): """Draws the current scene to the canvas. """ if self.destroyed: return width, height = self.GetSize() if width == 0 or height == 0: return if not self._setGLContext(): return opts = self.opts axes = (opts.xax, opts.yax, opts.zax) overlays, globjs = self._getGLObjects() # See comment in SliceCanvas._draw # regarding this test if any([not g.ready() for g in globjs]): return if opts.renderMode == 'offscreen': log.debug('Rendering to off-screen texture') rt = self._offscreenRenderTexture lo = [None] * 3 hi = [None] * 3 lo[opts.xax], hi[opts.xax] = opts.displayBounds.x lo[opts.yax], hi[opts.yax] = opts.displayBounds.y lo[opts.zax], hi[opts.zax] = opts.zrange rt.bindAsRenderTarget() rt.setRenderViewport(opts.xax, opts.yax, lo, hi) glroutines.clear((0, 0, 0, 0)) else: self._setViewport(invertX=False, invertY=False) glroutines.clear(opts.bgColour) startSlice = opts.ncols * opts.topRow endSlice = startSlice + opts.nrows * opts.ncols if endSlice > self._nslices: endSlice = self._nslices # Draw all the slices for all the overlays. for overlay, globj in zip(overlays, globjs): display = self.displayCtx.getDisplay(overlay) globj = self._glObjects.get(overlay, None) if not display.enabled: continue log.debug('Drawing {} slices ({} - {}) for ' 'overlay {} directly to canvas'.format( endSlice - startSlice, startSlice, endSlice, overlay)) zposes = self._sliceLocs[ overlay][startSlice:endSlice] xforms = self._transforms[overlay][startSlice:endSlice] xforms = self.__prepareSliceTransforms(globj, xforms) if opts.renderMode == 'prerender': rt, name = self._prerenderTextures.get(overlay, (None, None)) if rt is None: continue log.debug('Drawing {} slices ({} - {}) for overlay {} ' 'from pre-rendered texture'.format( endSlice - startSlice, startSlice, endSlice, overlay)) for zpos, xform in zip(zposes, xforms): rt.draw(zpos, xform) else: globj.preDraw() globj.drawAll(axes, zposes, xforms) globj.postDraw() if opts.renderMode == 'offscreen': rt.unbindAsRenderTarget() rt.restoreViewport() self._setViewport(invertX=False, invertY=False) glroutines.clear(opts.bgColour) rt.drawOnBounds( 0, opts.displayBounds.xlo, opts.displayBounds.xhi, opts.displayBounds.ylo, opts.displayBounds.yHi, opts.xax, opts.yax) if len(self.overlayList) > 0: if opts.showCursor: self._drawCursor() if opts.showGridLines: self._drawGridLines() if opts.highlightSlice: self._drawSliceHighlight() self.getAnnotations().draw(opts.pos[opts.zax])