Matplotlib FuncAnimation嵌入PySide6后动画逐渐变慢问题排查
解决Matplotlib FuncAnimation嵌入PySide6后动画变慢+显示最近10条数据的问题
问题概述
- 将Matplotlib FuncAnimation动画嵌入PySide6组件,实现串口数据读取、实时绘图、CSV数据记录功能
- 运行约20分钟后动画显著变慢,帧执行时间从初始的1秒内逐渐增至1秒以上,调整
interval仅能延缓无法解决 - 移除
ax.cla()后,图表无法仅显示最近10条数据
核心性能瓶颈
- 重复读写整个CSV文件:每次动画帧都读取完整CSV并写入,文件越大IO耗时越长
- 频繁生成Excel文件:每次帧都覆盖写入Excel,IO开销随数据量线性增长
- 持续新增绘图线条:移除
ax.cla()后,每次plot都会新增一组线条,Matplotlib渲染压力持续累积 - 串口读取逻辑不严谨:未处理串口数据的完整性,可能导致重复解析或无效数据
优化后的完整代码
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.figure import Figure from PySide6.QtCore import Qt from matplotlib.animation import FuncAnimation import pandas as pd import serial import csv import time import openpyxl # 初始化串口 ser = serial.Serial() ser.baudrate = 28800 ser.port = 'COM3' ser.timeout = 1 ser.parity = serial.PARITY_ODD ser.bytesize = serial.EIGHTBITS ser.open() # 图片组件 class Widget(QWidget): def __init__(self): super().__init__() self.int4_value = 0 self.image_label = QLabel() self.green_pixmap = QPixmap("C:/Users/mlee/Downloads/mid_green_background (1).png").scaled(20, 20) self.red_pixmap = QPixmap("C:/Users/mlee/Downloads/solid_red_background (1).jpg").scaled(20, 20) self.image_label.setPixmap(self.green_pixmap) v_layout = QVBoxLayout() v_layout.addWidget(self.image_label, alignment=Qt.AlignTop) self.setLayout(v_layout) def update_int4_value(self, new_value): self.int4_value = new_value self.image_label.setPixmap(self.green_pixmap if new_value == 1365 else self.red_pixmap) # 主窗口 class Window(QMainWindow): def __init__(self): self.screenshot_counter = self.load_screenshot_counter() super().__init__() self.widget1 = Widget() self.app = app self.setWindowTitle("Custom") # 菜单栏 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_action = save_menu.addAction("Screenshot") screenshot_action.triggered.connect(self.screenshot) # 布局与图表初始化 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) self.addToolBar(NavigationToolbar(self.canvas, self)) # 初始化数据缓存与文件 self.data_cache = [] self.csv_fields = ['Value1', 'Value2', 'Value3', 'Value4', 'Value5', 'Value6'] with open('csv_graph.csv', 'w', newline='') as csvfile: writer = csv.DictWriter(csvfile, fieldnames=self.csv_fields) writer.writeheader() self.setup_plot() self.ani = FuncAnimation(self.canvas.figure, self.animate, interval=1000, cache_frame_data=False) # 每30秒同步一次Excel,减少IO频率 self.excel_sync_timer = QtCore.QTimer(self) self.excel_sync_timer.timeout.connect(self.sync_to_excel) self.excel_sync_timer.start(30000) def setup_plot(self): # 初始化绘图对象,复用线条 self.ax = self.fig.add_subplot(111) self.ax.set_xlabel('Time(seconds)') self.ax.set_ylabel('Value') self.ax.set_title('Real-time Serial Data') # 创建初始线条对象,后续仅更新数据 self.line1, = self.ax.plot([], [], color='Red', label='Red') self.line2, = self.ax.plot([], [], color='Blue', label='Blue') self.line3, = self.ax.plot([], [], color='Purple', label='Purple') self.line4, = self.ax.plot([], [], color='Green', label='Green') self.ax.legend(loc='upper left') # 设置坐标轴范围(可根据实际数据调整) self.ax.set_ylim(0, 2000) def animate(self, i): start_time = time.time() # 读取串口数据,确保读取完整的数据包(假设每个数据包固定14字节) packet_size = 14 while ser.inWaiting() >= packet_size: new_value = ser.read(packet_size) if len(new_value) == packet_size: # 解析数据 int1 = int.from_bytes(new_value[2:4], byteorder='little') int2 = int.from_bytes(new_value[4:6], byteorder='little') int3 = int.from_bytes(new_value[6:8], byteorder='little') int4 = int.from_bytes(new_value[8:10], byteorder='little') int5 = int.from_bytes(new_value[10:12], byteorder='little') int6 = int.from_bytes(new_value[12:14], byteorder='little') # 更新图片组件 self.widget1.update_int4_value(int4) # 更新内存缓存 data_row = [int1, int2, int3, int4, int5, int6] self.data_cache.append(data_row) # 保持缓存最近10条数据用于绘图 if len(self.data_cache) > 10: self.data_cache.pop(0) # 追加写入CSV(仅写当前行,不读整个文件) with open('csv_graph.csv', 'a', newline='') as csvfile: csv_writer = csv.writer(csvfile) csv_writer.writerow(data_row) # 更新绘图数据 x = [row[5] for row in self.data_cache] y1 = [row[0] for row in self.data_cache] y2 = [row[1] for row in self.data_cache] y3 = [row[2] for row in self.data_cache] y4 = [row[3] for row in self.data_cache] # 更新线条数据,避免重绘整个图表 self.line1.set_data(x, y1) self.line2.set_data(x, y2) self.line3.set_data(x, y3) self.line4.set_data(x, y4) # 自动调整X轴范围以显示最近数据 self.ax.set_xlim(min(x) - 1, max(x) + 1) # 刷新画布 self.canvas.draw() end_time = time.time() print(f"Frame {i}: Execution Time = {(end_time - start_time):.2f} seconds") return self.line1, self.line2, self.line3, self.line4 def sync_to_excel(self): # 批量同步到Excel,减少IO操作 full_data = pd.read_csv('csv_graph.csv') with pd.ExcelWriter("C:/Users/mlee/Documents/Excel_CSV/New_Excel.xlsx", mode='a', engine='openpyxl', if_sheet_exists='replace') as writer: full_data.to_excel(writer, sheet_name='Sheet_1') def quit(self): ser.close() self.excel_sync_timer.stop() self.app.quit() def load_screenshot_counter(self): counter_file = Path("screenshot_counter.txt") return int(counter_file.read_text()) if counter_file.exists() else 1 def save_screenshot_counter(self): counter_file = Path("C:/Users/mlee/Downloads/screenshot_counter.txt") counter_file.write_text(str(self.screenshot_counter)) def screenshot(self): 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() sys.exit(app.exec())
关键优化说明
- 内存缓存数据:用
self.data_cache列表存储最近10条数据,避免每次读取整个CSV文件,仅追加写入新数据 - 复用绘图线条:初始化时创建
line1-line4对象,后续仅调用set_data()更新数据,避免重复生成线条导致的渲染压力 - 批量同步Excel:用Qt定时器每30秒同步一次Excel,替代每次帧都写入,大幅减少IO开销
- 优化串口读取:按固定数据包大小读取,避免不完整数据解析,同时清空串口缓冲区中的冗余数据
- 移除无效操作:优化图片组件的Pixmap加载逻辑,避免重复缩放;删除不必要的调试打印(可按需保留)
内容的提问来源于stack exchange,提问作者xKei
相关产品推荐
相关产品推荐

