diff --git a/fsl/fsleyes/gl/textures/colourmaptexture.py b/fsl/fsleyes/gl/textures/colourmaptexture.py index 55b889633e1b3f1cd9c18806c33907bf8097a003..18a0ba894d71870381bb61a480ee7605a8dc442a 100644 --- a/fsl/fsleyes/gl/textures/colourmaptexture.py +++ b/fsl/fsleyes/gl/textures/colourmaptexture.py @@ -145,14 +145,16 @@ class ColourMapTexture(texture.Texture): def set(self, **kwargs): """Set any parameters on this ``ColourMapTexture``. Valid keyword arguments are: - - - ``cmap`` - - ``invert`` - - ``interp`` - - ``alpha`` - - ``resolution`` - - ``displayRange`` - - ``border`` + + ================ ============================ + ``cmap`` See :meth:`setColourMap`. + ``invert`` See :meth:`setInvert`. + ``interp`` See :meth:`setInterp`. + ``alpha`` See :meth:`setAlpha`. + ``resolution`` See :meth:`setResolution`. + ``displayRange`` See :meth:`setDisplayRange`. + ``border`` See :meth:`setBorder`. + ================ ============================ """ # None is a valid value for any attributes, diff --git a/fsl/fsleyes/gl/textures/imagetexture.py b/fsl/fsleyes/gl/textures/imagetexture.py index 1fd5cab929ea242dd3455079590d62e1a310de9f..0a38f439ece6ffef89caa44ae9cf44a2d0b19614 100644 --- a/fsl/fsleyes/gl/textures/imagetexture.py +++ b/fsl/fsleyes/gl/textures/imagetexture.py @@ -1,9 +1,13 @@ #!/usr/bin/env python # -# imagetexture.py - +# imagetexture.py - The ImageTexture class. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module provides the :class:`ImageTexture` class, a 3D :class:`.Texture` +for storing a :class:`.Image` instance. +""" + import logging @@ -21,18 +25,26 @@ log = logging.getLogger(__name__) class ImageTexture(texture.Texture): - """This class contains the logic required to create and manage a 3D - texture which represents a :class:`~fsl.data.image.Image` instance. - - Once created, the following attributes are available on an - :class:`ImageTexture` object: - - - todo - - - ``voxValXform``: An affine transformation matrix which encodes an - offset and scale, for transforming from the - texture values [0.0, 1.0] to the actual data values. - - ``invVoxValXform``: Inverted version of the ``voxValXform`` matrix. + """The ``ImageTexture`` class contains the logic required to create and + manage a 3D texture which represents a :class:`.Image` instance. + + Once created, the :class:`.Image` instance is available as an attribute + of an :class:`ImageTexture` object, called `image``. Additionally, a + number of other attributes are added by the :meth:`__determineTextureType` + method - see its documentation for more details. + + A number of texture settings can be configured through the following + methods: + + .. autosummary:: + :nosignatures: + + set + setInterp + setPrefilter + setResolution + setVolume + setNormalise """ def __init__(self, @@ -42,22 +54,25 @@ class ImageTexture(texture.Texture): normalise=False, prefilter=None, interp=gl.GL_NEAREST): - """Create an :class:`ImageTexture`. + """Create an :class:`ImageTexture`. A listener is added to the + :attr:`.Image.data` property, so that the texture data can be + refreshed whenever the image data changes - see the + :meth:`__imageDataChanged` method. - :arg name: A name for the texture. + :arg name: A unique name for the texture. - :arg image: The :class:`~fsl.data.image.Image` instance. + :arg image: The :class:`.Image` instance. :arg nvals: Number of values per voxel. For example. a normal MRI or fMRI image contains only one value for each voxel. However, DTI data contains three values per voxel. :arg normalise: If ``True``, the image data is normalised to lie in the - range [0.0, 1.0]. + range ``[0.0, 1.0]``. :arg prefilter: An optional function which may perform any pre-processing on the data before it is copied to the - GPU - see the :meth:`_prepareTextureData` method. + GPU - see the :meth:`__prepareTextureData` method. """ texture.Texture.__init__(self, name, 3) @@ -71,7 +86,7 @@ class ImageTexture(texture.Texture): 'image shape {}'.format(nvals, image.shape)) self.image = image - self.nvals = nvals + self.__nvals = nvals self.__interp = None self.__resolution = None self.__volume = None @@ -98,27 +113,62 @@ class ImageTexture(texture.Texture): def destroy(self): - """Deletes the texture identifier """ + """Must be called when this ``ImageTexture`` is no longer needed. + Deletes the texture handle, and removes the listener on the + :attr:`.Image.data` property. + """ texture.Texture.destroy(self) self.image.removeListener('data', self.__name) - def __imageDataChanged(self, refresh=True): + def setInterp(self, interp): + """Sets the texture interpolation - either ``GL_NEAREST`` or + ``GL_LINEAR``. + """ + self.set(interp=interp) - data = self.image.data + + def setPrefilter(self, prefilter): + """Sets the prefilter function - see :meth:`__init__`. """ + self.set(prefilter=prefilter) - if self.__prefilter is not None: - data = self.__prefilter(data) - self.__dataMin = float(data.min()) - self.__dataMax = float(data.max()) + def setResolution(self, resolution): + """Sets the image texture resolution - this value is passed to the + :func:`.routines.subsample` function, in the + :meth:`__prepareTextureData` method. + """ + self.set(resolution=resolution) - if refresh: - self.refresh() + + def setVolume(self, volume): + """For 4D :class:`.Image` instances, specifies the volume to use + as the 3D texture data. + """ + self.set(volume=volume) + + + def setNormalise(self, normalise): + """Enable/disable normalisation - if ``True``, the image data is + normalised to lie in the range ``[0, 1]`` before being stored as + a texture. + """ + self.set(normalise=normalise) def set(self, **kwargs): + """Set any parameters on this ``ImageTexture``. Valid keyword + arguments are: + + ============== ========================== + ``interp`` See :meth:`setInterp`. + ``prefilter`` See :meth:`setPrefilter`. + ``resolution`` See :meth:`setResolution`. + ``volume`` See :meth:`setVolume`. + ``normalise`` See :meth:`setNormalise`. + ============== ========================== + """ interp = kwargs.get('interp', self.__interp) prefilter = kwargs.get('prefilter', self.__prefilter) resolution = kwargs.get('resolution', self.__resolution) @@ -155,61 +205,147 @@ class ImageTexture(texture.Texture): self.refresh() + + def refresh(self, *a): + """(Re-)generates the OpenGL texture used to store the image data. + """ + + self.__determineTextureType() + data = self.__prepareTextureData() + + # It is assumed that, for textures with more than one + # value per voxel (e.g. RGB textures), the data is + # arranged accordingly, i.e. with the voxel value + # dimension the fastest changing + if len(data.shape) == 4: self.textureShape = data.shape[1:] + else: self.textureShape = data.shape + + log.debug('Refreshing 3D texture (id {}) for ' + '{} (data shape: {})'.format( + self.getTextureHandle(), + self.getTextureName(), + self.textureShape)) + + # The image data is flattened, with fortran dimension + # ordering, so the data, as stored on the GPU, has its + # first dimension as the fastest changing. + data = data.ravel(order='F') + + # Enable storage of tightly packed data of any size (i.e. + # our texture shape does not have to be divisible by 4). + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) + + self.bindTexture() + + # set interpolation routine + gl.glTexParameteri(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_MAG_FILTER, + self.__interp) + gl.glTexParameteri(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_MIN_FILTER, + self.__interp) + + # Clamp texture borders to the edge + # values - it is the responsibility + # of the rendering logic to not draw + # anything outside of the image space + gl.glTexParameteri(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_WRAP_S, + gl.GL_CLAMP_TO_EDGE) + gl.glTexParameteri(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_WRAP_T, + gl.GL_CLAMP_TO_EDGE) + gl.glTexParameteri(gl.GL_TEXTURE_3D, + gl.GL_TEXTURE_WRAP_R, + gl.GL_CLAMP_TO_EDGE) + + # create the texture according to + # the format determined by the + # _determineTextureType method. + gl.glTexImage3D(gl.GL_TEXTURE_3D, + 0, + self.texIntFmt, + self.textureShape[0], + self.textureShape[1], + self.textureShape[2], + 0, + self.texFmt, + self.texDtype, + data) + + self.unbindTexture() + + + def __imageDataChanged(self, refresh=True): + """Called when the :attr:`.Image.data` property changes. Refreshes + the texture data accordingly. + """ + + data = self.image.data + + if self.__prefilter is not None: + data = self.__prefilter(data) + + self.__dataMin = float(data.min()) + self.__dataMax = float(data.max()) - def setInterp( self, interp): self.set(interp=interp) - def setPrefilter( self, prefilter): self.set(prefilter=prefilter) - def setResolution(self, resolution): self.set(resolution=resolution) - def setVolume( self, volume): self.set(volume=volume) - def setNormalise( self, normalise): self.set(normalise=normalise) + if refresh: + self.refresh() - def _determineTextureType(self): + def __determineTextureType(self): """Figures out how the image data should be stored as an OpenGL 3D texture. + Regardless of its native data type, the image data is stored in an - unsigned integer format. This method figures out the best data type - to use - if the data is already in an unsigned integer format, it - may be used as-is. Otherwise, the data needs to be cast and - potentially normalised before it can be used as texture data. + unsigned integer format. This method figures out the best data type to + use - if the data is already in an unsigned integer format, it may be + used as-is. Otherwise, the data needs to be cast and potentially + normalised before it can be used as texture data. + Internally (e.g. in GLSL shader code), the GPU automatically - normalises texture data to the range [0.0, 1.0]. This method therefore - calculates an appropriate transformation matrix which may be used to - transform these normalised values back to the raw data values. + normalises texture data to the range ``[0.0, 1.0]``. This method + therefore calculates an appropriate transformation matrix which may be + used to transform these normalised values back to the raw data values. + + + .. note:: OpenGL does different things to 3D 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]``! This could be + overcome by using a more recent versions of OpenGL, or by + using the ``ARB.texture_rg.GL_R32``F data format. Here, we + simply cast floating point data to an unsigned integer type, + normalise it to the appropriate range, and calculate a + transformation matrix to transform back to the data range. - .. note:: - OpenGL does different things to 3D 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]! This could be - overcome by using a more recent versions of OpenGL, or by using - the ARB.texture_rg.GL_R32F data format. Here, we simply cast - floating point data to an unsigned integer type, normalise it - to the appropriate range, and calculate a transformation matrix - to transform back to the data range. - - This method sets the following attributes on thius ``ImageTexture`` + This method sets the following attributes on this ``ImageTexture`` instance: - - ``texFmt``: The texture format (e.g. ``GL_RGB``, - ``GL_LUMINANCE``) + ================== ============================================== + ``texFmt`` The texture format (e.g. ``GL_RGB``, + ``GL_LUMINANCE``, etc). - - ``texIntFmt``: The internal texture format used by OpenGL for - storage (e.g. ``GL_RGB16``, ``GL_LUMINANCE8``). + ``texIntFmt`` The internal texture format used by OpenGL for + storage (e.g. ``GL_RGB16``, ``GL_LUMINANCE8``, + etc). - - ``texDtype``: The raw type of the texture data (e.g. - ``GL_UNSIGNED_SHORT``) + ``texDtype`` The raw type of the texture data (e.g. + ``GL_UNSIGNED_SHORT``) - - ``voxValXform``: 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 original range. + ``voxValXform`` 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. - - ``invVoxValXform``: Inverse of ``voxValXform``. + ``invVoxValXform`` Inverse of ``voxValXform``. + ================== ============================================== """ data = self.image.data @@ -235,17 +371,17 @@ class ImageTexture(texture.Texture): elif dtype == np.int16: texDtype = gl.GL_UNSIGNED_SHORT # The texture format - if self.nvals == 1: texFmt = gl.GL_LUMINANCE - elif self.nvals == 2: texFmt = gl.GL_LUMINANCE_ALPHA - elif self.nvals == 3: texFmt = gl.GL_RGB - elif self.nvals == 4: texFmt = gl.GL_RGBA + if self.__nvals == 1: texFmt = gl.GL_LUMINANCE + elif self.__nvals == 2: texFmt = gl.GL_LUMINANCE_ALPHA + elif self.__nvals == 3: texFmt = gl.GL_RGB + elif self.__nvals == 4: texFmt = gl.GL_RGBA else: raise ValueError('Cannot create texture representation ' 'for {} (nvals: {})'.format(self.tag, - self.nvals)) + self.__nvals)) # Internal texture format - if self.nvals == 1: + if self.__nvals == 1: if self.__normalise: intFmt = gl.GL_LUMINANCE16 elif dtype == np.uint8: intFmt = gl.GL_LUMINANCE8 @@ -253,21 +389,21 @@ class ImageTexture(texture.Texture): elif dtype == np.uint16: intFmt = gl.GL_LUMINANCE16 elif dtype == np.int16: intFmt = gl.GL_LUMINANCE16 - elif self.nvals == 2: + elif self.__nvals == 2: if self.__normalise: intFmt = gl.GL_LUMINANCE16_ALPHA16 elif dtype == np.uint8: intFmt = gl.GL_LUMINANCE8_ALPHA8 elif dtype == np.int8: intFmt = gl.GL_LUMINANCE8_ALPHA8 elif dtype == np.uint16: intFmt = gl.GL_LUMINANCE16_ALPHA16 elif dtype == np.int16: intFmt = gl.GL_LUMINANCE16_ALPHA16 - elif self.nvals == 3: + elif self.__nvals == 3: if self.__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 self.nvals == 4: + elif self.__nvals == 4: if self.__normalise: intFmt = gl.GL_RGBA16 elif dtype == np.uint8: intFmt = gl.GL_RGBA8 elif dtype == np.int8: intFmt = gl.GL_RGBA8 @@ -291,6 +427,7 @@ class ImageTexture(texture.Texture): voxValXform = transform.scaleOffsetXform(scale, offset) + # This is all just for logging purposes if log.getEffectiveLevel() == logging.DEBUG: if texDtype == gl.GL_UNSIGNED_BYTE: @@ -341,7 +478,7 @@ class ImageTexture(texture.Texture): self.invVoxValXform = transform.invert(voxValXform) - def _prepareTextureData(self): + def __prepareTextureData(self): """This method prepares and returns the image data, ready to be used as GL texture data. @@ -372,7 +509,7 @@ class ImageTexture(texture.Texture): if volume is None: volume = 0 - if image.is4DImage() and self.nvals == 1: + if image.is4DImage() and self.__nvals == 1: data = data[..., volume] if resolution is not None: @@ -395,75 +532,3 @@ class ImageTexture(texture.Texture): elif dtype == np.int16: data = np.array(data + 32768, dtype=np.uint16) return data - - - def refresh(self, *a): - """(Re-)generates the OpenGL image texture used to store the image - data. - """ - - self._determineTextureType() - data = self._prepareTextureData() - - # It is assumed that, for textures with more than one - # value per voxel (e.g. RGB textures), the data is - # arranged accordingly, i.e. with the voxel value - # dimension the fastest changing - if len(data.shape) == 4: self.textureShape = data.shape[1:] - else: self.textureShape = data.shape - - log.debug('Refreshing 3D texture (id {}) for ' - '{} (data shape: {})'.format( - self.getTextureHandle(), - self.getTextureName(), - self.textureShape)) - - # The image data is flattened, with fortran dimension - # ordering, so the data, as stored on the GPU, has its - # first dimension as the fastest changing. - data = data.ravel(order='F') - - # Enable storage of tightly packed data of any size (i.e. - # our texture shape does not have to be divisible by 4). - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - - self.bindTexture() - - # set interpolation routine - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_MAG_FILTER, - self.__interp) - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_MIN_FILTER, - self.__interp) - - # Clamp texture borders to the edge - # values - it is the responsibility - # of the rendering logic to not draw - # anything outside of the image space - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_WRAP_S, - gl.GL_CLAMP_TO_EDGE) - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_WRAP_T, - gl.GL_CLAMP_TO_EDGE) - gl.glTexParameteri(gl.GL_TEXTURE_3D, - gl.GL_TEXTURE_WRAP_R, - gl.GL_CLAMP_TO_EDGE) - - # create the texture according to - # the format determined by the - # _determineTextureType method. - gl.glTexImage3D(gl.GL_TEXTURE_3D, - 0, - self.texIntFmt, - self.textureShape[0], - self.textureShape[1], - self.textureShape[2], - 0, - self.texFmt, - self.texDtype, - data) - - self.unbindTexture() diff --git a/fsl/fsleyes/gl/textures/selectiontexture.py b/fsl/fsleyes/gl/textures/selectiontexture.py index a4d20a0f09b66a117cab9da0c0b9b1ff94ca3bb6..8a3d761f0c010d2345bc2fb69a3bbd1f00626dd4 100644 --- a/fsl/fsleyes/gl/textures/selectiontexture.py +++ b/fsl/fsleyes/gl/textures/selectiontexture.py @@ -1,9 +1,16 @@ #!/usr/bin/env python # -# selectiontexture.py - see fsl.fsleyes.editor.selection.Selection +# selectiontexture.py - The SelectionTexture class. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # +"""This module provides the :class:`SelectionTexture` class, a +:class:`.Texture` type which can be used to store :class:`.Selection` +instances. + +The :class:`SelectionTexture` class is used by the :class:`.VoxelSelection` +annotation. +""" import logging @@ -17,20 +24,34 @@ log = logging.getLogger(__name__) class SelectionTexture(texture.Texture): + """The ``SelectionTexture`` class is a :class:`.Texture` which can be used + to represent a :class:`.Selection` instance. The ``Selection`` image array + is stored as a single channel 3D texture, which is updated whenever the + :attr:`.Selection.selection` property changes, and whenever the + :meth:`refresh` method is called. + """ + def __init__(self, name, selection): + """Create a ``SelectionTexture``. + + :arg name: A unique name for this ``SelectionTexture``. + + :arg selection: The :class:`.Selection` instance. + """ texture.Texture.__init__(self, name, 3) - self.selection = selection + self.__selection = selection - selection.addListener('selection', name, self._selectionChanged) + selection.addListener('selection', name, self.__selectionChanged) - self._init() + self.__init() self.refresh() - def _init(self): + def __init(self): + """Called by :meth:`__init__`. Configures the GL texture. """ self.bindTexture() @@ -55,7 +76,7 @@ class SelectionTexture(texture.Texture): gl.GL_TEXTURE_WRAP_R, gl.GL_CLAMP_TO_BORDER) - shape = self.selection.selection.shape + shape = self.__selection.selection.shape gl.glTexImage3D(gl.GL_TEXTURE_3D, 0, gl.GL_ALPHA8, @@ -69,11 +90,35 @@ class SelectionTexture(texture.Texture): self.unbindTexture() + + def destroy(self): + """Must be called when this ``SelectionTexture`` is no longer needed. + Calls the :meth:`.Texture.destroy` method, and removes the listener + on the :attr:`.Selection.selection` property. + """ + texture.Texture.destroy(self) + self.__selection.removeListener('selection', self.getTextureName()) + self.__selection = None + def refresh(self, block=None, offset=None): + """Refreshes the texture data from the :class:`.Selection` image + data. + + If ``block`` and ``offset`` are not provided, the entire texture is + refreshed from the :class:`.Selection` instance. If you know that only + part of the selection data has changed, you can use the ``block`` and + ``offset`` arguments to refresh a specific region of the texture + (which will be faster than a full : refresh). + + :arg block: A 3D ``numpy`` array containing the new selection data. + + :arg offset: A tuple specifying the ``(x, y, z)`` offset of the + ``block`` into the selection array. + """ if block is None or offset is None: - data = self.selection.selection + data = self.__selection.selection offset = [0, 0, 0] else: data = block @@ -98,12 +143,15 @@ class SelectionTexture(texture.Texture): self.unbindTexture() - def _selectionChanged(self, *a): + def __selectionChanged(self, *a): + """Called when the :attr:`.Selection.selection` changes. Updates + the texture data via the :meth:`refresh` method. + """ - old, new, offset = self.selection.getLastChange() + old, new, offset = self.__selection.getLastChange() if old is None or new is None: - data = self.selection.selection + data = self.__selection.selection offset = [0, 0, 0] else: data = new