Pillow-SIMDを使った画像処理やディープラーニングの高速化の紹介

Pillow-SIMDを使ってマルチプロセス化なしで画像処理を高速化する方法を紹介します。PyTorchが対応している高速化方法ですが、実はKerasでも動かせました。簡単ながら痒いところに手が届くのでおすすめです。

## きっかけ
Pillow（PIL）を使って画像処理や前処理をすることが多いのですが、Python特有の悩みとして

**デフォルトだとCPUに対してマルチコアの処理をしてくれない**

というのがあります。特にディープラーニングでは、JPEGの読み込みが速度のボトルネックとなることが多いので、画像の読み込みや前処理の高速化ができるととても嬉しいのです。

PillowやOpenCVをmultiprocessingで並列化して高速化するという方法もありますが、マルチプロセス化はデバッグが面倒になるので、ライブラリ一個で完結してくれるとすごいありがたいのです。

そんなときにおすすめなのが、**Pillow-SIMD**というライブラリです。
https://github.com/uploadcare/pillow-simd

PyTorchの情報探したときに見つけたものです。中身はPILフォークなので、純粋にPillowの代替品として使えますし、前処理でPillowを使っているKerasでも使えたりします（末尾のColabノートブックを参考にしてください）。

## インストール方法
たった2行でOK。従来のPillowをPillow-SIMDで置き換えます。

```
pip uninstall -y pillow
pip install pillow-simd
```

Windows10の場合はインストールで失敗することがあるので、非公式のバイナリを落としてきます[関連Issue](https://github.com/uploadcare/pillow-simd/issues/9)

https://www.lfd.uci.edu/~gohlke/pythonlibs/#pillow-simd

```
pip install 保存ディレクトリ\Pillow_SIMD-5.3.0.post0-cp37-cp37m-win_amd64.whl
```

とすればOKです。


## 実験
### Cats and Dogsデータセット
犬猫画像を25000枚ほど集めた「Cats and Dogsデータセット」を実験台として用います。画像分類の初歩として有名なデータセット。中身は全てJPEG画像です。


https://www.microsoft.com/en-us/download/details.aspx?id=54765

### 通常のPillowの場合
「LANCZOS法でリサイズ→画像のシャープ化」という若干重めの処理を行います。

```python
import time
import glob
from PIL import Image, ImageEnhance
from tqdm import tqdm

def normal_pillow():
    files = sorted(glob.glob("PetImages/Cat/*.jpg"))
    files += sorted(glob.glob("PetImages/Dog/*.jpg"))
    start_time = time.time()
    list = []
    for f in tqdm(files):
        try:
            with Image.open(f) as img:
                img = img.resize((128, 128), Image.LANCZOS)
                img = ImageEnhance.Sharpness(img).enhance(2.0)
        except:
            list.append(f)
            continue
    print("経過時間 = ", time.time()-start_time, "s")
    return list
```

ColabのGPU環境で計測したところ（PillowはGPUによる高速化はありません）、処理時間は**139秒**となりました。

またKerasで浅めのニューラルネットワークを訓練します。犬と猫の画像を分類します。（前後に壊れたファイルの削除を挟んでいます）

```python
from tensorflow.keras import layers
import tensorflow.keras as keras

def create_shallow_network():
    def conv_bn_relu(x, ch):
        x = layers.Conv2D(ch, 5, padding="same")(x)
        x = layers.BatchNormalization()(x)
        x = layers.Activation("relu")(x)
        return x
    
    input = layers.Input((128, 128, 3))
    x = conv_bn_relu(input, 16)
    x = layers.AveragePooling2D(4)(x)
    x = conv_bn_relu(x, 32)
    x = layers.AveragePooling2D(4)(x)
    x = conv_bn_relu(x, 64)
    x = layers.GlobalAveragePooling2D()(x)
    x = layers.Dense(2, activation="softmax")(x)
    return keras.models.Model(input, x)

def train():
    model = create_shallow_network()
    model.compile("adam", "categorical_crossentropy", ["acc"])
    gen = keras.preprocessing.image.ImageDataGenerator(rescale=1.0/255
             ).flow_from_directory("PetImages", target_size=(128, 128), batch_size=128)
    model.fit_generator(gen, steps_per_epoch=25000//128, epochs=5)    
```

1エポックあたり**77秒**かかっています。これはJPEGの読み込みがボトルネックになっているせいで、Pillow-SIMDを使えばもっと高速化できます。


```
Epoch 4/5
195/195 [==============================] - 77s 395ms/step - loss: 0.4427 - acc: 0.7932
Epoch 5/5
195/195 [==============================] - 77s 396ms/step - loss: 0.4169 - acc: 0.8099
```

### Pillow-SIMDの場合
Pillow-SIMDをインストールし、ランタイムを再起動します。

```
pip uninstall -y pillow
pip install pillow-simd
```

先程と同じ、「読み込み→リサイズ→シャープ化」の処理をします。**コードは全く変更する必要はありません**。Pillow自体を置き換えているためです。

```python
import time
import glob
from PIL import Image, ImageEnhance
from tqdm import tqdm

def simd_pillow():
    files = sorted(glob.glob("PetImages/Cat/*.jpg"))
    files += sorted(glob.glob("PetImages/Dog/*.jpg"))
    start_time = time.time()
    for f in tqdm(files):
        try:
            with Image.open(f) as img:
                img = img.resize((128, 128), Image.LANCZOS)
                img = ImageEnhance.Sharpness(img).enhance(2.0)
        except:
            continue
    print("経過時間 = ", time.time()-start_time, "s")    
```

結果は**61秒**となりました。**139秒から2倍以上の高速化**です。ちなみにColabのCPUは2コアなのを考えると、それっぽい結果になります。

KerasのコードもそのままでPillow-SIMDに対応してくれます。

```
Epoch 4/5
195/195 [==============================] - 53s 273ms/step - loss: 0.4747 - acc: 0.7764
Epoch 5/5
195/195 [==============================] - 54s 277ms/step - loss: 0.4439 - acc: 0.7936
```

1エポックあたり**77秒から53秒に短縮**することができました。KerasのImageDataGeneratorが内部的にPillowを使っているので、Pillow-SIMDに置き換えてあげることで結果的にCPUを効率よく使ってくれるようになります（公式サポートではないので、もしかしたらうまくいかない処理があるかもしれません）。

前処理が重くなったり、CPUのコア数・スレッド数が多くなればもっと高速化の効果は出やすいかと思われます。

※ただし、必ずしもCPU100％まで使ってくれることではないようです。CPUの多い環境で計測したところ、通常のPillow→Pillow-SIMDへの変更で、CPU使用率が10%から20%まで上がりました。そのため並列化が意味がなくなったというわけではないと思います。このコードがforループを使ったシーケンシャルな書き方なので、そのせいもあるかもしれません。

### もっと速くする
Kerasの場合はfit_generatorに```use_multiprocessing```（スレッドベースのマルチプロセス化：デフォルトでFalse）、```workers```（スレッドベースのワーカー数：デフォルトで1）という引数があるので、環境によってはこの値を変えることでさらに高速化（CPUをもっと使ってくれる）ようになります。

Colab環境では、```use_multiprocessing=True```とすると逆に遅くなってしまったため、```workers=4```と増やしてみました。

```python
def train_multiworker():
    model = create_shallow_network()
    model.compile("adam", "categorical_crossentropy", ["acc"])
    gen = keras.preprocessing.image.ImageDataGenerator(rescale=1.0/255
             ).flow_from_directory("PetImages", target_size=(128, 128), batch_size=128)
    model.fit_generator(gen, steps_per_epoch=25000//128, epochs=5, workers=4) #ワーカーの数を4にした
```

その結果、1エポック当たり**42秒**まで短縮することができました。

このように、本気で速度出したい場合は、マルチプロセス化、ワーカー数の増加などとセットで使うといいかもしれません。Pillow-SIMD単体でも十分効果はあるでしょう。

## まとめ

|              [秒]              | 通常のPillow | Pillow-SIMD |
|:------------------------------:|:------------:|:-----------:|
| リサイズ＋シャープ化 |      139     |      **61**     |
|         CNNの1エポック         |      77      |      **53**    |
|         + worker=4         |      -      |      **42**    |

Pillow-SIMD、速くて簡単。とても便利！　以上！

## コード
今回用いたColabのノートブックはこちらにあります。

https://colab.research.google.com/drive/1FYOjS4AWBWZbcBK0UX-Sc4heewvD5j3l

## お知らせ
技術書典6で頒布したモザイク本の通販を下記URLで行っています。会場にこられなかったけど欲しいという方は、ぜひご利用ください。

**『DeepCreamPyで学ぶモザイク除去』通販
https://note.mu/koshian2/n/naa60d5c9ebba**

ディープラーニングや機械学習における画像処理の基本や応用を学びながら、モザイク除去技術DeepCreamPyを使いこなし、自分で実装するまでを目指す解説書です。