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

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

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

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

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

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

https://github.com/pashango2/Image4Layer

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

```
pip install image4layer
```

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


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

```python
from PIL import Image, ImageChops

img = Image.open("sample.png")
effect_img = Image.open("effect.png")
```
![B9BSxGZmEQpmAAAAAElFTkSuQmCC.png](https://qiita-image-store.s3.amazonaws.com/0/140075/aab8183a-2ea7-445a-28fe-85406f2b0f00.png) ![kL+ySM465ToAlAAAAAElFTkSuQmCC.png](https://qiita-image-store.s3.amazonaws.com/0/140075/f9d5991d-1090-21ee-c029-b1b48b47556f.png)

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


#### 覆い焼き（リニア）
```python
ImageChops.add(img, effect_img)
```
![wE7Grw7M8iQMgAAAABJRU5ErkJggg==.png](https://qiita-image-store.s3.amazonaws.com/0/140075/2dc58d5b-b3ff-6b86-470f-d83a5f5073ba.png)

#### 減算
```
ImageChops.subtract(img, effect_img)
```
![X+81Pf98mz2EwAAAABJRU5ErkJggg==.png](https://qiita-image-store.s3.amazonaws.com/0/140075/5e8d9011-2519-21b9-39c2-b50c4d31e200.png)

#### 乗算
```
ImageChops.multiply(img, effect_img)
```
![8yo3kAAAAASUVORK5CYII=.png](https://qiita-image-store.s3.amazonaws.com/0/140075/4f2288f2-dbe2-9170-c1a6-7b29a98a72f1.png)

#### スクリーン
```
ImageChops.screen(img, effect_img)
```
![P8BtLqhlqZTLrgAAAAASUVORK5CYII=.png](https://qiita-image-store.s3.amazonaws.com/0/140075/9d4f989f-cee7-a819-c8aa-f5cd19f5ba25.png)

#### 比較（明）／比較（暗）
```
ImageChops.lighter(img, effect_img)
ImageChops.darker(img, effect_img)
```
![X+81Pf98mz2EwAAAABJRU5ErkJggg==.png](https://qiita-image-store.s3.amazonaws.com/0/140075/9ed4a37e-bb2f-e93e-5606-59ab936ccc93.png) ![wDMatqtY+79MgAAAABJRU5ErkJggg==.png](https://qiita-image-store.s3.amazonaws.com/0/140075/762c19be-c549-3900-de95-a9ec06907f2b.png)

#### 差の絶対値
```python
ImageChops.difference(img, effect_img)
```
![diff.png](https://qiita-image-store.s3.amazonaws.com/0/140075/69d9c338-59ab-0ef1-c27a-0f12ea12b817.png)

#### オフセット
```python
ImageChops.offset(img, 100, 100)
```
![offset.png](https://qiita-image-store.s3.amazonaws.com/0/140075/388aa659-b6ed-da57-bab9-b1f121b6ba54.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バンドに対して比較（明）をする例である。

```python
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](https://qiita-image-store.s3.amazonaws.com/0/140075/137fde5e-6bba-0170-8780-749532bc46f2.png)

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

```python
ImageMath.eval("(a < 128) * 100", a=img_r).convert("L")
```
![fill.png](https://qiita-image-store.s3.amazonaws.com/0/140075/22ce2264-e1b5-c5c3-a63c-eb4c97452c4a.png)

Python関数呼び出しも可能だが、引数で渡されるImageはピクセルの数値ではなく演算オブジェクト(ImageMath._Operand)であることに留意すべし。

 数値ではないためピクセルの値によってif文で処理を分けることは不可能である。 ピクセルの値によって処理を分けたい場合はマスク合成する必要がある。
以下は値が128以上と以下で処理を分ける例である。

```python
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](https://qiita-image-store.s3.amazonaws.com/0/140075/14c678c3-a074-1bab-9840-df4300439284.png)

シングルバンドしか扱えないので、マルチバンドを分解、処理、統合する一連の関数を作成する。
float演算の方が自由度が高いので、ついでにfloatに変換しておく。

```python
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)
```

以上を踏まえて、複雑な描画モードを実装していく。

#### オーバーレイ
```python
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](https://qiita-image-store.s3.amazonaws.com/0/140075/570a1743-0fcc-4aa8-4a76-a469e6b00abf.png)

#### ソフトライト
```python
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](https://qiita-image-store.s3.amazonaws.com/0/140075/2572e2bb-445d-f0dc-ee6d-dd533337bb4b.png)

#### ハードライト
```python
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](https://qiita-image-store.s3.amazonaws.com/0/140075/a01426b8-a2f7-ec13-2c89-21d585f204dc.png)

## 処理速度について
処理速度を比較するためにオーバーレイをImage.putpixelで実装してみた。

```python
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ライフを。