PIL/Pillowのみを使ってPhotoShopなどの描画モードを実装する。
簡単に思いつく方法はImage.getpixel/putpixelを使う方法だが、非常に遅い。
numpyを使う方法もあるが、ツールとして提供することを考えると依存モジュールは極力少なくしたい。
なお、描画モードの数は非常に多いため全ては列挙しない。
しかし、本稿を読めば他の描画モードも容易く実装できるだろう。
Image4Layerモジュールを作りました
PhotoShopのブレンドモードのpillow実装をパッケージ化しました。
インストールはpipで行えます。
pip install image4layer
今記事の手法を使った高速なブレンドモードの実装となります。
ImageChopsモジュールを使う
ImageChopsモジュールは気軽に画像合成ができる。
from PIL import Image, ImageChops
img = Image.open("sample.png")
effect_img = Image.open("effect.png")
左が元画像、右がエフェクト用画像である、この2つの画像をサンプルとして使用していく。
覆い焼き(リニア)
ImageChops.add(img, effect_img)
減算
ImageChops.subtract(img, effect_img)
乗算
ImageChops.multiply(img, effect_img)
スクリーン
ImageChops.screen(img, effect_img)
比較(明)/比較(暗)
ImageChops.lighter(img, effect_img)
ImageChops.darker(img, effect_img)
差の絶対値
ImageChops.difference(img, effect_img)
オフセット
ImageChops.offset(img, 100, 100)
ImageMathモジュールを使う
ImageChopsにはない描画モードはImageMathモジュールで実装する。
ImageMathモジュールは非常にクセがあるが、慣れてしまえば自由な画像変換を高速に行えるようになる。
注意すべき点は以下の通り。
- シングルバンドImageのみ演算可能、マルチバンドImageはImage.splitで分割する
- floatで演算も可能だが、値の範囲は0.0~1.0ではなく0.0~255.0
- 演算中のImageはmodeが"I"(int)、または"F"(float)になる、最後にmodeを"L"にコンバートする
- 各種演算(+,-,*,/,**,%)をするごとに新しいImageが作成される。
- 演算はピクセルごとではなくImageごとに行われる。
以下の例では、Rバンドに対して比較(明)をする例である。
from PIL import ImageMath
img_r = img.split()[0]
eff_r = effect_img.split()[0]
ImageMath.eval("convert(max(a, b), 'L')", a=img_r, b=eff_r)
等号を指定することも可能で、その場合は値が0または1になる。
以下の例では値が128以下のピクセルに値100が設定される。
ImageMath.eval("(a < 128) * 100", a=img_r).convert("L")
Python関数呼び出しも可能だが、引数で渡されるImageはピクセルの数値ではなく演算オブジェクト(ImageMath._Operand)であることに留意すべし。
数値ではないためピクセルの値によってif文で処理を分けることは不可能である。 ピクセルの値によって処理を分けたい場合はマスク合成する必要がある。
以下は値が128以上と以下で処理を分ける例である。
def _threshold(a):
# 値が128以下なら255、128以上なら1/2にする
div2 = a / 2 * (a >= 128)
white = (a < 128) * 255
return div2 + white
ImageMath.eval("func(a)", a=img_r, func=_threshold).convert("L")
シングルバンドしか扱えないので、マルチバンドを分解、処理、統合する一連の関数を作成する。
float演算の方が自由度が高いので、ついでにfloatに変換しておく。
def _blend_f(bands1, bands2, func):
blend = "convert(func(float(a), float(b)), 'L')"
bands = [
ImageMath.eval(
blend,
a=a,
b=b,
func=func
)
for a, b in zip(bands1, bands2)
]
return Image.merge("RGB", bands)
以上を踏まえて、複雑な描画モードを実装していく。
オーバーレイ
def _over_lay(a, b):
_cl = 2 * a * b / 255
_ch = 2 * (a + b - a * b / 255) - 255
return _cl * (a < 128) + _ch * (a >= 128)
_blend_f(img.split(), effect_img.split(), _over_lay)
ソフトライト
def _soft_light(a, b):
_cl = (a / 255) ** ((255 - b) / 128) * 255
_ch = (a / 255) ** (128 / b) * 255
return _cl * (b < 128) + _ch * (b >= 128)
_blend_f(img.split(), effect_img.split(), _soft_light)
ハードライト
def _hard_light(a, b):
_cl = 2 * a * b / 255
_ch = 2.0 * (a + b - a * b / 255.0) - 255.0
return _cl * (b < 128) + _ch * (b >= 128)
_blend_f(img.split(), effect_img.split(), _hard_light)
処理速度について
処理速度を比較するためにオーバーレイをImage.putpixelで実装してみた。
def _put_pixel_overlay(a, b):
c = Image.new(a.mode, a.size)
for x in range(a.size[0]):
for y in range(b.size[1]):
cola = a.getpixel((x, y))
colb = b.getpixel((x, y))
colc = [
_a * _b * 2 / 255 if _a < 128 else (2 *(_a + _b - _a * _b / 255) - 255)
for _a, _b in zip(cola, colb)
]
c.putpixel((x, y), tuple(int(_) for _ in colc))
return c
実行速度は以下の通り。
%timeit _put_pixel_overlay(img, effect_img)
1 loop, best of 3: 663 ms per loop
%timeit _blend_f(img.split(), effect_img.split(), _over_lay)
100 loops, best of 3: 5.63 ms per loop
ImageMath版の方が100倍以上、高速である。
パッケージ化しました
Photoshopの描画モードを実装したパッケージをアップしました。
インストールはpipで簡単に行なえます。
pip install image4layer
最後に
巷のPILのコードを見ているとImage.getpixel/putpixelを使用する場面が散見される。
Image.getpixel/putpixelは画像作成に使用するものであり、画像変換で使用するのは最終手段である。
numpyを使用して画像変換をする例もあるが、やはりPILのみのコンパクトな実装は魅力的である。
PILはコンパクトながら、非常に強力で高速なライブラリである。
良いPILライフを。