You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Matplotlib FuncAnimation嵌入PySide6后动画逐渐变慢问题排查

解决Matplotlib FuncAnimation嵌入PySide6后动画变慢+显示最近10条数据的问题

问题概述

  • 将Matplotlib FuncAnimation动画嵌入PySide6组件,实现串口数据读取、实时绘图、CSV数据记录功能
  • 运行约20分钟后动画显著变慢,帧执行时间从初始的1秒内逐渐增至1秒以上,调整interval仅能延缓无法解决
  • 移除ax.cla()后,图表无法仅显示最近10条数据

核心性能瓶颈

  1. 重复读写整个CSV文件:每次动画帧都读取完整CSV并写入,文件越大IO耗时越长
  2. 频繁生成Excel文件:每次帧都覆盖写入Excel,IO开销随数据量线性增长
  3. 持续新增绘图线条:移除ax.cla()后,每次plot都会新增一组线条,Matplotlib渲染压力持续累积
  4. 串口读取逻辑不严谨:未处理串口数据的完整性,可能导致重复解析或无效数据

优化后的完整代码

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

关键优化说明

  1. 内存缓存数据:用self.data_cache列表存储最近10条数据,避免每次读取整个CSV文件,仅追加写入新数据
  2. 复用绘图线条:初始化时创建line1-line4对象,后续仅调用set_data()更新数据,避免重复生成线条导致的渲染压力
  3. 批量同步Excel:用Qt定时器每30秒同步一次Excel,替代每次帧都写入,大幅减少IO开销
  4. 优化串口读取:按固定数据包大小读取,避免不完整数据解析,同时清空串口缓冲区中的冗余数据
  5. 移除无效操作:优化图片组件的Pixmap加载逻辑,避免重复缩放;删除不必要的调试打印(可按需保留)

内容的提问来源于stack exchange,提问作者xKei

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 13:45:55