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)
问题原因与修复方案
核心原因
plt.show()默认会阻塞主线程,首次调用plot_fft_psd时,必须关闭FFT/PSD窗口才会继续执行后续代码,导致串口读取和ECG绘制暂停,图表无法及时渲染。- 每次调用
plot_fft_psd都会新建图表对象,重复创建的窗口会引发Matplotlib渲染冲突,导致首次绘图无显示。
修复步骤
- 提前初始化FFT/PSD图表,复用已有图表和线条对象,避免重复创建。
- 使用非阻塞模式显示图表,让主线程持续处理串口数据和ECG绘制。
- 更新数据时刷新图表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
相关产品推荐
相关产品推荐

