PIL/Pillowで色を置換する場合、簡単に思いつくのはImage.getpixel/Image.putpixelを使う方法だ。
しかしながら、Image.getpixel/Image.putpixelは1枚の画像を処理するのに数秒かかってしまうほど遅い、非常に遅い。
numpyを使って高速に置換する方法もあるが、色の置換ごときに依存モジュールを増やすのも馬鹿らしい。
そこでシンプルにPIL/Pillowのみで高速に色を置換する方法を考えた。
サンプルとして以下の画像の服の色(255, 204, 0)を(48,255,48)に置換してみよう。
なお画像はRGB24ビットでアルファなし、コードはpython2で記述する。
画像を色ごとに分解する
まず、Image.splitを使い画像を各色ごとのバンドに分解する。
from __future__ import unicode_literals, print_function, absolute_import
from PIL import Image, ImageChops
img = Image.open("sample.png")
r, g, b = img.split()
各RGBバンドの内容は以下のようになる。
Image.pointで特定の色ごとに2値化する
src_color = (255, 204, 0)
_r = r.point(lambda _: 1 if _ == src_color[0] else 0, mode="1")
_g = g.point(lambda _: 1 if _ == src_color[1] else 0, mode="1")
_b = b.point(lambda _: 1 if _ == src_color[2] else 0, mode="1")
Image.pointはテーブル変換するメソッドである、今回はlambdaを使用して各バンドごとに特定の値のみ抜き出す画像を生成している。
ポイントは引数にmode="1"を指定しているところで、modeに"1"を指定すると色深度が1ビットの画像が生成される。
これは後に述べるImageChopsモジュールでの変換に必要な処理である。
各RGBバンドの内容は以下のようになる。
各バンドをAND合成してマスクを生成する
ImageChopsモジュールは、加算合成や乗算合成など様々な合成処理を行える便利なモジュールである。
今回はImageChops.logical_andを使用してAND合成をする、このメソッドはmode="1"のImageしか受け付けない。
mask = ImageChops.logical_and(_r, _g)
mask = ImageChops.logical_and(mask, _b)
すべてのバンドで1になっているピクセル(つまり特定の色)を抜き出したマスクができる。
あとはこのマスクを使用して置換したい色を塗ればOK。
dst_color = (48,255,48)
img.paste(Image.new("RGB", img.size, dst_color), mask=mask)
処理速度
getpixel/putpixelを使用した場合と処理を比べてみた。
In [27]: %time replace_put_pixel(img, src_color, dst_color)
Wall time: 234 ms
In [28]: %time replace_fast(img, src_color, dst_color)
Wall time: 1.96 ms
getpixel/putpixel版は234 msなのに対し、今回の方法は1.96 msとなった。
実に100倍以上の速さである。