Source code for fsleyes.gl.textures.data

#
# data.py - Functions for preparing OpenGL texture data.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module contains functions for working with OpenGL texture data:

.. autosummary::
   :nosignatures:

   numTextureDims
   canUseFloatTextures
   oneChannelFormat
   getTextureType
   prepareData
"""


import logging
import inspect

import numpy                       as np
import OpenGL.GL                   as gl
import OpenGL.extensions           as glexts
import OpenGL.GL.ARB.texture_float as arbtf

import fsl.utils.memoize           as memoize
import fsl.transform.affine        as affine
import fsleyes.gl.routines         as glroutines


log = logging.getLogger(__name__)


# Used for log messages
GL_TYPE_NAMES = {

    gl.GL_UNSIGNED_BYTE       : 'GL_UNSIGNED_BYTE',
    gl.GL_UNSIGNED_SHORT      : 'GL_UNSIGNED_SHORT',
    gl.GL_FLOAT               : 'GL_FLOAT',

    gl.GL_ALPHA               : 'GL_ALPHA',
    gl.GL_RED                 : 'GL_RED',
    gl.GL_LUMINANCE           : 'GL_LUMINANCE',
    gl.GL_RGB                 : 'GL_RGB',
    gl.GL_RGBA                : 'GL_RGBA',

    gl.GL_LUMINANCE8          : 'GL_LUMINANCE8',
    gl.GL_LUMINANCE16         : 'GL_LUMINANCE16',
    arbtf.GL_LUMINANCE16F_ARB : 'GL_LUMINANCE16F',
    arbtf.GL_LUMINANCE32F_ARB : 'GL_LUMINANCE32F',
    gl.GL_R32F                : 'GL_R32F',

    gl.GL_ALPHA8              : 'GL_ALPHA8',

    gl.GL_R8                  : 'GL_R8',
    gl.GL_R16                 : 'GL_R16',

    gl.GL_RGB8                : 'GL_RGB8',
    gl.GL_RGB16               : 'GL_RGB16',
    gl.GL_RGB32F              : 'GL_RGB32F',

    gl.GL_RGBA8               : 'GL_RGBA8',
    gl.GL_RGBA16              : 'GL_RGBA16',
    gl.GL_RGBA32F             : 'GL_RGBA32F',
}


[docs]def _makeInstance(dtype): """Used by :func:`oneChannelFormat` and :func:`getTextureType`. If a ``numpy.dtype`` class is given, converts it into an instance. """ if inspect.isclass(dtype): dtype = np.zeros([0], dtype=dtype).dtype return dtype
[docs]def numTextureDims(shape): """Given a 3D image shape, returns the number of dimensions needd to store the image as a texture - either ``2`` or ``3``. :arg shape: 3D image shape :returns: 2 if a 2D texture can be used to store the image data, 3 otherwise. """ max3d = gl.glGetInteger(gl.GL_MAX_3D_TEXTURE_SIZE) max2d = gl.glGetInteger(gl.GL_MAX_TEXTURE_SIZE) def checklim(shape, lim): if any([d > lim for d in shape]): raise RuntimeError( 'Cannot create an OpenGL texture for {} - it exceeds ' 'the hardware limits on this platform (2D: {}, 3D: {}' .format(shape, max2d, max3d)) shape = [d for d in shape[:3] if d > 1] # force scalar/vector shape to be 2D if len(shape) == 0: shape = [1, 1] elif len(shape) == 1: shape = [shape[0], 1] if len(shape) == 3: checklim(shape, max3d) else: checklim(shape, max2d) return len(shape)
@memoize.memoize def canUseFloatTextures(nvals=1): """Returns ``True`` if this GL environment supports floating point textures, ``False`` otherwise. The test is based on the availability of the ``ARB_texture_float`` extension. :arg nvals: Number of values per voxel :returns: A tuple containing: - ``True`` if floating point textures are supported, ``False`` otherwise - The base texture format to use (``None`` if floating point textures are not supported) - The internal texture format to use (``None`` if floating point textures are not supported) """ # We need the texture_float extension. The # texture_rg extension just provides some # nicer/more modern data types, but is not # necessary. floatSupported = glexts.hasExtension('GL_ARB_texture_float') rgSupported = glexts.hasExtension('GL_ARB_texture_rg') if not floatSupported: return False, None, None if rgSupported: if nvals == 1: baseFmt = gl.GL_RED elif nvals == 3: baseFmt = gl.GL_RGB elif nvals == 4: baseFmt = gl.GL_RGBA if nvals == 1: intFmt = gl.GL_R32F elif nvals == 3: intFmt = gl.GL_RGB32F elif nvals == 4: intFmt = gl.GL_RGBA32F return True, baseFmt, intFmt else: if nvals == 1: baseFmt = gl.GL_LUMINANCE elif nvals == 3: baseFmt = gl.GL_RGB elif nvals == 4: baseFmt = gl.GL_RGBA if nvals == 1: intFmt = arbtf.GL_LUMINANCE32F_ARB elif nvals == 3: intFmt = gl.GL_RGB32F elif nvals == 4: intFmt = gl.GL_RGBA32F return True, baseFmt, intFmt @memoize.memoize def oneChannelFormat(dtype): """Determines suitable one-channel base and internal texture formats to use for the given ``numpy`` data type. :return: A tuple containing: - the base texture format to use - the internal texture format to use .. note:: This is used by the :func:`getTextureType` function. The returned formats may be ignored for floating point data types, depending on whether floating point textures are supported - see :func:`canUseFloatTextures`. """ # Make sure we have a numpy dtype # *instance*, and not a class. dtype = _makeInstance(dtype) floatSupported = glexts.hasExtension('GL_ARB_texture_float') rgSupported = glexts.hasExtension('GL_ARB_texture_rg') nbits = dtype.itemsize * 8 if rgSupported: if nbits == 8: return gl.GL_RED, gl.GL_R8 else: return gl.GL_RED, gl.GL_R16 # GL_RED does not exist in old OpenGLs - # we have to use luminance instead. else: if nbits == 8: return gl.GL_LUMINANCE, gl.GL_LUMINANCE8 # But GL_LUMINANCE is deprecated in GL 3.x, # and some more recent GL drivers seem to # have trouble displaying GL_LUMINANCE16 # textures - displayting them at what seems # to be a down-sampled (e.g. using a 4 bit # storage format) version of the data. So we # store the data as floating point if we can. elif floatSupported: return gl.GL_LUMINANCE, arbtf.GL_LUMINANCE16F_ARB # Hopefully float textures are supported # on recent GL drivers which don't support # LUMINANCE_16 else: return gl.GL_LUMINANCE, gl.GL_LUMINANCE16 @memoize.memoize def getTextureType(normalise, dtype, nvals): """Figures out the GL data type, and the base/internal texture formats in which the specified data should be stored. This function just figures out the data types which should be used to store the data as a texture. Any necessary data conversion/transformation is performed by the :func:`prepareData` method. The data can be stored as a GL texture as-is if: - it is of type ``uint8`` or ``uint16`` - it is of type ``float32``, *and* this GL environment has support for floating point textures. Support for floating point textures is determined by the :func:`canUseFlotaTextures` function. In all other cases, the data needs to be converted to a supported data type, and potentially normalised, before it can be used as texture data. If floating point textures are available, the data is converted to float32. Otherwise, the data is converted to ``uint16``, and normalised to take the full ``uint16`` data range (``0-65535``), and a transformation matrix is saved, allowing transformation of this normalised data back to its original data range. .. note:: OpenGL does different things to texture data depending on its type: unsigned integer types are normalised from ``[0, INT_MAX]`` to ``[0, 1]``. Floating point texture data types are, by default, *clamped* (not normalised), to the range ``[0, 1]``! We can overcome by this by using a true floating point texture, which is accomplished by using one of the data types provided by the ``ARB_texture_float`` extension. If this extension is not available, we have no choice but to normalise the data. :arg normalise: Whether the data is to be normalised or not :arg dtype: The original data type (e.g. ``np.uint8``) :arg nvals: Number of values per voxel. Must be either ``1``, ``3``, or ``4``. :returns: A tuple containing: - The raw type of the texture data (e.g. ``GL_UNSIGNED_SHORT``) - The texture format (e.g. ``GL_RGB``, ``GL_LUMINANCE``, etc). - The internal texture format used by OpenGL for storage (e.g. ``GL_RGB16``, ``GL_LUMINANCE8``, etc). """ dtype = _makeInstance(dtype) floatTextures, fBaseFmt, fIntFmt = canUseFloatTextures(nvals) ocBaseFmt, ocIntFmt = oneChannelFormat(dtype) isFloat = issubclass(dtype.type, np.floating) # Signed data types are a pain in the arse. # We have to store them as unsigned, and # apply an offset. # Note: Throughout this function, it is assumed # that if the data type is not supported, # then the normalise flag will have been # set to True. # Data type if normalise: texDtype = gl.GL_UNSIGNED_SHORT elif dtype == np.uint8: texDtype = gl.GL_UNSIGNED_BYTE elif dtype == np.int8: texDtype = gl.GL_UNSIGNED_BYTE elif dtype == np.uint16: texDtype = gl.GL_UNSIGNED_SHORT elif dtype == np.int16: texDtype = gl.GL_UNSIGNED_SHORT elif floatTextures: texDtype = gl.GL_FLOAT # Base texture format if floatTextures and isFloat: baseFmt = fBaseFmt elif nvals == 1: baseFmt = ocBaseFmt elif nvals == 3: baseFmt = gl.GL_RGB elif nvals == 4: baseFmt = gl.GL_RGBA # Internal texture format if nvals == 1: if normalise: intFmt = ocIntFmt elif dtype == np.uint8: intFmt = ocIntFmt elif dtype == np.int8: intFmt = ocIntFmt elif dtype == np.uint16: intFmt = ocIntFmt elif dtype == np.int16: intFmt = ocIntFmt elif floatTextures: intFmt = fIntFmt elif nvals == 3: if normalise: intFmt = gl.GL_RGB16 elif dtype == np.uint8: intFmt = gl.GL_RGB8 elif dtype == np.int8: intFmt = gl.GL_RGB8 elif dtype == np.uint16: intFmt = gl.GL_RGB16 elif dtype == np.int16: intFmt = gl.GL_RGB16 elif floatTextures: intFmt = fIntFmt elif nvals == 4: if normalise: intFmt = gl.GL_RGBA16 elif dtype == np.uint8: intFmt = gl.GL_RGBA8 elif dtype == np.int8: intFmt = gl.GL_RGBA8 elif dtype == np.uint16: intFmt = gl.GL_RGBA16 elif dtype == np.int16: intFmt = gl.GL_RGBA16 elif floatTextures: intFmt = fIntFmt return texDtype, baseFmt, intFmt
[docs]def prepareData(data, prefilter=None, prefilterRange=None, resolution=None, scales=None, normalise=None, normaliseRange=None): """This function 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``. """ dtype = data.dtype floatTextures = canUseFloatTextures() 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 = affine.scaleOffsetXform(scale, offset) invVoxValXform = affine.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) return data, voxValXform, invVoxValXform