(详细分析)基于pyqt5、pyqtgraph和GLViewWidget画3D散点图,并增加图例与坐标轴

目录

  • 引言
  • 一、pyqtgraph官方3D散点图代码示例分析
  • 二、遇到的问题及解决办法
    • 1、基于python,采用多线程显示多窗口的问题
      • 1.1 具体问题
      • 1.2 解决办法
    • 2、窗口排版正确,但实际运行显示区域小,且旁边有一圈较大留白
      • 解决办法
    • 3、如何动态更新显示点位置,并有点的飘动效果
  • 三、GLViewWidget部件功能扩展
    • 1、简单的坐标轴(只有线)
    • 2、增加坐标轴及刻度
  • 四、pyqt5开发过程关键问题总结

引言

本博客为项目开发过程中,3D散点图学习记录,对示例代码和开发过程做了详细的记录与分析。在留下时光脚印的同时,希望也能帮助到屏幕前的你。
具体的开发及软件打包可见:点击查看
项目开发遇到的坑总结帖见:点击查看

一、pyqtgraph官方3D散点图代码示例分析

代码如下,已添加了详细的注释。

# -*- coding: utf-8 -*-
"""
Demonstrates use of GLScatterPlotItem with rapidly-updating plots.
"""

# import initExample

from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl
import numpy as np
import time

app = QtGui.QApplication([])
w = gl.GLViewWidget()  # 定义窗口w为GLViewWidget部件
w.opts['distance'] = 20  # 初始视角高度
w.show()  # 显示窗口
w.setWindowTitle('pyqtgraph example: GLScatterPlotItem')  # 定义窗口标题

# g用来显示白色网格
g = gl.GLGridItem()
w.addItem(g)
# 注:网格的大小可以设置:
# g = gl.GLGridItem()
# size_axes = distance * 3
# g.setSize(x=size_axes, y=size_axes, z=size_axes)
# w.addItem(g)

# **************************************************************************************
#  一、例子:点集
#  First example is a set of points with pxMode=False
#  These demonstrate the ability to have points with real size down to a very small scale
#
pos = np.empty((53, 3))  # 存放点的位置,为53 * 3的向量,感觉说是矩阵更合适
size = np.empty((53))  # 存放点的大小
color = np.empty((53, 4))  # 存放点的颜色
pos[0] = (1, 0, 0)  # 第一个点的坐标
size[0] = 0.5  # 第一个点的大小
color[0] = (1.0, 0.0, 0.0, 1)  # 红色,最后一位为透明度
pos[1] = (0, 1, 0)
size[1] = 0.5
color[1] = (0.0, 0.0, 1.0, 1)  # 蓝色
pos[2] = (0, 0, 1)
size[2] = 1
color[2] = (0.0, 1.0, 0.0, 1)  # 绿色
pos[3] = (2, 0, 0)
size[3] = 0.5
color[3] = (1.0, 1.0, 0.0, 1)  # 黄色
pos[4] = (3, 0, 0)
size[4] = 0.5
color[4] = (1.0, 0.0, 1.0, 1)  # 紫红色
pos[5] = (4, 0, 0)
size[5] = 0.5
color[5] = (0.0, 1.0, 1.0, 1)  # 天蓝色
pos[6] = (5, 0, 0)
size[6] = 0.5
color[6] = (1.0, 1.0, 1.0, 1)  # 白色


z = 0.5
d = 6.0
# 对pos从第3个元素开始操作,生成最后一直往上叠的小绿点
for i in range(7, 53):
    pos[i] = (0, 0, z)
    size[i] = 2. / d
    color[i] = (0.0, 1.0, 0.0, 0.5)  # 绿色,第4个0为透明
    z *= 0.5
    d *= 2.0

sp1 = gl.GLScatterPlotItem(pos=pos, size=size, color=color, pxMode=False)  # 设置Item
sp1.translate(5, 5, 0)  # 平移sp1,即横轴坐标整体+5,纵轴坐标整体+5
w.addItem(sp1)  # 当w使用addItem()后,才会生效显示图像

# **************************************************************************************
#  二、立方体区域点集,并迅速更新颜色
#  Second example shows a volume of points with rapidly updating color
#  and pxMode=True
#

pos2 = np.random.random(size=(100000, 3))  # 生成随机数点集
pos2 *= [10, -10, 10]  # 区域的长宽高都为10,于第四卦限
pos2[0] = (0, 0, 0)  # 第一个点为原点
color2 = np.ones((pos2.shape[0], 4))  # pos2的行数(点数),4列的元素全为1的向量
d2 = (pos2 ** 2).sum(axis=1) ** 0.5
size = np.random.random(size=pos2.shape[0]) * 10  # 点的大小
sp2 = gl.GLScatterPlotItem(pos=pos2, color=(1, 1, 1, 1), size=size)  # (1, 1, 1, 1)为白色
phase = 0.

w.addItem(sp2)

# **************************************************************************************
#  三、点网格
#  Third example shows a grid of points with rapidly updating position
#  and pxMode = False
#

pos3 = np.zeros((100, 100, 3))
pos3[:, :, :2] = np.mgrid[:100, :100].transpose(1, 2, 0) * [-0.1, 0.1]
pos3 = pos3.reshape(10000, 3)  # 经过上述操作得到10000行3列的ndarray,初始点铺平在平面的四分之一范围上
d3 = (pos3 ** 2).sum(axis=1) ** 0.5

sp3 = gl.GLScatterPlotItem(pos=pos3, color=(1, 1, 1, .3), size=0.1, pxMode=False)

w.addItem(sp3)


def update():
    """
    更新
    - 每次运行update都事phase减0.1,从而更新color2,和pos3的点的位置与color
    :return:
    """
    # update volume colors
    global phase, sp2, d2
    s = -np.cos(d2 * 2 + phase)
    color2 = np.empty((len(d2), 4), dtype=np.float32)
    color2[:, 3] = np.clip(s * 0.1, 0, 1)
    color2[:, 0] = np.clip(s * 3.0, 0, 1)
    color2[:, 1] = np.clip(s * 1.0, 0, 1)
    color2[:, 2] = np.clip(s ** 3, 0, 1)
    sp2.setData(color=color2)

    # start = time.process_time()
    phase -= 0.1
    # update surface positions and colors
    global sp3, d3, pos3
    # 每次运行更新pos3的点的位置
    z = -np.cos(d3 * 2 + phase)
    pos3[:, 2] = z
    color = np.empty((len(d3), 4), dtype=np.float32)
    color[:, 3] = 0.3
    color[:, 0] = np.clip(z * 3.0, 0, 1)
    color[:, 1] = np.clip(z * 1.0, 0, 1)
    color[:, 2] = np.clip(z ** 3, 0, 1)
    sp3.setData(pos=pos3, color=color)
    # end = time.process_time()
    # print('Time spent on update sp3 is: %.5f' % (end - start))

# 定时触发,每50ms运行一次update函数更新
t = QtCore.QTimer()
t.timeout.connect(update)
t.start(50)


# Start Qt event loop unless running in interactive mode.
if __name__ == '__main__':
    import sys

    if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
        QtGui.QApplication.instance().exec_()


补充:

  • (0, 0, 0, 0)第4个代表透明度,若为0则为透明无色,(1.0, 0.0, 0.0, 1) # 红色,(0.0, 0.0, 1.0, 1) # 蓝色,(0.0, 1.0, 0.0, 1) # 绿色,根据颜色学,自己可以用红绿蓝调配出自己想要的颜色。

  • pos为ndarray向量,具体的格式为:

array([[1.00000000e+00, 0.00000000e+00, 0.00000000e+00],
       [0.00000000e+00, 1.00000000e+00, 0.00000000e+00],
       [0.00000000e+00, 0.00000000e+00, 1.00000000e+00],
       [0.00000000e+00, 0.00000000e+00, 5.00000000e-01],
       [0.00000000e+00, 0.00000000e+00, 2.50000000e-01],
       [0.00000000e+00, 0.00000000e+00, 1.25000000e-01],
       [0.00000000e+00, 0.00000000e+00, 6.25000000e-02],
       [0.00000000e+00, 0.00000000e+00, 3.12500000e-02],
       [0.00000000e+00, 0.00000000e+00, 1.56250000e-02],
       [0.00000000e+00, 0.00000000e+00, 7.81250000e-03],
       [0.00000000e+00, 0.00000000e+00, 3.90625000e-03],
       [0.00000000e+00, 0.00000000e+00, 1.95312500e-03],
       [0.00000000e+00, 0.00000000e+00, 9.76562500e-04],
       [0.00000000e+00, 0.00000000e+00, 4.88281250e-04],
       [0.00000000e+00, 0.00000000e+00, 2.44140625e-04],
       [0.00000000e+00, 0.00000000e+00, 1.22070312e-04],
       [0.00000000e+00, 0.00000000e+00, 6.10351562e-05],
       [0.00000000e+00, 0.00000000e+00, 3.05175781e-05],
       [0.00000000e+00, 0.00000000e+00, 1.52587891e-05],
       [0.00000000e+00, 0.00000000e+00, 7.62939453e-06],
       [0.00000000e+00, 0.00000000e+00, 3.81469727e-06],
       [0.00000000e+00, 0.00000000e+00, 1.90734863e-06],
       [0.00000000e+00, 0.00000000e+00, 9.53674316e-07],
       [0.00000000e+00, 0.00000000e+00, 4.76837158e-07],
       [0.00000000e+00, 0.00000000e+00, 2.38418579e-07],
       [0.00000000e+00, 0.00000000e+00, 1.19209290e-07],
       [0.00000000e+00, 0.00000000e+00, 5.96046448e-08],
       [0.00000000e+00, 0.00000000e+00, 2.98023224e-08],
       [0.00000000e+00, 0.00000000e+00, 1.49011612e-08],
       [0.00000000e+00, 0.00000000e+00, 7.45058060e-09],
       [0.00000000e+00, 0.00000000e+00, 3.72529030e-09],
       [0.00000000e+00, 0.00000000e+00, 1.86264515e-09],
       [0.00000000e+00, 0.00000000e+00, 9.31322575e-10],
       [0.00000000e+00, 0.00000000e+00, 4.65661287e-10],
       [0.00000000e+00, 0.00000000e+00, 2.32830644e-10],
       [0.00000000e+00, 0.00000000e+00, 1.16415322e-10],
       [0.00000000e+00, 0.00000000e+00, 5.82076609e-11],
       [0.00000000e+00, 0.00000000e+00, 2.91038305e-11],
       [0.00000000e+00, 0.00000000e+00, 1.45519152e-11],
       [0.00000000e+00, 0.00000000e+00, 7.27595761e-12],
       [0.00000000e+00, 0.00000000e+00, 3.63797881e-12],
       [0.00000000e+00, 0.00000000e+00, 1.81898940e-12],
       [0.00000000e+00, 0.00000000e+00, 9.09494702e-13],
       [0.00000000e+00, 0.00000000e+00, 4.54747351e-13],
       [0.00000000e+00, 0.00000000e+00, 2.27373675e-13],
       [0.00000000e+00, 0.00000000e+00, 1.13686838e-13],
       [0.00000000e+00, 0.00000000e+00, 5.68434189e-14],
       [0.00000000e+00, 0.00000000e+00, 2.84217094e-14],
       [0.00000000e+00, 0.00000000e+00, 1.42108547e-14],
       [0.00000000e+00, 0.00000000e+00, 7.10542736e-15],
       [0.00000000e+00, 0.00000000e+00, 3.55271368e-15],
       [0.00000000e+00, 0.00000000e+00, 1.77635684e-15],
       [0.00000000e+00, 0.00000000e+00, 8.88178420e-16]])

对此,我理解的为可以看做它是一个list,然后元素又都为一个包含三个元素的list。(当然与真正的list并不一样,此处仅为方便理解)

二、遇到的问题及解决办法

1、基于python,采用多线程显示多窗口的问题

今天的老百姓真呀真高兴!
解决了困扰了一周多的软件卡顿问题。

1.1 具体问题

绘制3D散点图的窗口是我项目中的一部分,充当了子窗口的角色。主窗口用于处理按钮、socket收发和数据解析处理等,若收到节点数据则会把数据交给3d图窗口绘图。最关键的一点是,我的数据收发是以20ms为周期的,也就是说每20ms,主窗口和子窗口都要刷新至少一次。而实际的运行过程中,这两个窗口运行都比较卡顿,大概每秒刷新次数为3次,这严重影响了代码的性能。

1.2 解决办法

经过很多次检查测试(都是在QThread层面进行优化),卡顿并没有太明显的改进效果,窗口的显示仍然没有达到我每秒50帧的要求。思来想去,最终的原因终于浮出水面:

  • python语言相对C++等运行要稍微慢一点,而且最关键的是python的多线程严格意义上说并不是真正的多线程,GIL锁的存在造成了这一缺陷。因此,当我们多个窗口刷新需要同时进行稍微耗时的操作时,python基本是串行的去做的。这个坑在做对时延要求较高的多线程项目时,一定要注意!!!
  • 不过python的多进程是没有问题的,大家可以使用多进程来解决卡顿问题。

2、窗口排版正确,但实际运行显示区域小,且旁边有一圈较大留白

解决办法

我是在qtdesigner集成环境中设计的窗口UI,3D画图控件为GLViewWidget,点击窗口后,在Property Editor窗口——sizePolicy做如下图设置:
(详细分析)基于pyqt5、pyqtgraph和GLViewWidget画3D散点图,并增加图例与坐标轴_第1张图片
采用PyUIC工具生成的代码为:

from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_NodeStatus(object):
    def setupUi(self, NodeStatus):
        NodeStatus.setObjectName("NodeStatus")
        NodeStatus.resize(686, 426)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
        sizePolicy.setHorizontalStretch(0)
        sizePolicy.setVerticalStretch(0)
        sizePolicy.setHeightForWidth(NodeStatus.sizePolicy().hasHeightForWidth())
        NodeStatus.setSizePolicy(sizePolicy)
        NodeStatus.setStyleSheet("*{    \n"
"    font-family:微软雅黑;\n"
"    font-size:15px;\n"
"    color: #1d649c;\n"
"}\n"
"")
        self.verticalLayout = QtWidgets.QVBoxLayout(NodeStatus)
        self.verticalLayout.setObjectName("verticalLayout")
        self.guiplot = GLViewWidget(NodeStatus)
        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
        sizePolicy.setHorizontalStretch(1)
        sizePolicy.setVerticalStretch(1)
        sizePolicy.setHeightForWidth(self.guiplot.sizePolicy().hasHeightForWidth())
        self.guiplot.setSizePolicy(sizePolicy)
        self.guiplot.setAutoFillBackground(False)
        self.guiplot.setObjectName("guiplot")
        self.verticalLayout.addWidget(self.guiplot)

        self.retranslateUi(NodeStatus)
        QtCore.QMetaObject.connectSlotsByName(NodeStatus)

    def retranslateUi(self, NodeStatus):
        _translate = QtCore.QCoreApplication.translate
        NodeStatus.setWindowTitle(_translate("NodeStatus", "节点状态"))
from pyqtgraph.opengl import GLViewWidget

最终窗口显示正常:
(详细分析)基于pyqt5、pyqtgraph和GLViewWidget画3D散点图,并增加图例与坐标轴_第2张图片

3、如何动态更新显示点位置,并有点的飘动效果

今天收到私信,他想要的效果为如何动态更新点的位置。而他刷新画布的方式是在循环内直接对部件GLViewWidget进行操作,并遇到了内存泄漏的问题。这个问题我考虑了一下,对GLViewWidget进行初始化,会直接把该部件内的所有内容(画布、坐标轴等)直接删除,想继续更新点的位置,需要重新添加画布和坐标轴等,这一块是需要占用内存和时间的,不仅容易造成内存泄漏,也可能会造成窗口卡顿(多线程QThread情况下)。为方便大家检索,单独写了一篇。
我的解决办法如下:
详见:点击查看

三、GLViewWidget部件功能扩展

1、简单的坐标轴(只有线)

若对坐标轴无较高要求,可以直接借助GLAxisItem来显示坐标轴:

        # 添加坐标轴Item
        ax = gl.GLAxisItem()
        ax.setSize(40, 40, 40)
        self.guiplot.addItem(ax)

(详细分析)基于pyqt5、pyqtgraph和GLViewWidget画3D散点图,并增加图例与坐标轴_第3张图片

2、增加坐标轴及刻度

案例:

# **********************************************************************
# 给GLViewWidget图像加坐标轴
# https://stackoverflow.com/questions/56890547/how-to-add-axis-features-labels-ticks-values-to-a-3d-plot-with-glviewwidget
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl
import pyqtgraph as pg
import OpenGL.GL as ogl
import numpy as np

class CustomTextItem(gl.GLGraphicsItem.GLGraphicsItem):
    def __init__(self, X, Y, Z, text):
        gl.GLGraphicsItem.GLGraphicsItem.__init__(self)
        self.text = text
        self.X = X
        self.Y = Y
        self.Z = Z

    def setGLViewWidget(self, GLViewWidget):
        self.GLViewWidget = GLViewWidget

    def setText(self, text):
        self.text = text
        self.update()

    def setX(self, X):
        self.X = X
        self.update()

    def setY(self, Y):
        self.Y = Y
        self.update()

    def setZ(self, Z):
        self.Z = Z
        self.update()

    def paint(self):
        self.GLViewWidget.qglColor(QtCore.Qt.black)
        self.GLViewWidget.renderText(self.X, self.Y, self.Z, self.text)


class Custom3DAxis(gl.GLAxisItem):
    """Class defined to extend 'gl.GLAxisItem'."""
    def __init__(self, parent, color=(0,0,0,.6)):
        gl.GLAxisItem.__init__(self)
        self.parent = parent
        self.c = color

    def add_labels(self):
        """Adds axes labels."""
        x,y,z = self.size()
        #X label
        self.xLabel = CustomTextItem(X=x/2, Y=-y/20, Z=-z/20, text="X")
        self.xLabel.setGLViewWidget(self.parent)
        self.parent.addItem(self.xLabel)
        #Y label
        self.yLabel = CustomTextItem(X=-x/20, Y=y/2, Z=-z/20, text="Y")
        self.yLabel.setGLViewWidget(self.parent)
        self.parent.addItem(self.yLabel)
        #Z label
        self.zLabel = CustomTextItem(X=-x/20, Y=-y/20, Z=z/2, text="Z")
        self.zLabel.setGLViewWidget(self.parent)
        self.parent.addItem(self.zLabel)

    def add_tick_values(self, xticks=[], yticks=[], zticks=[]):
        """Adds ticks values."""
        x,y,z = self.size()
        xtpos = np.linspace(0, x, len(xticks))
        ytpos = np.linspace(0, y, len(yticks))
        ztpos = np.linspace(0, z, len(zticks))
        #X label
        for i, xt in enumerate(xticks):
            val = CustomTextItem(X=xtpos[i], Y=-y/20, Z=-z/20, text=str(xt))
            val.setGLViewWidget(self.parent)
            self.parent.addItem(val)
        #Y label
        for i, yt in enumerate(yticks):
            val = CustomTextItem(X=-x/20, Y=ytpos[i], Z=-z/20, text=str(yt))
            val.setGLViewWidget(self.parent)
            self.parent.addItem(val)
        #Z label
        for i, zt in enumerate(zticks):
            val = CustomTextItem(X=-x/20, Y=-y/20, Z=ztpos[i], text=str(zt))
            val.setGLViewWidget(self.parent)
            self.parent.addItem(val)

    def paint(self):
        self.setupGLState()
        if self.antialias:
            ogl.glEnable(ogl.GL_LINE_SMOOTH)
            ogl.glHint(ogl.GL_LINE_SMOOTH_HINT, ogl.GL_NICEST)
        ogl.glBegin(ogl.GL_LINES)

        x,y,z = self.size()
        #Draw Z
        ogl.glColor4f(self.c[0], self.c[1], self.c[2], self.c[3])
        ogl.glVertex3f(0, 0, 0)
        ogl.glVertex3f(0, 0, z)
        #Draw Y
        ogl.glColor4f(self.c[0], self.c[1], self.c[2], self.c[3])
        ogl.glVertex3f(0, 0, 0)
        ogl.glVertex3f(0, y, 0)
        #Draw X
        ogl.glColor4f(self.c[0], self.c[1], self.c[2], self.c[3])
        ogl.glVertex3f(0, 0, 0)
        ogl.glVertex3f(x, 0, 0)
        ogl.glEnd()


app = QtGui.QApplication([])
fig1 = gl.GLViewWidget()
background_color = app.palette().color(QtGui.QPalette.Background)
fig1.setBackgroundColor(background_color)

n = 51
y = np.linspace(-10,10,n)
x = np.linspace(-10,10,100)
for i in range(n):
    yi = np.array([y[i]]*100)
    d = (x**2 + yi**2)**0.5
    z = 10 * np.cos(d) / (d+1)
    pts = np.vstack([x,yi,z]).transpose()
    plt = gl.GLLinePlotItem(pos=pts, color=pg.glColor((i,n*1.3)), width=(i+1)/10., antialias=True)
    fig1.addItem(plt)


axis = Custom3DAxis(fig1, color=(0.2,0.2,0.2,.6))
axis.setSize(x=12, y=12, z=12)
# Add axes labels
axis.add_labels()
# Add axes tick values
axis.add_tick_values(xticks=[0,4,8,12], yticks=[0,6,12], zticks=[0,3,6,9,12])
fig1.addItem(axis)
fig1.opts['distance'] = 40

fig1.show()

if __name__ == '__main__':
    import sys
    if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
        QtGui.QApplication.instance().exec_()

效果展示:
(详细分析)基于pyqt5、pyqtgraph和GLViewWidget画3D散点图,并增加图例与坐标轴_第4张图片

四、pyqt5开发过程关键问题总结

点击查看

如有其它问题,欢迎留言,一起学习进步~
未完待续

你可能感兴趣的:(基于python,python,pyqt5,3d)