本博客为项目开发过程中,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并不一样,此处仅为方便理解)
今天的老百姓真呀真高兴!
解决了困扰了一周多的软件卡顿问题。
绘制3D散点图的窗口是我项目中的一部分,充当了子窗口的角色。主窗口用于处理按钮、socket收发和数据解析处理等,若收到节点数据则会把数据交给3d图窗口绘图。最关键的一点是,我的数据收发是以20ms为周期的,也就是说每20ms,主窗口和子窗口都要刷新至少一次。而实际的运行过程中,这两个窗口运行都比较卡顿,大概每秒刷新次数为3次,这严重影响了代码的性能。
经过很多次检查测试(都是在QThread层面进行优化),卡顿并没有太明显的改进效果,窗口的显示仍然没有达到我每秒50帧的要求。思来想去,最终的原因终于浮出水面:
我是在qtdesigner集成环境中设计的窗口UI,3D画图控件为GLViewWidget,点击窗口后,在Property Editor窗口——sizePolicy做如下图设置:
采用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
今天收到私信,他想要的效果为如何动态更新点的位置。而他刷新画布的方式是在循环内直接对部件GLViewWidget进行操作,并遇到了内存泄漏的问题。这个问题我考虑了一下,对GLViewWidget进行初始化,会直接把该部件内的所有内容(画布、坐标轴等)直接删除,想继续更新点的位置,需要重新添加画布和坐标轴等,这一块是需要占用内存和时间的,不仅容易造成内存泄漏,也可能会造成窗口卡顿(多线程QThread情况下)。为方便大家检索,单独写了一篇。
我的解决办法如下:
详见:点击查看
若对坐标轴无较高要求,可以直接借助GLAxisItem
来显示坐标轴:
# 添加坐标轴Item
ax = gl.GLAxisItem()
ax.setSize(40, 40, 40)
self.guiplot.addItem(ax)
案例:
# **********************************************************************
# 给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_()
点击查看
如有其它问题,欢迎留言,一起学习进步~
未完待续