Kerasでデータ拡張(Data Augmentation)後の画像を表示する

### 2019/07/24更新内容
KerasのImageDataGeneratorの出力を簡単に確認できるGUIツールを作りました。
![simulator](https://user-images.githubusercontent.com/35373553/61723441-1da4e400-ada7-11e9-982b-e602c43c9776.gif)
https://github.com/takurooo/Keras_DataAugmentationSimulator

# 概要
Kerasの[ImageDataGeneratorクラス](https://keras.io/ja/preprocessing/image/)が生成する画像ファイルを表示してみて、設定パラメータによってどんな画像が生成されるのか確認してみる記事。
ImageDataGeneratorはデータ拡張を行うためのクラスで名前の通り画像に関する処理をしてくれる。



# データ拡張(Data Augmentation)とは
機械学習を実際にやってみようと思うと学習データ集めに苦労する。学習データが少ないと過学習(Overfitting)が起きてしまい汎化性能(未知のデータに対する性能)がでないと言われている。

この問題を緩和するために使われるのが「**データ拡張**」。

データ拡張は画像に変換処理(反転、拡大、縮小など)を加えることで、学習データの「水増し」を行う。
水増しされることで同じ画像が学習されることが少なくなるので汎化性能が改善される。

# Keras ImageDataGeneratorクラスとは
機械学習のライブラリではデータ拡張をサポートしているものがある。
今回はKerasのImageDataGeneratorクラスを試してみる。

ImageDataGeneratorは様々な変換処理を画像に加えることができるが、今回はそのうち以下のパラメータを使って画像の可視化をしていく。

| パラメータ         |説明               | 
|:-----------------|:------------------|
|rotation_range    |画像を回転させる角度  |
|width_shift_range |水平にシフトする画像横幅に対する割合|
|height_shift_range|水平にシフトする画像縦に対する割合 |
|horizontal_flip   |水平方向反転 |
|vertical_flip     |垂直方向反転 |
|zoom_range        |拡大縮小範囲 |
|shear_range       |シアー強度   |


# 今回試したコードの概要
以下コードの説明。
コードの説明後、変換後の画像を見ていきます。

コードは以下のリポジトリに置いてあります。
https://github.com/takurooo/Keras-show_ImageDataGenerator

### Import

```python:show_generator.py
import matplotlib.pyplot as plt
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator
```
- `keras.preprocessing.image`にデータ拡張に便利な関数が入っている。
- `matplotlib`は画像の可視化に使う。

### 画像の読み込み
```python:show_generator.py
# 画像ファイルをPIL形式でオープン
img = image.load_img(img_path)
# PIL形式をnumpyのndarray形式に変換
x = image.img_to_array(img)
# (height, width, 3) -> (1, height, width, 3)
x = x.reshape((1,) + x.shape)
```
次にImageDataGeneratorクラスに渡す画像データを用意する。
ImageDataGeneratorクラスがndarray形式の4次元配列を要求するのでその変換もここで行っておく。


### ImageDataGenerator生成
```python:show_generator.py
datagen = ImageDataGenerator(
           rotation_range=0,
           width_shift_range=0,
           height_shift_range=0,
           shear_range=0,
           zoom_range=0,
           horizontal_flip=False,
           vertical_flip=False)
```
ImageDataGeneratorクラスを生成。
生成時に渡すパラメータを色々変えると様々なデータ拡張を行ってくれるので、今回はここを変えてみてどんな変換が起きるか見てみる。
上記設定は何もしない設定になっている。

### 画像生成、表示
```python:show_generator.py
max_img_num = 16
imgs = []
for d in datagen.flow(x, batch_size=1):
    # このあと画像を表示するためにndarrayをPIL形式に変換して保存する
    imgs.append(image.array_to_img(d[0], scale=True))
    # datagen.flowは無限ループするため必要な枚数取得できたらループを抜ける
    if (len(imgs) % max_img_num) == 0:
        break
show_imgs(imgs, row=4, col=4)
```
ImageDataGeneratorから変換画像を取得して表示。
表示は以下の関数で行っている。


```python:show_generator.py
def show_imgs(imgs, row, col):
    """Show PILimages as row*col
     # Arguments
            imgs: 1-D array, include PILimages
            row: Int, row for plt.subplot
            col: Int, column for plt.subplot
    """
    if len(imgs) != (row * col):
        raise ValueError("Invalid imgs len:{} col:{} row:{}".format(len(imgs), row, col))

    for i, img in enumerate(imgs):
        plot_num = i+1
        plt.subplot(row, col, plot_num)
        plt.tick_params(labelbottom="off") # x軸の削除
        plt.tick_params(labelleft="off") # y軸の削除
        plt.imshow(img)
    plt.show()
```


# rotation_range
`rotation_range`は画像を回転させるパラメータ。
設定は整数で何度回転させていか設定可能。
例えば90度と設定すると、Kerasの中で「-90〜90」の中でランダムに1つ数字を選んで回転処理するようになっている。
以下変換後の画像。

■`rotation_range=90`
![rotation_range_90.png](https://qiita-image-store.s3.amazonaws.com/0/250063/42d56a71-f7d2-2878-1165-3cdbf5dd5808.png)
回転させることで元の画像にない領域がでてくる。
これはImageDataGenerator生成時に設定する`fill_mode`でどの画素で埋めるかを指定可能。
`fill_mode`には、`constant`,`nearest`,`reflect`,`wrap`があり上記例では`fill_mode=nearest`になっている。  
  
  
■`fill_mode=constant`(cvalで埋める色を変えられます)
![rotation_range_90_constant.png](https://qiita-image-store.s3.amazonaws.com/0/250063/467b96d6-9be0-cbc2-4a35-3c1469a0749e.png)

■`fill_mode=reflect`
![rotation_range_90_reflect.png](https://qiita-image-store.s3.amazonaws.com/0/250063/f049a689-9d20-e28b-45c0-01cbdcf60bb7.png)

■`fill_mode=wrap`
![rotation_range_90_wrap.png](https://qiita-image-store.s3.amazonaws.com/0/250063/22d26136-8b83-97d3-7dcb-7e554d84e44e.png)


# width_shift_range / height_shift_range
画像をシフトさせます。
少しややこしいのがこの変数は3つの型でシフトを指定できる。

| 型 | 意味 |
|:--|:--|
| int  | シフトさせる画素数 |
| 1-D array  |  シフトさせる画素数 |
| float  |  シフトさせる割合（元画像の解像度に対して） |

`int`型の場合、`width_shift_range=１２８`と設定すると、「-127〜127」の数字の中からランダムに選択して、選択した画素数シフトする。

`1-D array`型の場合、基本的には`int`と同じだがランダムに選ばれる数字を限定することができる。例えば、`width_shift_range=[128,256]`と設定すると「-256,-128,128,256」の中からランダムにシフトする画素数が選択される。

`float`型の場合、画像の幅に対する割合で表現するので、元画像が水平480で`width_shift_range=0.5`とするとシフト量は「-240〜240」画素シフトする。

画素数で指定するのは面倒くさいので`float`で設定するのが楽。
シフトしてできた元画像にない部分は`fill_mode`で指定可能。

■`width_shift_range=0.5`
![widht_0.5.png](https://qiita-image-store.s3.amazonaws.com/0/250063/383a0bbc-ac9e-ec92-1d8c-c478ce33ef4f.png)

■`width_shift_range=0.2`
![widht_0.2.png](https://qiita-image-store.s3.amazonaws.com/0/250063/92df6cf3-40e6-99e6-346e-d4d29a64fd83.png)

■`height_shift_range=0.5`
![height_0.5.png](https://qiita-image-store.s3.amazonaws.com/0/250063/e1d74459-9050-f8b9-c3b2-8476592940b5.png)


# horizontal_flip / vertical_flip
ランダムに反転。

■`horizontal_flip=True`
![horizontal_flip.png](https://qiita-image-store.s3.amazonaws.com/0/250063/00d37f69-74a4-e8de-6663-d2c273447244.png)

■`vertical_flip=True`
![vertical_flip.png](https://qiita-image-store.s3.amazonaws.com/0/250063/94daa05c-ca9f-8b73-8b10-968ef5490801.png)


# zoom_range
拡大したり縮小したり。
こちらもシフトと同じで複数の型で指定可能。

| 型          | 意味                                                                 |
|:------------|:--------------------------------------------------------------------|
| 1-D array   |  拡大縮小させる範囲 [lower, upper]                                     |
| float       |  拡大縮小させる範囲 [lower, upper] = [1-zoom_range, 1+zoom_range]      |

lower〜upperの範囲で乱数を2つ取り、それぞれx軸とy軸の拡大率にする。
kerasの中では具体的には以下のようなことをしている。

```sample.py
x, y = np.random.uniform(lower, upper, 2)
```

ややこしいですが、
`x > 1`の場合、画像内の物体が縮小されたように見える。
`0 < x < 1`の場合、画像内の物体が拡大されたように見える。
マイナスの場合はさらに反転が加わる。



■`zoom_range=[1,1.5]`
![zoom_1_1.5.png](https://qiita-image-store.s3.amazonaws.com/0/250063/65adefc1-d6d7-a65d-3b13-159c870ce528.png)


■`zoom_range=[0.5,1]`
![zoom_0.5_1.png](https://qiita-image-store.s3.amazonaws.com/0/250063/7b338aab-67ea-9795-7201-10c95d059ff6.png)

■`zoom_range=[-1.5,-1]`
![zoom_-1.5_-1.png](https://qiita-image-store.s3.amazonaws.com/0/250063/5a899a7a-aad2-f1a6-f12d-04da26d4c64a.png)

■`zoom_range=[-1,-0.5]`
![zoom_-1_-0.5.png](https://qiita-image-store.s3.amazonaws.com/0/250063/57312fbb-d6ae-680a-d748-be1be061a922.png)


# shear_range
シアー強度というもので画像変換を行う。
せん断写像というらしいが、いまいちわかっていないので実験した画像だけのせておく。


■`shear_range=40`
![shear_40.png](https://qiita-image-store.s3.amazonaws.com/0/250063/0da368a7-c4c2-8d5f-0092-f17f3c231e15.png)

■`shear_range=90`
![shear_90.png](https://qiita-image-store.s3.amazonaws.com/0/250063/4f78a7b8-f0f5-6dd0-da10-149074275119.png)


# まとめ
今回は画像の可視化のためにImageDataGeneratorクラスを使ってみたが、KerasではImageDataGeneratorクラスをモデル学習時に渡すことで、これまで見てきた変換をランダムに適用したミニバッチは作ってくれるようになっているので、学習プロセスに簡単にデータ拡張を組み込めるようになっている。

データ拡張を便利だが、変換を強くかけすぎると現実には存在しないような画像になってしまい、意味のないデータになってしまう(shear_range=90とか)。なので今回のように変換パラメータを色々試して、実際に画像を確認してから使用することが効果的だと思う。



# 参考
- [Keras Documentation](https://keras.io/ja/preprocessing/image/)
- [keras_preprocessing/image.py](https://github.com/keras-team/keras-preprocessing/blob/master/keras_preprocessing/image.py)


# 最後に
Kerasの他の記事も公開しているのでKerasを勉強し始めの方は参考にしてみてください。

* [KerasでMNIST](https://qiita.com/takurooo/items/82837e44b466e7634c98)
* [KerasのModelクラスに関するプロパティとメソッドまとめ](https://qiita.com/takurooo/items/f52c3cdad09da07f9d7f)
* [KerasのConv2DTransposeの動作について](https://qiita.com/takurooo/items/9a9f387390f5fcf5a516)