2

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_())
SuperCiocia
  • 1,823
  • 6
  • 23
  • 40

1 Answers1

3

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

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thanks! I knew vispy supported a native window, but I had read all the docs and forums I could find and didn't find any mention of this for glumpy - is there a doc page for this? I will try and implement it when I'm back – SuperCiocia Mar 16 '21 at 02:30
  • @SuperCiocia Well that is the only drawback, that part is not documented so I had to analyze the source code. I recommend you create an issue where it asks to expose the "native window" as a property. – eyllanesc Mar 16 '21 at 02:54
  • Thanks for changing it to the example I asked. Why are showEvent and closeEvent needed? – SuperCiocia Mar 16 '21 at 03:40
  • @SuperCiocia 1) The explanation of the closeEvent is in one of the links that you provide, the one of the showEvent is because the glumpy window seems that it is not notified by the layout that the container has changed its size (it seems a bug). – eyllanesc Mar 16 '21 at 03:43
  • how can I add *two* different glumpy plots, both in the right sector but one top and one bottom? I tried `.nextRow()` but it turns out it only works with `GraphicsLayoutWidget` – SuperCiocia Mar 16 '21 at 05:17
  • @SuperCiocia I recommend you read about Qt Layouts(use QVBoxLayout) which are very different from pyqtgraph layouts. I will not answer topics other than those of the post since the objective of the comments is not to talk about other topics. – eyllanesc Mar 16 '21 at 05:20
  • Thakns. I tried a bit but always end up making a mess. I asked another question here (https://stackoverflow.com/questions/66649897/stack-two-widgets-in-a-gui). I really have no experience with GUIs sorry – SuperCiocia Mar 16 '21 at 06:08