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会放大性能开销。
优化步骤
直接操作内部数据,避免API封装开销
PyQtGraph的ScatterPlotItem内部维护spots列表,直接修改每个spot的brush属性,再手动触发一次重绘,比调用setBrush效率高得多。预转换颜色为QColor对象
提前把颜色映射表转换成QColor数组,避免每次更新时重复创建对象,减少计算量。批量重绘,减少触发次数
修改完所有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

