淺析Python 讀取圖像文件的性能對(duì)比
使用 Python 讀取一個(gè)保存在本地硬盤上的視頻文件,視頻文件的編碼方式是使用的原始的 RGBA 格式寫入的,即無壓縮的原始視頻文件。最開始直接使用 Python 對(duì)讀取到的文件數(shù)據(jù)進(jìn)行處理,然后顯示在 Matplotlib 窗口上,后來發(fā)現(xiàn)視頻播放的速度比同樣的處理邏輯的 C++ 代碼慢了很多,嘗試了不同的方法,最終實(shí)現(xiàn)了在 Python 中讀取并顯示視頻文件,幀率能夠達(dá)到 120 FPS 以上。
讀取一幀圖片數(shù)據(jù)并顯示在窗口上
最簡單的方法是直接在 Python 中讀取文件,然后逐像素的分配 RGB 值到窗口中,最開始使用的是 matplotlib 的 pyplot 組件。
一些用到的常量:
FILE_NAME = "I:/video.dat" WIDTH = 2096 HEIGHT = 150 CHANNELS = 4 PACK_SIZE = WIDTH * HEIGHT * CHANNELS
每幀圖片的寬度是 2096 個(gè)像素,高度是 150 個(gè)像素,CHANNELS 指的是 RGBA 四個(gè)通道,因此 PACK_SIZE 的大小就是一副圖片占用空間的字節(jié)數(shù)。
首先需要讀取文件。由于視頻編碼沒有任何壓縮處理,大概 70s 的視頻(每幀約占 1.2M 空間,每秒 60 幀)占用達(dá) 4Gb 的空間,所以我們不能直接將整個(gè)文件讀取到內(nèi)存中,借助 Python functools
提供的 partial 方法,我們可以每次從文件中讀取一小部分?jǐn)?shù)據(jù),將 partial 用 iter 包裝起來,變成可迭代的對(duì)象,每次讀取一幀圖片后,使用 next 讀取下一幀的數(shù)據(jù),接下來先用這個(gè)方法將保存在文件中的一幀數(shù)據(jù)讀取顯示在窗口中。
with open( file, 'rb') as f: e1 = cv.getTickCount() records = iter( partial( f.read, PACK_SIZE), b'' ) # 生成一個(gè) iterator frame = next( records ) # 讀取一幀數(shù)據(jù) 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()
需要說明的是,在保存文件時(shí)第 4 個(gè)通道保存的是透明度,因此值為 0,但在 matplotlib (包括 opencv)的窗口中顯示時(shí)第 4 個(gè)通道保存的一般是不透明度。我將第 4 個(gè)通道直接賦值成 255,以便能夠正常顯示圖片。
這樣就可以在我們的窗口中顯示一張圖片了,不過由于圖片的寬長比不協(xié)調(diào),使用 matplotlib 繪制出來的窗口必須要縮放到很大才可以讓圖片顯示的比較清楚。
為了方便稍后的性能比較,這里統(tǒng)一使用 opencv 提供的 getTickCount
方法測(cè)量用時(shí)。可以從控制臺(tái)中看到顯示一張圖片,從讀取文件到最終顯示大概要用 1.21s 的時(shí)間。如果我們只測(cè)量三層嵌套循環(huán)的用時(shí),可以發(fā)現(xiàn)有 0.8s 的時(shí)間都浪費(fèi)在循環(huán)上了。
讀取并顯示一幀圖片用時(shí) 1.21s
在處理循環(huán)上用時(shí) 0.8s
約百萬級(jí)別的循環(huán)處理,同樣的代碼放在 C++ 里面性能完全沒有問題,在 Python 中執(zhí)行起來就不一樣了。在 Python 中這樣的處理速度最多就 1.2 fps。我們暫時(shí)不考慮其他方法進(jìn)行優(yōu)化,而是將多幀圖片動(dòng)態(tài)的顯示在窗口上,達(dá)到播放視頻的效果。
連續(xù)讀取圖片并顯示
這時(shí)我們繼續(xù)讀取文件并顯示在窗口上,為了能夠動(dòng)態(tài)的顯示圖片,我們可以使用 matplotlib.animation 動(dòng)態(tài)顯示圖片,之前的程序需要進(jìn)行相應(yīng)的改動(dòng):
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 = 這個(gè)賦值操作 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
函數(shù)中執(zhí)行的,使用 matplotlib.animation.FuncAnimation
函數(shù)循環(huán)讀取每幀數(shù)據(jù)(給這個(gè)函數(shù)傳遞的 interval = 30 這個(gè)沒有作用,因?yàn)樘幚硭俣雀簧希?。另外值得注意的是不要省略?a = animation.FuncAnimation( fig, animateFromData, interval=30 )
這一行的賦值操作,雖然不太清楚原理,但是當(dāng)我把 a =
刪掉的時(shí)候,程序莫名的無法正常工作了。
控制臺(tái)中顯示的處理速度:
由于對(duì) matplotlib 的了解不多,最開始我以為是 matplotlib 顯示圖像過慢導(dǎo)致了幀率上不去,打印出代碼的用時(shí)后發(fā)現(xiàn)不是 matplotlib 的問題。因此我也使用了 PyQt5 對(duì)圖像進(jìn)行顯示,結(jié)果依然是 1~2 幀的處理速度。因?yàn)橹皇菗Q用了 Qt 的界面進(jìn)行顯示,邏輯處理的代碼依然沿用的 matplotlib.animation
提供的方法,所以并沒有本質(zhì)上的區(qū)別。這段用 Qt 顯示圖片的代碼來自于 github matplotlib issue,我對(duì)其進(jìn)行了一些適配。
使用 Numpy 的數(shù)組處理 api
我們知道,顯示圖片這么慢的原因就是在于 Python 處理 2096 * 150 這個(gè)兩層循環(huán)占用了大量時(shí)間。接下來我們換用一種 numpy 的 reshape 方法將文件中的像素?cái)?shù)據(jù)讀取到內(nèi)存中。注意 reshape 方法接收一個(gè) ndarray 對(duì)象。我這種每幀數(shù)據(jù)創(chuàng)造一個(gè) ndarray 數(shù)組的方法可能會(huì)存在內(nèi)存泄漏的風(fēng)險(xiǎn),實(shí)際上可以調(diào)用一個(gè) ndarray 數(shù)組對(duì)象的 reshape 方法。這里不再深究。
重新定義一個(gè)用于動(dòng)態(tài)顯示圖片的函數(shù) optAnimateFromData
,將其作為參數(shù)傳遞個(gè) 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 )
效果如下,可以看到使用 numpy
的 reshape
方法后,處理用時(shí)大幅減少,幀率可以達(dá)到 8~9 幀。然而經(jīng)過優(yōu)化后的處理速度仍然是比較慢的:
優(yōu)化過的代碼執(zhí)行結(jié)果
使用 Numpy 提供的 memmap
在用 Python 進(jìn)行機(jī)器學(xué)習(xí)的過程中,發(fā)現(xiàn)如果完全使用 Python 的話,很多運(yùn)算量大的程序也是可以跑的起來的,所以我確信可以用 Python 解決我的這個(gè)問題。在我不懈努力下找到 Numpy 提供的 memmap api,這個(gè) API 以數(shù)組的方式建立硬盤文件到內(nèi)存的映射,使用這個(gè) 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 讀取到的數(shù)據(jù) img 直接顯示在窗口中 cv.imshow( "file", img)
,每一幀打印出顯示該幀所用的時(shí)間,最后顯示總的時(shí)間和讀取到的數(shù)據(jù)大?。?/p>
執(zhí)行效率最高的結(jié)果
讀取速度非??欤繋脮r(shí)只需幾毫秒。這樣的處理速度完全可以滿足 60FPS 的需求。
總結(jié)
Python 語言寫程序非常方便,但是原生的 Python 代碼執(zhí)行效率確實(shí)不如 C++,當(dāng)然了,比 JS 還是要快一些。使用 Python 開發(fā)一些性能要求高的程序時(shí),要么使用 Numpy 這樣的庫,要么自己編寫一個(gè) C 語言庫供 Python 調(diào)用。在實(shí)驗(yàn)過程中,我還使用 Flask 讀取文件后以流的形式發(fā)送的瀏覽器,讓瀏覽器中的 JS 文件進(jìn)行顯示,不過同樣存在著很嚴(yán)重的性能問題和內(nèi)存泄漏問題。這個(gè)過程留到之后再講。
本文中的相應(yīng)代碼可以在 github 上查看。
Reference
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
深入解析Python中的__builtins__內(nèi)建對(duì)象
__builtins__ 是內(nèi)建模塊__builtin__中的對(duì)象,使用Python中的內(nèi)建函數(shù)時(shí)會(huì)通過__builtins__引導(dǎo),這里我們就來深入解析Python中的__builtins__內(nèi)建對(duì)象,需要的朋友可以參考下2016-06-06python 捕獲shell腳本的輸出結(jié)果實(shí)例
下面小編就為大家?guī)硪黄猵ython 捕獲shell腳本的輸出結(jié)果實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-01-01windows下安裝python paramiko模塊的代碼
windows下安裝python paramiko模塊,有需要的朋友可以參考下2013-02-02Python制作當(dāng)年第一款手機(jī)游戲-貪吃蛇游戲(練習(xí))
這篇文章主要介紹了Python制作當(dāng)年第一款手機(jī)游戲-貪吃蛇游戲,文章利用Python?pygame做一個(gè)貪吃蛇的小游戲而且講清楚每一段代碼是用來干嘛的,需要的朋友可以參考一下2022-01-01Python Selenium中等待設(shè)置的實(shí)現(xiàn)
本文主要介紹了Python Selenium中等待設(shè)置的實(shí)現(xiàn),過詳實(shí)的示例代碼,深入介紹了顯式等待、隱式等待、自定義等待條件、多重等待條件、頁面加載狀態(tài)的等待、元素存在與可見性等待、Fluent等待以及異步JavaScript加載的等待,感興趣的可以了解一下2023-12-12