diff --git a/fsl/fsleyes/colourmaps.py b/fsl/fsleyes/colourmaps.py index 4cae6337aa6f5f7539eaec38fc7c486cbeddf9c3..be1b36f0e1d73759682cded01bca04bde8a153ed 100644 --- a/fsl/fsleyes/colourmaps.py +++ b/fsl/fsleyes/colourmaps.py @@ -965,7 +965,7 @@ class LookupTable(props.HasProperties): """Create a new label with the given value, or updates the colour/name/enabled states associated with the given value. - :arg value: The label value to add/update. + :arg value: The label value to add/update. Must be an integer. :arg name: Label name :arg colour: Label colour :arg enabled: Label enabled state diff --git a/fsl/fsleyes/gl/textures/__init__.py b/fsl/fsleyes/gl/textures/__init__.py index 8ddaf9bc7007e1a1ca035f4efb893cd88a5c6a92..36bba39b054aafa4ddeaa5c35c518562a63ff021 100644 --- a/fsl/fsleyes/gl/textures/__init__.py +++ b/fsl/fsleyes/gl/textures/__init__.py @@ -5,11 +5,28 @@ # Author: Paul McCarthy <pauldmccarthy@gmail.com> # """This package is a container for a collection of classes which use OpenGL -textures for various purposes. +textures for various purposes. -The :mod:`.texture` sub-module contains the definition of the :class:`Texture` -class, the base class for all texture types. +.. todo:: There is a lot of duplicate code in the various texture sub-classes. + This will hopefully be rectified at some stage in the future - + shared code will be moved into the :class:`.Texture` class. + + +The following texture types are defined in this package: + +.. autosummary:: + :nosignatures: + + Texture + Texture2D + ImageTexture + ColourMapTexture + LookupTableTexture + SelectionTexture + RenderTexture + GLObjectRenderTexture + RenderTextureStack """ diff --git a/fsl/fsleyes/gl/textures/colourmaptexture.py b/fsl/fsleyes/gl/textures/colourmaptexture.py index 4c08c26ce67ac410a682beceadf59fb9261667c4..55b889633e1b3f1cd9c18806c33907bf8097a003 100644 --- a/fsl/fsleyes/gl/textures/colourmaptexture.py +++ b/fsl/fsleyes/gl/textures/colourmaptexture.py @@ -1,14 +1,17 @@ #!/usr/bin/env python # -# colourmaptexture.py - +# colourmaptexture.py - The ColourMapTexture class. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module provides the :class:`ColourMapTexture` class, a 1D +:class:`.Texture` which can be used to store a RGBA colour map. +""" + import logging import collections - import numpy as np import OpenGL.GL as gl @@ -19,9 +22,51 @@ log = logging.getLogger(__name__) class ColourMapTexture(texture.Texture): + """The ``ColourMapTexture`` class is a :class:`.Texture` which stores + a RGB or RGBA colour map. + + A ``ColourMapTexture`` maps a data range to to a colour map. The data + range may be specified by the :meth:`setDisplayRange` method, and the + colour map by the :meth:`setColourMap` method. Alternately, both can + be specified with the :meth:`set` method. + + + In OpenGL, textures are indexed with a number between 0.0 and 1.0. So + in order to map the data range to texture coordinates, an offset/scale + transformation must be applied to data values. The ``ColourMapTexture`` + calculates this transformation, and makes it available via the + :meth:`getCoordinateTransform` method. + + + The colour map itself can be specified in a number of ways: + + - A ``numpy`` array of size :math:`N\\times 3` or :math:`N\\times 4`, + containing RGB or RGBA colour values, with colour values in the range + ``[0, 1]``. + + - A function which accepts an array of values in the range ``[0, 1]``, + and returns an array of size :math:`N\\times 3` or :math:`N\\times 4`, + specifying the RGB/RGBA colours that correspond to the input values. + + + Some other methods are provided, for configuring the colour map: + + .. autosummary:: + :nosignatures: + + setAlpha + setInvert + setResolution + setInterp + setBorder + """ def __init__(self, name): + """Create a ``ColourMapTexture``. + + :arg name: A unique name for this ``ColourMapTexture``. + """ texture.Texture.__init__(self, name, 1) @@ -35,23 +80,80 @@ class ColourMapTexture(texture.Texture): self.__coordXform = None - # CMAP can be either a function which transforms - # values to RGBA, or a N*4 numpy array containing - # RGBA values - def setColourMap( self, cmap): self.set(cmap=cmap) - def setResolution( self, res): self.set(resolution=res) - def setAlpha( self, alpha): self.set(alpha=alpha) - def setInvert( self, invert): self.set(invert=invert) - def setInterp( self, interp): self.set(interp=interp) - def setDisplayRange(self, drange): self.set(displayRange=drange) - def setBorder( self, border): self.set(border=border) + def setColourMap(self, cmap): + """Set the colour map stored by the ``ColourMapTexture``. + + :arg cmap: The colour map, either a ``numpy`` array of size + :math:`N\\times 3` or :math:`N\\times 4`, specifying + RGB/RGBA colours, or a function which accepts values + in the range ``[0, 1]``, and generates corresponding + RGB/RGBA colours. + """ + self.set(cmap=cmap) + + + def setResolution(self, res): + """Set the resolution (number of colours) of this ``ColourMapTexture``. + This setting is only applicable when the colour map is specified as a + function (see :meth:`setColourMap`). + """ + self.set(resolution=res) + + + def setAlpha(self, alpha): + """Set the transparency of all colours in the colour map. This setting + is only applicable when the colour map is specified as RGB values. + """ + self.set(alpha=alpha) + + + def setInvert(self, invert): + """Invert the values in the colour map. """ + self.set(invert=invert) + + + def setInterp(self, interp): + """Set the interpolation used by this ``ColourMapTexture`` - either + ``GL_NEAREST`` or ``GL_LINEAR``. + """ + self.set(interp=interp) + + + def setDisplayRange(self, drange): + """Set the data range which corresponds to the colours stored in this + ``ColourMapTexture``. A matrix which transforms values from from this + data range into texture coordinates is available via the + :meth:`getCoordinateTransform` method. + """ + self.set(displayRange=drange) + + + def setBorder(self, border): + """Set the texture border colour. If ``None``, the edge colours of the + colour map are used as the border. + """ + self.set(border=border) def getCoordinateTransform(self): + """Returns a matrix which transforms values from from the colour map + data range (see :meth:`setDisplayRange`) into texture coordinates. + """ return self.__coordXform def set(self, **kwargs): + """Set any parameters on this ``ColourMapTexture``. Valid keyword + arguments are: + + - ``cmap`` + - ``invert`` + - ``interp`` + - ``alpha`` + - ``resolution`` + - ``displayRange`` + - ``border`` + """ # None is a valid value for any attributes, # so we are using 'self' to test whether @@ -76,6 +178,14 @@ class ColourMapTexture(texture.Texture): def __prepareTextureSettings(self): + """Called by :meth:`__refresh`. Prepares all of the texture settings, + and returns a tuple containing: + + - An array containing the colour map data + - The display range + - The interpolation setting + - The border colour + """ alpha = self.__alpha cmap = self.__cmap @@ -130,6 +240,9 @@ class ColourMapTexture(texture.Texture): def __refresh(self): + """Called when any settings of this ``ColourMapTexture`` are changed. + Re-configures the texture. + """ cmap, drange, interp, border = self.__prepareTextureSettings() diff --git a/fsl/fsleyes/gl/textures/lookuptabletexture.py b/fsl/fsleyes/gl/textures/lookuptabletexture.py index 9e69ae790290cc94a1cfb81a0c85102224523b37..a826306a499a02d4d56c247ab47f6ce1a98a05c1 100644 --- a/fsl/fsleyes/gl/textures/lookuptabletexture.py +++ b/fsl/fsleyes/gl/textures/lookuptabletexture.py @@ -1,9 +1,13 @@ #!/usr/bin/env python # -# lookuptabletexture.py - +# lookuptabletexture.py - The LookupTableTexture class. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module provides the :class:`LookupTableTexture` class, a 1D +:class:`.Texture` which stores the colours of a :class:`.LookupTable` +as an OpenGL texture. +""" import logging @@ -19,8 +23,29 @@ log = logging.getLogger(__name__) class LookupTableTexture(texture.Texture): + """The ``LookupTableTexture`` class is a 1D :class:`.Texture` which stores + the colours of a :class:`.LookupTable` as an OpenGL texture. + + + A :class:`.LookupTable` stores a collection of label values (assumed to be + unsigned 16 bit integers), and colours associated with each label. This + mapping of ``{label : colour}`` is converted into a ``numpy`` array + of size :math:`max(labels)\\times 3` containing the lookup table, where + a label value can be used as an array index to retrieve the corresponding + colour. All aspects of a ``LookupTableTexture`` can be configured via the + :meth:`set` method. + + + As OpenGL textures are indexed by coordinates in the range ``[0.0, 1.0]``, + you will need to divide label values by :math:`max(labels)` to convert + them into texture coordinates. + """ def __init__(self, name): + """Create a ``LookupTableTexture``. + + :arg name: A uniqe name for this ``LookupTableTexture``. + """ texture.Texture.__init__(self, name, 1) @@ -31,6 +56,19 @@ class LookupTableTexture(texture.Texture): def set(self, **kwargs): + """Set any parameters on this ``ColourMapTexture``. Valid + keyword arguments are: + + ============== ====================================================== + ``lut`` The :class:`.LookupTable` instance. + ``alpha`` Transparency, a value between 0.0 and 1.0. Defaults to + 1.0 + ``brightness`` Brightness, a value between 0.0 and 1.0. Defaults to + 0.5. + ``contrast`` Contrast, a value between 0.0 and 1.0. Defaults to + 0.5. + ============== ====================================================== + """ lut = kwargs.get('lut', self) alpha = kwargs.get('alpha', self) @@ -46,10 +84,15 @@ class LookupTableTexture(texture.Texture): def refresh(self): + """Forces a refresh of this ``LookupTableTexture``. This method should + be called when the :class:`.LookupTable` has changed, so that the + underlying texture is kept consistent with it. + """ self.__refresh() def __refresh(self, *a): + """Configures the underlying OpenGL texture. """ lut = self.__lut alpha = self.__alpha diff --git a/fsl/fsleyes/gl/textures/texture.py b/fsl/fsleyes/gl/textures/texture.py index 0b5485c7ddeadc1a29c50822c8860d8a50cc69a0..86edd2400662087fc776e36d36ccc50be0a71489 100644 --- a/fsl/fsleyes/gl/textures/texture.py +++ b/fsl/fsleyes/gl/textures/texture.py @@ -1,9 +1,12 @@ #!/usr/bin/env python # -# texture.py - +# texture.py - The Texture and Texture2D classes. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module provides the :class:`Texture` and :class:`Texture2D classes, +which are the base classes for all other texture types. +""" import logging @@ -17,12 +20,75 @@ log = logging.getLogger(__name__) class Texture(object): - """All subclasses must accept a ``name`` as the first parameter to their - ``__init__`` method, and must pass said ``name`` through to this - ``__init__`` method. + """The ``Texture`` class is the base class for all other texture types in + *FSLeyes*. This class is not intended to be used directly - use one of the + sub-classes instead. This class provides a few convenience methods for + working with textures: + + .. autosummary:: + :nosignatures: + + getTextureName + getTextureHandle + + + The :meth:`bindTexture` and :meth:`unbindTexture` methods allow you to + bind a texture object to a GL texture unit. For example, let's say we + have a texture object called ``tex``, and we want to use it:: + + import OpenGL.GL as gl + + + # Bind the texture before doing any configuration - + # we don't need to specify a texture unit here. + tex.bindTexture() + + # Use nearest neighbour interpolation + gl.glTexParameteri(gl.GL_TEXTURE_2D + gl.GL_TEXTURE_MIN_FILTER, + gl.GL_NEAREST) + gl.glTexParameteri(gl.GL_TEXTURE_2D + gl.GL_TEXTURE_MAG_FILTER, + gl.GL_NEAREST) + + tex.unbindTexture() + + # ... + + # When we want to use the texture in a + # scene render, we need to bind it to + # a texture unit. + tex.bindTexture(gl.GL_TEXTURE0) + + # ... + # Do the render + # ... + + tex.unbindTexture() + + + .. note:: Despite what is shown in the example above, you shouldn't need + to manually configure texture objects with calls to + ``glTexParameter`` - most things can be performed through + methods of the ``Texture`` sub-classes, for example + :class:`.ImageTexture` and :class:`.Texture2D`. + + + See the :mod:`.resources` module for a method of sharing texture + resources. """ + def __init__(self, name, ndims): + """Create a ``Texture``. + + :arg name: The name of this texture - should be unique. + :arg ndims: Number of dimensions - must be 1, 2 or 3. + + .. note:: All subclasses must accept a ``name`` as the first parameter + to their ``__init__`` method, and must pass said ``name`` + through to the :meth:`__init__` method. + """ self.__texture = gl.glGenTextures(1) self.__name = name @@ -41,15 +107,11 @@ class Texture(object): self.__name, self.__texture)) - def getTextureName(self): - return self.__name - - def getTextureHandle(self): - return self.__texture - - def destroy(self): + """Must be called when this ``Texture`` is no longer needed. Deletes + the texture handle. + """ log.debug('Deleting {} ({}) for {}: {}'.format(type(self).__name__, id(self), @@ -60,7 +122,24 @@ class Texture(object): self.__texture = None + def getTextureName(self): + """Returns the name of this texture. This is not the GL texture name, + rather it is the unique name passed into :meth:`__init__`. + """ + return self.__name + + + def getTextureHandle(self): + """Returns the GL texture handle for this texture. """ + return self.__texture + + def bindTexture(self, textureUnit=None): + """Activates and binds this texture. + + :arg textureUnit: The texture unit to bind this texture to, e.g. + ``GL_TEXTURE0``. + """ if textureUnit is not None: gl.glActiveTexture(textureUnit) @@ -72,6 +151,7 @@ class Texture(object): def unbindTexture(self): + """Unbinds this texture. """ if self.__textureUnit is not None: gl.glActiveTexture(self.__textureUnit) @@ -83,8 +163,26 @@ class Texture(object): class Texture2D(Texture): + """The ``Texture2D` class represents a two-dimensional RGBA texture. A + ``Texture2D`` instance can be used in one of two ways: + + - Setting the texture data via the :meth:`setData` method, and then + drawing it to a scene via :meth:`draw` or :meth:`drawOnBounds`. + + - Setting the texture size via :meth:`setSize`, and then drawing to it + by some other means (see e.g. the :class:`.RenderTexture` class, a + sub-class of ``Texture2D``). + """ def __init__(self, name, interp=gl.GL_NEAREST): + """Create a ``Texture2D` instance. + + :arg name: Unique name for this ``Texture2D``. + + :arg interp: Initial interpolation - ``GL_NEAREST`` or ``GL_LINEAR``. + This can be changed later on via the + :meth:`setInterpolation` method. + """ Texture.__init__(self, name, 2) self.__data = None @@ -96,13 +194,15 @@ class Texture2D(Texture): def setInterpolation(self, interp): + """Change the texture interpolation - valid values are ``GL_NEAREST`` + or ``GL_LINEAR``. + """ self.__interp = interp self.refresh() def setSize(self, width, height): - """ - Sets the width/height for this texture. + """Sets the width/height for this texture. This method also clears the data for this texture, if it has been previously set via the :meth:`setData` method. @@ -129,15 +229,13 @@ class Texture2D(Texture): def getSize(self): - """ - """ + """Return the current ``(width, height)`` of this ``Texture2D``. """ return self.__width, self.__height def setData(self, data): - """ - Sets the data for this texture - the width and height are determined - from data shape (which is assumed to be 4*width*height). + """Sets the data for this texture - the width and height are determined + from data shape, which is assumed to be 4*width*height. """ self.__setSize(data.shape[1], data.shape[2]) @@ -147,6 +245,9 @@ class Texture2D(Texture): def refresh(self): + """Configures this ``Texture2D``. This includes setting up + interpolation, and setting the texture size and data. + """ if any((self.__width is None, self.__height is None, @@ -218,6 +319,15 @@ class Texture2D(Texture): def draw(self, vertices, xform=None): + """Draw the contents of this ``Texture2D`` to a region specified by + the given vertices. + + :arg vertices: A ``numpy`` array of shape ``6 * 3`` specifying the + region, made up of two triangles, to which this + ``Texture2D`` should be rendered. + + :arg xform: A transformation to be applied to the vertices. + """ if vertices.shape != (6, 3): raise ValueError('Six vertices must be provided') @@ -262,6 +372,22 @@ class Texture2D(Texture): def drawOnBounds(self, zpos, xmin, xmax, ymin, ymax, xax, yax, xform=None): + """Draws the contents of this ``Texture2D`` to a rectangle. This is a + convenience method which creates a set of vertices, and passes them to + the :meth:`draw` method. + + :arg zpos: Position along the Z axis, in the display coordinate + system. + :arg xmin: Minimum X axis coordinate. + :arg xmax: Maximum X axis coordinate. + :arg ymin: Minimum Y axis coordinate. + :arg ymax: Maximum Y axis coordinate. + :arg xax: Display space axis which maps to the horizontal screen + axis. + :arg yax: Display space axis which maps to the vertical screen + axis. + :arg xform: Transformation matrix to apply to the vertices. + """ zax = 3 - xax - yax vertices = np.zeros((6, 3), dtype=np.float32)