【问题标题】:Accurate timer with PyQt使用 PyQt 的精确计时器
【发布时间】:2020-03-01 03:15:21
【问题描述】:

我正在使用 pyqtgraph 绘制从传感器接收到的大量数据。

为此,我创建了一个线程来获取数据并放入队列中。为了绘制数据,如果队列不为空,我会使用计时器定期检查。 问题是计时器(QTimer)的准确性似乎非常糟糕。我的意思是当测量线程中的负载较低(睡眠 1000/100 毫秒)时,精度很好,但是当负载增加(睡眠 10 毫秒)时,我用于绘制数据的更新函数不会被回调期间。

这是一个示例代码:

import sys
import time
from queue import Queue
from random import random

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

data_queue = Queue()


class WorkerThread(QtCore.QThread):
    def __init__(self, parent):
        super(WorkerThread, self).__init__(parent=parent)

    def run(self):
        t_init = time.time()
        while True:
            # Generating random data
            values = [(time.time()-t_init, random()) for _ in range(200)]
            data_queue.put(values)
            print("adding data")
            self.msleep(10)


class GraphPlot(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(GraphPlot, self).__init__(parent)

        self.mainbox = QtWidgets.QWidget()
        self.setCentralWidget(self.mainbox)
        self.mainbox.setLayout(QtWidgets.QVBoxLayout())

        self.canvas = pg.GraphicsLayoutWidget()
        self.mainbox.layout().addWidget(self.canvas)

        self.analogPlot = self.canvas.addPlot(title='Real-time data')
        self.drawplot = self.analogPlot.plot(pen='r')

        numPoints = 20000
        self.t = np.zeros(numPoints, dtype=int)
        self.x = np.zeros(numPoints, dtype=int)

        self.worker = WorkerThread(self)
        self.worker.start()

        self.timer = pg.QtCore.QTimer()
        self.timer.setTimerType(QtCore.Qt.PreciseTimer)
        self.timer.timeout.connect(self._update)
        self.timer.start(1)

    def _update(self):
        print('start:', time.time())
        size = data_queue.qsize()
        if size > 0:
            for _ in range(size):
                values = data_queue.get()
                for v in values:
                    self.t = np.append(self.t[1:], v[0])
                    self.x = np.append(self.x[1:], v[1])
            self.drawplot.setData(self.t, self.x)        
        print('end:', time.time())


app = QtWidgets.QApplication(sys.argv)
plot = GraphPlot()
plot.show()
sys.exit(app.exec_())

输出的摘录:

start: 1572893919.9067862
adding data
end: 1572893919.9217482    <-- 
adding data
start: 1572893919.9586473  <-- there should be 1ms of difference with last 'end'
                               actually, there is around 37ms

无论测量线程上的负载如何,我都希望计时器与同一周期同步。我尝试降低前一个线程的优先级,但并没有解决问题。

【问题讨论】:

  • 您想要更低的延迟?操作系统可以实现的延迟有一个限制,发生了很多事情,如果您正在运行线程并且存在上下文切换、页面错误等,那么 1ms 非常低,您很可能会遇到一些抖动在这种情况下,或者永远不会得到 1 毫秒的延迟,
  • 好吧 QTimer 是一个异步函数——对我来说这意味着独立的进程,因为它似乎不受 Python 线程 GIL 的影响——而且它是事件驱动的,因此它的调用被放置在事件处理程序队列然后在事件处理程序到达它时进行处理,这可能会受到许多事情的影响——始终如一地获得 1ms 延迟的唯一方法是在事件处理程序之外执行——这意味着你以某种方式将它放在线性代码中。我知道的细节不多,但是有无数种方法可以做到这一点,没有一种是明显错误的,只是由于不同的原因而不同

标签: python pyqt pyqtgraph


【解决方案1】:

QTimer documentation 部分回答了您的问题:

如果系统繁忙,所有计时器类型都可能比预期的超时时间晚 或无法提供要求的准确性。在这种超时情况下 溢出,即使多次超时,Qt 也只会发出一次 timeout() 已过期,然后将恢复原来的间隔。

问题是在你从超时调用_update 之后,Qt 需要一些时间来处理self.drawplot.setData() 之后发生的事情,这基本上是计算图形信息并将其实际绘制在屏幕上。
你没有得到 1ms 的延迟,因为 Qt 不能那么快地工作。

即使 QTimer 可以在另一个线程中工作(“异步”,但要注意这个词的含义),它总是依赖于它创建或驻留的线程(QTimer 不能从一个线程启动或停止不同于它的一个)。因此,由于您已经在窗口线程(Qt 主事件循环)中创建了计时器,其超时精度取决于该循环处理 所有 其事件的能力,并且由于很多事件是与 GUI 绘制相关(在我们看来,这似乎很快,但实际上 缓慢,因为它对 CPU 要求很高),您可以很容易地理解为什么您永远无法获得 1ms 的间隔。并且不要忘记即使 pyqtgraph 非常快,我们仍然在谈论 Python。

虽然 1ms QTimer 可以达到更好的精度(为它创建一个单独的线程),但无论如何您都不会从中获得任何好处:即使使用非常快的计算机,您实际上需要的是更新屏幕为 1000Hz,而大多数图形硬件不能比 100-200Hz 快得多;这意味着即使您拥有高端系统,每 4 毫秒您也不会获得超过一次的更新。

如果您需要在每次有新数据时更新绘图,最好使用信号和插槽,这也可以避免对队列进行任何不必要的检查,并确保尽可能多地更新绘图:

class WorkerThread(QtCore.QThread):
    newData = QtCore.pyqtSignal(object)
    def __init__(self, parent):
        super(WorkerThread, self).__init__(parent=parent)

    def run(self):
        t_init = time.time()
        while True:
            # Generating random data
            values = [(time.time()-t_init, random()) for _ in range(200)]
            print("adding data")
            self.newData.emit(values)
            self.msleep(10)

class GraphPlot(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(GraphPlot, self).__init__(parent)

        self.mainbox = QtWidgets.QWidget()
        self.setCentralWidget(self.mainbox)
        self.mainbox.setLayout(QtWidgets.QVBoxLayout())

        self.canvas = pg.GraphicsLayoutWidget()
        self.mainbox.layout().addWidget(self.canvas)

        self.analogPlot = self.canvas.addPlot(title='Real-time data')
        self.drawplot = self.analogPlot.plot(pen='r')

        numPoints = 20000
        self.t = np.zeros(numPoints, dtype=int)
        self.x = np.zeros(numPoints, dtype=int)

        self.worker = WorkerThread(self)
        self.worker.newData.connect(self.newData)
        self.worker.start()

    def newData(self, data):
        print('start:', time.time())
        for v in data:
            self.t = np.append(self.t[1:], v[0])
            self.x = np.append(self.x[1:], v[1])
        self.drawplot.setData(self.t, self.x)
        print('end:', time.time())

你不会得到 1 毫秒的更新,但无论如何也不需要它;另外,请记住,以这种速度打印总是会以某种方式影响性能。

最后,将PreciseTimer 设置为 1 毫秒的间隔没有任何优势,因为无论如何,大多数平台上的计时器精度约为 1 毫秒(如文档 linked before 中同一段落开头所述),并且设置只有更长的时间间隔才需要精度(我会说至少 25-50 毫秒)。

还有一个关于 QTimer here 的有趣答案,解释了当你创建一个超时时基本上会发生什么。

【讨论】:

  • 感谢所有这些细节。我的第一个实现是使用线性代码,即我用计时器调用更新函数,在这个更新函数中,我获取数据然后更新 PlotItem。使用这种方法,我的 fps 是线程的两倍。我不明白为什么使用线程会有这样的差异,应该更好,不是吗?
  • 对不起,你能说得更具体些吗?在您的第一个实现中(如果我理解正确,这比您的示例更快?)您是如何“获取”数据的?计时器间隔是多少?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-14
  • 2017-08-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多