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_())

Answer

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

Leave a Reply

Your email address will not be published. Required fields are marked *