Python 读取图像文件的性能对比
使用 Python 读取一个保存在本地硬盘上的视频文件,视频文件的编码方式是使用的原始的 RGBA 格式写入的,即无压缩的原始视频文件。最开始直接使用 Python 对读取到的文件数据进行处理,然后显示在 Matplotlib 窗口上,后来发现视频播放的速度比同样的处理逻辑的 C++ 代码慢了很多,尝试了不同的方法,最终实现了在 Python 中读取并显示视频文件,帧率能够达到 120 FPS 以上。
读取一帧图片数据并显示在窗口上最简单的方法是直接在 Python 中读取文件,然后逐像素的分配 RGB 值到窗口中,最开始使用的是 matplotlib 的 pyplot 组件。
一些用到的常量:
FILE_NAME = "I:/video.dat" WIDTH = 2096 HEIGHT = 150 CHANNELS = 4 PACK_SIZE = WIDTH * HEIGHT * CHANNELS每帧图片的宽度是 2096 个像素,高度是 150 个像素,CHANNELS 指的是 RGBA 四个通道,因此 PACK_SIZE 的大小就是一副图片占用空间的字节数。
首先需要读取文件。由于视频编码没有任何压缩处理,大概 70s 的视频(每帧约占 1.2M 空间,每秒 60 帧)占用达 4Gb 的空间,所以我们不能直接将整个文件读取到内存中,借助 Python functools 提供的 方法,我们可以每次从文件中读取一小部分数据,将 partial 用 iter 包装起来,变成可迭代的对象,每次读取一帧图片后,使用 next 读取下一帧的数据,接下来先用这个方法将保存在文件中的一帧数据读取显示在窗口中。
with open( file, 'rb') as f: e1 = cv.getTickCount() records = iter( partial( f.read, PACK_SIZE), b'' ) # 生成一个 iterator frame = next( records ) # 读取一帧数据 img = np.zeros( ( HEIGHT, WIDTH, CHANNELS ), dtype = np.uint8) for y in range(0, HEIGHT): for x in range( 0, WIDTH ): pos = (y * WIDTH + x) * CHANNELS for i in range( 0, CHANNELS - 1 ): img[y][x][i] = frame[ pos + i ] img[y][x][3] = 255 plt.imshow( img ) plt.tight_layout() plt.subplots_adjust(left=0, right=1, top=1, bottom=0) plt.xticks([]) plt.yticks([]) e2 = cv.getTickCount() elapsed = ( e2 - e1 ) / cv.getTickFrequency() print("Time Used: ", elapsed ) plt.show()需要说明的是,在保存文件时第 4 个通道保存的是透明度,因此值为 0,但在 matplotlib (包括 opencv)的窗口中显示时第 4 个通道保存的一般是不透明度。我将第 4 个通道直接赋值成 255,以便能够正常显示图片。
这样就可以在我们的窗口中显示一张图片了,不过由于图片的宽长比不协调,使用 matplotlib 绘制出来的窗口必须要缩放到很大才可以让图片显示的比较清楚。
为了方便稍后的性能比较,这里统一使用 opencv 提供的 getTickCount 方法测量用时。可以从控制台中看到显示一张图片,从读取文件到最终显示大概要用 1.21s 的时间。如果我们只测量三层嵌套循环的用时,可以发现有 0.8s 的时间都浪费在循环上了。
读取并显示一帧图片用时 1.21s
在处理循环上用时 0.8s
约百万级别的循环处理,同样的代码放在 C++ 里面性能完全没有问题,在 Python 中执行起来就不一样了。在 Python 中这样的处理速度最多就 1.2 fps。我们暂时不考虑其他方法进行优化,而是将多帧图片动态的显示在窗口上,达到播放视频的效果。
连续读取图片并显示这时我们继续读取文件并显示在窗口上,为了能够动态的显示图片,我们可以使用 matplotlib.animation 动态显示图片,之前的程序需要进行相应的改动:
fig = plt.figure() ax1 = fig.add_subplot(1, 1, 1) try: img = np.zeros( ( HEIGHT, WIDTH, CHANNELS ), dtype = np.uint8) f = open( FILE_NAME, 'rb' ) records = iter( partial( f.read, PACK_SIZE ), b'' ) def animateFromData(i): e1 = cv.getTickCount() frame = next( records ) # drop a line data for y in range( 0, HEIGHT ): for x in range( 0, WIDTH ): pos = (y * WIDTH + x) * CHANNELS for i in range( 0, CHANNELS - 1 ): img[y][x][i] = frame[ pos + i] img[y][x][3] = 255 ax1.clear() ax1.imshow( img ) e2 = cv.getTickCount() elapsed = ( e2 - e1 ) / cv.getTickFrequency() print( "FPS: %.2f, Used time: %.3f" % (1 / elapsed, elapsed )) a = animation.FuncAnimation( fig, animateFromData, interval=30 ) # 这里不要省略掉 a = 这个赋值操作 plt.tight_layout() plt.subplots_adjust(left=0, right=1, top=1, bottom=0) plt.xticks([]) plt.yticks([]) plt.show() except StopIteration: pass finally: f.close()和第 1 部分稍有不同的是,我们显示每帧图片的代码是在 animateFromData 函数中执行的,使用 matplotlib.animation.FuncAnimation 函数循环读取每帧数据(给这个函数传递的 interval = 30 这个没有作用,因为处理速度跟不上)。另外值得注意的是不要省略掉 a = animation.FuncAnimation( fig, animateFromData, interval=30 ) 这一行的赋值操作,虽然不太清楚原理,但是当我把 a = 删掉的时候,程序莫名的无法正常工作了。
控制台中显示的处理速度: