在 pyqt5 gui 中显示 matplotlib 动画图并同时保存在 mp4 中的问题
Posted
技术标签:
【中文标题】在 pyqt5 gui 中显示 matplotlib 动画图并同时保存在 mp4 中的问题【英文标题】:issues in displaying matplotlib animated plot in pyqt5 gui and save in mp4 at the same time 【发布时间】:2021-02-09 19:39:27 【问题描述】:目标:同时在mp4中显示和保存绘图
我可以保存视频,但是当它完成保存时,它开始覆盖画布,这会导致崩溃并且不会在 gui 中显示绘图。 我想我必须使用某种线程来运行动画,但我被困在如何做到这一点。我也可以想象我的代码中有很多缺陷。
main.py
import matplotlib.animation as animation
from matplotlib import style
import csv
import os.path
from os import path
import matplotlib.animation as animation
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import datetime
import serial.tools.list_ports
from pyqtgraph import PlotWidget, plot
import pyqtgraph as pg
import random
from numpy import diff
import numpy as np
LARGE_FONT = ("Verdana", 12)
import pandas as pd
style.use('fivethirtyeight')
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import (QTreeWidget, QTreeWidgetItem, QPushButton, QLabel, QDialog, QVBoxLayout, QApplication, QLineEdit)
from PyQt5.QtWidgets import (QPushButton, QDialog, QTreeWidget,
QTreeWidgetItem, QVBoxLayout,
QHBoxLayout, QFrame, QLabel, QComboBox,
QApplication, QTreeWidgetItemIterator, QMessageBox, QProxyStyle)
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
import matplotlib
from matplotlib.figure import Figure
matplotlib.use('QT5Agg')
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
plt.rcParams['animation.ffmpeg_path'] = 'C:/ffmpeg/bin/ffmpeg.exe'
from anim import Ui_MainWindow
plt.ion()
writer = animation.FFMpegWriter()
plt.show()
class Test(QtWidgets.QMainWindow):
def __init__(self, parent = None):
super().__init__(parent)
QtWidgets.QMainWindow.__init__(self, parent)
self.i = 0
self.ui = Ui_MainWindow()
self.ui.setupUi(parent)
vlay1 = QVBoxLayout()
self.plotWidget1 = FigureCanvas(Figure())
toolbar1 = NavigationToolbar(self.plotWidget1, self)
openButton = QtWidgets.QPushButton("Open a file")
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(openButton.sizePolicy().hasHeightForWidth())
openButton.setSizePolicy(sizePolicy)
dateTimeEdit = QtWidgets.QDateTimeEdit()
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(dateTimeEdit.sizePolicy().hasHeightForWidth())
dateTimeEdit.setSizePolicy(sizePolicy)
vlay1.addWidget(openButton)
vlay1.addWidget(dateTimeEdit)
vlay1.addWidget(toolbar1)
vlay1.addWidget(self.plotWidget1)
self.ui.gridLayout.addLayout(vlay1, 0, 0, 1, 1)
openButton.clicked.connect(self.openFile)
self.ax1 = self.plotWidget1.figure.subplots()
self.line, = self.ax1.plot([], [], marker='o')
self.ax1.set_xlabel("Real")
self.ax1.set_ylabel("Imaginary")
self.ax1.relim()
self.ax1.autoscale_view(True, True, True)
self.ax1.set_title(str(self.i))
self.plotWidget1.figure.tight_layout()
self.openFile()
anim = animation.FuncAnimation(self.plotWidget1.figure, self.animate, init_func=self.init,
frames=self.len-1, interval=1, blit=True)
anim.save('plot.mp4', writer=writer, dpi=300, )
def readFile(self):
df = pd.read_excel(self.fileName)
self.Real1 = df["Real 1"]
self.Real2 = df["Real 2"]
self.Real3 = df["Real 3"]
self.Real4 = df["Real 4"]
self.Real5 = df["Real 5"]
self.Img1 = df["Imaginary 1 "]
self.Img2 = df["Imaginary 2"]
self.Img3 = df["Imaginary 3"]
self.Img4 = df["Imaginary 4"]
self.Img5 = df["Imaginary 5"]
self.len = len(self.Img1)
def getDataX(self):
self.i += 1
if self.i == self.len - 1:
print("complete")
return [self.Real1[self.i], self.Real2[self.i], self.Real3[self.i], self.Real4[self.i], self.Real5[self.i]]
def getDataY(self):
return [abs(self.Img1[self.i]), abs(self.Img2[self.i]), abs(self.Img3[self.i]), abs(self.Img4[self.i]), abs(self.Img5[self.i])]
def openFile(self):
dirAndName = QtWidgets.QFileDialog.getOpenFileName(self,'Open a File', "*.xlsx")
if dirAndName[0] != []:
self.fileName = dirAndName[0]
self.readFile()
print(dirAndName)
def init(self):
self.line.set_data([], [])
return self.line,
def animate(self, i):
x = self.getDataX()
y = self.getDataY()
x = np.asarray(x)
y = np.asarray(y)
self.ax1.set_title(str(self.i))
self.line.set_data(x, y)
self.ax1.relim()
self.ax1.autoscale_view(True, True, True)
return self.line,
def Home():
f = QtWidgets.QMainWindow()
#f.resize(1699, 980)
c = Test(f)
f.show()
r = qApp.exec_()
if __name__ == "__main__":
qApp = QtWidgets.QApplication(sys.argv)
qApp.setStyle(QStyleFactory.create('Fusion'))
proxy = QProxyStyle(qApp.style())
qApp.setStyle(proxy)
Home()
Anim.py
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 600)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout.setObjectName("gridLayout")
self.verticalLayout = QtWidgets.QVBoxLayout()
self.verticalLayout.setObjectName("verticalLayout")
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.pushButton.sizePolicy().hasHeightForWidth())
self.pushButton.setSizePolicy(sizePolicy)
self.pushButton.setObjectName("pushButton")
self.verticalLayout.addWidget(self.pushButton)
self.dateTimeEdit = QtWidgets.QDateTimeEdit(self.centralwidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.dateTimeEdit.sizePolicy().hasHeightForWidth())
self.dateTimeEdit.setSizePolicy(sizePolicy)
self.dateTimeEdit.setObjectName("dateTimeEdit")
self.verticalLayout.addWidget(self.dateTimeEdit)
self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.pushButton.setText(_translate("MainWindow", "Open File"))
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
*** 我出于其他原因使用 Anim.py,即使我的 main.py 再次创建 qpushbutton 和 qtimeedit ***
【问题讨论】:
【参考方案1】:免责声明:我不会使用 OP 的代码,因为它包含不必要的元素,只会分散注意力以及无用的导入,因此我将展示一个仅关注所需功能的示例。
如果要保存并显示 GUI,则不应使用 save() 方法,因为这会阻塞,而应直接使用编写器 (FFMpegWriter),使用 setup() 方法进行配置,然后使用 grab_frame () 方法来记录帧。需要注意的是,录制动画的速度会降低,因为录制一帧会消耗时间,导致事件循环很忙。
import sys
import numpy as np
from PyQt5 import QtCore, QtWidgets
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.figure import Figure
from matplotlib import animation
class ApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.animation_button = QtWidgets.QPushButton("Start", checkable=True)
self.canvas = FigureCanvas(Figure(figsize=(5, 3)))
self.ax = self.canvas.figure.subplots()
t = np.linspace(0, 10, 101)
x, y = self.generate_data(0)
(self._line,) = self.ax.plot(x, y)
self.writer = animation.FFMpegWriter()
self.writer.setup(self.canvas.figure, outfile="plot.mp4", dpi=300)
self.anim = animation.FuncAnimation(
self.canvas.figure,
self.callback_animation,
frames=200,
interval=20,
blit=True,
)
self.animation_button.toggled.connect(self.handle_toggled)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
layout = QtWidgets.QVBoxLayout(central_widget)
layout.addWidget(self.canvas)
layout.addWidget(self.animation_button)
def callback_animation(self, i):
x, y = self.generate_data(i)
self._line.set_data(x, y)
if self.animation_button.isChecked():
self.writer.grab_frame()
return (self._line,)
def generate_data(self, i):
x = np.linspace(0, 2, 1000)
y = np.sin(2 * np.pi * (x - 0.01 * i))
return x, y
def handle_toggled(self):
self.animation_button.setText(
"Stop" if self.animation_button.isChecked() else "Start"
)
def closeEvent(self, event):
super().closeEvent(event)
self.writer.finish()
if __name__ == "__main__":
qapp = QtWidgets.QApplication.instance()
if not qapp:
qapp = QtWidgets.QApplication(sys.argv)
app = ApplicationWindow()
app.show()
app.activateWindow()
app.raise_()
qapp.exec_()
【讨论】:
谢谢,您的代码编写能力非常出色。但我收到了这个错误File .... /lib/subprocess.py", line 1307 in _execute_child hp, ht, pid, tid = _winapi.CreateProcess(executable, args, FileNotFoundError: [WinError 2] The system cannot find the file specified
已修复,这是 ffmpeg 的路径问题。谢谢
如何将ffmpeg.exe打包到pyinstaller包中?为了让您的代码正常工作,我需要输入 plt.rcParams['animation.ffmpeg_path'] = 'C:/ffmpeg/bin/ffmpeg.exe
但现在当我使用 pyinstaller 时,.exe 文件显示“执行脚本失败”以上是关于在 pyqt5 gui 中显示 matplotlib 动画图并同时保存在 mp4 中的问题的主要内容,如果未能解决你的问题,请参考以下文章
Linux:在 PyQt5 GUI 上显示所有当前连接的 USB 记忆棒的名称
在 pyqt5 gui 中显示 matplotlib 动画图并同时保存在 mp4 中的问题