浅析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中find()方法的使用
May 18 Python
初步剖析C语言编程中的结构体
Jan 16 Python
python正则表达式之作业计算器
Mar 18 Python
Python数据类型中的“冒号“[::]——分片与步长操作示例
Jan 24 Python
详解python字节码
Feb 07 Python
Python实现的根据文件名查找数据文件功能示例
May 02 Python
解决Python 中英文混输格式对齐的问题
Jul 16 Python
Python3非对称加密算法RSA实例详解
Dec 06 Python
解决python 读取excel时 日期变成数字并加.0的问题
Oct 08 Python
Python 脚本的三种执行方式小结
Dec 21 Python
Python使用urllib模块对URL网址中的中文编码与解码实例详解
Feb 18 Python
使用python客户端访问impala的操作方式
Mar 28 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设计模式 php实现装饰器模式(decorator)
2015/12/07 PHP
让你的PHP7更快之Hugepage用法分析
2016/05/31 PHP
php的4种常用运行方式详解
2016/12/22 PHP
使两个iframe的高度与内容自适应,且相等
2006/11/20 Javascript
Javascript 面向对象 对象(Object)
2010/05/13 Javascript
javascript使用activex控件的代码
2011/01/27 Javascript
关于图片按比例自适应缩放的js代码
2011/10/30 Javascript
关于onchange事件在IE和FF下的表现及解决方法
2014/03/08 Javascript
js 动态为textbox添加下拉框数据源的方法
2014/04/24 Javascript
JavaScript驾驭网页-CSS与DOM
2016/03/24 Javascript
学JavaScript七大注意事项【必看】
2016/05/04 Javascript
BootStrap入门教程(二)之固定的内置样式
2016/09/19 Javascript
Angularjs自定义指令Directive详解
2017/05/27 Javascript
vue实现移动端图片裁剪上传功能
2020/08/18 Javascript
webpack+vue+express(hot)热启动调试简单配置方法
2018/09/19 Javascript
angular4+百分比进度显示插件用法示例
2019/05/05 Javascript
微信小程序上传文件到阿里OSS教程
2019/05/20 Javascript
vue使用高德地图点击下钻上浮效果的实现思路
2019/10/12 Javascript
在vue中通过render函数给子组件设置ref操作
2020/11/17 Vue.js
[01:03:41]完美世界DOTA2联赛PWL S3 DLG vs Phoenix 第一场 12.17
2020/12/19 DOTA
详解python中requirements.txt的一切
2017/03/03 Python
使用Python对SQLite数据库操作
2017/04/06 Python
python的内存管理和垃圾回收机制详解
2019/05/18 Python
Python 进程之间共享数据(全局变量)的方法
2019/07/16 Python
Python第三方库的几种安装方式(小结)
2020/04/03 Python
Python分析微信好友性别比例和省份城市分布比例的方法示例【基于itchat模块】
2020/05/29 Python
python 解决pycharm运行py文件只有unittest选项的问题
2020/09/01 Python
吉力贝官方网站:Jelly Belly
2019/03/11 全球购物
德国W家官网,可直邮中国的母婴商城:Windeln.de
2021/03/03 全球购物
shell变量的作用空间是什么
2013/08/17 面试题
化学相关工作求职信
2013/10/02 职场文书
继承权公证书
2014/04/09 职场文书
经典毕业生求职信
2014/07/12 职场文书
大专毕业生自我鉴定范文(2篇)
2014/09/27 职场文书
考教师资格证不要错过的4个最佳时机
2019/07/17 职场文书
SQL Server——索引+基于单表的数据插入与简单查询【1】
2021/04/05 SQL Server