Search code examples
pythonpyqt5glumpy

Add glumpy widget to PyQt5 GUI


I have a simple GUI onto which I am plotting images. I create Widgets for text outputs and arrays plots, and I would like to add one for 3D visualtion using glumpy, say to show this example from the glumpy documentation.

What I would like is an "extra" slot on which to have the glumpy output:

enter image description here

I saw for example in this GitHub thread that people were referring to PyQt5 and glumpy integration, but I only see snippets of code and noting that works as a standalone example.

Also, it seems glumpy is already using PyQt5 in the backend (here), but I don't understand this well enough to know if and how I can access it a posteriori?


This is my MWE:

from PyQt5 import QtGui, QtCore
import pyqtgraph as pg
import sys

width = 1000
height = 500

class layout():
    def setup(self, window):
        self.window = window
        self.window.resize(width, height)

        self.centralwidget = QtGui.QWidget(self.window)
        self.horizontallayout = QtGui.QHBoxLayout(self.centralwidget)
        self.window.setCentralWidget(self.centralwidget)

        self.dialogue = QtGui.QTextEdit()
        self.horizontallayout.addWidget(self.dialogue)

        self.plot = pg.GraphicsLayoutWidget(self.window)
        self.horizontallayout.addWidget(self.plot)

        self.plot1 = self.plot.addPlot(colspan=1)

class Window(pg.Qt.QtGui.QMainWindow, layout):

    def __init__(self, shot = None):

        super(Window, self).__init__()
        self.setup(self)
        self.show()

if __name__ == '__main__':
    app = pg.Qt.QtGui.QApplication([])
    Window()
    sys.exit(app.exec_())

Solution

  • You have to get the internal QGLWidget through the "_native_window" attribute. The following example is based on the official example geometry-surface.py .

    import sys
    
    from PyQt5 import QtGui, QtCore
    import pyqtgraph as pg
    
    import numpy as np
    
    
    from glumpy import app as glumpy_app, gl, gloo, data, library
    from glumpy.geometry import primitives
    from glumpy.transforms import Trackball
    
    
    width = 1000
    height = 500
    
    glumpy_app.use("qt5")
    
    
    vertex = """
    #include "misc/spatial-filters.frag"
    uniform float height;
    uniform sampler2D data;
    uniform vec2 data_shape;
    attribute vec3 position;
    attribute vec2 texcoord;
    varying vec3 v_position;
    varying vec2 v_texcoord;
    void main()
    {
        float z = height*Bicubic(data, data_shape, texcoord).r;
        gl_Position = <transform>;
        v_texcoord = texcoord;
        v_position = vec3(position.xy, z);
    }
    """
    
    fragment = """
    #include "misc/spatial-filters.frag"
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 normal;
    uniform sampler2D texture;
    uniform float height;
    uniform vec4 color;
    uniform sampler2D data;
    uniform vec2 data_shape;
    uniform vec3 light_color[3];
    uniform vec3 light_position[3];
    varying vec3 v_position;
    varying vec2 v_texcoord;
    float lighting(vec3 v_normal, vec3 light_position)
    {
        // Calculate normal in world coordinates
        vec3 n = normalize(normal * vec4(v_normal,1.0)).xyz;
        // Calculate the location of this fragment (pixel) in world coordinates
        vec3 position = vec3(view * model * vec4(v_position, 1));
        // Calculate the vector from this pixels surface to the light source
        vec3 surface_to_light = light_position - position;
        // Calculate the cosine of the angle of incidence (brightness)
        float brightness = dot(n, surface_to_light) /
                          (length(surface_to_light) * length(n));
        brightness = max(min(brightness,1.0),0.0);
        return brightness;
    }
    void main()
    {
        mat4 model = <transform.trackball_model>;
        // Extract data value
        float value = Bicubic(data, data_shape, v_texcoord).r;
        // Compute surface normal using neighbour values
        float hx0 = height*Bicubic(data, data_shape, v_texcoord+vec2(+1,0)/data_shape).r;
        float hx1 = height*Bicubic(data, data_shape, v_texcoord+vec2(-1,0)/data_shape).r;
        float hy0 = height*Bicubic(data, data_shape, v_texcoord+vec2(0,+1)/data_shape).r;
        float hy1 = height*Bicubic(data, data_shape, v_texcoord+vec2(0,-1)/data_shape).r;
        vec3 dx = vec3(2.0/data_shape.x,0.0,hx0-hx1);
        vec3 dy = vec3(0.0,2.0/data_shape.y,hy0-hy1);
        vec3 v_normal = normalize(cross(dx,dy));
        // Map value to rgb color
        float c = 0.6 + 0.4*texture2D(texture, v_texcoord).r;
        vec4 l1 = vec4(light_color[0] * lighting(v_normal, light_position[0]), 1);
        vec4 l2 = vec4(light_color[1] * lighting(v_normal, light_position[1]), 1);
        vec4 l3 = vec4(light_color[2] * lighting(v_normal, light_position[2]), 1);
        gl_FragColor = color * vec4(c,c,c,1) * (0.5 + 0.5*(l1+l2+l3));
    } """
    
    
    def func3(x, y):
        return (1 - x / 2 + x ** 5 + y ** 3) * np.exp(-(x ** 2) - y ** 2)
    
    
    class layout:
        def setup(self, window):
            self.window = window
            self.window.resize(width, height)
    
            self.dialogue = QtGui.QTextEdit()
    
            self.plot = pg.GraphicsLayoutWidget(self.window)
            self.plot1 = self.plot.addPlot(colspan=1)
    
            self.glumpy_window = glumpy_app.Window(color=(1, 1, 1, 1))
            self.glumpy_window._native_window
    
            self.centralwidget = QtGui.QWidget(self.window)
            self.horizontallayout = QtGui.QHBoxLayout(self.centralwidget)
            self.window.setCentralWidget(self.centralwidget)
    
            self.horizontallayout.addWidget(self.dialogue, stretch=1)
            self.horizontallayout.addWidget(self.plot, stretch=1)
            self.horizontallayout.addWidget(self.glumpy_window._native_window, stretch=1)
    
    
    class Window(pg.Qt.QtGui.QMainWindow, layout):
        def __init__(self, shot=None):
            super(Window, self).__init__()
            self.setup(self)
    
            n = 64
            self.surface = gloo.Program(vertex, fragment)
            self.vertices, self.s_indices = primitives.plane(2.0, n=n)
            self.surface.bind(self.vertices)
    
            I = []
            for i in range(n):
                I.append(i)
            for i in range(1, n):
                I.append(n - 1 + i * n)
            for i in range(n - 1):
                I.append(n * n - 1 - i)
            for i in range(n - 1):
                I.append(n * (n - 1) - i * n)
            self.b_indices = np.array(I, dtype=np.uint32).view(gloo.IndexBuffer)
    
            x = np.linspace(-2.0, 2.0, 32).astype(np.float32)
            y = np.linspace(-2.0, 2.0, 32).astype(np.float32)
            X, Y = np.meshgrid(x, y)
            Z = func3(X, Y)
    
            self.surface["data"] = (Z - Z.min()) / (Z.max() - Z.min())
            self.surface["data"].interpolation = gl.GL_NEAREST
            self.surface["data_shape"] = Z.shape[1], Z.shape[0]
            self.surface["u_kernel"] = data.get("spatial-filters.npy")
            self.surface["u_kernel"].interpolation = gl.GL_LINEAR
            self.surface["texture"] = data.checkerboard(32, 24)
    
            self.transform = Trackball("vec4(position.xy, z, 1.0)")
            self.surface["transform"] = self.transform
            self.glumpy_window.attach(self.transform)
    
            T = (Z - Z.min()) / (Z.max() - Z.min())
    
            self.surface["height"] = 0.75
            self.surface["light_position[0]"] = 3, 0, 0 + 5
            self.surface["light_position[1]"] = 0, 3, 0 + 5
            self.surface["light_position[2]"] = -3, -3, +5
            self.surface["light_color[0]"] = 1, 0, 0
            self.surface["light_color[1]"] = 0, 1, 0
            self.surface["light_color[2]"] = 0, 0, 1
            phi, theta = -45, 0
            self.time = 0
    
            self.glumpy_window.set_handler("on_init", self.on_init)
            self.glumpy_window.set_handler("on_draw", self.on_draw)
    
        def on_init(self):
            gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
            gl.glPolygonOffset(1, 1)
            gl.glEnable(gl.GL_LINE_SMOOTH)
            gl.glLineWidth(2.5)
    
        def on_draw(self, dt):
            self.time += dt
            self.glumpy_window.clear()
    
            self.surface["data"]
    
            gl.glDisable(gl.GL_BLEND)
            gl.glEnable(gl.GL_DEPTH_TEST)
            gl.glEnable(gl.GL_POLYGON_OFFSET_FILL)
            self.surface["color"] = 1, 1, 1, 1
    
            self.surface.draw(gl.GL_TRIANGLES, self.s_indices)
    
            gl.glDisable(gl.GL_POLYGON_OFFSET_FILL)
            gl.glEnable(gl.GL_BLEND)
            gl.glDepthMask(gl.GL_FALSE)
            self.surface["color"] = 0, 0, 0, 1
            self.surface.draw(gl.GL_LINE_LOOP, self.b_indices)
            gl.glDepthMask(gl.GL_TRUE)
    
            model = self.surface["transform"]["model"].reshape(4, 4)
            view = self.surface["transform"]["view"].reshape(4, 4)
            self.surface["view"] = view
            self.surface["model"] = model
            self.surface["normal"] = np.array(np.matrix(np.dot(view, model)).I.T)
            self.surface["height"] = 0.75 * np.cos(self.time)
    
        def showEvent(self, event):
            super().showEvent(event)
            self.glumpy_window.dispatch_event("on_resize", *self.glumpy_window.get_size())
    
        def closeEvent(self, event):
            super().closeEvent(event)
            self.glumpy_window.close()
    
    
    if __name__ == "__main__":
        app = pg.Qt.QtGui.QApplication([])
        w = Window()
        w.show()
        glumpy_app.run()
    

    enter image description here