You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyQtGraph中散点图颜色的高效更新方法探究

问题描述

背景

我正在开发一个GUI,用于展示4组六边形像素的实时更新数据,我认为最简便的方式是通过PyQtGraph绘制散点图,并根据传感器数据更新其填充(brush)颜色。

方法

我参考了PyQtGraph的官方示例脚本ScatterPlotItem.py和ScatterPlotSpeedTest.py,适配了自身的特定布局。

问题

当前脚本性能非常缓慢,帧率仅约4-7fps,远低于官方示例的1000+fps。我原本以为仅更新点的颜色会很快,这让我十分意外。我仍在学习PyQtGraph,目前在update函数中使用ScatterPlotItem.setBrush(),但该方法似乎效率极低(我认为这是更新缓慢的根源)。是否存在更优/更快的散点图项填充颜色更新方式?

当前代码

import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, QtCore
from time import perf_counter


# This function generates the hexgonal array x's and y's in the required ordering.
# It is ugly, but works for now.
def drawHexGridLoop2(origin, depth, apothem, padding):
    
    def getCoords(xs, ys):
        xs = [item for sublist in xs for item in sublist]
        ys = [item for sublist in ys for item in sublist]
        coords = list(zip(xs, ys))      
        return coords
    
    def flattenList(l):
        rv = [item for sublist in l for item in sublist]
        return rv
    
    ang60 = np.deg2rad(60)
    xs = [[origin[0]]]
    ys = [[origin[1]]]
    labels = [['1']]
    labelN = 2
    thisX = 0
    thisY = 0
    for d in range(1, depth):
        thisXArr = []
        thisYArr = []
        thisLabelArr = []
        loc = 1
        n=0
        while n < d*6:
            if n == 0:
                anchorN = 0
                thisX = round(xs[-1][0] + 2*apothem, 8)
                thisY = round(ys[-1][0], 8)
                anchorX = xs[-1][anchorN]
                anchorY = ys[-1][anchorN]
                thisXArr.append(thisX)
                thisYArr.append(thisY)
                thisLabelArr.append(str(labelN))
                labelN += 1

            else:
                thisX = round(anchorX + 2*apothem*np.cos(-1*ang60*loc), 8)
                thisY = round(anchorY + 2*apothem*np.sin(-1*ang60*loc), 8)
                if (thisX, thisY) in getCoords(xs, ys):
                    anchorN += 1
                    anchorX = xs[-1][anchorN]
                    anchorY = ys[-1][anchorN]
                    loc -= 1
                    continue
                thisXArr.append(thisX)
                thisYArr.append(thisY)
                thisLabelArr.append(str(labelN))
                labelN += 1
                loc += 1
            n += 1              
        xs.append(thisXArr)
        ys.append(thisYArr)
        labels.append(thisLabelArr)
    xs = flattenList(xs)
    ys = flattenList(ys)
    labels = flattenList(labels)
    return xs, ys, labels


# Function to create the scatter plot in each viewbox.
# Adapted from ScatterPlotItem.py
def createArray(w):
    s = pg.ScatterPlotItem(
        pxMode=False,  # Set pxMode=False to allow spots to transform with the view
        hoverable=True,
        hoverPen=pg.mkPen('g'),
        hoverSize=hexSize
    )
    spots = []
    xs, ys, labels = drawHexGridLoop2((0, 0), 14, 1e-6, 0)
    for i, thing in enumerate(xs):
        spots.append({'pos': (xs[i], ys[i]), 'size': hexSize, 'pen': {'color': 'w', 'width': 2}, 'brush':pg.intColor(10, 10), 'symbol':'h'})
    s.addPoints(spots)
    w.addItem(s)

    return w, s, spots, xs, ys

hexSize = 2.2e-6
app = pg.mkQApp("Scatter Plot Item Example") 
mw = QtWidgets.QMainWindow()
mw.resize(800,800)
view = pg.GraphicsLayoutWidget()  ## GraphicsView with GraphicsLayout inserted by default
mw.setCentralWidget(view)
mw.show()
mw.setWindowTitle('pyqtgraph example: ScatterPlot')
view.ci.setBorder((50, 50, 100))

## create four areas to add plots
w1 = view.addViewBox()
w1.setAspectLocked()
w2 = view.addViewBox()
w2.setAspectLocked()
view.nextRow()
w3 = view.addViewBox()
w3.setAspectLocked()
w4 = view.addViewBox()
w4.setAspectLocked()

# Create the scatter plots.
w1, s1, spots1, xs, ys = createArray(w1)
w2, s2, spots1, xs, ys = createArray(w2)
w3, s3, spots1, xs, ys = createArray(w3)
w4, s4, spots1, xs, ys = createArray(w4)

# Create the color map.
# Adapted from https://github.com/pyqtgraph/pyqtgraph/issues/1712#issuecomment-819745370
nPts = 255
colormap = pg.colormap.get('cividis')
valueRange = np.linspace(0, 255, num=nPts)
colors = colormap.getLookupTable(0, 1, nPts=nPts)

# This is really slow!
fps = None
lastTime = perf_counter()
def update():
    global fps, lastTime
    z = np.random.randint(0,255, size=547)
    brushes = colors[np.searchsorted(valueRange, z)]
    s1.setBrush(brushes) # Is there a faster way to do this?
    s2.setBrush(brushes)
    s3.setBrush(brushes)
    s4.setBrush(brushes)
    now = perf_counter()
    dt = now - lastTime
    lastTime = now
    if fps is None:
        fps = 1.0 / dt
    else:
        s = np.clip(dt * 3., 0, 1)
        fps = fps * (1 - s) + (1.0 / dt) * s
    mw.setWindowTitle('%0.2f fps' % fps)

    
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)

if __name__ == '__main__':
    pg.exec()
优化方案

核心问题

ScatterPlotItem.setBrush()批量更新效率低的原因是:每次调用都会重新创建所有QBrush对象,且触发多次重绘逻辑;同时更新4个独立的ScatterPlotItem会放大性能开销。

优化步骤

  1. 直接操作内部数据,避免API封装开销
    PyQtGraph的ScatterPlotItem内部维护spots列表,直接修改每个spot的brush属性,再手动触发一次重绘,比调用setBrush效率高得多。

  2. 预转换颜色为QColor对象
    提前把颜色映射表转换成QColor数组,避免每次更新时重复创建对象,减少计算量。

  3. 批量重绘,减少触发次数
    修改完所有spot的brush后,调用sigPlotChanged.emit()触发一次重绘,替代setBrush自动触发的多次重绘。

优化后的代码片段

预转换颜色列表

替换原颜色映射表创建代码:

nPts = 255
colormap = pg.colormap.get('cividis')
# 预生成QColor数组,避免每次更新重复转换
color_list = [pg.mkColor(c) for c in colormap.getLookupTable(0, 1, nPts=nPts)]

修改update函数

def update():
    global fps, lastTime
    z = np.random.randint(0,255, size=547)
    
    # 遍历所有散点图项,直接修改内部spot的brush
    for s in [s1, s2, s3, s4]:
        spots = s.spots()
        for i, spot in enumerate(spots):
            spot.brush = pg.mkBrush(color_list[z[i]])
        # 手动触发一次重绘
        s.sigPlotChanged.emit()
    
    # FPS计算逻辑保持不变
    now = perf_counter()
    dt = now - lastTime
    lastTime = now
    if fps is None:
        fps = 1.0 / dt
    else:
        s = np.clip(dt * 3., 0, 1)
        fps = fps * (1 - s) + (1.0 / dt) * s
    mw.setWindowTitle('%0.2f fps' % fps)

额外优化建议

  • 复用坐标数据:当前为4个ScatterPlotItem重复生成相同的六边形坐标,只需生成一次后复制给四个item,减少初始化开销。
  • 关闭不必要交互:如果不需要hover效果,去掉hoverable=True可进一步提升性能。
  • 开启OpenGL渲染:创建ScatterPlotItem时添加useOpenGL=True,利用GPU加速渲染,对大量点的场景提升显著:
    s = pg.ScatterPlotItem(
        pxMode=False,
        hoverable=True,
        hoverPen=pg.mkPen('g'),
        hoverSize=hexSize,
        useOpenGL=True  # 启用GPU加速
    )
    

内容的提问来源于stack exchange,提问作者zdhughes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 18:15:47