微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

pyqtgraph / PlotCurveItem的实时可视化瓶颈

如何解决pyqtgraph / PlotCurveItem的实时可视化瓶颈

我目前正在使用pyqtgraph可视化64个独立数据迹线/图的实时数据。虽然速度确实不错,但我注意到,如果样本缓冲区长度超过2000点,速度会大大降低。对以下代码进行概要分析可以得出function.py:1440(arrayToQPath)似乎产生了重大影响:

import numpy
import cProfile
import logging

import pyqtgraph as pg
from PyQt5 import QtCore,uic
from PyQt5.QtGui import *
from PyQt5.QtCore import QRect,QTimer


def program(columns=8,samples=10000,channels=64):
    app = QApplication([])
    win = pg.GraphicsWindow()
    pg.setConfigOptions(imageAxisOrder='row-major')
    win.resize(1280,768)
    win.ci.layout.setSpacing(0)
    win.ci.layout.setContentsMargins(0,0)

    data            = numpy.zeros((samples,channels+1))
    plots           = [win.addplot(row=i/columns+1,col=i%columns) for i in range(channels)]
    curves          = list()

    x = numpy.linspace(0,1,samples,endpoint=True)
    f = 2 # Frequency in Hz
    A = 1 # Amplitude in Unit
    y = A * numpy.sin(2*numpy.pi*f*x).reshape((samples,1)) # Signal

    data[:,0]   = x
    data[:,1:]  = numpy.repeat(y,channels,axis=1)
    
    for chn_no,p in enumerate(plots,1):
        c       = pg.PlotCurveItem(pen=(chn_no,channels * 1.3))
        p.addItem(c)
        curves.append((c,chn_no))
          
    def update():
        nonlocal data

        data[:,1:] = numpy.roll(data[:,1:],100,axis=0)
            
        for curve,data_index in curves:
            curve.setData(data[:,0],data[:,data_index])

    timer = QTimer()
    timer.timeout.connect(update)
    timer.start(30)
    return app.exec_()   


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    cProfile.run("program()",sort="cumtime")
    #program()
  ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000  533.660  533.660 {built-in method builtins.exec}
        1    0.053    0.053  533.660  533.660 <string>:1(<module>)
        1    0.018    0.018  533.607  533.607 pyqtgraph_test.py:11(program)
        1    9.181    9.181  532.209  532.209 {built-in method exec_}
     2709    0.015    0.000  401.728    0.148 GraphicsView.py:153(paintEvent)
     2709   15.572    0.006  401.696    0.148 {paintEvent}
   173376    0.193    0.000  345.725    0.002 debug.py:89(w)
   173376    1.599    0.000  345.532    0.002 PlotCurveItem.py:452(paint)
   173312    0.671    0.000  271.973    0.002 PlotCurveItem.py:440(getPath)
   173312    0.744    0.000  271.153    0.002 PlotCurveItem.py:416(generatePath)
   173312  266.888    0.002  270.409    0.002 functions.py:1440(arrayToQPath)
     2709    5.102    0.002  113.195    0.042 pyqtgraph_test.py:36(update)
   173440    0.193    0.000  100.616    0.001 PlotCurveItem.py:297(setData)
   173440    8.718    0.000  100.424    0.001 PlotCurveItem.py:337(updateData)

因此,每次通话大约要花费1.5毫秒。在玩arrayToQPath时,我注意到arrayToQPath中的ds >> path似乎大部分时间都在消耗(该行的结果被注释掉了):

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000  190.847  190.847 {built-in method builtins.exec}
        1    0.050    0.050  190.847  190.847 <string>:1(<module>)
        1    0.017    0.017  190.796  190.796 pyqtgraph_test.py:11(program)
        1    7.438    7.438  189.395  189.395 {built-in method exec_}
     2221    4.165    0.002   88.497    0.040 pyqtgraph_test.py:36(update)
     2221    0.010    0.000   86.830    0.039 GraphicsView.py:153(paintEvent)
     2221   11.494    0.005   86.806    0.039 {paintEvent}
   142208    0.152    0.000   77.941    0.001 PlotCurveItem.py:297(setData)
   142208    4.500    0.000   77.789    0.001 PlotCurveItem.py:337(updateData)

ds是QtCore.QDataStream,路径是QPainterPath。但是,>>操作要花很多时间的原因完全使我无法理解。所以我正在寻找一种可能的方法来加快渲染速度,并希望坚持使用pyqtgraph,即不执行切换到例如现在可见。

原始函数。pyarrayToQPath:

def arrayToQPath(x,y,connect='all'):
    """Convert an array of x,y coordinats to QPainterPath as efficiently as possible.
    The *connect* argument may be 'all',indicating that each point should be
    connected to the next; 'pairs',indicating that each pair of points
    should be connected,or an array of int32 values (0 or 1) indicating
    connections.
    """

    ## Create all vertices in path. The method used below creates a binary format so that all
    ## vertices can be read in at once. This binary format may change in future versions of Qt,## so the original (slower) method is left here for emergencies:
        #path.moveto(x[0],y[0])
        #if connect == 'all':
            #for i in range(1,y.shape[0]):
                #path.lineto(x[i],y[i])
        #elif connect == 'pairs':
            #for i in range(1,y.shape[0]):
                #if i%2 == 0:
                    #path.lineto(x[i],y[i])
                #else:
                    #path.moveto(x[i],y[i])
        #elif isinstance(connect,np.ndarray):
            #for i in range(1,y.shape[0]):
                #if connect[i] == 1:
                    #path.lineto(x[i],y[i])
        #else:
            #raise Exception('connect argument must be "all","pairs",or array')

    ## Speed this up using >> operator
    ## Format is:
    ##    numVerts(i4)   0(i4)
    ##    x(f8)   y(f8)   0(i4)    <-- 0 means this vertex does not connect
    ##    x(f8)   y(f8)   1(i4)    <-- 1 means this vertex connects to the prevIoUs vertex
    ##    ...
    ##    0(i4)
    ##
    ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i')

    path = QtGui.QPainterPath()

    #profiler = debug.Profiler()
    n = x.shape[0]
    # create empty array,pad with extra space on either end
    arr = np.empty(n+2,dtype=[('x','>f8'),('y',('c','>i4')])
    # write first two integers
    #profiler('allocate empty')
    byteview = arr.view(dtype=np.ubyte)
    byteview[:12] = 0
    byteview.data[12:20] = struct.pack('>ii',n,0)
    #profiler('pack header')
    # Fill array with vertex values
    arr[1:-1]['x'] = x
    arr[1:-1]['y'] = y

    # decide which points are connected by lines
    if eq(connect,'all'):
        arr[1:-1]['c'] = 1
    elif eq(connect,'pairs'):
        arr[1:-1]['c'][::2] = 1
        arr[1:-1]['c'][1::2] = 0
    elif eq(connect,'finite'):
        arr[1:-1]['c'] = np.isfinite(x) & np.isfinite(y)
    elif isinstance(connect,np.ndarray):
        arr[1:-1]['c'] = connect
    else:
        raise Exception('connect argument must be "all","finite",or array')

    #profiler('fill array')
    # write last 0
    lastInd = 20*(n+1)
    byteview.data[lastInd:lastInd+4] = struct.pack('>i',0)
    #profiler('footer')
    # create datastream object and stream into path

    ## Avoiding this method because QByteArray(str) leaks memory in PySide
    #buf = QtCore.QByteArray(arr.data[12:lastInd+4])  # I think one unnecessary copy happens here

    path.strn = byteview.data[12:lastInd+4] # make sure data doesn't run away
    try:
        buf = QtCore.QByteArray.fromrawData(path.strn)
    except TypeError:
        buf = QtCore.QByteArray(bytes(path.strn))
    #profiler('create buffer')
    ds = QtCore.QDataStream(buf)

    ds >> path
    #profiler('load')

    return path

编辑:

仔细研究QT,发现C ++中的QDataStream >>运算符相当慢。它是如此之慢,以至于覆盖旧的QtGui.QPainterPath()中的元素的位置而不是创建新的元素的速度更快:

import timeit
import struct
import numpy as np
from PyQt5 import QtGui,QtCore

no_trys = 1000

def test(pass_data,samples = 10000):
    path = QtGui.QPainterPath()

    n = samples
    # create empty array,pad with extra space on either end
    arr = np.zeros(n+2,'>i4')])
    # write first two integers
    byteview = arr.view(dtype=np.ubyte)
    byteview.data[12:20] = struct.pack('>ii',0)

    # write last 0
    lastInd = 20*(n+1)
    # create datastream object and stream into path
    path.strn = byteview.data[12:lastInd+4] # make sure data doesn't run away
    buf = QtCore.QByteArray.fromrawData(path.strn)
    ds = QtCore.QDataStream(buf)

    path.reserve(n)
    if pass_data:
        ds >> path

    def func1():
        nonlocal path

        ds = QtCore.QDataStream(buf)
        ds >> path

    def func2():
        nonlocal path
        values = [(i,i,i) for i in range(samples)]
        map(path.setElementPositionAt,values)

    print(timeit.timeit(func1,number=no_trys))
    print(timeit.timeit(func2,number=no_trys))


test(True)

对于DataStream,结果为1.32 s,对地图(path.setElementPositionAt,值)为0.9 s。

在我的计算机上对以下C ++代码段进行概要分析会导致8秒钟以上的时间:

#include <QtCore/QDataStream>
#include <QtGui/QPainterPath>

int function2(const int samples)
{
    auto size = 8 + samples * 20 + 4;

    std::vector<char> data(size,0);

    memcpy(data.data(),&samples,4);

    QByteArray buf(QByteArray::fromrawData(data.data(),size));
    QDataStream ds(buf);

    float ret;
    for (int counter = 0; counter < samples; counter++)
    {
        int type = 1;
        double x = 0,y = 0;

        ds >> type >> x >> y;
        ret = type + x + y;
    }
    return ret;
}

int main()
{    
    const int samples = 10000;
    const int tries = 10000;
    int ret = 0;

    auto start = std::chrono::high_resolution_clock::Now();

    for (auto counter = 0; counter < tries; counter++)
    {
        ret += function2(samples);
    }
    auto end = std::chrono::high_resolution_clock::Now();
    std::chrono::duration<double> elapsed = end - start;

    std::cout << "done\n";
    std::cout << "Elapsed time: " << elapsed.count() << " s\n";
    std::cout << ret;

    return 0;
}

解决方法

最简单的解决方案是激活OpenGL模式,即安装 PyOpenGL PyOpenGL-accelerate 模块并启用OpenGL。这样,就完全省去了createPath部分。我只是在应用程序中添加了以下代码块:

try:
    import OpenGL
    pg.setConfigOption('useOpenGL',True)
    pg.setConfigOption('enableExperimental',True)
except Exception as e:
    print(f"Enabling OpenGL failed with {e}. Will result in slow rendering. Try installing PyOpenGL.")

我的PC可以绘制30000个数据点的64条迹线而不会费力。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。