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

2022-01-12 00:00:00 python python-3.x matplotlib pyqt5 plot

问题描述

几年前,我已经尝试在 PyQt5 GUI 中嵌入实时 matplotlib 图.实时图显示了从传感器捕获的实时数据流、一些过程……我得到了它的工作,您可以在此处阅读相关帖子:

  •  

    2.第二个例子

    我在这里找到了另一个实时 matplotlib 图示例:
    这没什么大不了的,但我只是想知道为什么.

    解决方案

    第二种情况(使用FuncAnimation)更快,因为它使用blitting",这可以避免重绘帧之间不会改变的东西.

    matplotlib 网站上提供的嵌入 qt 的示例没有考虑速度,因此性能较差.您会注意到它在每次迭代时调用 ax.clear()ax.plot(),导致每次都重新绘制整个画布.如果要使用与 FuncAnimation 中的代码相同的代码(也就是说,创建一个 Axes 和一个艺术家,并更新艺术家中的数据,而不是每次都创建一个新艺术家)你应该得到非常接近我相信的相同性能.

    Some years ago, I already experimented with embedding live matplotlib plots in a PyQt5 GUI. 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:

    • Matplotlib animation inside your own GUI

    • How do I plot in real-time in a while loop using matplotlib?

    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.

     

    1. First example

    This example is based on this article:
    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.

    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:

    #####################################################################################
    #                                                                                   #
    #                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

    I found another example of live matplotlib graphs here:
    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. Therefore, I've modified the code a bit, to get the plot in a PyQt5 window:

    #####################################################################################
    #                                                                                   #
    #                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. The second example can go much faster.

     

    3. Questions

    I've got a couple of questions I'd like to present to you:

    • The QtCore and QtWidgets classes can be imported like this:
      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:
      from matplotlib.backends.backend_qt5agg import FigureCanvas
      or like this: 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.
       

    • 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. I'd expect that one to be better.
       

    • Do you have any suggestions to make the second example even faster?

     

    4. Edits

    Based on the webpage:
    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.

    The example provided on the matplotlib website for embedding in qt was not written with speed in mind, hence the poorer performance. You'll notice that it calls ax.clear() and ax.plot() at each iteration, causing the whole canvas to be redrawn everytime. 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.

相关文章