Matplotlib实时子图渲染转OpenCV显示帧率低,求60+FPS优化方案
核心问题原因
你当前帧率低的核心原因是Matplotlib的渲染逻辑开销过大:每帧都新建画布、重绘所有子图、色条、标题,再转成numpy数组传入OpenCV,单帧处理耗时远超过16ms(60FPS的单帧阈值)。
使用pyqtgraph可以完美解决这个问题,它基于Qt的GPU加速渲染管线,原生支持numpy数组操作,无需反复创建销毁绘图对象,实测2路子图+色条更新可以轻松跑到100FPS以上。
pyqtgraph配置说明
- inferno色卡调用:pyqtgraph内置了和Matplotlib同系列的色卡,直接调用
pg.colormap.get('inferno')即可获取inferno色卡实例。 - 数值归一化:
- 自动归一化:
ImageItem的setLevels()参数留空,会自动根据当前帧的最值做归一化 - 手动固定归一化范围:传入
levels=[min_val, max_val]即可固定色温映射范围,适合温度这种需要固定量程的场景
- 自动归一化:
- 色条配置:直接创建
pg.ColorBarItem实例,和对应子图的ImageItem绑定,会自动同步色卡和数值范围,不需要每帧重绘。 - 子图布局:使用
pg.GraphicsLayoutWidget可以快速实现网格状的子图布局,不需要手动计算行列位置。
改造后示例代码
import sys import math import numpy as np import cv2 from PyQt5.QtWidgets import QApplication import pyqtgraph as pg import pandas as pd # 原有工具函数保留 def rescale_frame(frame, percent): width = int(frame.shape[1] * percent / 100) height = int(frame.shape[0] * percent / 100) return cv2.resize(frame, (width, height), interpolation=cv2.INTER_AREA) def round_half_away_from_zero(x): return int(np.floor(x + 0.5)) def counts2temp(x, cam_type): # 原有温度转换逻辑保留 return x def retrieveVariableWP(path): # 原有pkl读取逻辑保留 import pickle with open(path, 'rb') as f: return pickle.load(f) class ThermalDisplay: def __init__(self, cam_names, cols=2, cmap='inferno', fixed_levels=None): self.app = QApplication(sys.argv) self.win = pg.GraphicsLayoutWidget(show=True, title="Real-Time Thermal Imaging from Thermal Cameras") self.win.resize(1500, 1000) # 配置色卡 self.cmap = pg.colormap.get(cmap) self.cam_names = cam_names self.cols = cols self.rows = int(math.ceil(len(cam_names)/cols)) self.fixed_levels = fixed_levels # 预创建所有子图、ImageItem、色条,仅初始化一次 self.img_items = {} self.color_bars = {} self.plot_items = {} for idx, cam_name in enumerate(cam_names): row = idx // cols col = idx % cols # 创建子图 plt = self.win.addPlot(row=row, col=col, title=f"{cam_name} - 0") plt.setMouseEnabled(x=False, y=False) # 禁用无关交互减少开销 # 创建图像渲染对象 img_item = pg.ImageItem() img_item.setColorMap(self.cmap) plt.addItem(img_item) # 创建关联色条 bar = pg.ColorBarItem(values=[0, 100] if fixed_levels is None else fixed_levels, label='Temperature / °C') bar.setImageItem(img_item) self.win.addItem(bar, row=row, col=col+1) self.plot_items[cam_name] = plt self.img_items[cam_name] = img_item self.color_bars[cam_name] = bar def update_frames(self, frame_dict, img_id): """每帧仅更新数据,无冗余对象创建""" for cam_name, frame in frame_dict.items(): if cam_name not in self.img_items: continue # 更新图像数据,转置匹配矩阵显示方向 self.img_items[cam_name].setImage(frame.T, levels=self.fixed_levels) # 更新子图标题 self.plot_items[cam_name].setTitle(f"{cam_name} - {img_id}") # 刷新界面 self.app.processEvents() def get_opencv_frame(self): """如需导出到OpenCV显示,调用此方法获取BGR格式帧""" img = self.win.grab().toImage() bits = img.constBits() bits.setsize(img.height() * img.width() * 4) arr = np.array(bits).reshape(img.height(), img.width(), 4) # 转OpenCV默认BGR格式,丢弃alpha通道 return cv2.cvtColor(arr, cv2.COLOR_RGBA2BGR) if __name__ == "__main__": system_settings = {} system_settings["OpenCV_Rescale_Image_Percent"] = False # 读取本地数据 data = retrieveVariableWP("last_img2.pkl") new_data = {} for d in data.keys(): new_data[d] = [np.array(x) for x in data[d]] cam_names = list(new_data.keys()) total_img_frames = max(len(v) for v in new_data.values()) # 全局仅初始化一次显示对象 # 需要固定温度范围时传入fixed_levels参数即可,例如fixed_levels=[20, 120] display = ThermalDisplay(cam_names, cols=2, cmap='inferno', fixed_levels=None) while True: for img_id in range(total_img_frames): last_img = {} for d in cam_names: frame = new_data[d][img_id] width, height = frame.shape[:2] ROI_x = [0, -round_half_away_from_zero(height * 2.5 / 100)] ROI_y = [0, -round_half_away_from_zero(width * 2.5 / 100)] if d == "PT1000ST_FLR1": img_cv_data = pd.DataFrame(frame).apply(counts2temp, args=("1",)) elif d == "FLIR_AX5": img_cv_data = pd.DataFrame(frame).apply(counts2temp, args=("2",)) else: img_cv_data = pd.DataFrame(frame) last_img[d] = img_cv_data.to_numpy(dtype=float)[ROI_x[0]:ROI_x[1], ROI_y[0]:ROI_y[1]] if system_settings["OpenCV_Rescale_Image_Percent"] and system_settings["OpenCV_Rescale_Image_Percent"] > 0: last_img[d] = rescale_frame(last_img[d], system_settings["OpenCV_Rescale_Image_Percent"]) # 刷新帧内容 display.update_frames(last_img, img_id) # 如需使用OpenCV窗口显示,取消下方注释即可 # opencv_img = display.get_opencv_frame() # cv2.imshow("Live Camera Feeds", opencv_img) # cv2.waitKey(1)
额外性能优化提示
- 若无OpenCV输出的强制要求,直接使用pyqtgraph原生窗口显示性能最高,可稳定在60FPS以上
- 若必须使用OpenCV输出,
get_opencv_frame调用会有少量开销,适当降低窗口分辨率即可达到60FPS - 温度转换逻辑可改为纯numpy实现,替换pandas的apply操作,能进一步提升数据处理速度
内容的提问来源于stack exchange,提问作者t.abraham
相关产品推荐
相关产品推荐

