【Python/Pillow(PIL)】画像の輝度値の取得/設定

画像を開き輝度値を取得/設定するのは、画像処理を行う、はじめの一歩的な処理ですよね。

まずは、最も基本的なgetpixel/putpixelを使った方法を紹介します。

getpixel()を使った輝度値の取得

getpixel()の構文は以下の通りです。

value = Image.getpixel(xy)

xyは画像の左上を原点とするxy座標で、(x, y)のようにタプルで指定します。

戻り値が指定した画像の輝度値となり、モノクロ画像の場合は、指定した座標の輝度値が戻り、カラーの場合は(r, g, b)のように3つの要素のタプルが戻ってきます。カラー画像でも ‘RGBA’ のように透過付きの画像データの場合は(r, g, b, a)のように4つの要素のタプルが戻ります。

実行例

putpixel()を使った輝度値の設定

putpixel()の構文は以下の通りです。

Image.putpixel(xy, value)

引数はgetpixel()と同じ用に、xyには(x, y)のように座標をタプルを指定します。valueの部分には、モノクロの場合は、指定座標の輝度値を、カラーの場合は(r, g, b)もしくは(r, g, b, a)のように輝度値をタプルで指定します。

さらに輝度値の値に0~255の範囲を超えて指定した場合、値が負の場合は0、値が256以上の場合は255に修正されます。

実行例

処理の高速化の検討

C#でも似た関数は処理時間が遅いで有名でしたが、getpixel()、putpixel()も処理時間が遅いらしい。

そこでいくつかの輝度値の取得/設定方法を試して処理時間の比較を行ってみたいと思います。

 

まずは処理時間の基準となるgetpixel(), putpixel() を使った処理時間を計測します。

輝度値を取得し、コントラストを調整し、輝度値を画像に設定するサンプルです。

from PIL import Image
import numpy as np
import time

# 元画像を保持
img_original = Image.open("Mandrill.bmp")
img = img_original.copy()
width, height = img.size

############################################
# getpixel(), putpixel() を使った方法
start = time.perf_counter()

for y in range(height):
    for x in range(width):
        r, g, b = img.getpixel((x, y))
        img.putpixel((x, y), (r * 5 - 500, g * 5 - 500, b * 5 - 500))

print("getpixel(), putpixel() を使った方法\t", (time.perf_counter() - start) * 1000, "msec")

上記プログラムを実行すると、以下のようになります。

 

処理前画像

処理後画像

 

この他に以下の方法を試してみます。

  • getdata(), putdata() を使った方法
    getdata()は画像全体の輝度値を画像の左上から順に各画素の輝度値(R, G, B)の値がタプルの一次元のリストで取得します。
    putdata()は輝度値(R, G, B)のタプルの一次元のリストを指定し、画像全体の輝度値を設定します。
  • numpy を使った方法
    Pillowの画像データからNumPyの画像データへ変換し、NumPyデータを処理し、Pillowの画像データに戻しています。
  • Pillow <-> numpy の相互変換だけの時間
    PillowとNumPyの画像データの変換時間を参考に計測します。
  • point() を使った方法
    輝度値の取得/設定の処理時間の評価の趣旨から外れますが、画像処理に周辺画素の輝度値を用いない場合、LUT(Look Up Table)を用いると高速に処理が行えるため、Pillowのpoint()メソッドでLUT変換を行った処理時間を参考に計測しています。

 

使用した全プログラム

from PIL import Image
import numpy as np
import time

# 元画像を保持
img_original = Image.open("Mandrill.bmp")
img = img_original.copy()
width, height = img.size

############################################
# getpixel(), putpixel() を使った方法
start = time.perf_counter()

for y in range(height):
    for x in range(width):
        r, g, b = img.getpixel((x, y))
        img.putpixel((x, y), (r * 5 - 500, g * 5 - 500, b * 5 - 500))

print("getpixel(), putpixel() を使った方法\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# getdata(), putdata() を使った方法
img = img_original.copy()
start = time.perf_counter()

data = img.getdata()
# 処理後のデータをlistで確保
data_dst = [None] * len(data)

for y in range(height):
    for x in range(width):
        r, g, b = data[x + y * width]
        data_dst[x + y * width] = (r * 5 - 500, g * 5 - 500, b * 5 - 500)

img.putdata(data_dst)

print("getdata(), putdata() を使った方法\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# numpy を使った方法
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換
numpy_iamge = np.array(img)

for y in range(height):
    for x in range(width):
        r = numpy_iamge[y, x, 0]
        g = numpy_iamge[y, x, 1]
        b = numpy_iamge[y, x, 2]
        r = r * 5 - 500
        g = g * 5 - 500
        b = b * 5 - 500
        if r < 0:
            r = 0
        if g < 0:
            g = 0
        if b < 0: b = 0 if r > 255:
            r = 255
        if g > 255:
            g = 255
        if b > 255:
            b = 255
        numpy_iamge[y, x, 0] = r
        numpy_iamge[y, x, 1] = g
        numpy_iamge[y, x, 2] = b

# numpy → pillowへ変換
img = Image.fromarray(numpy_iamge)

print("numpy を使った方法\t\t\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# numpyらしい処理 その1
# numpyの配列(ndarray)をそのまま演算する
# 0~255に制限するのにclipを用いる
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換(計算後、負になるのでint32型へ変換)
numpy_iamge = np.array(img, dtype = np.int32)

dst_img = numpy_iamge * 5 - 500

# 0~255のuint8型へ変換
dst_img = dst_img.clip(0, 255).astype(np.uint8)

# numpy → pillowへ変換
img = Image.fromarray(dst_img)

print("numpyらしい処理 その1\t\t\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# numpyらしい処理 その2
# LUTを使った変換
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換
numpy_iamge = np.array(img)

# LUT(Look Up Table)の作成
lut = np.empty(256, dtype = np.uint8)
for i in range(256):
    val = i * 5 - 500
    if val < 0:
        val = 0
    if val > 255:
        val = 255
    lut[i] = val

# LUTを介して変換
numpy_iamge = lut[numpy_iamge]

# numpy → pillowへ変換
img = Image.fromarray(numpy_iamge)

print("numpyらしい処理 その2\t\t\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# Pillow ⇔ numpy の相互変換だけの時間
img = img_original.copy()
start = time.perf_counter()
# pillow → numpyへ変換
numpy_iamge = np.array(img)
# numpy → pillowへ変換
img = Image.fromarray(numpy_iamge)

print("Pillow ⇔ numpy の相互変換だけの時間\t", (time.perf_counter() - start) * 1000, "msec")

############################################
# point() を使った方法
img = img_original.copy()
start = time.perf_counter()

# LUT(Look Up Table)の作成
lut = []
for i in range(256):
    val = i * 5 - 500
    if val < 0:
        val = 0 
    if val > 255:
        val = 255
    lut.append(val)
# R, G, Bに同じLUTを使用
lut = lut * 3

# pointメソッドでLUT変換を行う
img = img.point(lut)

print("point() を使った方法\t\t\t", (time.perf_counter() - start) * 1000, "msec")

処理時間の比較(使用した画像は256×256の24bitカラー画像)

方法 処理時間
getpixel(), putpixel() を使った方法 99.223 msec
getdata(), putdata() を使った方法 37.833 msec
numpy を使った方法 440.229 msec
(参考)numpyらしい処理 その1 2.081 msec
(参考)numpyらしい処理 その2 1.041 msec
(参考)Pillow <-> numpy の相互変換だけの時間 0.254 msec
(参考)point() を使った方法 0.156 msec

まとめ

輝度値の取得/設定を行う処理については、getdata(), putdata() を使った方法が一番速い結果となりました。

numpyを使うと、もう少し速いかと思っていたのですが、あまりに遅かったので、Pillow <-> numpy の相互変換の処理時間を計測してみましたが、やはり画像処理している部分が遅い事が分かりました。
numpyの処理だけ、0~255に輝度値が入るようにif文で処理をしていますが、これは、numpyのデータがuint8(8bitの符号なし整数)になるため、この処理を入れないと、下図のように変な画像になってしまいます。

逆に、getpixel(), putpixel() も getdata(), putdata() も、輝度値に0~255の範囲外の値を指定しても0~255の範囲に調整してくれるので、これは便利です。

ただし、numpyに画像データを変換すると、OpenCVも使えるので、numpyで画像処理するなら、使える処理があれば極力OpenCVを使うようにするとよいでしょうね。
numpyのデータはfor文で値を参照すると、どうしても遅いようです。

また、参考にpoint()メソッドによりLUTを使った処理時間を計測してみましたが、こちらは爆速でした!

今回はpoint()メソッドを使いましたが、他にもPillowでできる画像処理のメソッドが用意されているので、おいおい紹介したいと思います。

結局、Pythonでベタな画像処理をしてはいけないということですね。
OpenCVなどに無いオリジナルの画像処理をしたい場合は、やっぱりC言語のライブラリで処理を行う必要があるんでしょうね。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください