#
# glmip.py - Maximum intensity projection
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""The :class:`GLMIP` class can be used to render
maximum-intensity-projections of an :class:`.Image` overlay onto a 2D canvas.
"""
import OpenGL.GL as gl
import fsl.utils.idle as idle
import fsleyes.gl as fslgl
import fsleyes.gl.textures as textures
import fsleyes.gl.resources as glresources
from . import glimageobject
[docs]class GLMIP(glimageobject.GLImageObject):
"""The ``GLMIP`` class is a :class:`.GLImageObject` which can be used to
render maximum-intensity-projections of an :class:`.Image` overlay onto a
2D canvas.
There is no support for rending a MIP onto a 3D canvas, as the
:class:`.GLVolume` can be used to achieve a MIP-like effect.
To use the ``GLMIP``, the :class:`.Display.overlayType` attribute for the
image must be set to ``'mip'``. See the :class:`.MIPOpts` class for more
details.
The ``GLMIP`` class uses functions defined in the :mod:`.gl21.glmip_funcs`
module - there is currently no support for OpenGL 1.4.
"""
[docs] def __init__(self, image, overlayList, displayCtx, canvas, threedee):
"""Create a ``GLMIP``.
:arg image: An :class:`.Image` object.
:arg overlayList: The :class:`.OverlayList`
:arg displayCtx: The :class:`.DisplayContext` object managing the
scene.
:arg canvas: The canvas doing the drawing.
:arg threedee: Set up for 2D or 3D rendering.
"""
glimageobject.GLImageObject.__init__(self,
image,
overlayList,
displayCtx,
canvas,
threedee)
self.shader = None
self.imageTexture = None
self.cmapTexture = textures.ColourMapTexture(self.name)
self.addDisplayListeners()
self.refreshImageTexture()
self.refreshCmapTextures()
def init():
fslgl.glmip_funcs.init(self)
self.notify()
idle.idleWhen(init, self.textureReady)
[docs] def destroy(self):
"""Clears up resources used by the ``GLMIP``. """
self.cmapTexture.destroy()
self.removeDisplayListeners()
self.imageTexture.deregister(self.name)
glresources.delete(self.imageTexture.name)
fslgl.glmip_funcs.destroy(self)
glimageobject.GLImageObject.destroy(self)
self.shader = None
self.cmapTexture = None
self.imageTexture = None
[docs] def addDisplayListeners(self):
"""Adds a bunch of listeners to the :class:`.Display` object, and the
associated :class:`.MIPOpts` instance, which define how the image
should be displayed.
"""
display = self.display
opts = self.opts
name = self.name
def shader(*a):
self.updateShaderState()
def cmap(*a):
self.refreshCmapTextures()
self.updateShaderState(alwaysNotify=True)
opts .addListener('window', name, shader, weak=False)
opts .addListener('minimum', name, shader, weak=False)
opts .addListener('absolute', name, shader, weak=False)
opts .addListener('displayRange', name, cmap, weak=False)
opts .addListener('clippingRange', name, shader, weak=False)
opts .addListener('invertClipping', name, shader, weak=False)
display .addListener('alpha', name, cmap, weak=False)
opts .addListener('cmap', name, cmap, weak=False)
opts .addListener('gamma', name, cmap, weak=False)
opts .addListener('interpolateCmaps', name, cmap, weak=False)
opts .addListener('negativeCmap', name, cmap, weak=False)
opts .addListener('cmapResolution', name, cmap, weak=False)
opts .addListener('useNegativeCmap', name, cmap, weak=False)
opts .addListener('invert', name, cmap, weak=False)
opts .addListener('volume', name, self.__volumeChanged)
opts .addListener('interpolation', name, self.__interpChanged)
opts .addListener('transform', name, self.notify)
opts .addListener('displayXform', name, self.notify)
# 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 listeners added by :meth:`addDisplayListeners`. """
display = self.display
opts = self.opts
name = self.name
if self.__syncListenersRegistered:
opts.removeSyncChangeListener('volume', name)
opts .removeListener('window', name)
opts .removeListener('minimum', name)
opts .removeListener('absolute', name)
opts .removeListener('displayRange', name)
opts .removeListener('clippingRange', name)
opts .removeListener('invertClipping', name)
display .removeListener('alpha', name)
opts .removeListener('cmap', name)
opts .removeListener('gamma', name)
opts .removeListener('interpolateCmaps', name)
opts .removeListener('negativeCmap', name)
opts .removeListener('cmapResolution', name)
opts .removeListener('useNegativeCmap', name)
opts .removeListener('invert', name)
opts .removeListener('volume', name)
opts .removeListener('interpolation', name)
opts .removeListener('transform', name)
opts .removeListener('displayXform', 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 refreshCmapTextures(self):
"""Updates the colour map texture in line with the current
:class:`.Display` and :class:`MIPOpts` settings.
"""
display = self.display
opts = self.opts
alpha = display.alpha / 100.0
cmap = opts.cmap
interp = opts.interpolateCmaps
res = opts.cmapResolution
gamma = opts.realGamma(opts.gamma)
invert = opts.invert
dmin = opts.displayRange[0]
dmax = opts.displayRange[1]
if interp: interp = gl.GL_LINEAR
else: interp = gl.GL_NEAREST
self.cmapTexture.set(cmap=cmap,
invert=invert,
alpha=alpha,
resolution=res,
gamma=gamma,
interp=interp,
displayRange=(dmin, dmax))
[docs] def updateShaderState(self, *args, **kwargs):
"""Calls :func:`.gl21.glmip_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.glmip_funcs.updateShaderState(self) or alwaysNotify:
self.notify()
idle.idleWhen(func,
self.ready,
name=self.name,
skipIfQueued=True)
[docs] def ready(self):
"""Returns ``True`` if this ``GLMIP`` 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 preDraw(self, xform=None, bbox=None):
"""Binds textures. """
self.imageTexture.bindTexture(gl.GL_TEXTURE0)
self.cmapTexture .bindTexture(gl.GL_TEXTURE1)
[docs] def draw2D(self, zpos, axes, xform=None, bbox=None):
"""Calls :func:`.gl21.glmip_funcs.draw2D`. """
fslgl.glmip_funcs.draw2D(self, zpos, axes, xform, bbox)
[docs] def draw3D(self, xform=None, bbox=None):
"""Does nothing. """
pass
[docs] def postDraw(self, xform=None, bbox=None):
"""Unbinds textures. """
self.imageTexture .unbindTexture()
self.cmapTexture .unbindTexture()
def __volumeChanged(self, *a):
"""Called when the :attr:`.NiftiOpts.volume` property changes. Updates
the image texture accordingly.
"""
self.imageTexture.set(volume=self.opts.index()[3:])
def __interpChanged(self, *a):
"""Called when the :attr:`.MIPOpts.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)