PyQt:从图像文件或字节流生成QImage的速度测试
一幅从相机获取的3072 * 2048像素的图片,原始像素格式为bayer-rb,将其分别保存为png、bmp、jpg以及raw(直接存储元素的原始bayer-rb字节数据)格式的本地文件,然后使用Qt转换为QImage并显示,统计用时。
使用QImage()直接读取png格式并生成QImage对象:
import sys import cv2 import numpy as np from PySide6.QtCore import QElapsedTimer from PySide6.QtGui import QPixmap, QImage from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel class MyWidget(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): layout = QVBoxLayout() self.label = QLabel() layout.addWidget(self.label) self.setLayout(layout) if __name__ == "__main__": app = QApplication(sys.argv) widget = MyWidget() widget.show() # 计时器 timer = QElapsedTimer() timer.start() q_image = QImage("out.png") print(f"转换QImage耗时:{timer.elapsed()} ms") timer.restart() pixmap = QPixmap.fromImage(q_image) print(f"转换QPixmap耗时:{timer.elapsed()} ms") timer.restart() widget.label.setPixmap(pixmap) print(f"设置QPixmap耗时:{timer.elapsed()} ms") # 宽[3072], 高[2048] sys.exit(app.exec())用时:
转换QImage耗时:136 ms 转换QPixmap耗时:0 ms 设置QPixmap耗时:0 ms使用QImage()直接读取bmp格式并生成QImage对象:
import sys import cv2 import numpy as np from PySide6.QtCore import QElapsedTimer from PySide6.QtGui import QPixmap, QImage from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel class MyWidget(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): layout = QVBoxLayout() self.label = QLabel() layout.addWidget(self.label) self.setLayout(layout) if __name__ == "__main__": app = QApplication(sys.argv) widget = MyWidget() widget.show() # 计时器 timer = QElapsedTimer() timer.start() q_image = QImage("out.bmp") print(f"转换QImage耗时:{timer.elapsed()} ms") timer.restart() pixmap = QPixmap.fromImage(q_image) print(f"转换QPixmap耗时:{timer.elapsed()} ms") timer.restart() widget.label.setPixmap(pixmap) print(f"设置QPixmap耗时:{timer.elapsed()} ms") # 宽[3072], 高[2048] sys.exit(app.exec())用时:
转换QImage耗时:56 ms 转换QPixmap耗时:0 ms 设置QPixmap耗时:0 msjpg格式:
import sys import cv2 import numpy as np from PySide6.QtCore import QElapsedTimer from PySide6.QtGui import QPixmap, QImage from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel class MyWidget(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): layout = QVBoxLayout() self.label = QLabel() layout.addWidget(self.label) self.setLayout(layout) if __name__ == "__main__": app = QApplication(sys.argv) widget = MyWidget() widget.show() # 计时器 timer = QElapsedTimer() timer.start() q_image = QImage("out.jpg") print(f"转换QImage耗时:{timer.elapsed()} ms") timer.restart() pixmap = QPixmap.fromImage(q_image) print(f"转换QPixmap耗时:{timer.elapsed()} ms") timer.restart() widget.label.setPixmap(pixmap) print(f"设置QPixmap耗时:{timer.elapsed()} ms") # 宽[3072], 高[2048] sys.exit(app.exec())用时:
转换QImage耗时:91 ms 转换QPixmap耗时:0 ms 设置QPixmap耗时:0 msbayer格式字节流
由于Qt不直接支持bayer格式,使用numpy进行转换:
import sys import cv2 import numpy as np from PySide6.QtCore import QElapsedTimer from PySide6.QtGui import QPixmap, QImage from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel class MyWidget(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): layout = QVBoxLayout() self.label = QLabel() layout.addWidget(self.label) self.setLayout(layout) def bayer_bytes_to_qimage(bayer_bytes, width, height): bayer_array = np.frombuffer(bayer_bytes, dtype=np.uint8).reshape((height, width)) # 将bayer格式字节流转换为numpy数组 rgb_array = cv2.cvtColor(bayer_array, cv2.COLOR_BAYER_RG2BGR) # 将bayer格式数组转换为RGB格式数组(如果raw是使用opencv创建的,有可能cv2.COLOR_BAYER_RG2BGR格式转换) q_image = QImage(rgb_array.data, rgb_array.shape[1], rgb_array.shape[0], w * 3, QImage.Format.Format_RGB888) return q_image if __name__ == "__main__": app = QApplication(sys.argv) widget = MyWidget() widget.show() # 计时器 timer = QElapsedTimer() timer.start() # 模拟字节流:读取文件为字节(实际场景可能是网络返回、数据库读取) with open("out.raw", "rb") as f: bayer_bytes = f.read() print(f"读取raw文件时间: {timer.elapsed()} ms") h =2048 w = 3072 timer.restart() q_image = bayer_bytes_to_qimage(bayer_bytes, w, h) print(f"转换QImage耗时:{timer.elapsed()} ms") timer.restart() pixmap = QPixmap.fromImage(q_image) print(f"转换QPixmap耗时:{timer.elapsed()} ms") timer.restart() widget.label.setPixmap(pixmap) print(f"设置QPixmap耗时:{timer.elapsed()} ms") # 宽[3072], 高[2048] sys.exit(app.exec())用时:
读取raw文件时间: 2 ms cv2转换耗时:4 ms 转换QPixmap耗时:7 ms- 用时对比:
| 读取格式 | 读文件用时(ms) | 转QImage耗时(ms) | 转换QPixmap耗时(ms) | 总耗时(ms) |
| png | -- | 136 | 0 | 136 |
| bmp | -- | 56 | 0 | 56 |
| jpg | -- | 91 | 0 | 91 |
| raw | 2 | 4 | 7 | 13 |
使用原始图像字节流并使用numpy转换,效率远远比Qt直接读取图像文件高,其中的原因是numpy支持不创建副本的内存视图引用和矩阵转换。
验证,直接读取bmp图像文件的像素字节并用numpy转换为数组后转为QImage:
import sys import cv2 import numpy as np from PySide6.QtCore import QElapsedTimer from PySide6.QtGui import QPixmap, QImage from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel class MyWidget(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): layout = QVBoxLayout() self.label = QLabel() layout.addWidget(self.label) self.setLayout(layout) if __name__ == "__main__": app = QApplication(sys.argv) widget = MyWidget() widget.show() # 计时器 timer = QElapsedTimer() timer.start() h = 2048 w = 3072 with open("out.bmp", "rb") as f: f.seek(54) image_bytes = f.read() # 去除bmp头部信息 print(f"读取bmp文件时间: {timer.elapsed()} ms") timer.restart() rgb_array = np.frombuffer(image_bytes, dtype=np.uint8).reshape((h, w, 3)) # 将字节流转换为numpy数组 rgb_arrayy = np.flipud(rgb_array) # 垂直翻转(bmp图像像素是从左下角开始存储的) q_image = QImage(rgb_array.data, w, h, w * 3, QImage.Format.Format_RGB888) # 将numpy数组转换为QImage print(f"转换QImage耗时:{timer.elapsed()} ms") timer.restart() pixmap = QPixmap.fromImage(q_image) print(f"转换QPixmap耗时:{timer.elapsed()} ms") timer.restart() widget.label.setPixmap(pixmap) print(f"设置QPixmap耗时:{timer.elapsed()} ms") # 宽[3072], 高[2048] sys.exit(app.exec())总耗时17ms:
读取bmp文件时间: 9 ms 转换QImage耗时:0 ms 转换QPixmap耗时:8 ms 设置QPixmap耗时:0 ms然后也使用Qt从图像文件的原始字节流生成QPixmap进行比对:
import sys import cv2 import numpy as np from PySide6.QtCore import QElapsedTimer from PySide6.QtGui import QPixmap, QImage from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel class MyWidget(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): layout = QVBoxLayout() self.label = QLabel() layout.addWidget(self.label) self.setLayout(layout) if __name__ == "__main__": app = QApplication(sys.argv) widget = MyWidget() widget.show() # 计时器 timer = QElapsedTimer() timer.start() with open("out.bmp", "rb") as f: image_bytes = f.read() print(f"读取bmp文件时间: {timer.elapsed()} ms") timer.restart() pixmap = QPixmap() pixmap.loadFromData(image_bytes) print(f"转换QPixmap耗时:{timer.elapsed()} ms") timer.restart() widget.label.setPixmap(pixmap) print(f"设置QPixmap耗时:{timer.elapsed()} ms") # 宽[3072], 高[2048] sys.exit(app.exec())总耗时45ms:
读取bmp文件时间: 9 ms 转换QPixmap耗时:36 ms 设置QPixmap耗时:0 ms当然也可以分两步,先用原始字节流生成QImage再转换QPixmap,结果差不多:
import sys from PySide6.QtCore import QElapsedTimer from PySide6.QtGui import QPixmap, QImage from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel class MyWidget(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): layout = QVBoxLayout() self.label = QLabel() layout.addWidget(self.label) self.setLayout(layout) if __name__ == "__main__": app = QApplication(sys.argv) widget = MyWidget() widget.show() # 计时器 timer = QElapsedTimer() timer.start() h = 2048 w = 3072 with open("out.bmp", "rb") as f: image_bytes = f.read() print(f"读取bmp文件时间: {timer.elapsed()} ms") timer.restart() q_image = QImage() q_image.loadFromData(image_bytes) print(f"转换QImage耗时:{timer.elapsed()} ms") timer.restart() pixmap = QPixmap.fromImage(q_image) print(f"转换QPixmap耗时:{timer.elapsed()} ms") timer.restart() widget.label.setPixmap(pixmap) print(f"设置QPixmap耗时:{timer.elapsed()} ms") # 宽[3072], 高[2048] sys.exit(app.exec())总耗时54ms:
读取bmp文件时间: 11 ms 转换QImage耗时:43 ms 转换QPixmap耗时:0 ms 设置QPixmap耗时:0 ms- 结论:
无论是读取本地文件或者使用字节流,使用numpy把像素字节转换成数组再生成QImage的效率远远高于Qt本身的QImage生成功能(实测4倍以上)。
附:将相机原始bayer字节流保存为.raw文件的方法
def save_bayer_to_raw( pixel_data: bytes, width: int, height: int, pixel_format: str, # BayerRG8, BayerRG10, BayerRG10P, BayerRG12, BayerRG12P output_path: str = "output.raw" ): """ 海康相机Bayer裸数据 + 自定义二进制头 → 保存为 .raw 文件 自定义头格式:固定128字节,方便后续读取解析 """ # -------------------------- # 1. 构造自定义文件头(128字节) # -------------------------- header_size = 128 header_bytes = bytearray(header_size) # 偏移0:宽度 (4字节 int) w_bytes = width.to_bytes(4, byteorder='little') header_bytes[:4] = w_bytes # 偏移4:高度 (4字节 int) h_bytes = height.to_bytes(4, byteorder='little') header_bytes[4:8 ] = h_bytes # 偏移8:位深 (4字节 int) bit_depth = { "BayerRG8": 8, "BayerRG10": 10, "BayerRG10P": 10, "BayerRG12": 12, "BayerRG12P": 12 }[pixel_format] bit_depth_bytes = bit_depth.to_bytes(4, byteorder='little') # 位深 header_bytes[8:12] = bit_depth_bytes # 偏移12:像素格式字符串(最多32字节) fmt_bytes = pixel_format.encode("utf-8") # 像素格式 fmt_bytes = fmt_bytes.ljust(32, b'\x00') # 不足补0 header_bytes[12:44] = fmt_bytes # 偏移64:数据总长度(4字节 unsigned long long) data_len = len(pixel_data) l_bytes = data_len.to_bytes(4, byteorder='little', signed=False) header_bytes[64:68] = l_bytes # -------------------------- # 2. 拼接:头 + 裸像素数据 # -------------------------- raw_file_bytes = b''.join([header_bytes, pixel_data]) # -------------------------- # 3. 二进制写入文件 # -------------------------- with open(output_path, "wb") as f: f.write(raw_file_bytes) print(f"✅ 保存成功:{output_path}") print(f" 尺寸:{width}x{height}") print(f" 格式:{pixel_format}") print(f" 头长度:{header_size} 像素数据长度:{data_len}")以及从含宽高信息的.raw文件读取bayer字节流和转换成RGB数组的方法:
import cv2 import numpy as np def read_bayer_raw(raw_file_path): with open(raw_file_path, "rb") as f: # 从本地文件读取bayer-rg格式字节流(这个字节流也可以是从相机获取的) head_bytes = f.read(128) # 读取文件头 # 解析头部数据 w = int.from_bytes(head_bytes[:4], byteorder='little') h = int.from_bytes(head_bytes[4:8], byteorder='little') bit_depth = int.from_bytes(head_bytes[8:12], byteorder='little') pixel_format = head_bytes[12:44].decode("utf-8").strip("\x00") data_len = int.from_bytes(head_bytes[64:68], byteorder='little') pixel_data = f.read(data_len) return w, h, bit_depth, pixel_format, data_len, pixel_data def bayer_to_rgb8(bayer_data: bytes, width: int, height: int, bit_depth:int) -> np.ndarray: """ 将海康MV相机的Bayer RG数据转换为RGB8格式图像 :param bayer_data: 相机输出的bayerRG原始字节数据 :param width: 图像宽度(像素) :param height: 图像高度(像素) :return: RGB8格式的numpy数组(uint8) """ # 步骤1:转成np数列 # rg_array = np.frombuffer(bayer_data) # 步骤2:数据归一化到8位(0~255) if bit_depth == 8: bayer_array = np.frombuffer(bayer_data, dtype=np.uint8).reshape((h, w)) uint8_array = (bayer_array << 2).astype(np.uint8) elif bit_depth in [10, 12]: bayer_array = np.frombuffer(bayer_data, dtype=np.uint16).reshape((h, w)) uint8_array = (bayer_array << 4).astype(np.uint8) else: raise ValueError("不支持的位深") # 步骤3:Bayer RG(对应OpenCV的BAYER_RGGB格式)转RGB8 rgb8_image = cv2.cvtColor(uint8_array, cv2.COLOR_BAYER_RG2RGB) return rgb8_image w, h, bit_depth, pixel_format, data_len, pixel_data = read_bayer_raw("bayer-rg12_with_info.raw") bgr_img = bayer_to_rgb8(pixel_data, w, h, bit_depth) cv2.imshow("bayer", bgr_img) # cv2.imwrite("bayer10.png", bgr_img) cv2.waitKey(0) cv2.destroyAllWindows() # rg10:12,582,912 3072*2048=6,291,456*2=12,582,912