如何在PyQt5 GUI中制作快速的matplotlib实时图表

4
几年前,我已经尝试将实时的 `matplotlib` 绘图嵌入到一个 `PyQt5` 图形用户界面中。实时绘图显示来自传感器、某些过程等数据流的实时数据。我已经把它做好了,你可以在这里阅读相关的帖子: 现在我需要再次实现同样的功能。我记得我的先前方法可行,但无法跟上快速的数据流。我在互联网上找到了一些示例代码,我想向您展示其中几个。其中一个明显比另一个快,但我不知道为什么。我想获得更多信息。我相信深入的理解将使我与 `PyQt5` 和 `matplotlib` 的交互更加高效。

1. 第一个示例

此示例基于以下文章:
https://matplotlib.org/3.1.1/gallery/user_interfaces/embedding_in_qt_sgskip.html
这篇文章来自于官方的 `matplotlib` 网站,介绍了如何将一个 matplotlib 图形嵌入到一个 `PyQt5` 窗口中。
我对示例代码进行了一些小的调整,但基本原理仍然相同。请将以下代码复制粘贴到 Python 文件中并运行:
#####################################################################################
#                                                                                   #
#                PLOT A LIVE GRAPH IN A PYQT WINDOW                                 #
#                EXAMPLE 1                                                          #
#               ------------------------------------                                #
# This code is inspired on:                                                         #
# https://matplotlib.org/3.1.1/gallery/user_interfaces/embedding_in_qt_sgskip.html  #
#                                                                                   #
#####################################################################################

from __future__ import annotations
from typing import *
import sys
import os
from matplotlib.backends.qt_compat import QtCore, QtWidgets
# from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
# from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib as mpl
import numpy as np

class ApplicationWindow(QtWidgets.QMainWindow):
    '''
    The PyQt5 main window.

    '''
    def __init__(self):
        super().__init__()
        # 1. Window settings
        self.setGeometry(300, 300, 800, 400)
        self.setWindowTitle("Matplotlib live plot in PyQt - example 1")
        self.frm = QtWidgets.QFrame(self)
        self.frm.setStyleSheet("QWidget { background-color: #eeeeec; }")
        self.lyt = QtWidgets.QVBoxLayout()
        self.frm.setLayout(self.lyt)
        self.setCentralWidget(self.frm)

        # 2. Place the matplotlib figure
        self.myFig = MyFigureCanvas(x_len=200, y_range=[0, 100], interval=20)
        self.lyt.addWidget(self.myFig)

        # 3. Show
        self.show()
        return

class MyFigureCanvas(FigureCanvas):
    '''
    This is the FigureCanvas in which the live plot is drawn.

    '''
    def __init__(self, x_len:int, y_range:List, interval:int) -> None:
        '''
        :param x_len:       The nr of data points shown in one plot.
        :param y_range:     Range on y-axis.
        :param interval:    Get a new datapoint every .. milliseconds.

        '''
        super().__init__(mpl.figure.Figure())
        # Range settings
        self._x_len_ = x_len
        self._y_range_ = y_range

        # Store two lists _x_ and _y_
        self._x_ = list(range(0, x_len))
        self._y_ = [0] * x_len

        # Store a figure ax
        self._ax_ = self.figure.subplots()

        # Initiate the timer
        self._timer_ = self.new_timer(interval, [(self._update_canvas_, (), {})])
        self._timer_.start()
        return

    def _update_canvas_(self) -> None:
        '''
        This function gets called regularly by the timer.

        '''
        self._y_.append(round(get_next_datapoint(), 2))     # Add new datapoint
        self._y_ = self._y_[-self._x_len_:]                 # Truncate list _y_
        self._ax_.clear()                                   # Clear ax
        self._ax_.plot(self._x_, self._y_)                  # Plot y(x)
        self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
        self.draw()
        return

# Data source
# ------------
n = np.linspace(0, 499, 500)
d = 50 + 25 * (np.sin(n / 8.3)) + 10 * (np.sin(n / 7.5)) - 5 * (np.sin(n / 1.5))
i = 0
def get_next_datapoint():
    global i
    i += 1
    if i > 499:
        i = 0
    return d[i]

if __name__ == "__main__":
    qapp = QtWidgets.QApplication(sys.argv)
    app = ApplicationWindow()
    qapp.exec_()


您应该看到以下窗口: enter image description here 第二个示例:
我在这里找到了另一个实时matplotlib图形的例子:
https://learn.sparkfun.com/tutorials/graph-sensor-data-with-python-and-matplotlib/speeding-up-the-plot-animation
然而,作者没有使用PyQt5来嵌入他的实时绘图。因此,我稍微修改了一下代码,以在PyQt5窗口中获得绘图:
#####################################################################################
#                                                                                   #
#                PLOT A LIVE GRAPH IN A PYQT WINDOW                                 #
#                EXAMPLE 2                                                          #
#               ------------------------------------                                #
# This code is inspired on:                                                         #
# https://learn.sparkfun.com/tutorials/graph-sensor-data-with-python-and-matplotlib/speeding-up-the-plot-animation  #
#                                                                                   #
#####################################################################################

from __future__ import annotations
from typing import *
import sys
import os
from matplotlib.backends.qt_compat import QtCore, QtWidgets
# from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
# from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib as mpl
import matplotlib.figure as mpl_fig
import matplotlib.animation as anim
import numpy as np

class ApplicationWindow(QtWidgets.QMainWindow):
    '''
    The PyQt5 main window.

    '''
    def __init__(self):
        super().__init__()
        # 1. Window settings
        self.setGeometry(300, 300, 800, 400)
        self.setWindowTitle("Matplotlib live plot in PyQt - example 2")
        self.frm = QtWidgets.QFrame(self)
        self.frm.setStyleSheet("QWidget { background-color: #eeeeec; }")
        self.lyt = QtWidgets.QVBoxLayout()
        self.frm.setLayout(self.lyt)
        self.setCentralWidget(self.frm)

        # 2. Place the matplotlib figure
        self.myFig = MyFigureCanvas(x_len=200, y_range=[0, 100], interval=20)
        self.lyt.addWidget(self.myFig)

        # 3. Show
        self.show()
        return

class MyFigureCanvas(FigureCanvas, anim.FuncAnimation):
    '''
    This is the FigureCanvas in which the live plot is drawn.

    '''
    def __init__(self, x_len:int, y_range:List, interval:int) -> None:
        '''
        :param x_len:       The nr of data points shown in one plot.
        :param y_range:     Range on y-axis.
        :param interval:    Get a new datapoint every .. milliseconds.

        '''
        FigureCanvas.__init__(self, mpl_fig.Figure())
        # Range settings
        self._x_len_ = x_len
        self._y_range_ = y_range

        # Store two lists _x_ and _y_
        x = list(range(0, x_len))
        y = [0] * x_len

        # Store a figure and ax
        self._ax_  = self.figure.subplots()
        self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
        self._line_, = self._ax_.plot(x, y)

        # Call superclass constructors
        anim.FuncAnimation.__init__(self, self.figure, self._update_canvas_, fargs=(y,), interval=interval, blit=True)
        return

    def _update_canvas_(self, i, y) -> None:
        '''
        This function gets called regularly by the timer.

        '''
        y.append(round(get_next_datapoint(), 2))     # Add new datapoint
        y = y[-self._x_len_:]                        # Truncate list _y_
        self._line_.set_ydata(y)
        return self._line_,

# Data source
# ------------
n = np.linspace(0, 499, 500)
d = 50 + 25 * (np.sin(n / 8.3)) + 10 * (np.sin(n / 7.5)) - 5 * (np.sin(n / 1.5))
i = 0
def get_next_datapoint():
    global i
    i += 1
    if i > 499:
        i = 0
    return d[i]

if __name__ == "__main__":
    qapp = QtWidgets.QApplication(sys.argv)
    app = ApplicationWindow()
    qapp.exec_()


结果得到的动态图是完全相同的。但是,如果你开始使用MyFigureCanvas()构造函数中的interval参数,你会发现第一个例子无法跟随。第二个例子可以更快地运行。
3. 问题
我有几个问题想要向您提出:
- QtCoreQtWidgets类可以这样导入:
from matplotlib.backends.qt_compat import QtCore, QtWidgets
或者这样导入:
from PyQt5 import QtWidgets, QtCore
两种方法都一样好。是否有理由优先选择其中一种?
- FigureCanvas可以这样导入:
from matplotlib.backends.backend_qt5agg import FigureCanvas
或者这样导入:
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
但是我已经弄清楚了为什么。 backend_qt5agg文件似乎将FigureCanvas定义为FigureCanvasQTAgg的别名。
- 为什么第二个例子比第一个例子快那么多?老实说,这让我感到惊讶。第一个示例基于官方matplotlib网站上的一个网页。我期望那个更好。
- 您有任何建议可以使第二个示例运行得更快吗?
4. 编辑
根据这个网页:
https://bastibe.de/2013-05-30-speeding-up-matplotlib.html
我修改了第一个示例以增加其速度。请查看以下代码:
#####################################################################################
#                                                                                   #
#                PLOT A LIVE GRAPH IN A PYQT WINDOW                                 #
#                EXAMPLE 1 (modified for extra speed)                               #
#               --------------------------------------                              #
# This code is inspired on:                                                         #
# https://matplotlib.org/3.1.1/gallery/user_interfaces/embedding_in_qt_sgskip.html  #
# and on:                                                                           #
# https://bastibe.de/2013-05-30-speeding-up-matplotlib.html                         #
#                                                                                   #
#####################################################################################

from __future__ import annotations
from typing import *
import sys
import os
from matplotlib.backends.qt_compat import QtCore, QtWidgets
# from PyQt5 import QtWidgets, QtCore
from matplotlib.backends.backend_qt5agg import FigureCanvas
# from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import matplotlib as mpl
import numpy as np

class ApplicationWindow(QtWidgets.QMainWindow):
    '''
    The PyQt5 main window.

    '''
    def __init__(self):
        super().__init__()
        # 1. Window settings
        self.setGeometry(300, 300, 800, 400)
        self.setWindowTitle("Matplotlib live plot in PyQt - example 1 (modified for extra speed)")
        self.frm = QtWidgets.QFrame(self)
        self.frm.setStyleSheet("QWidget { background-color: #eeeeec; }")
        self.lyt = QtWidgets.QVBoxLayout()
        self.frm.setLayout(self.lyt)
        self.setCentralWidget(self.frm)

        # 2. Place the matplotlib figure
        self.myFig = MyFigureCanvas(x_len=200, y_range=[0, 100], interval=1)
        self.lyt.addWidget(self.myFig)

        # 3. Show
        self.show()
        return

class MyFigureCanvas(FigureCanvas):
    '''
    This is the FigureCanvas in which the live plot is drawn.

    '''
    def __init__(self, x_len:int, y_range:List, interval:int) -> None:
        '''
        :param x_len:       The nr of data points shown in one plot.
        :param y_range:     Range on y-axis.
        :param interval:    Get a new datapoint every .. milliseconds.

        '''
        super().__init__(mpl.figure.Figure())
        # Range settings
        self._x_len_ = x_len
        self._y_range_ = y_range

        # Store two lists _x_ and _y_
        self._x_ = list(range(0, x_len))
        self._y_ = [0] * x_len

        # Store a figure ax
        self._ax_ = self.figure.subplots()
        self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1]) # added
        self._line_, = self._ax_.plot(self._x_, self._y_)                  # added
        self.draw()                                                        # added

        # Initiate the timer
        self._timer_ = self.new_timer(interval, [(self._update_canvas_, (), {})])
        self._timer_.start()
        return

    def _update_canvas_(self) -> None:
        '''
        This function gets called regularly by the timer.

        '''
        self._y_.append(round(get_next_datapoint(), 2))     # Add new datapoint
        self._y_ = self._y_[-self._x_len_:]                 # Truncate list y

        # Previous code
        # --------------
        # self._ax_.clear()                                   # Clear ax
        # self._ax_.plot(self._x_, self._y_)                  # Plot y(x)
        # self._ax_.set_ylim(ymin=self._y_range_[0], ymax=self._y_range_[1])
        # self.draw()

        # New code
        # ---------
        self._line_.set_ydata(self._y_)
        self._ax_.draw_artist(self._ax_.patch)
        self._ax_.draw_artist(self._line_)
        self.update()
        self.flush_events()
        return

# Data source
# ------------
n = np.linspace(0, 499, 500)
d = 50 + 25 * (np.sin(n / 8.3)) + 10 * (np.sin(n / 7.5)) - 5 * (np.sin(n / 1.5))
i = 0
def get_next_datapoint():
    global i
    i += 1
    if i > 499:
        i = 0
    return d[i]

if __name__ == "__main__":
    qapp = QtWidgets.QApplication(sys.argv)
    app = ApplicationWindow()
    qapp.exec_()


结果非常惊人。这些修改使得第一个示例的速度显著提高!然而,我不知道这是否使得第一个示例现在和第二个示例的速度相同。它们肯定接近。有人知道哪个更快吗?
此外,我注意到左侧缺少一条竖线,顶部缺少一条横线: enter image description here 这不是什么大问题,但我只是想知道为什么会这样。

3
这个链接可以帮助你:https://bastibe.de/2013-05-30-speeding-up-matplotlib.html。 - Right leg
2
(1) 我总是直接从PyQt5导入。Matplotlib的东西只是一个包装器,允许使用pyqt4或pyqt5运行相同的代码。(2) 我想你找到了原因。(3) 第二个代码使用blitting。部分内容在这里有解释。(4) 不,如果您需要更快的动画,请不要使用Matplotlib。pyqtgraph会很方便,就像这个问答中所述。 - ImportanceOfBeingErnest
嗨@Rightleg,感谢您提供这个链接。基于该网页,我能够修改第一个示例,从而获得巨大的速度提升。但是我不知道它是否已经达到了第二个示例的速度,它们现在肯定非常接近了。 - K.Mulier
嗨@ImportanceOfBeingErnest,感谢您提供关于pyqtgraph的提示。我以前尝试过它,并且同意它是一个不错的选择。然而,这个包的开发似乎几乎被放弃了。最后一个版本0.10.0是在2016年11月5日发布的。 - K.Mulier
只要它能工作... 我的意思是尽管matplotlib不断发展,但blitting功能甚至更旧,从未被触及过。 - ImportanceOfBeingErnest
我没有看到这个问题被更新。因此,顶部和左侧缺失的脊柱是由于仅重新绘制了轴补丁,而不是脊柱。它们因此被埋在更新后的补丁下面。如果您查看我链接的解决方案,它使用fig.canvas.restore_region(axbackground)来恢复背景,并仅使线本身闪烁。 - ImportanceOfBeingErnest
1个回答

3
第二种情况(使用 FuncAnimation)更快,因为 它使用了 "blitting", 可以避免在每一帧之间重新绘制不变的元素。
matplotlib 网站提供的嵌入 qt 的示例并没有考虑速度,因此性能较差。您会注意到它在每次迭代时都调用 ax.clear()ax.plot(),导致整个画布每次都要重新绘制。如果您使用与 FuncAnimation 中相同的代码(也就是创建一个 Axes 和一个 artist,并更新 artist 中的数据,而不是每次创建一个新的 artist),您应该可以获得非常接近的性能。

1
嗨@Diziet Asahi,非常感谢您提供的这些见解。我正在尝试只更新某些艺术家的数据,以加快第一个示例。第一个示例(不使用FuncAnimation)是否会比第二个示例(使用FuncAnimation)表现更好? - K.Mulier
1
我认为不会,因为在FuncAnimation中使用blitting应该会提供一些性能提升。然而,@right-leg提供的博客文章手动实现了blitting,并且可以在你的代码中实现。很有趣的是哪种方法提供更好的性能,请报告结果。 - Diziet Asahi
嗨@Diziet Asahi,我刚刚做了一个测试(你可以在我的帖子的编辑中阅读所有内容)。修改后的第一个示例现在非常快。但我无法确定它是否与原始的第二个示例同样快。 - K.Mulier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接