#
# render.py - Generate screenshots of overlays using OpenGL.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""The ``render`` module is a program which provides off-screen rendering
capability for scenes which can otherwise be displayed via *FSLeyes*.
"""
import os.path as op
import sys
import logging
import textwrap
import numpy as np
import fsl.utils.idle as idle
import fsleyes_widgets.utils.layout as fsllayout
import fsleyes
import fsleyes.version as version
import fsleyes.overlay as fsloverlay
import fsleyes.colourmaps as fslcm
import fsleyes.parseargs as parseargs
import fsleyes.displaycontext as displaycontext
import fsleyes.displaycontext.orthoopts as orthoopts
import fsleyes.displaycontext.lightboxopts as lightboxopts
import fsleyes.displaycontext.scene3dopts as scene3dopts
import fsleyes.controls.colourbar as cbar
import fsleyes.plugins.tools.saveannotations as saveannotations
import fsleyes.gl as fslgl
import fsleyes.gl.textures.imagetexture as imagetexture
import fsleyes.gl.ortholabels as ortholabels
import fsleyes.gl.offscreenslicecanvas as slicecanvas
import fsleyes.gl.offscreenlightboxcanvas as lightboxcanvas
import fsleyes.gl.offscreenscene3dcanvas as scene3dcanvas
log = logging.getLogger(__name__)
[docs]def main(args=None, hook=None):
"""Entry point for ``render``.
Creates and renders an OpenGL scene, and saves it to a file, according
to the specified command line arguments (which default to
``sys.argv[1:]``).
"""
if args is None:
args = sys.argv[1:]
# Initialise FSLeyes and implement
# hacks. This must come first as it
# does a number of important things.
fsleyes.initialise()
# Initialise colour maps module
fslcm.init()
# Create a GL context
fslgl.getGLContext(offscreen=True, createApp=True)
# Now that GL inititalisation is over,
# make sure that the idle loop executes
# all tasks synchronously, instead of
# trying to schedule them on the wx
# event loop. And make sure image textures
# don't use separate threads for data
# processing.
with idle.idleLoop.synchronous(), \
imagetexture.ImageTexture.enableThreading(False):
# Parse arguments, and
# configure logging/debugging
namespace = parseArgs(args)
fsleyes.configLogging(namespace.verbose, namespace.noisy)
# Initialise the fsleyes.gl modules
fslgl.bootstrap(namespace.glversion)
# Create a description of the scene
overlayList, displayCtx, sceneOpts = makeDisplayContext(namespace)
import matplotlib.image as mplimg
# Render that scene, and save it to file
bitmap, bg = render(
namespace, overlayList, displayCtx, sceneOpts, hook)
if namespace.crop is not None:
bitmap = autocrop(bitmap, bg, namespace.crop)
# Alpha-blending does work, but the final
# pixel values seem to take on the alpha
# value of the most recently drawn item,
# which is undesirable. So we save out
# as rgb
bitmap = bitmap[:, :, :3]
mplimg.imsave(namespace.outfile, bitmap)
# Clear the GL context
fslgl.shutdown()
[docs]def parseArgs(argv):
"""Creates an argument parser which accepts options for off-screen
rendering. Uses the :mod:`fsleyes.parseargs` module to peform the
actual parsing.
:returns: An ``argparse.Namespace`` object containing the parsed
arguments.
"""
mainParser = parseargs.ArgumentParser(
add_help=False,
formatter_class=parseargs.FSLeyesHelpFormatter)
mainParser.add_argument('-of',
'--outfile',
help='Output image file name')
mainParser.add_argument('-c',
'--crop',
type=int,
metavar='BORDER',
help='Auto-crop image, leaving a '
'border on each side')
mainParser.add_argument('-sz',
'--size',
type=int, nargs=2,
metavar=('W', 'H'),
help='Size in pixels (width, height)',
default=(800, 600))
name = 'render'
prolog = 'FSLeyes render version {}\n'.format(version.__version__)
optStr = '-of outfile'
description = textwrap.dedent("""\
FSLeyes screenshot generator.
Use the '--scene' option to choose between orthographic
('ortho'), lightbox ('lightbox'), or 3D ('3d') views.
""")
namespace = parseargs.parseArgs(
mainParser,
argv,
name,
prolog=prolog,
desc=description,
usageProlog=optStr,
argOpts=['-of', '--outfile',
'-sz', '--size',
'-c', '--crop'],
shortHelpExtra=['--outfile', '--size', '--crop'])
if namespace.outfile is None:
log.error('outfile is required')
mainParser.print_usage()
sys.exit(1)
namespace.outfile = op.abspath(namespace.outfile)
if namespace.scene not in ('ortho', 'lightbox', '3d'):
log.info('Unknown scene specified ("{}") - defaulting '
'to ortho'.format(namespace.scene))
namespace.scene = 'ortho'
return namespace
[docs]def makeDisplayContext(namespace):
"""Creates :class:`.OverlayList`, :class:`.DisplayContext``, and
:class:`.SceneOpts` instances which represent the scene to be rendered,
as described by the arguments in the given ``namespace`` object.
"""
# Create an overlay list and display context.
# The DisplayContext, Display and DisplayOpts
# classes are designed to be created in a
# parent-child hierarchy. So we need to create
# a 'dummy' master display context to make
# things work properly.
overlayList = fsloverlay.OverlayList()
masterDisplayCtx = displaycontext.DisplayContext(overlayList)
childDisplayCtx = displaycontext.DisplayContext(overlayList,
parent=masterDisplayCtx)
# We have to artificially create a ref to the
# master display context, otherwise it may get
# gc'd arbitrarily. The parent reference in the
# child creation above is ultimately stored as
# a weakref, so we need to create a real one.
childDisplayCtx.masterDisplayCtx = masterDisplayCtx
# The handleOverlayArgs function uses the
# fsleyes.overlay.loadOverlays function,
# which will call these functions as it
# goes through the list of overlay to be
# loaded.
def load(ovl):
log.info('Loading overlay {} ...'.format(ovl))
def error(ovl, error):
log.error('Error loading overlay {}: {}'.format(ovl, error))
raise error
# Load the overlays specified on the command
# line, and configure their display properties
parseargs.applyMainArgs( namespace,
overlayList,
masterDisplayCtx)
parseargs.applyOverlayArgs(namespace,
overlayList,
masterDisplayCtx,
loadFunc=load,
errorFunc=error)
# Create a SceneOpts instance describing
# the scene to be rendered. The parseargs
# module assumes that GL canvases have
# already been created, so we use mock
# objects to trick it. The options applied
# to these mock objects are applied to the
# real canvases later on, in the render
# function below.
if namespace.scene == 'ortho':
sceneOpts = orthoopts.OrthoOpts(MockCanvasPanel(3))
elif namespace.scene == 'lightbox':
sceneOpts = lightboxopts.LightBoxOpts(MockCanvasPanel(1))
elif namespace.scene == '3d':
sceneOpts = scene3dopts.Scene3DOpts(MockCanvasPanel(1))
# 3D views default to
# world display space
if namespace.scene == '3d':
childDisplayCtx.displaySpace = 'world'
parseargs.applySceneArgs(namespace,
overlayList,
childDisplayCtx,
sceneOpts)
# Centre the location. The DisplayContext
# will typically centre its location on
# initialisation, but this may not work
# if any overlay arguments change the bounds
# of an overlay (e.g. mesh reference image)
if namespace.worldLoc is None and namespace.voxelLoc is None:
b = childDisplayCtx.bounds
childDisplayCtx.location = [
b.xlo + 0.5 * b.xlen,
b.ylo + 0.5 * b.ylen,
b.zlo + 0.5 * b.zlen]
# This has to be applied after applySceneArgs,
# in case the user used the '-std'/'-std1mm'
# options.
if namespace.selectedOverlay is not None:
masterDisplayCtx.selectedOverlay = namespace.selectedOverlay
if len(overlayList) == 0:
raise RuntimeError('At least one overlay must be specified')
return overlayList, childDisplayCtx, sceneOpts
[docs]def render(namespace, overlayList, displayCtx, sceneOpts, hook=None):
"""Renders the scene, and returns a tuple containing the bitmap and the
background colour.
:arg namespace: ``argparse.Namespace`` object containing command line
arguments.
:arg overlayList: The :class:`.OverlayList` instance.
:arg displayCtx: The :class:`.DisplayContext` instance.
:arg sceneOpts: The :class:`.SceneOpts` instance.
:arg hook: Function which is called after the canvases have been
created, but before the scene is rendered. Can be used
to perform any configuration/drawing in addition to that
specified via ``namespace``.
.. note:: The ``hook`` argument was added for testing purposes, but may
be useful in other situations.
"""
# Calculate canvas and colour bar sizes
# so that the entire scene will fit in
# the width/height specified by the user
width, height = namespace.size
(width, height), (cbarWidth, cbarHeight) = \
adjustSizeForColourBar(width,
height,
sceneOpts.showColourBar,
sceneOpts.colourBarLocation,
sceneOpts.labelSize)
# Lightbox view -> only one canvas
if namespace.scene == 'lightbox':
c = createLightBoxCanvas(namespace,
width,
height,
overlayList,
displayCtx,
sceneOpts)
canvases = [c]
# Ortho view -> up to three canvases
elif namespace.scene == 'ortho':
canvases = createOrthoCanvases(namespace,
width,
height,
overlayList,
displayCtx,
sceneOpts)
labelMgr = ortholabels.OrthoLabels(overlayList,
displayCtx,
sceneOpts,
*canvases)
labelMgr.refreshLabels()
# 3D -> one 3D canvas
elif namespace.scene == '3d':
c = create3DCanvas(namespace,
width,
height,
overlayList,
displayCtx,
sceneOpts)
canvases = [c]
# Do we need to do a neuro/radio l/r flip?
if namespace.scene in ('ortho', 'lightbox'):
inRadio = displayCtx.displaySpaceIsRadiological()
lrFlip = displayCtx.radioOrientation != inRadio
if lrFlip:
for c in canvases:
if c.opts.zax in (1, 2):
c.opts.invertX = True
# fix orthographic projection if
# showing an ortho grid layout.
# Note that, if the user chose 'grid',
# but also chose to hide one or more
# canvases, the createOrthoCanvases
# function will have adjusted the
# value of sceneOpts.layout. So
# if layout == grid, we definitely
# have three canvases.
#
# The createOrthoCanvases also
# re-orders the canvases, which
# we're assuming knowledge of,
# by indexing canvases[1].
if namespace.scene == 'ortho' and sceneOpts.layout == 'grid':
canvases[1].opts.invertX = True
# Load annotations, only on ortho
if namespace.scene == 'ortho' and namespace.annotations is not None:
saveannotations.loadAnnotations(MockOrthoPanel(canvases),
namespace.annotations)
# Configure each of the canvases (with those
# properties that are common to both ortho and
# lightbox canvases) and render them one by one
canvasBmps = []
# Call hook if provided (used for testing)
if hook is not None:
hook(overlayList, displayCtx, sceneOpts, canvases)
for c in canvases:
c.opts.pos = displayCtx.location
# HACK If a SliceCanvas/LightBoxCanvas
# is rendering the sceen to an off-screen
# texture due to the low performance
# setting, its internal viewport will not
# be set until after all GLObjects have
# been rendered. But some GLObjects (e.g.
# GLLabel) need to know the current
# viewport.
#
# This is very much an edge case, as who
# would be using a low performance setting
# for off-screen rendering?
if namespace.scene in ('ortho', 'lightbox') and \
namespace.performance is not None and \
int(namespace.performance) < 3:
c._setViewport()
c.draw()
canvasBmps.append(c.getBitmap())
# destroy the canvases
for c in canvases:
c.destroy()
canvases = None
# layout the bitmaps
if namespace.scene in ('lightbox', '3d'):
layout = fsllayout.Bitmap(canvasBmps[0])
elif len(canvasBmps) > 0:
layout = fsllayout.buildOrthoLayout(canvasBmps,
None,
sceneOpts.layout,
False,
0)
else:
layout = fsllayout.Space(width, height)
# Render a colour bar if required
if sceneOpts.showColourBar:
cbarBmp = buildColourBarBitmap(overlayList,
displayCtx,
cbarWidth,
cbarHeight,
sceneOpts)
if cbarBmp is not None:
layout = buildColourBarLayout(layout,
cbarBmp,
sceneOpts.colourBarLocation,
sceneOpts.colourBarLabelSide)
# Turn the layout tree into a bitmap image
bgColour = [c * 255 for c in sceneOpts.bgColour]
return fsllayout.layoutToBitmap(layout, bgColour), bgColour
[docs]def createLightBoxCanvas(namespace,
width,
height,
overlayList,
displayCtx,
sceneOpts):
"""Creates, configures, and returns an :class:`.OffScreenLightBoxCanvas`.
:arg namespace: ``argparse.Namespace`` object.
:arg width: Available width in pixels.
:arg height: Available height in pixels.
:arg overlayList: The :class:`.OverlayList` instance.
:arg displayCtx: The :class:`.DisplayContext` instance.
:arg sceneOpts: The :class:`.SceneOpts` instance.
"""
canvas = lightboxcanvas.OffScreenLightBoxCanvas(
overlayList,
displayCtx,
zax=sceneOpts.zax,
width=width,
height=height)
if sceneOpts.zrange == (0, 0):
sceneOpts.zrange = displayCtx.bounds.getRange(sceneOpts.zax)
opts = canvas.opts
opts.showCursor = sceneOpts.showCursor
opts.bgColour = sceneOpts.bgColour
opts.cursorColour = sceneOpts.cursorColour
opts.renderMode = sceneOpts.renderMode
opts.zax = sceneOpts.zax
opts.sliceSpacing = sceneOpts.sliceSpacing
opts.nrows = sceneOpts.nrows
opts.ncols = sceneOpts.ncols
opts.zrange = sceneOpts.zrange
opts.showGridLines = sceneOpts.showGridLines
opts.highlightSlice = sceneOpts.highlightSlice
return canvas
[docs]def createOrthoCanvases(namespace,
width,
height,
overlayList,
displayCtx,
sceneOpts):
"""Creates, configures, and returns up to three
:class:`.OffScreenSliceCanvas` instances, for rendering the scene.
:arg namespace: ``argparse.Namespace`` object.
:arg width: Available width in pixels.
:arg height: Available height in pixels.
:arg overlayList: The :class:`.OverlayList` instance.
:arg displayCtx: The :class:`.DisplayContext` instance.
:arg sceneOpts: The :class:`.SceneOpts` instance.
"""
canvases = []
# Build a list containing the horizontal
# and vertical axes for each canvas
canvasAxes = []
zooms = []
centres = []
inverts = []
if sceneOpts.showXCanvas:
canvasAxes.append((1, 2))
zooms .append(sceneOpts.xzoom)
centres .append(sceneOpts.panel.getGLCanvases()[0].centre)
inverts .append((sceneOpts.invertXHorizontal,
sceneOpts.invertXVertical))
if sceneOpts.showYCanvas:
canvasAxes.append((0, 2))
zooms .append(sceneOpts.yzoom)
centres .append(sceneOpts.panel.getGLCanvases()[1].centre)
inverts .append((sceneOpts.invertYHorizontal,
sceneOpts.invertYVertical))
if sceneOpts.showZCanvas:
canvasAxes.append((0, 1))
zooms .append(sceneOpts.zzoom)
centres .append(sceneOpts.panel.getGLCanvases()[2].centre)
inverts .append((sceneOpts.invertZHorizontal,
sceneOpts.invertZVertical))
# Grid layout only makes sense if
# we're displaying 3 canvases
if sceneOpts.layout == 'grid' and len(canvasAxes) <= 2:
sceneOpts.layout = 'horizontal'
if sceneOpts.layout == 'grid':
canvasAxes = [canvasAxes[1], canvasAxes[0], canvasAxes[2]]
centres = [centres[ 1], centres[ 0], centres[ 2]]
zooms = [zooms[ 1], zooms[ 0], zooms[ 2]]
inverts = [inverts[ 1], inverts[ 0], inverts[ 2]]
# Calculate the size in pixels for each canvas
sizes = calculateOrthoCanvasSizes(overlayList,
displayCtx,
width,
height,
canvasAxes,
sceneOpts.layout)
# Configure the properties on each canvas
for ((width, height),
(xax, yax),
zoom,
centre,
(invertx, inverty)) in zip(sizes,
canvasAxes,
zooms,
centres,
inverts):
zax = 3 - xax - yax
c = slicecanvas.OffScreenSliceCanvas(
overlayList,
displayCtx,
zax=zax,
width=int(width),
height=int(height))
opts = c.opts
opts.showCursor = sceneOpts.showCursor
opts.cursorColour = sceneOpts.cursorColour
opts.cursorGap = sceneOpts.cursorGap
opts.bgColour = sceneOpts.bgColour
opts.renderMode = sceneOpts.renderMode
opts.invertX = invertx
opts.invertY = inverty
if zoom is not None:
opts.zoom = zoom
# Default to centering
# on the cursor
if centre is None:
centre = [displayCtx.location[xax], displayCtx.location[yax]]
c.centreDisplayAt(*centre)
canvases.append(c)
return canvases
[docs]def create3DCanvas(namespace,
width,
height,
overlayList,
displayCtx,
sceneOpts):
"""Creates, configures, and returns an :class:`.OffScreenScene3DCanvas`.
:arg namespace: ``argparse.Namespace`` object.
:arg width: Available width in pixels.
:arg height: Available height in pixels.
:arg overlayList: The :class:`.OverlayList` instance.
:arg displayCtx: The :class:`.DisplayContext` instance.
:arg sceneOpts: The :class:`.SceneOpts` instance.
"""
canvas = scene3dcanvas.OffScreenScene3DCanvas(
overlayList,
displayCtx,
width=width,
height=height)
opts = canvas.opts
opts.showCursor = sceneOpts.showCursor
opts.cursorColour = sceneOpts.cursorColour
opts.bgColour = sceneOpts.bgColour
opts.showLegend = sceneOpts.showLegend
opts.legendColour = sceneOpts.fgColour
opts.occlusion = sceneOpts.occlusion
opts.light = sceneOpts.light
opts.zoom = sceneOpts.zoom
opts.offset = sceneOpts.offset
opts.rotation = sceneOpts.rotation
if parseargs.wasSpecified(namespace, sceneOpts, 'lightPos') or \
parseargs.wasSpecified(namespace, sceneOpts, 'lightDistance'):
opts.lightPos = sceneOpts.lightPos
opts.lightDistance = sceneOpts.lightDistance
canvas.resetLightPos = False
else:
canvas.defaultLightPos()
return canvas
[docs]def buildColourBarBitmap(overlayList,
displayCtx,
width,
height,
sceneOpts):
"""If the currently selected overlay has a display range,
creates and returns a bitmap containing a colour bar. Returns
``None`` otherwise.
:arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`.
:arg width: Colour bar width in pixels.
:arg height: Colour bar height in pixels.
:arg sceneOpts: :class:`.SceneOpts` instance containing display
settings.
"""
overlay = displayCtx.getSelectedOverlay()
display = displayCtx.getDisplay(overlay)
opts = display.opts
cbarLocation = sceneOpts.colourBarLocation
cbarSize = sceneOpts.colourBarSize
if cbarLocation in ('top', 'bottom'): width = width * cbarSize / 100.0
elif cbarLocation in ('left', 'right'): height = height * cbarSize / 100.0
if not isinstance(opts, displaycontext.ColourMapOpts):
return None
if cbarLocation in ('top', 'bottom'): orient = 'horizontal'
elif cbarLocation in ('left', 'right'): orient = 'vertical'
cb = cbar.ColourBar(overlayList, displayCtx)
cb.orientation = orient
cb.labelSide = sceneOpts.colourBarLabelSide
cb.bgColour = sceneOpts.bgColour
cb.textColour = sceneOpts.fgColour
cb.fontSize = sceneOpts.labelSize
cbarBmp = cb.colourBar(width, height)
# The colourBarBitmap function returns a w*h*4
# array, but the fsleyes_widgets.utils.layout.Bitmap
# (see the next function) assumes a h*w*4 array
cbarBmp = cbarBmp.transpose((1, 0, 2))
return cbarBmp
[docs]def buildColourBarLayout(canvasLayout,
cbarBmp,
cbarLocation,
cbarLabelSide):
"""Given a layout object containing the rendered canvas bitmaps,
creates a new layout which incorporates the given colour bar bitmap.
:arg canvasLayout: An object describing the canvas layout (see
:mod:`fsleyes_widgets.utils.layout`)
:arg cbarBmp: A bitmap containing a rendered colour bar.
:arg cbarLocation: Colour bar location (see :func:`buildColourBarBitmap`).
:arg cbarLabelSide: Colour bar label side (see
:func:`buildColourBarBitmap`).
"""
cbarBmp = fsllayout.Bitmap(cbarBmp)
if cbarLocation in ('top', 'left'): items = [cbarBmp, canvasLayout]
elif cbarLocation in ('bottom', 'right'): items = [canvasLayout, cbarBmp]
if cbarLocation in ('top', 'bottom'): return fsllayout.VBox(items)
elif cbarLocation in ('left', 'right'): return fsllayout.HBox(items)
[docs]def adjustSizeForColourBar(width,
height,
showColourBar,
colourBarLocation,
fontSize):
"""Calculates the widths and heights of the image display space, and the
colour bar if it is enabled.
:arg width: Desired width in pixels
:arg height: Desired height in pixels
:arg showColourBar: ``True`` if a colour bar is to be shown, ``False``
otherwise.
:arg colourBarLocation: Colour bar location (see
:func:`buildColourBarBitmap`).
:arg fontSize Font size (points) used in colour bar labels.
:returns: Two tuples - the first tuple contains the
``(width, height)`` of the available canvas space,
and the second contains the ``(width, height)`` of
the colour bar.
"""
if showColourBar:
cbarWidth = int(round(cbar.colourBarMinorAxisSize(fontSize)))
if colourBarLocation in ('top', 'bottom'):
height = height - cbarWidth
cbarHeight = cbarWidth
cbarWidth = width
else:
width = width - cbarWidth
cbarHeight = height
else:
cbarWidth = 0
cbarHeight = 0
return (width, height), (cbarWidth, cbarHeight)
[docs]def calculateOrthoCanvasSizes(overlayList,
displayCtx,
width,
height,
canvasAxes,
layout):
"""Calculates the sizes, in pixels, for each canvas to be displayed in an
orthographic layout.
:arg overlayList: The :class:`.OverlayList`.
:arg displayCtx: The :class:`.DisplayContext`.
:arg width: Available width in pixels.
:arg height: Available height in pixels.
:arg canvasAxes: A sequence of ``(xax, yax)`` indices, one for each
bitmap in ``canvasBmps``.
:arg layout: Either ``'horizontal'``, ``'vertical'``, or ``'grid'``,
describing the canvas layout.
:returns: A list of ``(width, height)`` tuples, one for each
canvas, each specifying the canvas width and height in
pixels.
"""
bounds = displayCtx.bounds
axisLens = [bounds.xlen, bounds.ylen, bounds.zlen]
# Grid layout only makes sense if we're
# displaying all three canvases
if layout == 'grid' and len(canvasAxes) <= 2:
raise ValueError('Grid layout only supports 3 canvases')
# Distribute the height across canvas heights
return fsllayout.calcSizes(layout,
canvasAxes,
axisLens,
width,
height)
[docs]def autocrop(data, bgColour, border=0):
"""Crops the given bitmap image on all sides where the ``bgColour`` is
the only colour present.
If the image is completely empty. it is not cropped.
:arg data: ``numpy`` array of shape ``(w, h, 4)`` containing the image.
:arg bgColour: Sequence of length 4 containing the background colour to
crop.
:arg border: Number of pixels to leave around each side.
"""
w, h = data.shape[:2]
low, hiw = 0, w
loh, hih = 0, h
while np.all(data[low, :] == bgColour): low += 1
while np.all(data[hiw - 1, :] == bgColour): hiw -= 1
while np.all(data[:, loh] == bgColour): loh += 1
while np.all(data[:, hih - 1] == bgColour): hih -= 1
if low < hiw and loh < hih:
data = data[low:hiw, loh:hih, :]
if border > 0:
w, h, c = data.shape
new = np.zeros((w + 2 * border, h + 2 * border, c),
dtype=data.dtype)
new[:, :] = bgColour
new[border:-border, border:-border, :] = data
data = new
return data
[docs]class MockSliceCanvas:
"""Used in place of a :class:`.SliceCanvas`. The :mod:`.parseargs` module
needs access to ``SliceCanvas`` instances to apply some command line
options. However, ``render`` calls :func:`.parseargs.applySceneArgs`
before any ``SliceCanvas`` instances have been created.
Instances of this class are just used to capture those options, so they
can later be applied to the real ``SliceCanvas`` instances.
The following arguments may be applied to this class.
- ``--xcentre``
- ``--ycentre``
- ``--zcentre``
"""
[docs] def __init__(self):
self.centre = None
[docs] def centreDisplayAt(self, x, y):
self.centre = x, y
[docs]class MockCanvasPanel:
"""Used in place of a :class:`.CanvasPanel`. This is used as a container
for :class:`MockSliceCanvas` instances.
"""
[docs] def __init__(self, ncanvases):
self.canvases = [MockSliceCanvas() for i in range(ncanvases)]
[docs] def getGLCanvases(self):
return self.canvases
[docs]class MockOrthoPanel:
"""Used in place of an :class:`.OrthoPanel`. This is used as a container
for three :class:`SliceCanvas` instances.
"""
[docs] def __init__(self, canvases):
self.canvases = canvases
[docs] def getGLCanvases(self):
return self.canvases
[docs] def getXCanvas(self):
return self.canvases[0]
[docs] def getYCanvas(self):
return self.canvases[1]
[docs] def getZCanvas(self):
return self.canvases[2]
if __name__ == '__main__':
main()