浅析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程序中的运用
Mar 02 Python
Python使用numpy模块创建数组操作示例
Jun 20 Python
Python单向链表和双向链表原理与用法实例详解
Aug 31 Python
浅谈django三种缓存模式的使用及注意点
Sep 30 Python
对Python3之进程池与回调函数的实例详解
Jan 22 Python
Djang的model创建的字段和参数详解
Jul 27 Python
Python IDE Pycharm中的快捷键列表用法
Aug 08 Python
python中bytes和str类型的区别
Oct 21 Python
python怎么自定义捕获错误
Jun 29 Python
flask开启多线程的具体方法
Aug 02 Python
python反扒机制的5种解决方法
Feb 06 Python
python游戏开发Pygame框架
Apr 22 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 strtotime函数详解
2009/12/18 PHP
PHP备份/还原MySQL数据库的代码
2011/01/06 PHP
PHP Session_Regenerate_ID函数双释放内存破坏漏洞
2011/01/27 PHP
PHP 八种基本的数据类型小结
2011/06/01 PHP
php抓取页面的几种方法详解
2013/06/17 PHP
PHP执行shell脚本运行程序不产生core文件的方法
2016/12/28 PHP
PHP框架自动加载类文件原理详解
2017/06/06 PHP
PHP文件系统管理(实例讲解)
2017/09/19 PHP
Laravel使用Queue队列的技巧汇总
2019/09/02 PHP
Laravel 类和接口注入相关的代码
2019/10/15 PHP
document 和 document.all 分别什么时候用
2006/06/22 Javascript
用js实现随机返回数组的一个元素
2007/08/13 Javascript
javascript 支持ie和firefox杰奇翻页函数
2008/07/22 Javascript
jQuery阻止事件冒泡具体实现
2013/10/11 Javascript
node.js中的path.extname方法使用说明
2014/12/09 Javascript
javascript修改图片src的方法
2015/01/27 Javascript
js正则表达式匹配数字字母下划线等
2015/04/14 Javascript
使用JQuery选择HTML遍历函数的方法
2016/09/17 Javascript
BootStrap表单验证实例代码
2017/01/13 Javascript
JS实现的新闻列表自动滚动效果示例
2019/01/30 Javascript
vuex 动态注册方法 registerModule的实现
2019/07/03 Javascript
Vue中消息横向滚动时setInterval清不掉的问题及解决方法
2019/08/23 Javascript
vue 使用高德地图vue-amap组件过程解析
2019/09/07 Javascript
python实现自动解数独小程序
2019/01/21 Python
安装好Pycharm后如何配置Python解释器简易教程
2019/06/28 Python
python用opencv完成图像分割并进行目标物的提取
2020/05/25 Python
Otel.com:折扣酒店预订
2017/08/24 全球购物
NIHAOMARKET官方海外旗舰店:意大利你好华人超市
2018/01/27 全球购物
数据库笔试题
2013/05/09 面试题
毕业生的自我鉴定
2013/10/29 职场文书
机械电子工程专业推荐信范文
2013/11/20 职场文书
房地产融资计划书
2014/01/10 职场文书
小学后勤管理制度
2014/01/14 职场文书
2014年小学体育工作总结
2014/12/11 职场文书
2015医德医风个人工作总结
2015/04/02 职场文书
详解nginx安装过程并代理下载服务器文件
2022/02/12 Servers