浅析Python 读取图像文件的性能对比


Posted in Python onMarch 07, 2019

使用 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 方法,我们可以每次从文件中读取一小部分数据,将 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 的时间都浪费在循环上了。

浅析Python 读取图像文件的性能对比

读取并显示一帧图片用时 1.21s

浅析Python 读取图像文件的性能对比

在处理循环上用时 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 = 删掉的时候,程序莫名的无法正常工作了。

控制台中显示的处理速度:

浅析Python 读取图像文件的性能对比

由于对 matplotlib 的了解不多,最开始我以为是 matplotlib 显示图像过慢导致了帧率上不去,打印出代码的用时后发现不是 matplotlib 的问题。因此我也使用了 PyQt5 对图像进行显示,结果依然是 1~2 帧的处理速度。因为只是换用了 Qt 的界面进行显示,逻辑处理的代码依然沿用的 matplotlib.animation 提供的方法,所以并没有本质上的区别。这段用 Qt 显示图片的代码来自于 github matplotlib issue,我对其进行了一些适配。

使用 Numpy 的数组处理 api

我们知道,显示图片这么慢的原因就是在于 Python 处理 2096 * 150 这个两层循环占用了大量时间。接下来我们换用一种 numpy 的 reshape 方法将文件中的像素数据读取到内存中。注意 reshape 方法接收一个 ndarray 对象。我这种每帧数据创造一个 ndarray 数组的方法可能会存在内存泄漏的风险,实际上可以调用一个 ndarray 数组对象的 reshape 方法。这里不再深究。

重新定义一个用于动态显示图片的函数 optAnimateFromData,将其作为参数传递个 FuncAnimation

def optAnimateFromData(i):
  e1 = cv.getTickCount()
  frame = next( records ) # one image data
  img = np.reshape( np.array( list( frame ), dtype = np.uint8 ), ( HEIGHT, WIDTH, CHANNELS ) )
  img[ : , : , 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, optAnimateFromData, interval=30 )

效果如下,可以看到使用 numpyreshape 方法后,处理用时大幅减少,帧率可以达到 8~9 帧。然而经过优化后的处理速度仍然是比较慢的:

浅析Python 读取图像文件的性能对比

优化过的代码执行结果

使用 Numpy 提供的 memmap

在用 Python 进行机器学习的过程中,发现如果完全使用 Python 的话,很多运算量大的程序也是可以跑的起来的,所以我确信可以用 Python 解决我的这个问题。在我不懈努力下找到 Numpy 提供的 memmap api,这个 API 以数组的方式建立硬盘文件到内存的映射,使用这个 API 后程序就简单一些了:

cv.namedWindow("file")
count = 0
start = time.time()
try:
  number = 1
  while True:
    e1 = cv.getTickCount()
    img = np.memmap(filename=FILE_NAME, dtype=np.uint8, shape=SHAPE, mode="r+", offset=count )
    count += PACK_SIZE
    cv.imshow( "file", img )
    e2 = cv.getTickCount()
    elapsed = ( e2 - e1 ) / cv.getTickFrequency()
    print("FPS: %.2f Used time: %.3f" % (number / elapsed, elapsed ))
    key = cv.waitKey(20)
    if key == 27: # exit on ESC
      break
except StopIteration:
  pass
finally:
  end = time.time()
  print( 'File Data read: {:.2f}Gb'.format( count / 1024 / 1024 / 1024), ' time used: {:.2f}s'.format( end - start ) )
  cv.destroyAllWindows()

将 memmap 读取到的数据 img 直接显示在窗口中 cv.imshow( "file", img),每一帧打印出显示该帧所用的时间,最后显示总的时间和读取到的数据大小:

浅析Python 读取图像文件的性能对比

执行效率最高的结果

读取速度非常快,每帧用时只需几毫秒。这样的处理速度完全可以满足 60FPS 的需求。

总结

Python 语言写程序非常方便,但是原生的 Python 代码执行效率确实不如 C++,当然了,比 JS 还是要快一些。使用 Python 开发一些性能要求高的程序时,要么使用 Numpy 这样的库,要么自己编写一个 C 语言库供 Python 调用。在实验过程中,我还使用 Flask 读取文件后以流的形式发送的浏览器,让浏览器中的 JS 文件进行显示,不过同样存在着很严重的性能问题和内存泄漏问题。这个过程留到之后再讲。

本文中的相应代码可以在 github 上查看。

Reference

functools

partial

opencv

matplotlib animation

numpy

numpy reshape

memmap

matplotlib issue on github

C 语言扩展

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python基于回溯法子集树模板解决数字组合问题实例
Sep 02 Python
Pyqt5如何让QMessageBox按钮显示中文示例代码
Apr 11 Python
Flask框架模板渲染操作简单示例
Jul 31 Python
pytorch索引查找 index_select的例子
Aug 18 Python
python seaborn heatmap可视化相关性矩阵实例
Jun 03 Python
Python实现上下文管理器的方法
Aug 07 Python
Python制作数据预测集成工具(值得收藏)
Aug 21 Python
python 三种方法实现对Excel表格的读写
Nov 19 Python
最新Python idle下载、安装与使用教程图文详解
Nov 28 Python
windows安装python超详细图文教程
May 21 Python
python用海龟绘图写贪吃蛇游戏
Jun 18 Python
使用pd.merge表连接出现多余行的问题解决
Jun 16 Python
python try 异常处理(史上最全)
Mar 07 #Python
通过shell+python实现企业微信预警
Mar 07 #Python
Python一个简单的通信程序(客户端 服务器)
Mar 06 #Python
用Python写一个模拟qq聊天小程序的代码实例
Mar 06 #Python
Python二叉树的镜像转换实现方法示例
Mar 06 #Python
Python实现二叉树的常见遍历操作总结【7种方法】
Mar 06 #Python
Python中一般处理中文的几种方法
Mar 06 #Python
You might like
php开发文档 会员收费1期
2012/08/14 PHP
关于使用key/value数据库redis和TTSERVER的心得体会
2013/06/28 PHP
在Mac OS上自行编译安装Apache服务器和PHP解释器
2015/12/24 PHP
PHP基于cookie与session统计网站访问量并输出显示的方法
2016/01/15 PHP
Laravel框架实现多数据库连接操作详解
2019/07/12 PHP
JavaScript中的对象化编程
2008/01/16 Javascript
通过 Dom 方法提高 innerHTML 性能
2008/03/26 Javascript
js模拟弹出效果代码修正版
2008/08/07 Javascript
prototype 学习笔记整理
2009/07/17 Javascript
减少访问DOM的次数提升javascript性能
2014/02/24 Javascript
jQuery使用andSelf()来包含之前的选择集
2014/05/19 Javascript
jQuery使用empty()方法删除元素及其所有子元素的方法
2015/03/26 Javascript
php常见的页面跳转方法汇总
2015/04/15 Javascript
JavaScript中解析JSON数据的三种方法
2015/07/03 Javascript
浅谈bootstrap源码分析之tab(选项卡)
2016/06/06 Javascript
jQuery生成假加载动画效果
2016/12/01 Javascript
Ionic项目中Native Camera的使用方法
2017/06/07 Javascript
bootstrap里bootstrap动态加载下拉框的实例讲解
2018/08/10 Javascript
微信小程序生成分享海报方法(附带二维码生成)
2019/03/29 Javascript
如何让微信小程序页面之间的通信不再变困难
2019/06/03 Javascript
JavaScript this在函数中的指向及实例详解
2019/10/14 Javascript
关于Python数据结构中字典的心得
2017/12/04 Python
python进行两个表格对比的方法
2018/06/27 Python
python requests更换代理适用于IP频率限制的方法
2019/08/21 Python
利用python实现凯撒密码加解密功能
2020/03/31 Python
使用CSS3制作一个简单的Chrome模拟器
2015/07/15 HTML / CSS
CSS3解析抖音LOGO制作的方法步骤
2019/04/11 HTML / CSS
美国美食礼品篮网站:Gourmet Gift Baskets
2019/12/15 全球购物
数据库专业英语
2012/11/30 面试题
导师推荐信范文
2014/05/09 职场文书
国庆宣传标语
2014/06/30 职场文书
学校教师安全责任书
2014/07/23 职场文书
2014年党小组工作总结
2014/12/20 职场文书
五四青年节活动总结
2015/02/10 职场文书
浅谈golang package中init方法的多处定义及运行顺序问题
2021/05/06 Golang
如何在pycharm中快捷安装pip命令(如pygame)
2021/05/31 Python