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

Matplotlib实时绘图问题:首次FFT/PSD图表显示空白

问题描述

通过串口读取数据并使用Matplotlib绘制图表,串口数据格式为ecg 9.89 4.5 123 . . end psd 2.1, 33.2 3.4, 92.10 . . end fft 2.1, 33.2 3.4, 92.10 . . end。需求是绘制ECG实时动态图与FFT/PSD终态图,ECG绘制正常,但首次读取数据时FFT/PSD图表为空白,第二次发送串口数据才显示首次的FFT/PSD数据。添加日志后确认plot_fft_psd函数执行流程正常,fft_data与psd_data数据无误,但首次图表仍为空。

现有代码
def plot_fft_psd(fft_data, psd_data):
    print("plotting fft and psd")
    fig2, (ax_fft, ax_psd) = plt.subplots(2, 1, figsize=(10, 6), num='Figure 2 - FFT & PSD')

    print("First!!!!!!")
    fft_x, fft_y = zip(*fft_data)
    ax_fft.plot(fft_x, fft_y, marker='o', color='blue')

    print("Second!!!!!!")
    psd_x, psd_y = zip(*psd_data)
    ax_psd.plot(psd_x, psd_y, marker='o', color='orange')

    plt.tight_layout()
    plt.show()
    plt.pause(0.01)
    print("Third!!!!!!")


def plot_ecg(x_vals, y_vals, ax, line):
    line.set_xdata(x_vals)
    line.set_ydata(y_vals)

    ax.set_xlim(0, 180)
    ax.set_ylim(-150, 200)
    plt.draw()


def serial_main():
    ser = serial.Serial(SERIAL_PORT, BAUDRATE, timeout=1)
    
    fig1, ecg_ax = plt.subplots(figsize=(12, 6), num='Figure 1 - ECG')

    ecg_x_vals = []
    ecg_y_vals = []
    line, = ecg_ax.plot([], [], color='green')

    count = 0
    fft_data = []
    psd_data = []
    received_fft = False
    received_psd = False

    current_mode = None
    buffer = []

    while True:
        line_data = ser.readline().decode('utf-8').strip()

        print(f"> {line_data}")

        if line_data in ("ecg", "fft", "psd"):
            current_mode = line_data
            buffer = []
            continue

        if line_data == "end":
            if current_mode == "fft":
                fft_data = buffer
                received_fft = True

            elif current_mode == "psd":
                psd_data = buffer
                received_psd = True

            current_mode = None
            buffer = []

        try:
            if current_mode == "ecg":
                value = float(line_data)
                ecg_x_vals.append(count * 0.01)
                ecg_y_vals.append(value)

                plot_ecg(ecg_x_vals, ecg_y_vals, ecg_ax, line)
                plt.pause(0.001)
                count += 1

            elif current_mode in ("fft", "psd"):
                x_str, y_str = line_data.split(",")
                x = float(x_str.strip())
                y = float(y_str.strip())
                buffer.append((x, y))

        except ValueError:
            print(f"Invalid data skipped: {line_data}")
            continue
        except Exception as e:
            print(f"Unexpected error: {e} for line: {line_data}")
            continue

        if received_fft and received_psd:
            print("received fft and psd")
            plot_fft_psd(fft_data, psd_data)
            received_fft = False
            received_psd = False

    plt.pause(0.001)
问题原因与修复方案

核心原因

  1. plt.show()默认会阻塞主线程,首次调用plot_fft_psd时,必须关闭FFT/PSD窗口才会继续执行后续代码,导致串口读取和ECG绘制暂停,图表无法及时渲染。
  2. 每次调用plot_fft_psd都会新建图表对象,重复创建的窗口会引发Matplotlib渲染冲突,导致首次绘图无显示。

修复步骤

  1. 提前初始化FFT/PSD图表,复用已有图表和线条对象,避免重复创建。
  2. 使用非阻塞模式显示图表,让主线程持续处理串口数据和ECG绘制。
  3. 更新数据时刷新图表UI,确保数据及时显示。

修改后的代码

import matplotlib.pyplot as plt
import serial

# 替换为你的串口参数
SERIAL_PORT = 'COM3'
BAUDRATE = 9600

# 全局初始化FFT/PSD图表,仅创建一次
fig2, (ax_fft, ax_psd) = plt.subplots(2, 1, figsize=(10, 6), num='Figure 2 - FFT & PSD')
line_fft, = ax_fft.plot([], [], marker='o', color='blue')
line_psd, = ax_psd.plot([], [], marker='o', color='orange')
plt.tight_layout()
plt.show(block=False)  # 非阻塞显示图表

def plot_fft_psd(fft_data, psd_data):
    print("plotting fft and psd")
    
    # 更新FFT数据并调整坐标轴
    if fft_data:
        fft_x, fft_y = zip(*fft_data)
        line_fft.set_xdata(fft_x)
        line_fft.set_ydata(fft_y)
        ax_fft.relim()
        ax_fft.autoscale_view()
    
    # 更新PSD数据并调整坐标轴
    if psd_data:
        psd_x, psd_y = zip(*psd_data)
        line_psd.set_xdata(psd_x)
        line_psd.set_ydata(psd_y)
        ax_psd.relim()
        ax_psd.autoscale_view()
    
    # 强制刷新图表
    fig2.canvas.draw()
    fig2.canvas.flush_events()
    print("Third!!!!!!")


def plot_ecg(x_vals, y_vals, ax, line):
    line.set_xdata(x_vals)
    line.set_ydata(y_vals)

    ax.set_xlim(0, 180)
    ax.set_ylim(-150, 200)
    fig1.canvas.draw()
    fig1.canvas.flush_events()


def serial_main():
    ser = serial.Serial(SERIAL_PORT, BAUDRATE, timeout=1)
    
    global fig1
    fig1, ecg_ax = plt.subplots(figsize=(12, 6), num='Figure 1 - ECG')

    ecg_x_vals = []
    ecg_y_vals = []
    line, = ecg_ax.plot([], [], color='green')
    plt.show(block=False)  # ECG图表也设为非阻塞

    count = 0
    fft_data = []
    psd_data = []
    received_fft = False
    received_psd = False

    current_mode = None
    buffer = []

    while True:
        line_data = ser.readline().decode('utf-8').strip()

        print(f"> {line_data}")

        if line_data in ("ecg", "fft", "psd"):
            current_mode = line_data
            buffer = []
            continue

        if line_data == "end":
            if current_mode == "fft":
                fft_data = buffer
                received_fft = True

            elif current_mode == "psd":
                psd_data = buffer
                received_psd = True

            current_mode = None
            buffer = []

        try:
            if current_mode == "ecg":
                value = float(line_data)
                ecg_x_vals.append(count * 0.01)
                ecg_y_vals.append(value)

                plot_ecg(ecg_x_vals, ecg_y_vals, ecg_ax, line)
                plt.pause(0.001)
                count += 1

            elif current_mode in ("fft", "psd"):
                x_str, y_str = line_data.split(",")
                x = float(x_str.strip())
                y = float(y_str.strip())
                buffer.append((x, y))

        except ValueError:
            print(f"Invalid data skipped: {line_data}")
            continue
        except Exception as e:
            print(f"Unexpected error: {e} for line: {line_data}")
            continue

        if received_fft and received_psd:
            print("received fft and psd")
            plot_fft_psd(fft_data, psd_data)
            received_fft = False
            received_psd = False

    plt.pause(0.001)

if __name__ == "__main__":
    serial_main()

关键修改说明

  • 全局初始化FFT/PSD图表对象,避免重复创建导致的渲染冲突。
  • plt.show(block=False)让图表窗口不阻塞主线程,串口读取和ECG绘制可持续运行。
  • 更新数据时复用已有线条对象,调用relim()和autoscale_view()自动调整坐标轴范围,确保数据完整显示。
  • 用fig.canvas.draw()和fig.canvas.flush_events()高效刷新UI,替代原有的plt.draw()和plt.pause(),减少不必要的线程阻塞。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 10:50:52