【Python】PILで高速に全画素アクセスを行う

Pythonでの画像処理はOpenCVやPIL(Pillow)等のライブラリが揃っており主要なロジックに困る事はありませんが、オリジナルな画像処理を自前実装する機会はあるかと思います。画像の全画素を舐める様なアクセスでは、僅かな非効率が積み重なり処理時間が爆増することも…。
ここでは、スポット的な画素アクセスではなく全画素アクセス + x,yの軸情報が必要な場合に、効率的にアクセスする方法を下記パターンで比較します。

  • Image.getpixel() でアクセス
  • Numpy配列でアクセス
  • Image.getdata()でアクセス
    ・一重ループ
    ・二重ループ
    ・二重ループ(インデックスをキャッシュ)

 実験ソースは以下です。 1000x1000の画像の全ピクセルアクセス時間を比較します。

from PIL import Image
import sys, time
import numpy as np

# getpixelで画素アクセス
def getpixel():
    for y in range(height):
        for x in range(width):
            pixdata = img.getpixel((x,y))
            # print("[{},{}] : {}".format(y,x,pixdata))

# numpyで画素アクセス
def numpy():
    imgArray = np.array(img)
    for y in range(height):
        for x in range(width):
            pixdata = imgArray[y][x]
            # print("[{},{}] : {}".format(y,x,pixdata))

# getdataで画素アクセス
def getdata1():
    imgdata = img.getdata()
    for y in range(height):
        for x in range(width):
            pixdata = imgdata[y * width + x]
            # print("[{},{}] : {}".format(y,x,pixdata))

# getdataで画素アクセス(行インデックスのキャッシュあり)
def getdata2():
    imgdata = img.getdata()
    for y in range(height):
        ycache = y * width
        for x in range(width):
            pixdata = imgdata[ycache + x]
            # print("[{},{}] : {}".format(y,x,pixdata))

# getdataで画素アクセス(一重ループ)
def getdata3():
    imgdata = img.getdata()
    for i in range(height * width):
        x = i % width
        y = int(i / width)
        pixdata = imgdata[i]
        # print("[{},{}] : {}".format(y,x,pixdata))


if __name__ == '__main__':

    img = Image.open("1000x1000.png")
    width, height =img.size
    # 処理時間を比較
    for func in (getpixel, numpy, getdata1, getdata2, getdata3):

        start = time.time()
        func()
        end = time.time()
        print("{} : {:4.1f}ms".format(func.__name__, (end - start) * 1000))

MacbookPro2017 i5 2.3Ghzでの実行結果を載せます。
・getpixel  : 1270.1ms
・numpy    :  280.9ms
・getdata1 :  176.6ms
getdata2 : 135.8ms
・getdata3 :  332.3ms

getdata2()が一番速い結果となりました。ソースを見れば納得ですが、getdata1()からループ内の不要な乗算をキャッシュしています。
一方、getpixel()はgetdata2()に比べ10倍近く遅い結果に。そもそもループ毎に関数をコールしているのでそうなりますよね。
getdata3()は除算とmodの計算コストが加算・乗算に比べ大きいため時間がかかっています。

  • 結論

連続した画素アクセスはImage.getdata()で取得した一次元データを効率良くループするのが良いです。
一方、スポット的なアクセスであればImage.getpixel()で構わないでしょう。
冒頭でも述べましたが、まずはC/C++で内部実装された高速なライブラリがあるか探し、無ければ自前実装といった風に、基本はライブラリに頼るのが確実です。