python,读取图像文件并获取到像素数组的效率测试
场景需求:读取一个bmp图像文件,并把像素数据转换为RGB*uint8的numpy数组,这是一个很常用的操作。本次测试的目的是使用不同的读取方式,找到其中效率最高的。
素材:demo.bmp,4624*3742像素,RGB格式。
方法1:使用opencv直接读取,就可以获取到像素的数组
import cv2 from PyQt5.QtCore import QElapsedTimer # 创建定时器,统计用时 timer = QElapsedTimer() timer.start() img = cv2.imread("demo.bmp") print(f"读取bmp文件并转换为数组的时间: {timer.elapsed()} ms") print(img.shape)计时结果:
读取bmp文件并转换为数组的时间: 54 ms (3472, 4624, 3) 方法2:使用pillow读取,再转换为像素数组
import cv2 import numpy as np from PIL import Image from PyQt5.QtCore import QElapsedTimer timer = QElapsedTimer() timer.start() img = Image.open('demo.bmp') print(f"读取bmp文件的时间: {timer.elapsed()} ms") img_array = np.array(img) print(f"读取bmp文件并转换为数组的时间: {timer.elapsed()} ms") print(img_array.shape)计时结果:
读取bmp文件并转换为数组的时间: 14 ms 读取bmp文件并转换为数组的时间: 117 ms (3472, 4624, 3)方法3:使用qt
- 读取为QImage对象,再转换为像素数组
import numpy as np from PyQt5.QtCore import QElapsedTimer from PyQt5.QtGui import QImage timer = QElapsedTimer() timer.start() # 转换为BGR888格式 qimage = QImage('demo.bmp').convertToFormat(QImage.Format_BGR888) w, h = qimage.width(), qimage.height() # 4624 3472 print("读取bmp文件的耗时", timer.elapsed(), " ms") bits = qimage.bits() bits.setsize(qimage.byteCount()) img_array = np.frombuffer(bits, dtype=np.uint8).reshape(h, w, 3) print(f"读取bmp文件并转换为数组的时间: {timer.elapsed()} ms") print(img_array.shape)计时结果:
读取bmp文件并转换为数组的时间: 110 ms (3472, 4624, 3) - 不创建QImage对象,只读取像素数据并转换为数组
import numpy as np from PyQt5.QtCore import QElapsedTimer from PyQt5.QtGui import QImage, QImageReader timer = QElapsedTimer() timer.start() reader = QImageReader('demo.bmp') reader.setAutoTransform(False) img_bytes = reader.read().convertToFormat(QImage.Format_RGB888) h =img_bytes.height() w = img_bytes.width() bits = img_bytes.bits() bits.setsize(img_bytes.byteCount()) img_array = np.frombuffer(bits, dtype=np.uint8).reshape(h, w, 3) print(f"读取bmp文件并转换为数组的时间: {timer.elapsed()} ms") print(img_array.shape)计时结果:
读取bmp文件并转换为数组的时间: 109 ms (3472, 4624, 3) 方法4:使用python的原生文件打开方法
- 一次性全部读出数据后截取数据切片:
import numpy as np from PyQt5.QtCore import QElapsedTimer timer = QElapsedTimer() timer.start() with open("demo.bmp", "rb") as f: read_bytes = f.read() # 解析像素区的偏移量:第10-13字节,小端序,4字节整数 offset = int.from_bytes(read_bytes[10:14], byteorder='little') # 解析宽度:第18-21字节,小端序,4字节整数 w = int.from_bytes(read_bytes[18:22], byteorder='little') # 解析高度:第22-25字节,小端序,4字节整数 h = int.from_bytes(read_bytes[22:26], byteorder='little') # 截取bmp图片数据 image_bytes = read_bytes[offset:] img_array = np.frombuffer(image_bytes, dtype=np.uint8).reshape((h, w, 3)) # 将字节数据转换为numpy数组 img_array = np.ascontiguousarray(np.flipud(img_array)) # 垂直翻转(bmp图像像素是从左下角开始存储的),使用np.ascontiguousarray提高效率 print(f"读取bmp文件并转换为数组的时间: {timer.elapsed()} ms") print(img_array.shape)计时结果:
读取bmp文件并转换为数组的时间: 53 ms (3472, 4624, 3)- 分两次读取,先读取头文件再读取数据:
import numpy as np from PyQt5.QtCore import QElapsedTimer from os.path import getsize timer = QElapsedTimer() timer.start() file_size = getsize("demo.bmp") # 获取文件大小 with open("demo.bmp", "rb") as f: # 读取BMP文件头 header = f.read(26) # 解析像素区的偏移量:第10-13字节,小端序,4字节整数 offset = int.from_bytes(header[10:14], byteorder='little') # 解析宽度:第18-21字节,小端序,4字节整数 w = int.from_bytes(header[18:22], byteorder='little') # 解析高度:第22-25字节,小端序,4字节整数 h = int.from_bytes(header[22:26], byteorder='little') image_size = file_size - offset # 继续读取bmp图片数据 f.seek(offset) image_bytes = f.read() # print(f"读取bmp文件的时间: {timer.elapsed()} ms") img_array = np.frombuffer(image_bytes, dtype=np.uint8).reshape((h, w, 3)) # 将字节转换为numpy数组 img_array = np.ascontiguousarray(np.flipud(img_array)) # 垂直翻转(bmp图像像素是从左下角开始存储的) print(f"读取bmp文件并转换为数组的时间: {timer.elapsed()} ms") print(img_array.shape)用时接近:
读取bmp文件并转换为数组的时间: 51 ms (3472, 4624, 3)上面这两个方法的用时与opencv非常接近。
- 分两次读取,先读取文件头,在读取数据时指定数据长度(上面方法的改进):
import numpy as np from PyQt5.QtCore import QElapsedTimer timer = QElapsedTimer() timer.start() with open("demo.bmp", "rb") as f: # 读取BMP文件头 header = f.read(26) # 解析像素区的偏移量:第10-13字节,小端序,4字节整数 offset = int.from_bytes(header[10:14], byteorder='little') # 解析宽度:第18-21字节,小端序,4字节整数 w = int.from_bytes(header[18:22], byteorder='little') # 解析高度:第22-25字节,小端序,4字节整数 h = int.from_bytes(header[22:26], byteorder='little') image_size = w * h * 3 # 继续读取bmp图片数据 f.seek(offset) image_bytes = f.read(image_size) img_array = np.frombuffer(image_bytes, dtype=np.uint8).reshape((h, w, 3)) # 将字节转换为numpy数组 img_array = np.ascontiguousarray(np.flipud(img_array)) # 垂直翻转(bmp图像像素是从左下角开始存储的) print(f"读取bmp文件并转换为数组的时间: {timer.elapsed()} ms") print(img_array.shape)计时结果:
读取bmp文件并转换为数组的时间: 36 ms (3472, 4624, 3)与之前的代码相比,唯一改进的地方:image_bytes = f.read(image_size),在读取像素数据的时候指定了数据长度,用时就有了明显的减少。
阶段总结:
| 读取方法 | 用时(ms) | 备注 |
| 使用opencv读取 | 53 | 与python的原生打开方法接近 |
| 使用pillow读取 | 117 | 最慢 |
| 使用qt读取 | 110 | 次慢 |
| python的原生打开方法打开图像的像素字节 | 53 | 与opencv接近 |
| python的原生打开方法打开图像的像素字节,指定读取长度 | 36 | 最快 |
所以,使用python的原生打开方法打开图像的像素字节,并指定长度来读取,是最快的方法。但是这个方法的局限性在于,只对bmp文件最适用,因为bmp文件是逐字节存放像素的原始数据。
两个实用代码demo
- 一个兼容性强的也比较快的代码demo:
上面的代码都是针对bmp格式,下面的代码可以打开多种图像文件。
import cv2 import numpy as np from PyQt5.QtCore import QElapsedTimer # 创建定时器,统计用时 timer = QElapsedTimer() timer.start() with open("demo.bmp", "rb") as f: image_bytes = f.read() # 获取二进制数据 # 将二进制数据转为numpy数组,再用imdecode解码 img_array = np.frombuffer(image_bytes, dtype=np.uint8) img_array = cv2.imdecode(img_array, cv2.IMREAD_COLOR) # 解码参数同imread,也可以进行别的颜色设置 print(f"读取bmp文件并转换为数组的时间: {timer.elapsed()} ms") print(img_array.shape)读取bmp文件并转换为数组的时间: 50 ms (3472, 4624, 3)该段代码使用python原生的文件打开方法获取二进制数据,并使用opencv进行解码后获得像素阵列。读取速度与使用opencv直接读取接近,并且兼容所有opencv支持的图像格式,优势是可以同时获取到二进制数据的字节流,这个字节流可用来网络传输或保存为本地二进制文件。
- 一套自用的基于二进制数据字节流的极快的读写方法:
首先,保存文件为自定义的格式,文件分为两部分:文件头(head)和文件二进制字节(image_bytes)两部分。
文件头共14个字节,文件头数据的结构:
[0:2]:图像高度
[2:4]:图像宽度
[4:6]:图像色彩通道数
[6:10]:图像像素的字节数
[10:14]:像素格式(“BGR8"、"BAY8")
文件头后面所有数据是图像的像素字节,无论原图像是哪种格式都转为BGR 3*unit8格式。
将图像文件保存为我的格式的代码:
import cv2 img_array = cv2.imread("demo.bmp") # 读取一个图像文件,并默认转换为BGR*unit8的numpy数组 h, w, _ = img_array.shape # 高宽 c = 3 # 通道数 l = h * w * c # 像素字节数 t = "BGR8" # 图像类型(BGR) # 把hwclt转换为字节(高宽等图像参数) h_bytes = h.to_bytes(2, byteorder='little') w_bytes = w.to_bytes(2, byteorder='little') c_bytes = c.to_bytes(2, byteorder='little') l_bytes = l.to_bytes(4, byteorder='little', signed=False) t_bytes = t.encode('utf-8') print(t_bytes) # 获取图像的BGR字节流 bgr_bytes = img_array.tobytes() # image_bytes是图像的字节流 image_bytes = b''.join([h_bytes, w_bytes, c_bytes, l_bytes, t_bytes, bgr_bytes]) # 拼接字节流,hwclt在前,像素在后 with open("demo2.raw", "wb") as f: f.write(image_bytes)读取我的格式的文件并转换为数列:
import cv2 import numpy as np from PyQt5.QtCore import QElapsedTimer timer = QElapsedTimer() timer.start() with open("demo2.raw", "rb") as f: head = f.read(14) h = int.from_bytes(head[:2], byteorder='little') w = int.from_bytes(head[2:4], byteorder='little') c = int.from_bytes(head[4:6], byteorder='little') l = int.from_bytes(head[6:10], byteorder='little') t = head[10:14].decode('utf-8') print(h, w, c, l, t) image_bytes = f.read(l) if t == "BGR8": img_array = np.frombuffer(image_bytes, dtype=np.uint8).reshape((h, w, c)) # elif t == "BAY3": # img_array = np.frombuffer(image_bytes, dtype=np.uint8).reshape((h, w)) # img_array = cv2.cvtColor(img_array, cv2.COLOR_BAYER_RG2RGB) print("图像格式:", t) print(f"读取raw文件并转换为数组的时间: {timer.elapsed()} ms") cv2.imshow("img", img_array) cv2.waitKey(0)速度展示:
读取raw文件并转换为数组的时间: 19 ms上面代码中的image_bytes像素字节流,除了可以从本地读取,还可以是来自网络或者相机。
上面的方法,支持常见的图像格式,并将其转为BGR排列的unit8格式字节文件。唯一缺点就是保存为本地文件时,文件比较大(与同像素格式的bmp格式大小相同),为了减小文件体积,可将RGB格式的彩色图像文件保存为bayer格式,bayer格式的文件体积只有bmp文件的1/3。
- 将图像文件保存为bayer-RG8格式二进制文件的代码:
使用pillow:
import numpy as np from PIL import Image img = Image.open("red.bmp") # 读取一个彩色RGB图像 w, h = img.size # 宽高 c = 3 # 通道数 l = w * h # 像素字节数 t = "BAY8" # 图像类型(Bayer) # 把hwclt转换为字节(高宽等图像参数) h_bytes = h.to_bytes(2, byteorder='little') w_bytes = w.to_bytes(2, byteorder='little') c_bytes = c.to_bytes(2, byteorder='little') l_bytes = l.to_bytes(4, byteorder='little', signed=False) t_bytes = t.encode('utf-8') # 获取图像的RGB数组 rgb_array = np.array(img.convert('RGB')) ###########创建Bayer-rg格式数组 (RGGB排列)################# bayer_array = np.zeros((h, w), dtype=np.uint8) # 按照Bayer-rg (RGGB) 模式填充数据 # 偶数行偶数列 (0,0), (0,2)... - R通道 bayer_array[::2, ::2] = rgb_array[::2, ::2, 0] # 偶数行奇数列 (0,1), (0,3)... - G通道 bayer_array[::2, 1::2] = rgb_array[::2, 1::2, 1] # 奇数行偶数列 (1,0), (1,2)... - G通道 bayer_array[1::2, ::2] = rgb_array[1::2, ::2, 1] # 奇数行奇数列 (1,1), (1,3)... - B通道 bayer_array[1::2, 1::2] = rgb_array[1::2, 1::2, 2] ########################################################## # 将Bayer数组转换为字节流并保存 bayer_bytes = bayer_array.tobytes() image_bytes = b''.join([h_bytes, w_bytes, c_bytes, l_bytes, t_bytes, bayer_bytes]) # 拼接字节流,hwclt在前,像素在后 with open("demo3.raw", "wb") as f: f.write(image_bytes)使用opencv完成同样功能:
import cv2 import numpy as np from PyQt5.QtCore import QElapsedTimer # from PIL import Image timer = QElapsedTimer() timer.start() rgb_array = cv2.imread("red16.png", cv2.IMREAD_COLOR_RGB) # 读取一个图像文件并转为 RGB * 8uint 数列 h, w, _ = rgb_array.shape # 高宽 c = 3 # 通道数 l = w * h # bayer格式像素字节数 t = "BAY8" # 图像类型(Bayer-RG) # 把hwclt转换为字节(高宽等图像参数) h_bytes = h.to_bytes(2, byteorder='little') w_bytes = w.to_bytes(2, byteorder='little') c_bytes = c.to_bytes(2, byteorder='little') l_bytes = l.to_bytes(4, byteorder='little', signed=False) t_bytes = t.encode('utf-8') ###########创建Bayer-rg格式数组 (RGGB排列)################# bayer_array = np.zeros((h, w), dtype=np.uint8) # 按照Bayer-rg (RGGB) 模式填充数据 # 偶数行偶数列 (0,0), (0,2)... - R通道 bayer_array[::2, ::2] = rgb_array[::2, ::2, 0] # 偶数行奇数列 (0,1), (0,3)... - G通道 bayer_array[::2, 1::2] = rgb_array[::2, 1::2, 1] # 奇数行偶数列 (1,0), (1,2)... - G通道 bayer_array[1::2, ::2] = rgb_array[1::2, ::2, 1] # 奇数行奇数列 (1,1), (1,3)... - B通道 bayer_array[1::2, 1::2] = rgb_array[1::2, 1::2, 2] ########################################################## # 将Bayer数组转换为字节流并保存 bayer_bytes = bayer_array.tobytes() image_bytes = b''.join([h_bytes, w_bytes, c_bytes, l_bytes, t_bytes, bayer_bytes]) # 拼接字节流,hwclt在前,像素在后 with open("demo3.raw", "wb") as f: f.write(image_bytes) print(f"写入raw文件的时间: {timer.elapsed()} ms")- 读取BGR或bayer格式二进制文件的代码:
分两次读:
import cv2 import numpy as np from PyQt5.QtCore import QElapsedTimer timer = QElapsedTimer() timer.start() with open("demo3.raw", "rb") as f: head = f.read(14) h = int.from_bytes(head[:2], byteorder='little') w = int.from_bytes(head[2:4], byteorder='little') c = int.from_bytes(head[4:6], byteorder='little') l = int.from_bytes(head[6:10], byteorder='little') t = head[10:14].decode('utf-8') image_bytes = f.read(l) if t == "BGR8": img_array = np.frombuffer(image_bytes, dtype=np.uint8).reshape((h, w, c)) elif t == "BAY8": img_array = np.frombuffer(image_bytes, dtype=np.uint8).reshape((h, w)) img_array = cv2.cvtColor(img_array, cv2.COLOR_BAYER_RG2RGB) print("图像格式:", t) print(f"读取raw文件并转换为数组的时间: {timer.elapsed()} ms") cv2.imshow("img", img_array) # cv2.imwrite("demo3__.bmp", img_array) cv2.waitKey(0) cv2.destroyAllWindows()或一次读完:
import cv2 import numpy as np from PyQt5.QtCore import QElapsedTimer timer = QElapsedTimer() timer.start() with open("demo2.raw", "rb") as f: image_bytes = np.frombuffer(f.read(), dtype=np.uint8) head = image_bytes[:14].tobytes() h = int.from_bytes(head[:2], byteorder='little') w = int.from_bytes(head[2:4], byteorder='little') c = int.from_bytes(head[4:6], byteorder='little') l = int.from_bytes(head[6:10], byteorder='little') t = head[10:14].decode('utf-8') if t == "BGR8": img_array = np.frombuffer(image_bytes[14:], dtype=np.uint8).reshape((h, w, c)) elif t == "BAY8": img_array = np.frombuffer(image_bytes[14:], dtype=np.uint8).reshape((h, w)) img_array = cv2.cvtColor(img_array, cv2.COLOR_BAYER_RG2RGB) print("图像格式:", t) print(f"读取raw文件并转换为数组的时间: {timer.elapsed()} ms") cv2.imshow("img", img_array) # cv2.imwrite("demo3__.bmp", img_array) cv2.waitKey(0) cv2.destroyAllWindows()读取速度展示:
图像格式: BAY (3472, 4624, 3) 读取raw文件并转换为数组的时间: 15 ms总结,最佳实践:
1、保存时,将文件保存为Bayer-RG8格式的二进制文件,文件由文件头和像素数据两部分组成;
2、打开时,使用python原生的open(“image”, "rb")方法打开文件,先打开文件头,指定像素数据长度后再继续打开像素字节流;
3、结合以上两点,可获得兼顾到文件体积、存取速度的方法;
4、此处的bayer-RG8字节流,与相机常用的bayer-RG8字节流兼容,可直接将相机的帧字节数据加上文件头以后直接存为raw文件,读写效率很高。其他常见的RG10、RG12以及packed格式日后完善。