我正在尝试将使用 matplotlib funcAnimation 制作的动画图形放入 PySide6 小部件中。它从串行端口获取数据,绘制数据,并将其记录到 csv 中。我的问题是,经过一段时间后,它开始显着减慢。虽然数据每秒都会出现,但大约 20 分钟左右后,它开始每 2 秒更新一次。
我尝试改变间隔时间,虽然这确实使它持续的时间更长一点,但最终,它们仍然会不期望地减慢速度。我实现了一个检查执行时间的代码,并注意到它逐渐增加,直到超过一秒。然后我尝试将其从小部件中取出,但出现了相同的结果。我的其他一些想法是使用 blitting 或将其转换为列表/字典而不是 csv,但只是想在实现之前获得一些输入。我在 reddit 上看到了一篇有同样问题的帖子,其中 blitting 不起作用,他使用了 csv,所以我不确定我之前的想法:[Reddit Post](https://www.reddit.com/ r/AskProgramming/comments/owde4a/matplotlib_animation_exponnetially_slower_the/ 在这篇文章中,他们得到了一些似乎对他们有用的建议,但只有这个适用于我的代码: 为什么每个轴的 animate() 函数中都有 ax.set_xlabel()、ax.grid 等?这些应该在创建轴时设置一次,而不是在每次调用 animate() 时设置。 所以我改变了它并将其放入设置中,这意味着我必须在动画循环中删除我的 ax.cla() 。现在,它不再只显示最后 10 个值,所以我也需要修复这个问题。
我的代码:
`
import sys
from PySide6 import QtGui, QtCore
from PySide6.QtGui import QScreen, QPixmap
from pathlib import Path
from PySide6.QtWidgets import QWidget, QApplication, QPushButton, QVBoxLayout, QMainWindow, QHBoxLayout, QLabel
from matplotlib.backends.backend_qtagg import (
FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
from PySide6.QtCore import Qt
from matplotlib.animation import FuncAnimation
import pandas as pd
import serial
import matplotlib.image as image
import csv
import subprocess
import time
import openpyxl
import cProfile
#Initialize Serial Port
ser = serial.Serial()
ser.baudrate = 28800
ser.port = 'COM3'
timeout = 1
parity = serial.PARITY_ODD
bytesize = serial.EIGHTBITS
ser.open()
#Image Widget
class Widget(QWidget):
def __init__(self):
super().__init__()
#Initialize value that will select one image if the value is reached and another if not
self.int4_value = 0
#Creating the pixmap
self.image_label = QLabel()
self.original_pixmap = QPixmap("C:/Users/mlee/Downloads/mid_green_background (1).png")
self.scaled_pixmap = self.original_pixmap.scaled(20, 20)
self.image_label.setPixmap(self.scaled_pixmap)
#Layout
v_layout = QVBoxLayout()
v_layout.addWidget(self.image_label, alignment=Qt.AlignTop)
self.setLayout(v_layout)
def update_int4_value(self, new_value):
#Updating value to change image
self.int4_value = new_value
if self.int4_value == 1365:
self.original_pixmap = QPixmap("C:/Users/mlee/Downloads/mid_green_background (1).png")
else:
self.original_pixmap = QPixmap("C:/Users/mlee/Downloads/solid_red_background (1).jpg")
self.scaled_pixmap = self.original_pixmap.scaled(20, 20)
self.image_label.setPixmap(self.scaled_pixmap)
class Window(QMainWindow):
def __init__(self):
#Counts the number of screenshots
self.screenshot_counter = self.load_screenshot_counter()
super().__init__()
#Import widget
self.widget1 = Widget()
self.app = app
self.setWindowTitle("Custom")
#Add menu bar
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu("&File")
quit_action = file_menu.addAction("Quit")
quit_action.triggered.connect(self.quit)
save_menu = menu_bar.addMenu("&Save")
Screenshot = save_menu.addAction("Screenshot")
Screenshot.triggered.connect(self.screenshot)
#Set up graph as widget
self._main = QWidget()
self.setCentralWidget(self._main)
layout = QHBoxLayout(self._main)
self.fig = Figure(figsize=(5, 3))
self.canvas = FigureCanvas(self.fig)
layout.addWidget(self.canvas, stretch=24)
layout.addWidget(self.widget1, stretch=1)
#Set up toolbar
self.addToolBar(NavigationToolbar(self.canvas, self))
#Creating csv file to store incoming data and adding headers
with open('csv_graph.csv', 'w', newline='') as csvfile:
fieldnames = ['Value1', 'Value2', 'Value3', 'Value4', 'Value5', 'Value6']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
#Call the loop
self.setup()
def setup(self):
#At 500ms interval, failed after 1400 frames
#At 250 ms interval, failed after 1600 frames
#At 1ms interval, failed after 1800 frames
#Create the subplot
self.ax = self.fig.add_subplot(111)
# Legend and labels
self.ax.legend(loc='upper left')
self.ax.set_xlabel('Time(seconds)')
self.ax.set_ylabel('Value')
self.ax.set_title('Random Data')
#Animation function
self.ani = FuncAnimation(self.canvas.figure, self.animate, interval=1, cache_frame_data=False)
def animate(self, i):
#Timer to check execution time
start_time = time.time()
#Reading serial port data and only going htrough the loop if length is larger than 2
# since the first input is normally all 0
bytestoread = ser.inWaiting()
new_value = ser.read(bytestoread)
if len(new_value) >= 2:
#Turning all the bytes into ints
byte1 = new_value[2:4] # Extract the first byte
int1 = int.from_bytes(byte1, byteorder='little')
print(int1)
byte2 = new_value[4:6] # Extract the second byte
int2 = int.from_bytes(byte2, byteorder='little')
print(int2)
byte3 = new_value[6:8] # Extract the third byte
int3 = int.from_bytes(byte3, byteorder='little')
print(int3)
byte4 = new_value[8:10]
int4 = int.from_bytes(byte4, byteorder='little')
print(int4)
#Pass int4 to the image widget
self.widget1.update_int4_value(int4)
byte5 = new_value[10:12] # Day & Hour
int5 = int.from_bytes(byte5, byteorder='little')
# print(int5)
byte6 = new_value[12:14] # Minutes & Seconds
int6 = int.from_bytes(byte6, byteorder='little')
print(int6)
#Write the data into the csv
with open('csv_graph.csv', 'a', newline='') as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerow([int1, int2, int3, int4, int5, int6])
#Read from that csv and then take the last 10 rows
data = pd.read_csv('csv_graph.csv')
last_x_rows = data.tail(10)
#Assigning the values to variables
x = last_x_rows['Value6']
y1 = last_x_rows['Value1']
y2 = last_x_rows['Value2']
y3 = last_x_rows['Value3']
y4 = last_x_rows['Value4']
# Plotting
# self.ax.cla()
self.ax.plot(x, y1, color='Red', label='Red')
self.ax.plot(x, y2, color='Blue', label='Blue')
self.ax.plot(x, y3, color='Purple', label='Purple')
self.ax.plot(x, y4, color='Green', label='Green')
#Opening the csv in an excel
with pd.ExcelWriter("C:/Users/mlee/Documents/Excel_CSV/New_Excel.xlsx", mode='a', engine='openpyxl',
if_sheet_exists='replace') as writer:
data.to_excel(writer, sheet_name='Sheet_1')
#Execution time check
end_time = time.time()
self.execution_time = end_time - start_time
print(f"Frame {i}: Execution Time = {self.execution_time:.2f} seconds")
#Was trying to use blitting but didn't do it right
return self.ax
#Functions for menu bar
def quit(self):
self.app.quit()
def load_screenshot_counter(self):
counter_file = Path("screenshot_counter.txt")
if counter_file.exists():
with counter_file.open("r") as file:
return int(file.read())
else:
return 1
def save_screenshot_counter(self):
counter_file = Path("C:/Users/mlee/Downloads/screenshot_counter.txt")
with counter_file.open("w") as file:
file.write(str(self.screenshot_counter))
def screenshot(self):
# Get the primary screen
primary_screen = QApplication.primaryScreen()
# Get the available size of the screen (excluding reserved areas)
available_size = primary_screen.availableSize()
print(available_size)
shot = QScreen.grabWindow(QApplication.primaryScreen(), 0, 90, 95, 1410, 700)
file_path = f"C:/Users/mlee/Downloads/Screenshot_{self.screenshot_counter}.png"
shot.save(file_path, "PNG")
self.screenshot_counter += 1
self.save_screenshot_counter()
app = QApplication(sys.argv)
widget = Window()
widget.show()
with cProfile.Profile() as pr:
sys.exit(app.exec())
pr.print_stats(sort='cumtime')
`
首先,您正在动画函数中读取 .csv 文件,这不太好,因为每次在其中存储数据时该文件都会变得更大,因此当它读取数据时需要花费大量时间时间,并且重新绘制每帧的所有数据可能效率低下。相反,更新现有绘图数据通常更有效。首先解决从 .csv 读取数据的问题,您也可以使用名为 deque 的 “collections” 模块更有效地存储数据。双端队列具有快速附加功能,因此当您不断添加新数据点并且只需要维护固定数量的最新数据点时,设置了 maxlen 的双端队列会在添加新数据点时自动丢弃最旧的项目。这种行为非常适合您的情况。
from collections import deque
然后您可以为 deque
中的最后 10 个数据点创建缓冲区 self.data_buffer = deque(maxlen=10) # Buffer to store the last 10 data points
self.csv_data = [] # Make a list for storing the data
如果您要在 .csv 中写入信息,那么我建议您批量写入数据,而不是在每一帧上。创建一个类似这样的变量。
self.csv_batch_size = 50 # Adjust this accordingly
然后在您的动画函数中,您可以进行追加以便存储数据
new_data = [int1, int2, int3, int4, int5, int6]
self.data_buffer.append(new_data)
self.csv_data.append(new_data)
然后你可以做一个if语句来检查尺寸
if len(self.csv_data) >= self.csv_batch_size:
self.write_to_csv()
self.csv_data = []
然后更新剧情
df = pd.DataFrame(list(self.data_buffer), columns=['Value1', 'Value2', 'Value3', 'Value4', 'Value5', 'Value6'])
x = range(len(df))
self.ax.clear()
self.ax.plot(x, df['Value1'], color='Red')
self.ax.plot(x, df['Value2'], color='Blue')
然后你可以创建一个在 if 语句内部调用的函数来将数据保存在 csv 中。
def write_to_csv(self):
with open('csv_graph.csv', 'a', newline='') as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerows(self.csv_data)
这是我要采取的第一步,更不用说你要在 Excel 中编写每一帧,这太昂贵了,所以我会根据需要或不那么频繁地这样做。