PIL/Pillowで高速にPhotoShopなどの描画モードを実装する

  • 12
    いいね
  • 0
    コメント

PIL/Pillowのみを使ってPhotoShopなどの描画モードを実装する。

簡単に思いつく方法はImage.getpixel/putpixelを使う方法だが、非常に遅い。
numpyを使う方法もあるが、ツールとして提供することを考えると依存モジュールは極力少なくしたい。

なお、描画モードの数は非常に多いため全ては列挙しない。
しかし、本稿を読めば他の描画モードも容易く実装できるだろう。

Image4Layerモジュールを作りました

PhotoShopのブレンドモードのpillow実装をパッケージ化しました。

https://github.com/pashango2/Image4Layer

インストールはpipで行えます。

pip install image4layer

今記事の手法を使った高速なブレンドモードの実装となります。

ImageChopsモジュールを使う

ImageChopsモジュールは気軽に画像合成ができる。

from PIL import Image, ImageChops

img = Image.open("sample.png")
effect_img = Image.open("effect.png")

B9BSxGZmEQpmAAAAAElFTkSuQmCC.png kL+ySM465ToAlAAAAAElFTkSuQmCC.png

左が元画像、右がエフェクト用画像である、この2つの画像をサンプルとして使用していく。

覆い焼き(リニア)

ImageChops.add(img, effect_img)

wE7Grw7M8iQMgAAAABJRU5ErkJggg==.png

減算

ImageChops.subtract(img, effect_img)

X+81Pf98mz2EwAAAABJRU5ErkJggg==.png

乗算

ImageChops.multiply(img, effect_img)

8yo3kAAAAASUVORK5CYII=.png

スクリーン

ImageChops.screen(img, effect_img)

P8BtLqhlqZTLrgAAAAASUVORK5CYII=.png

比較(明)/比較(暗)

ImageChops.lighter(img, effect_img)
ImageChops.darker(img, effect_img)

X+81Pf98mz2EwAAAABJRU5ErkJggg==.png wDMatqtY+79MgAAAABJRU5ErkJggg==.png

差の絶対値

ImageChops.difference(img, effect_img)

diff.png

オフセット

ImageChops.offset(img, 100, 100)

offset.png

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)

AX6XflESotN9AAAAAElFTkSuQmCC.png

等号を指定することも可能で、その場合は値が0または1になる。
以下の例では値が128以下のピクセルに値100が設定される。

ImageMath.eval("(a < 128) * 100", a=img_r).convert("L")

fill.png

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")

white.png

シングルバンドしか扱えないので、マルチバンドを分解、処理、統合する一連の関数を作成する。
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)

overlay.png

ソフトライト

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)

softlight.png

ハードライト

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)

hardlight.png

処理速度について

処理速度を比較するためにオーバーレイを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の描画モードを実装したパッケージをアップしました。

https://github.com/pashango2/Image4Layer

インストールはpipで簡単に行なえます。

pip install image4layer

最後に

巷のPILのコードを見ているとImage.getpixel/putpixelを使用する場面が散見される。

Image.getpixel/putpixelは画像作成に使用するものであり、画像変換で使用するのは最終手段である。
numpyを使用して画像変換をする例もあるが、やはりPILのみのコンパクトな実装は魅力的である。
PILはコンパクトながら、非常に強力で高速なライブラリである。
良いPILライフを。