简体   繁体   English

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

[英]How to make a fast matplotlib live plot in a PyQt5 GUI

Some years ago, I already experimented with embedding live matplotlib plots in a PyQt5 GUI.几年前,我已经尝试在PyQt5 GUI 中嵌入实时matplotlib图。 Live plots show a data-stream real-time, captured from a sensor, some process, ... I got that working, and you can read the related posts here:实时图显示实时数据流,从传感器捕获,一些过程,......我得到了它的工作,你可以在这里阅读相关帖子:

Now I need to do the same thing again.现在我需要再次做同样的事情。 I remember my previous approach worked, but couldn't keep up with fast datastreams.我记得我以前的方法有效,但跟不上快速的数据流。 I found a couple of example codes on the internet, that I'd like to present to you.我在互联网上找到了一些示例代码,我想向您展示。 One of them is clearly faster than the other, but I don't know why.其中一个明显比另一个快,但我不知道为什么。 I'd like to gain more insights.我想获得更多的见解。 I believe a deeper understanding will enable me to keep my interactions with PyQt5 and matplotlib efficient.我相信更深入的理解将使我能够保持与PyQt5matplotlib交互高效。


1. First example 1. 第一个例子

This example is based on this article:这个例子基于这篇文章:
https://matplotlib.org/3.1.1/gallery/user_interfaces/embedding_in_qt_sgskip.html https://matplotlib.org/3.1.1/gallery/user_interfaces/embedding_in_qt_sgskip.html
The article is from the official matplotlib website, and explains how to embed a matplotlib figure in a PyQt5 window.文章来自matplotlib官方网站,解释了如何在PyQt5窗口中嵌入matplotlib图形。

I did a few minor adjustments to the example code, but the basics are still the same.我对示例代码做了一些小的调整,但基本原理仍然相同。 Please copy-paste the code below to a Python file and run it:请将下面的代码复制粘贴到 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_()

You should see the following window:您应该看到以下窗口:

在此处输入图片说明


2. Second example 2. 第二个例子

I found another example of live matplotlib graphs here:我在这里找到了另一个实时matplotlib图的例子:
https://learn.sparkfun.com/tutorials/graph-sensor-data-with-python-and-matplotlib/speeding-up-the-plot-animation https://learn.sparkfun.com/tutorials/graph-sensor-data-with-python-and-matplotlib/speeding-up-the-plot-animation
However, the author doesn't use PyQt5 to embed his live plot.然而,作者并没有使用PyQt5来嵌入他的现场情节。 Therefore, I've modified the code a bit, to get the plot in a PyQt5 window:因此,我稍微修改了代码,以在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_()

The resulting live plot is exactly the same.生成的实时绘图完全相同 However, if you start playing around with the interval parameter from the MyFigureCanvas() constructor, you will notice that the first example won't be able to follow.但是,如果您开始使用MyFigureCanvas()构造函数中的interval参数,您会注意到第一个示例将无法遵循。 The second example can go much faster.第二个例子可以更快。


3. Questions 3. 问题

I've got a couple of questions I'd like to present to you:我有几个问题想向您提出:

  • The QtCore and QtWidgets classes can be imported like this: QtCoreQtWidgets类可以这样导入:
    from matplotlib.backends.qt_compat import QtCore, QtWidgets
    or like this:或者像这样:
    from PyQt5 import QtWidgets, QtCore
    Both work equally well.两者都同样有效。 Is there a reason to prefer one over the other?是否有理由偏爱其中之一?

  • The FigureCanvas can be imported like this: FigureCanvas可以这样导入:
    from matplotlib.backends.backend_qt5agg import FigureCanvas
    or like this: from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas或者像这样: from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
    But I already figured out why.但我已经知道为什么了。 The backend_qt5agg file seems to define FigureCanvas as an alias for FigureCanvasQTAgg .backend_qt5agg文件似乎定义FigureCanvas为的别名FigureCanvasQTAgg

  • Why exactly is the second example so much faster than the first one?为什么第二个例子比第一个例子快这么多? Honestly, it surprises me.老实说,这让我感到惊讶。 The first example is based on a webpage from the official matplotlib website.第一个示例基于来自官方 matplotlib 网站的网页。 I'd expect that one to be better.我希望那个更好。

  • Do you have any suggestions to make the second example even faster?你有什么建议可以让第二个例子更快吗?


4. Edits 4. 编辑

Based on the webpage:基于网页:
https://bastibe.de/2013-05-30-speeding-up-matplotlib.html https://bastibe.de/2013-05-30-speeding-up-matplotlib.html
I modified the first example to increase its speed.我修改了第一个示例以提高其速度。 Please have a look at the code:请看一下代码:

#####################################################################################
#                                                                                   #
#                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_()

The result is pretty amazing.结果非常惊人。 The modifications make the first example definitely much faster!修改使第一个示例肯定要快得多! However, I don't know if this makes the first example equally fast now to the second example .但是,我不知道这是否使第一个示例现在与第二个示例同样快。 They're certainly close to each other.他们肯定彼此接近。 Anyone an idea who wins?谁知道谁赢了?

Also, I noticed that one vertical line on the left, and one horizontal line on top is missing:另外,我注意到左侧的一条垂直线和顶部的一条水平线丢失了: 在此处输入图片说明 It's not a big deal, but I just wonder why.这没什么大不了的,但我只是想知道为什么。

The second case (using FuncAnimation ) is faster because it uses "blitting" , which avoids redrawing things that do not change between frames.第二种情况(使用FuncAnimation )更快,因为它使用 "blitting" ,这避免了重绘在帧之间不会改变的东西。

The example provided on the matplotlib website for embedding in qt was not written with speed in mind, hence the poorer performance. matplotlib 网站上提供的嵌入 qt 的示例没有考虑速度,因此性能较差。 You'll notice that it calls ax.clear() and ax.plot() at each iteration, causing the whole canvas to be redrawn everytime.您会注意到它在每次迭代时调用ax.clear()ax.plot() ,导致每次都重新绘制整个画布。 If you were to use the same code as in the code with FuncAnimation (that is to say, create an Axes and an artist, and update the data in the artist instead of creating a new artists every time) you should get pretty close to the same performance I believe.如果您使用与FuncAnimation的代码相同的代码(也就是说,创建一个 Axes 和一个艺术家,并更新艺术家中的数据而不是每次都创建一个新的艺术家),您应该非常接近我相信同样的表现。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM