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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 20:12:00