画像を開き輝度値を取得/設定するのは、画像処理を行う、はじめの一歩的な処理ですよね。
まずは、最も基本的な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言語のライブラリで処理を行う必要があるんでしょうね。