Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

silx.gui.plot: Refactored PlotWidget OpenGL backend to enable extensions #3147

Merged
merged 4 commits into from
Jul 7, 2020

Conversation

t20100
Copy link
Member

@t20100 t20100 commented Jul 3, 2020

This PR is a small refactoring of the OpenGL backend which adds a GLPlotItem base class to rendering primitives.
Along with PlotWidget.addItem, this allows writing user's defined plot items with their own OpenGL rendering implementation.

@t20100
Copy link
Member Author

t20100 commented Jul 3, 2020

@PiRK you might be interested in this.

@t20100 t20100 changed the title Refactored PlotWidget OpenGL backend to enable extensions silx.gui.plot: Refactored PlotWidget OpenGL backend to enable extensions Jul 3, 2020
@PiRK
Copy link
Member

PiRK commented Jul 3, 2020

Thanks! Maybe I can get rid of my hackgl.py monkey patching.

@t20100
Copy link
Member Author

t20100 commented Jul 3, 2020

Yes, that's the idea, extending rather than patching.

@vallsv
Copy link
Contributor

vallsv commented Jul 3, 2020

When this PR is ready it would be nice to have a small example.

I have played recently with pyqtgraph OpenGL, it was really convenient to patch item rendering.

@t20100
Copy link
Member Author

t20100 commented Jul 3, 2020

This is not intended to be supported API, so I won't add an example for this in silx, but here is a sample code rewriting (well mostly copy/pasting) the Image item using classes from plot3d (for testing reuse) and enabling log-scaled axes (for the fun):

import string

import numpy

from silx.gui import colors, qt
from silx.gui.plot import Plot2D, PlotWidget, items

from silx.gui.plot.backends.BackendOpenGL import BackendOpenGL
from silx.gui.plot.backends.glutils import GLPlotItem
from silx.gui._glutils import gl, Program, Texture
from silx.gui.plot._utils import FLOAT32_MINPOS
from silx.gui.plot.backends.glutils.GLSupport import mat4Translate, mat4Scale
from silx.gui.plot.backends.glutils.GLTexture import Image

from silx.gui.plot3d import items as plot3d_items
from silx.gui.plot3d.scene import function


def _imageExtent(origin, scale, shape):
    xmin, ymin = origin
    xmax = xmin + scale[0] * shape[1]
    if xmin > xmax:
        xmin, xmax = xmax, xmin
    ymax = ymin + scale[1] * shape[0]
    if ymin > ymax:
        ymin, ymax = ymax, ymin
    return xmin, xmax, ymin, ymax


class GLImageBackendItem(GLPlotItem):
    
    _SHADERS = {
        'linear': {
            'vertex': """
    #version 120

    uniform mat4 matrix;
    attribute vec2 texCoords;
    attribute vec2 position;

    varying vec2 coords;

    void main(void) {
        coords = texCoords;
        gl_Position = matrix * vec4(position, 0.0, 1.0);
    }
    """,
            'fragTransform': """
    vec2 textureCoords(void) {
        return coords;
    }
    """},

        'log': {
            'vertex': """
    #version 120

    attribute vec2 position;
    uniform mat4 matrix;
    uniform mat4 matOffset;
    uniform bvec2 isLog;

    varying vec2 coords;

    const float oneOverLog10 = 0.43429448190325176;

    void main(void) {
        vec4 dataPos = matOffset * vec4(position, 0.0, 1.0);
        if (isLog.x) {
            dataPos.x = oneOverLog10 * log(dataPos.x);
        }
        if (isLog.y) {
            dataPos.y = oneOverLog10 * log(dataPos.y);
        }
        coords = dataPos.xy;
        gl_Position = matrix * dataPos;
    }
    """,
            'fragTransform': """
    uniform bvec2 isLog;
    uniform vec2 bounds_oneOverRange;
    uniform vec2 bounds_originOverRange;

    vec2 textureCoords(void) {
        vec2 pos = coords;
        if (isLog.x) {
            pos.x = pow(10., coords.x);
        }
        if (isLog.y) {
            pos.y = pow(10., coords.y);
        }
        return pos * bounds_oneOverRange - bounds_originOverRange;
        // TODO texture coords in range different from [0, 1]
    }
    """},

        'fragment': string.Template("""
    #version 120

    uniform sampler2D data;
    uniform float alpha;

    varying vec2 coords;

    $colormapDecl

    $textureCoordsDecl

    void main(void) {
        float value = texture2D(data, $textureCoordsCall()).r;
        gl_FragColor = $colormapCall(value);
        gl_FragColor.a *= alpha;
    }
    """)
    }

    _DATA_TEX_UNIT = 0
    _CMAP_TEX_UNIT = 1

    _INTERNAL_FORMATS = {
        numpy.dtype(numpy.float32): gl.GL_R32F,
        # Use normalized integer for unsigned int formats
        numpy.dtype(numpy.uint16): gl.GL_R16,
        numpy.dtype(numpy.uint8): gl.GL_R8,
    }

    SUPPORTED_NORMALIZATIONS = 'linear', 'log', 'sqrt', 'gamma', 'arcsinh'

    def __init__(self, data, origin, scale, colormap, alpha):
        """Create a 2D colormap

        :param data: The 2D scalar data array to display
        :type data: numpy.ndarray with 2 dimensions (dtype=numpy.float32)
        :param origin: (x, y) coordinates of the origin of the data array
        :type origin: 2-tuple of floats.
        :param scale: (sx, sy) scale factors of the data array.
                      This is the size of a data pixel in plot data space.
        :type scale: 2-tuple of floats.
        :param colormap: Colormap OpenGL function
        :param float alpha: Opacity from 0 (transparent) to 1 (opaque)
        """
        assert data.dtype in self._INTERNAL_FORMATS

        super().__init__()
        self.data = data
        assert len(origin) == 2
        self.origin = tuple(origin)
        assert len(scale) == 2
        self.scale = tuple(scale)

        self.colormap = colormap
        self._alpha = numpy.clip(alpha, 0., 1.)

        self._texture = None
        self._textureIsDirty = False

    def discard(self):
        if self.colormap._texture is not None:  # TODO missing discard in function.Colormap
            self.colormap._texture.discard()
            self.colormap._texture = None

        if self._texture is not None:
            self._texture.discard()
            self._texture = None
        self._textureIsDirty = False

    @property
    def alpha(self):
        return self._alpha

    def updateData(self, data):
        assert data.dtype in self._INTERNAL_FORMATS
        oldData = self.data
        self.data = data

        if self._texture is not None:
            if (self.data.shape != oldData.shape or
                    self.data.dtype != oldData.dtype):
                self.discard()
            else:
                self._textureIsDirty = True

    def prepare(self):
        if self._texture is None:
            internalFormat = self._INTERNAL_FORMATS[self.data.dtype]

            self._texture = Image(internalFormat,
                                  self.data,
                                  format_=gl.GL_RED,
                                  texUnit=self._DATA_TEX_UNIT)
        elif self._textureIsDirty:
            self._textureIsDirty = True
            self._texture.updateAll(format_=gl.GL_RED, data=self.data)

    def __program(self, scale):
        """Returns shader program for scale
        
        :param str scale: Axes scale: 'linear' or 'log'
        :rtype: Program
        """
        return Program(
            self._SHADERS[scale]['vertex'],
            self._SHADERS['fragment'].substitute(
                colormapDecl=self.colormap.decl,
                colormapCall=self.colormap.call,
                textureCoordsDecl=self._SHADERS[scale]['fragTransform'],
                textureCoordsCall='textureCoords'),
            attrib0='position')

    def _renderLinear(self, matrix):
        self.prepare()

        prog = self.__program('linear')
        prog.use()
        self.colormap.setupProgram(None, prog)

        gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)

        mat = numpy.dot(numpy.dot(matrix,
                                  mat4Translate(*self.origin)),
                        mat4Scale(*self.scale))
        gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
                              mat.astype(numpy.float32))

        gl.glUniform1f(prog.uniforms['alpha'], self.alpha)

        self._texture.render(prog.attributes['position'],
                             prog.attributes['texCoords'],
                             self._DATA_TEX_UNIT)

    def _renderLog10(self, matrix, isXLog, isYLog):
        xmin, _, ymin, _ = self.__extent()
        if ((isXLog and xmin < FLOAT32_MINPOS) or
                (isYLog and ymin < FLOAT32_MINPOS)):
            # Do not render images that are partly or totally <= 0
            return

        self.prepare()

        prog = self.__program('log')
        prog.use()
        self.colormap.setupProgram(None, prog)

        ox, oy = self.origin

        gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)

        gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
                              matrix.astype(numpy.float32))
        mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale))
        gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE,
                              mat.astype(numpy.float32))

        gl.glUniform2i(prog.uniforms['isLog'], isXLog, isYLog)

        ex = ox + self.scale[0] * self.data.shape[1]
        ey = oy + self.scale[1] * self.data.shape[0]

        xOneOverRange = 1. / (ex - ox)
        yOneOverRange = 1. / (ey - oy)
        gl.glUniform2f(prog.uniforms['bounds_originOverRange'],
                       ox * xOneOverRange, oy * yOneOverRange)
        gl.glUniform2f(prog.uniforms['bounds_oneOverRange'],
                       xOneOverRange, yOneOverRange)

        gl.glUniform1f(prog.uniforms['alpha'], self.alpha)

        try:
            tiles = self._texture.tiles
        except AttributeError:
            raise RuntimeError("No texture, discard has already been called")
        if len(tiles) > 1:
            raise NotImplementedError(
                "Image over multiple textures not supported with log scale")

        texture, vertices, info = tiles[0]

        texture.bind(self._DATA_TEX_UNIT)

        posAttrib = prog.attributes['position']
        stride = vertices.shape[-1] * vertices.itemsize
        gl.glEnableVertexAttribArray(posAttrib)
        gl.glVertexAttribPointer(posAttrib,
                                 2,
                                 gl.GL_FLOAT,
                                 gl.GL_FALSE,
                                 stride, vertices)

        gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))

    def render(self, matrix, isXLog, isYLog):
        if any((isXLog, isYLog)):
            self._renderLog10(matrix, isXLog, isYLog)
        else:
            self._renderLinear(matrix)

    def pick(self, x, y):
        xmin, xmax, ymin, ymax = self.__extent()
        if xmin <= x <= xmax and ymin <= y <= ymax:
            ox, oy = self.origin
            sx, sy = self.scale
            col = int((x - ox) / sx)
            row = int((y - oy) / sy)
            return (row,), (col,)
        else:
            return None

    def __extent(self):
        return _imageExtent(self.origin, self.scale, self.data.shape)


class GLImage(items.ImageBase, plot3d_items.ColormapMixIn):

    def __init__(self):
        items.ImageBase.__init__(self)
        plot3d_items.ColormapMixIn.__init__(self, function.Colormap())
        self._data = numpy.zeros((0, 0), dtype=numpy.float32)

    def _setPlot(self, plot):
        assert plot is None or isinstance(plot._backend, BackendOpenGL)
        super()._setPlot(plot)

    def _addBackendRenderer(self, backend):
        """Update backend renderer"""
        data = self.getData(copy=False)
        if data.size == 0:
            return None  # No data to display
        else:
            return GLImageBackendItem(
                data,
                origin=self.getOrigin(),
                scale=self.getScale(),
                colormap=self._getSceneColormap(),
                alpha=self.getAlpha())

    def _getBounds(self):
        if self.getData(copy=False).size == 0:  # Empty data
            return None

        xmin, xmax, ymin, ymax = _imageExtent(
            self.getOrigin(), self.getScale(), self.getData(copy=False).shape)

        plot = self.getPlot()
        if plot is not None and self._isPlotLinear(plot) and (xmin <= 0 or ymin <= 0):
            return None  # No display
        else:
            return xmin, xmax, ymin, ymax

    def getRgbaImageData(self, copy=True):  # TODO remove?
        """Get the displayed RGB(A) image

        :returns: Array of uint8 of shape (height, width, 4)
        :rtype: numpy.ndarray
        """
        return self.getColormap().applyToData(self)

    def setData(self, data, copy=True):
        """"Set the image data

        :param numpy.ndarray data: Data array with 2 dimensions (h, w)
        :param bool copy: True (Default) to get a copy,
                          False to use internal representation (do not modify!)
        """
        data = numpy.array(data, copy=copy)
        assert data.ndim == 2
        if data.dtype.kind == 'b':
            _logger.warning(
                'Converting boolean image to int8 to plot it.')
            data = numpy.array(data, copy=False, dtype=numpy.int8)
        elif numpy.iscomplexobj(data):
            _logger.warning(
                'Converting complex image to absolute value to plot it.')
            data = numpy.absolute(data)
        self._data = data
        self._setColormappedData(data, copy=False)

        # TODO hackish data range implementation
        if self.isVisible():
            plot = self.getPlot()
            if plot is not None:
                plot._invalidateDataRange()

        self._updated(items.ItemChangedType.DATA)


if __name__ == '__main__':
    app = qt.QApplication([])

    plot = Plot2D(backend='gl')
    # Log display working, but issue with reset zoom
    plot.getXAxis().setScale(items.Axis.LOGARITHMIC)
    plot.getYAxis().setScale(items.Axis.LOGARITHMIC)
    image = GLImage()
    image.setColormap(plot.getDefaultColormap())
    image.getColormap().setName('viridis')
    image.setOrigin((1., 1.))
    image.setData(numpy.random.random((20, 10)).astype(numpy.float32))
    plot.addItem(image)
    plot.resetZoom()
    plot.show()
    app.exec_()

@vallsv
Copy link
Contributor

vallsv commented Jul 3, 2020

Ok, i see, that's still really deep inside.

@t20100
Copy link
Member Author

t20100 commented Jul 3, 2020

Yes it gives a way to control OpenGL yourself without being restricted to what the backend provides... but it also mean you have to take care of it all.

@vallsv vallsv merged commit b0c4c16 into silx-kit:master Jul 7, 2020
@t20100 t20100 deleted the refactor-plot-opengl-backend branch July 22, 2020 08:30
@PiRK
Copy link
Member

PiRK commented Jul 22, 2020

Here is another example, useful for RGB tiff images that store somewhat independant data in each channel (e. g. biomarker intensity in epifluorescence microscopy).

It defines an RGB(A) image item with methods to show/hide individual color channels and modify luminosity and contrast for each individual channel. This methods works smoothly with 2 GB images, much smoother than adjusting the colormap limits for a single channel displayed as an ImageData item.


import numpy

from silx.gui.plot.backends.glutils import GLPlotItem
from silx.gui._glutils import Program, gl
from silx.gui.plot.backends.glutils.GLTexture import Image
from silx.gui.plot import items
from silx.gui.plot.items.image import _convertImageToRgba32
from silx.gui.plot.items.core import ItemChangedType
from silx.gui.plot.backends.glutils.GLSupport import mat4Translate, mat4Scale


class GLPlotRGBAImageCustom(GLPlotItem):
    _SHADERS = {
        'linear': {
            'vertex': """
        #version 120

        attribute vec2 position;
        attribute vec2 texCoords;
        uniform mat4 matrix;

        varying vec2 coords;

        void main(void) {
            gl_Position = matrix * vec4(position, 0.0, 1.0);
            coords = texCoords;
        }
        """,

            'fragment': """
        #version 120

        uniform sampler2D tex;
        uniform float alpha;
        uniform float rmin;
        uniform float gmin;
        uniform float bmin;
        uniform float rmax;
        uniform float gmax;
        uniform float bmax;
        uniform int rflag;
        uniform int gflag;
        uniform int bflag;

        varying vec2 coords;

        float truncate(in float value) {
            if(value > 1.) {
                return(1.);
            }
            else if(value < 0.) {
                return(0.);
            }
            else {
                return(value);
            }
        }

        float adjust_color_to_limits(in float value, in float min_, in float max_) {
            return(truncate((value - min_) / (max_ - min_)));
        }

        float apply_operations(in float value, in int mute_flag, in float min_, in float max_) {
            if(mute_flag == 0) {
                return(0.);
            }
            else {
                return(adjust_color_to_limits(value, min_, max_));
            }
        }

        void main(void) {
            gl_FragColor = texture2D(tex, coords);
            gl_FragColor.a *= alpha;
            gl_FragColor.r = apply_operations(gl_FragColor.r, rflag, rmin, rmax);
            gl_FragColor.g = apply_operations(gl_FragColor.g, gflag, gmin, gmax);
            gl_FragColor.b = apply_operations(gl_FragColor.b, bflag, bmin, bmax);
        }
        """
            },

        'log': {    # not used in MoreHisto
            'vertex': """
        #version 120

        attribute vec2 position;
        uniform mat4 matrix;
        uniform mat4 matOffset;
        uniform bvec2 isLog;

        varying vec2 coords;

        const float oneOverLog10 = 0.43429448190325176;

        void main(void) {
            vec4 dataPos = matOffset * vec4(position, 0.0, 1.0);
            if (isLog.x) {
                dataPos.x = oneOverLog10 * log(dataPos.x);
            }
            if (isLog.y) {
                dataPos.y = oneOverLog10 * log(dataPos.y);
            }
            coords = dataPos.xy;
            gl_Position = matrix * dataPos;
        }
        """,
            'fragment': """
        #version 120

        uniform sampler2D tex;
        uniform bvec2 isLog;
        uniform vec2 bounds_oneOverRange;
        uniform vec2 bounds_originOverRange;
        uniform float alpha;

        varying vec2 coords;

        vec2 textureCoords(void) {
            vec2 pos = coords;
            if (isLog.x) {
                pos.x = pow(10., coords.x);
            }
            if (isLog.y) {
                pos.y = pow(10., coords.y);
            }
            return pos * bounds_oneOverRange - bounds_originOverRange;
            // TODO texture coords in range different from [0, 1]
        }

        void main(void) {
            gl_FragColor = texture2D(tex, textureCoords());
            gl_FragColor.a *= alpha;
        }
        """}
        }

    _DATA_TEX_UNIT = 0

    _SUPPORTED_DTYPES = (numpy.dtype(numpy.float32),
                         numpy.dtype(numpy.uint8),
                         numpy.dtype(numpy.uint16))

    _linearProgram = Program(_SHADERS['linear']['vertex'],
                             _SHADERS['linear']['fragment'],
                             attrib0='position')

    _logProgram = Program(_SHADERS['log']['vertex'],
                          _SHADERS['log']['fragment'],
                          attrib0='position')

    def __init__(self, data, origin, scale, alpha,
                 rflag=1, gflag=1, bflag=1,
                 rmin=0., gmin=0., bmin=0.,
                 rmax=1., gmax=1., bmax=1.):
        """Create a 2D RGB(A) image from data

        :param data: The 2D image data array to display
        :type data: numpy.ndarray with 3 dimensions
                    (dtype=numpy.uint8 or numpy.float32)
        :param origin: (x, y) coordinates of the origin of the data array
        :type origin: 2-tuple of floats.
        :param scale: (sx, sy) scale factors of the data array.
                      This is the size of a data pixel in plot data space.
        :type scale: 2-tuple of floats.
        :param float alpha: Opacity from 0 (transparent) to 1 (opaque)
        """
        assert data.dtype in self._SUPPORTED_DTYPES
        super().__init__()
        self.data = data
        assert len(origin) == 2
        self.origin = tuple(origin)
        assert len(scale) == 2
        self.scale = tuple(scale)

        self._texture = None
        self._textureIsDirty = False
        self._alpha = numpy.clip(alpha, 0., 1.)

        self.rflag: int = rflag
        """Integer flag, with value 0 (hide red channel)
        or 1 (show red channel). """
        self.gflag: int = gflag
        self.bflag: int = bflag
        self.rmin: float = rmin
        """Float in range 0 -- 1. 
        The range rmin -- rmax will be scaled to cover the range 0 -- 1.
        Pixels with red values below rmin will be set to 0, pixels with
        value above rmax will be set to 1."""
        self.rmax: float = rmax
        self.gmin: float = gmin
        self.gmax: float = gmax
        self.bmin: float = bmin
        self.bmax: float = bmax

    @property
    def alpha(self):
        return self._alpha

    def discard(self):
        if self._texture is not None:
            self._texture.discard()
            self._texture = None
        self._textureIsDirty = False

    def updateData(self, data):
        assert data.dtype in self._SUPPORTED_DTYPES
        oldData = self.data
        self.data = data

        if self._texture is not None:
            if self.data.shape != oldData.shape:
                self.discard()
            else:
                self._textureIsDirty = True

    def prepare(self):
        if self._texture is None:
            formatName = 'GL_RGBA' if self.data.shape[2] == 4 else 'GL_RGB'
            format_ = getattr(gl, formatName)

            if self.data.dtype == numpy.uint16:
                formatName += '16'  # Use sized internal format for uint16
            internalFormat = getattr(gl, formatName)

            self._texture = Image(internalFormat,
                                  self.data,
                                  format_=format_,
                                  texUnit=self._DATA_TEX_UNIT)
        elif self._textureIsDirty:
            self._textureIsDirty = False

            # We should check that internal format is the same
            format_ = gl.GL_RGBA if self.data.shape[2] == 4 else gl.GL_RGB
            self._texture.updateAll(format_=format_, data=self.data)

    def _renderLinear(self, matrix):
        self.prepare()

        prog = self._linearProgram
        prog.use()

        gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
        gl.glUniform1i(prog.uniforms['rflag'], self.rflag)
        gl.glUniform1i(prog.uniforms['gflag'], self.gflag)
        gl.glUniform1i(prog.uniforms['bflag'], self.bflag)

        mat = numpy.dot(numpy.dot(matrix, mat4Translate(*self.origin)),
                        mat4Scale(*self.scale))
        gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
                              mat.astype(numpy.float32))

        gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
        gl.glUniform1f(prog.uniforms['rmin'], self.rmin)
        gl.glUniform1f(prog.uniforms['gmin'], self.gmin)
        gl.glUniform1f(prog.uniforms['bmin'], self.bmin)
        gl.glUniform1f(prog.uniforms['rmax'], self.rmax)
        gl.glUniform1f(prog.uniforms['gmax'], self.gmax)
        gl.glUniform1f(prog.uniforms['bmax'], self.bmax)

        self._texture.render(prog.attributes['position'],
                             prog.attributes['texCoords'],
                             self._DATA_TEX_UNIT)

    def _renderLog(self, matrix, isXLog, isYLog):
        self.prepare()

        prog = self._logProgram
        prog.use()

        ox, oy = self.origin

        gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)

        gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
                              matrix.astype(numpy.float32))
        mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale))
        gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE,
                              mat.astype(numpy.float32))

        gl.glUniform2i(prog.uniforms['isLog'], isXLog, isYLog)

        gl.glUniform1f(prog.uniforms['alpha'], self.alpha)

        ex = ox + self.scale[0] * self.data.shape[1]
        ey = oy + self.scale[1] * self.data.shape[0]

        xOneOverRange = 1. / (ex - ox)
        yOneOverRange = 1. / (ey - oy)
        gl.glUniform2f(prog.uniforms['bounds_originOverRange'],
                       ox * xOneOverRange, oy * yOneOverRange)
        gl.glUniform2f(prog.uniforms['bounds_oneOverRange'],
                       xOneOverRange, yOneOverRange)

        try:
            tiles = self._texture.tiles
        except AttributeError:
            raise RuntimeError("No texture, discard has already been called")
        if len(tiles) > 1:
            raise NotImplementedError(
                "Image over multiple textures not supported with log scale")

        texture, vertices, info = tiles[0]

        texture.bind(self._DATA_TEX_UNIT)

        posAttrib = prog.attributes['position']
        stride = vertices.shape[-1] * vertices.itemsize
        gl.glEnableVertexAttribArray(posAttrib)
        gl.glVertexAttribPointer(posAttrib,
                                 2,
                                 gl.GL_FLOAT,
                                 gl.GL_FALSE,
                                 stride, vertices)

        gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))

    def render(self, matrix, isXLog, isYLog):
        if any((isXLog, isYLog)):
            self._renderLog(matrix, isXLog, isYLog)
        else:
            self._renderLinear(matrix)


class CustomImageRgba(items.ImageBase):
    """RGB(A) image """

    def __init__(self):
        super().__init__()
        self._rflag: int = 1
        self._gflag: int = 1
        self._bflag: int = 1
        self._rmin: float = 0.
        self._rmax: float = 1.
        self._gmin: float = 0.
        self._gmax: float = 1.
        self._bmin: float = 0.
        self._bmax: float = 1.

    def setRgbVisibilityFlags(self, rflag: bool = True,
                              gflag: bool = True,
                              bflag: bool = True):
        self._rflag = int(rflag)
        self._gflag = int(gflag)
        self._bflag = int(bflag)
        self._updated()

    def setRgbLimits(self, rmin=0., rmax=1.,
                     gmin=0., gmax=1.,
                     bmin=0., bmax=1.):
        self._rmin = rmin
        self._rmax = rmax
        self._gmin = gmin
        self._gmax = gmax
        self._bmin = bmin
        self._bmax = bmax
        self._updated()

    def _addBackendRenderer(self, backend):
        """Update backend renderer"""
        plot = self.getPlot()
        assert plot is not None
        if not self._isPlotLinear(plot):
            # Do not render with non linear scales
            return None

        data = self.getData(copy=False)

        if data.size == 0:
            return None  # No data to display

        return GLPlotRGBAImageCustom(data, self.getOrigin(),
                                     self.getScale(), self.getAlpha(),
                                     self._rflag, self._gflag, self._bflag,
                                     self._rmin, self._gmin, self._bmin,
                                     self._rmax, self._gmax, self._bmax)

    def getRgbaImageData(self, copy=True):
        """Get the displayed RGB(A) image

        :returns: numpy.ndarray of uint8 of shape (height, width, 4)
        """
        return _convertImageToRgba32(self.getData(copy=False), copy=copy)

    def setData(self, data, copy=True):
        """Set the image data

        :param data: RGB(A) image data to set
        :param bool copy: True (Default) to get a copy,
                          False to use internal representation (do not modify!)
        """
        data = numpy.array(data, copy=copy)
        assert data.ndim == 3
        assert data.shape[-1] in (3, 4)
        self._data = data

        # TODO hackish data range implementation
        if self.isVisible():
            plot = self.getPlot()
            if plot is not None:
                plot._invalidateDataRange()

        self._updated(ItemChangedType.DATA)

Thanks again.

@t20100
Copy link
Member Author

t20100 commented Jul 22, 2020

Good to hear that it also works for you!

Not available yet, but FYI, I've made an item adapter that allows to use silx.gui.plot3d.scene inside PlotWidget's item.
plot3d.scene provides a scenegraph and thus it allows to reuse more code when developping home-made OpenGL plot items.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants