Source code for fsleyes.gl.glmask

#
# glmask.py - The GLMask class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`GLMask` class, which implements
functionality for rendering an :class:`.Image` overlay as a binary mask.
"""

import logging

import OpenGL.GL                 as gl

import fsl.utils.idle            as idle
import fsleyes.colourmaps        as colourmaps
import fsleyes.gl                as fslgl
import fsleyes.gl.textures       as textures
import fsleyes.gl.routines       as glroutines
import fsleyes.gl.resources      as glresources
import fsleyes.gl.shaders.filter as glfilter
from . import                       glimageobject


log = logging.getLogger(__name__)


[docs]class GLMask(glimageobject.GLImageObject): """The ``GLMask`` class encapsulates logic to render an :class:`.Image` instance as a binary mask in OpenGL. When created, a ``GLMask`` instance assumes that the provided :class:`.Image` instance has a :attr:`.Display.overlayType` of ``mask``, and that its associated :class:`.Display` instance contains a :class:`.MaskOpts` instance, containing mask-specific display properties. **Textures** A ``GLMask`` will use up to two textures: - An :class:`.ImageTexture` for storing the 3D image data. This texture is bound to texture unit 0. - A :class:`.RenderTexture`, used for edge filtering if necessary. This texture will be bound to texture unit 1. **2D rendering** On 2D canvases, A ``GLMask`` is rendered similarly to a :class:`.GLVolume` - a 2D slice is taken through the 3D image texture. If the :attr:`.MaskOpts.outline` property is active, this slice is rendered to an off-screen texture, which is then passed through an edge filter (see the :mod:`.filters` module). **Version dependent modules** The ``GLMask`` class makes use of the functions defined in the :mod:`.gl14.glmask_funcs` or the :mod:`.gl21.glmask_funcs` modules, which provide OpenGL version specific details for rendering. These version dependent modules must provide the following functions: ============================= ===================================== ``init(GLMask)`` Perform any necessary initialisation. ``destroy(GLMask)`` Perform any necessary cleanup ``compileShaders(GLMask)`` (Re-)compile the shader program ``updateShaderState(GLMask)`` Update the shader program state ``draw2D(GLMask, ...)`` Draw a slice of the image ``drawAll(GLMask, ...)`` Draw multiple slices of the image ============================= ===================================== """
[docs] def __init__(self, image, overlayList, displayCtx, canvas, threedee): """Create a ``GLMask``. :arg image: The :class:`.Image` instance. :arg overlayList: The :class:`.OverlayList` :arg displayCtx: The :class:`.DisplayContext` managing the scene. :arg canvas: The canvas doing the drawing. :arg threedee: 2D or 3D rendering """ glimageobject.GLImageObject.__init__(self, image, overlayList, displayCtx, canvas, threedee) # The shader attribute will be created # by the glmask_funcs module self.shader = None self.imageTexture = None self.edgeFilter = glfilter.Filter('edge', texture=1) self.renderTexture = textures.RenderTexture( self.name, interp=gl.GL_LINEAR, rttype='c') self.addDisplayListeners() self.refreshImageTexture() def init(): fslgl.glmask_funcs.init(self) self.notify() idle.idleWhen(init, self.textureReady)
[docs] def destroy(self): """Must be called when this ``GLMask`` is no longer needed. Destroys the :class:`.ImageTexture`. """ self.edgeFilter.destroy() self.renderTexture.destroy() self.imageTexture.deregister(self.name) glresources.delete(self.imageTexture.name) self.removeDisplayListeners() fslgl.glmask_funcs.destroy(self) glimageobject.GLImageObject.destroy(self)
[docs] def ready(self): """Returns ``True`` if this ``GLMask`` is ready to be drawn, ``False`` otherwise. """ return self.shader is not None and self.textureReady()
[docs] def textureReady(self): """Returns ``True`` if the ``imageTexture`` is ready to be used, ``False`` otherwise. """ return self.imageTexture is not None and self.imageTexture.ready()
[docs] def updateShaderState(self, *args, **kwargs): """Calls :func:`.gl14.gllabel_funcs.updateShaderState` or :func:`.gl21.gllabel_funcs.updateShaderState`, and :meth:`.Notifier.notify`. Uses :func:`.idle.idleWhen` to ensure that they don't get called until :meth:`ready` returns ``True``. """ alwaysNotify = kwargs.pop('alwaysNotify', None) def func(): if fslgl.glmask_funcs.updateShaderState(self) or alwaysNotify: self.notify() idle.idleWhen(func, self.ready, name=self.name, skipIfQueued=True)
[docs] def addDisplayListeners(self): """Adds a bunch of listeners to the :class:`.Display` object, and the associated :class:`.MaskOpts` instance, which define how the mask image should be displayed. """ display = self.display opts = self.opts name = self.name def update(*a): self.updateShaderState(alwaysNotify=True) display.addListener('alpha', name, update, weak=False) display.addListener('brightness', name, update, weak=False) display.addListener('contrast', name, update, weak=False) opts .addListener('colour', name, update, weak=False) opts .addListener('threshold', name, update, weak=False) opts .addListener('invert', name, update, weak=False) opts .addListener('outlineWidth', name, update, weak=False) opts .addListener('outline', name, self.notify) opts .addListener('transform', name, self.notify) opts .addListener('volume', name, self.__volumeChanged) opts .addListener('interpolation', name, self.__interpChanged) # See comment in GLVolume.addDisplayListeners about this self.__syncListenersRegistered = opts.getParent() is not None if self.__syncListenersRegistered: opts.addSyncChangeListener( 'volume', name, self.refreshImageTexture)
[docs] def removeDisplayListeners(self): """Removes all the listeners added by :meth:`addDisplayListeners`. """ display = self.display opts = self.opts name = self.name display.removeListener('alpha', name) display.removeListener('brightness', name) display.removeListener('contrast', name) opts .removeListener('colour', name) opts .removeListener('threshold', name) opts .removeListener('invert', name) opts .removeListener('outline', name) opts .removeListener('outlineWidth', name) opts .removeListener('transform', name) opts .removeListener('volume', name) opts .removeListener('interpolation', name) if self.__syncListenersRegistered: opts.removeSyncChangeListener('volume', name)
[docs] def refreshImageTexture(self): """Makes sure that the :class:`.ImageTexture`, used to store the :class:`.Image` data, is up to date. """ opts = self.opts texName = '{}_{}' .format(type(self).__name__, id(self.image)) unsynced = (opts.getParent() is None or not opts.isSyncedToParent('volume')) if unsynced: texName = '{}_unsync_{}'.format(texName, id(opts)) if self.imageTexture is not None: if self.imageTexture.name == texName: return self.imageTexture.deregister(self.name) glresources.delete(self.imageTexture.name) if opts.interpolation == 'none': interp = gl.GL_NEAREST else: interp = gl.GL_LINEAR self.imageTexture = glresources.get( texName, textures.ImageTexture, texName, self.image, interp=interp, volume=opts.index()[3:], notify=False) self.imageTexture.register(self.name, self.__imageTextureChanged)
[docs] def getColour(self): """Prepares and returns the mask colour for use in the fragment shader. """ display = self.display opts = self.opts alpha = display.alpha / 100.0 colour = opts.colour colour = colour[:3] colour = colourmaps.applyBricon(colour, display.brightness / 100.0, display.contrast / 100.0) return list(colour) + [alpha]
[docs] def getThreshold(self): """Prepares and returns the mask thresholds for use in the fragment shader. """ opts = self.opts xform = self.imageTexture.invVoxValXform lo = opts.threshold[0] * xform[0, 0] + xform[0, 3] hi = opts.threshold[1] * xform[0, 0] + xform[0, 3] return (lo, hi)
[docs] def preDraw(self, xform=None, bbox=None): """Binds the :class:`.ImageTexture` and calls the version-dependent ``preDraw`` function. """ w, h = self.canvas.GetSize() rtex = self.renderTexture rtex.shape = w, h with rtex.target(): gl.glClearColor(0, 0, 0, 0) gl.glClear(gl.GL_COLOR_BUFFER_BIT) self.imageTexture.bindTexture(gl.GL_TEXTURE0)
[docs] def draw2D(self, zpos, axes, xform=None, bbox=None): """Calls the version-dependent ``draw2D`` function, then applies the edge filter if necessary. """ opts = self.opts if not opts.outline: fslgl.glmask_funcs.draw2D(self, zpos, axes, xform, bbox) return owidth = float(opts.outlineWidth) rtex = self.renderTexture w, h = self.canvas.GetSize() lo, hi = self.canvas.getViewport() xax = axes[0] yax = axes[1] xmin, xmax = lo[xax], hi[xax] ymin, ymax = lo[yax], hi[yax] offsets = [owidth / w, owidth / h] # Draw the mask to the off-screen texture with glroutines.disabled(gl.GL_BLEND), rtex.target(xax, yax, lo, hi): fslgl.glmask_funcs.draw2D(self, zpos, axes, xform, bbox) # Run the texture through an edge detection # filter, drawing the result to screen self.edgeFilter.set(offsets=offsets, outline=1) self.edgeFilter.apply( rtex, zpos, xmin, xmax, ymin, ymax, xax, yax, textureUnit=gl.GL_TEXTURE1)
[docs] def drawAll(self, axes, zposes, xforms): """Calls the version-dependent ``drawAll`` function, then applies the edge filter if necessary. """ opts = self.opts rtex = self.renderTexture # Is taking max(z) hacky? It seems to work ok. zpos = max(zposes) owidth = opts.outlineWidth w, h = self.canvas.GetSize() lo, hi = self.canvas.getViewport() xax = axes[0] yax = axes[1] xmin, xmax = lo[xax], hi[xax] ymin, ymax = lo[yax], hi[yax] offsets = [owidth / w, owidth / h] # Draw all slices to the off-screen texture with glroutines.disabled(gl.GL_BLEND), rtex.target(xax, yax, lo, hi): fslgl.glmask_funcs.drawAll(self, axes, zposes, xforms) # if no outline, draw the texture directly if not opts.outline: rtex.drawOnBounds( zpos, xmin, xmax, ymin, ymax, xax, yax, textureUnit=gl.GL_TEXTURE1) else: # Run the texture through an edge detection # filter, drawing the result to screen self.edgeFilter.set(offsets=offsets, outline=1) self.edgeFilter.apply( rtex, zpos, xmin, xmax, ymin, ymax, xax, yax, textureUnit=gl.GL_TEXTURE1)
[docs] def draw3D(self, *args, **kwargs): """Does nothing. """ pass
[docs] def postDraw(self, xform=None, bbox=None): """Unbinds the ``ImageTexture``. """ self.imageTexture.unbindTexture()
def __volumeChanged(self, *a): """Called when the :attr:`.NiftiOpts.volume` changes. Updates the image texture. """ self.imageTexture.set(volume=self.opts.index()[3:]) def __interpChanged(self, *a): """Called when the :attr:`.MaskOpts.interpolation` changes. Updates the image texture. """ if self.opts.interpolation == 'none': interp = gl.GL_NEAREST else: interp = gl.GL_LINEAR self.imageTexture.set(interp=interp) def __imageTextureChanged(self, *a): """Called when the image texture data has changed. Triggers a refresh. """ self.updateShaderState(alwaysNotify=True) def __imageSyncChanged(self, *a): """Called when the :attr:`.NiftiOpts.volume` property is synchronised or un-synchronised. Calls :meth:`refreshImageTexture` and :meth:`updateShaderState`. """ self.refreshImageTexture() self.updateShaderState(alwaysNotify=True)