Python 讀取圖像文件的性能對比


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 方法,我們可以每次從文件中讀取一小部分數據,將 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 = 刪掉的時候,程序莫名的無法正常工作了。

控制台中顯示的處理速度:

由於對 matplotlib 的了解不多,最開始我以為是 matplotlib 顯示圖像過慢導致了幀率上不去,打印出代碼的用時后發現不是 matplotlib 的問題。因此我也使用了 PyQt5 對圖像進行顯示,結果依然是 1~2 幀的處理速度。因為只是換用了 Qt 的界面進行顯示,邏輯處理的代碼依然沿用的 matplotlib.animation 提供的方法,所以並沒有本質上的區別。這段用 Qt 顯示圖片的代碼來自於 github matplotlib issue,我對其進行了一些適配。

使用 Numpy 的數組處理 api

我們知道,顯示圖片這么慢的原因就是在於 Python 處理 2096 * 150 這個兩層循環占用了大量時間。接下來我們換用一種 numpyreshape 方法將文件中的像素數據讀取到內存中。注意 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 幀。然而經過優化后的處理速度仍然是比較慢的:


優化過的代碼執行結果

使用 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),每一幀打印出顯示該幀所用的時間,最后顯示總的時間和讀取到的數據大小:


執行效率最高的結果

讀取速度非常快,每幀用時只需幾毫秒。這樣的處理速度完全可以滿足 60FPS 的需求。

總結

Python 語言寫程序非常方便,但是原生的 Python 代碼執行效率確實不如 C++,當然了,比 JS 還是要快一些。使用 Python 開發一些性能要求高的程序時,要么使用 Numpy 這樣的庫,要么自己編寫一個 C 語言庫供 Python 調用。在實驗過程中,我還使用 Flask 讀取文件后以流的形式發送的瀏覽器,讓瀏覽器中的 JS 文件進行顯示,不過同樣存在着很嚴重的性能問題和內存泄漏問題。這個過程留到之后再講。

本文中的相應代碼可以在 github 上查看。

Reference

  1. functools partial
  2. opencv
  3. matplotlib animation
  4. numpy
  5. numpy reshape
  6. memmap
  7. matplotlib issue on github
  8. C 語言擴展


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM