Kerasによる画像の水増し操作

はじめに

今回は、前回の最後に言及した画像データの水増し(augmentation)というものを行っていきたいと思います.

データの水増しとは

まず、データの水増しとは何かというところから整理してみます.機械学習においては、何はともあれ学習データを用意する必要があります.手書き数字のMNISTではその学習データが大量(70000枚の画像データ!)に用意されていたわけですが、自分で用意する場合にはそれだけの枚数のデータを集めるのは容易ではありません.かといって、わずかなデータで学習を行うと前回の検討のように過学習が発生してしまいます.

その対策として、使われるのがデータの水増し(Data Augmentation)と呼ばれる操作になります.具体的には、オリジナルの画像を反転させたり、拡大させたり、ノイズを与えるなどして、複数の異なる画像へと変換処理をします.この操作により簡単にデータの枚数を増やすことができます.以下に主な変換例を挙げておきます.詳しくは後述いたします.

画像変換の例

  • 画像の回転(rotation_range)
  • 画像の反転(vertical_flip, horizotal_flip)
  • 画像の拡大および縮小(width_shift_range, height_shift_range)
  • 画像の水平および上下方向への移動(zoom_range)

この際、データの意味が変わるような変換を行わないように注意してください.例えば動物の画像を水平反転することは問題ないですが、文字や数字などを反転することは誤った学習データを作成することになります.同様に、角度を傾ける場合なども90度回転させたデータが現実的にありえるものかどうかを考慮する必要があります.

ImageDataGenerator

画像変換に詳しい方であれば、自分でコーディングも可能だと思うのですがKerasには既にImaegDataGenertorという画像変換用のクラスが用意されているので、今回もそれを使用します. ImageDatageratorの公式ドキュメントはコチラ

実装

では、実際の画像サンプルを使って画像変換を行っていきます. 今回使用したオリジナルの画像は以下、最終的にこの1枚の画像を16枚に増やしてみたいと思います.以下の画像がsample.jpgという名前で同じディレクトリに保存されているという前提で進めていきます.

f:id:shirakonotempura:20190109073521j:plain
白子の天ぷら

必要なライブラリのインポートと画像の読み込み

import keras
import numpy as np
from keras.utils import np_utils
from matplotlib import pyplot as plt
from keras.preprocessing import image
from keras.preprocessing.image import ImageDataGenerator

# サンプル画像を読み込み、4次元テンソルに変換(sample数, row, col, channel)
img = image.load_img("sample.jpg")
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
print(x.shape)

画像を表示する関数の定義(@takurooo様の記事より拝借)

変換された画像を表示する関数をここで定義しておきます.実際の運用では数値のデータがあればよいので、必ずしも画像を確認する必要はありませんが、今回は処理の影響を見るために変換された画像を確認していきます.

# 画像を表示する
def show_imgs(imgs, row, col):
    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()

ImageDataGeneratorクラスの作成

これを定義する際に、パラメータを変更することで画像変換を行うことができます. 以下は何も変更しない設定.以降、主な変換について順番に見ていきます.

# とりあえず、何も変更を与えない設定
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)

# 欲しい画像の枚数
max_img_num = 9
# 変換された画像データをリストに保存
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=3, col=3)
  • 出力結果(画像変換処理なし)
    f:id:shirakonotempura:20190109073825p:plain

画像の回転(rotation_range)

rotation_range = anlgleで、-angle~angleまでの整数からランダムに1つ選ばれた角度分だけの回転処理を行うことができます.

datagen = ImageDataGenerator(
           rotation_range=90, 
          )
max_img_num = 9
imgs = []
for d in datagen.flow(x, batch_size=1):
    imgs.append(image.array_to_img(d[0], scale=True))
    if (len(imgs) % max_img_num) == 0:
        break
show_imgs(imgs, row=3, col=3)
  • 出力結果(回転処理)
    f:id:shirakonotempura:20190109073849p:plain

縦軸に対する反転(horizontal_flip)

horizontal_flip = Trueで縦軸に対しランダムに反転処理を加えます.

datagen = ImageDataGenerator(
           horizontal_flip = True
          )
max_img_num = 9
imgs = []
for d in datagen.flow(x, batch_size=1):
    imgs.append(image.array_to_img(d[0], scale=True))
    if (len(imgs) % max_img_num) == 0:
        break
show_imgs(imgs, row=3, col=3)
  • 出力結果(水平反転処理)
    f:id:shirakonotempura:20190109074122p:plain

横軸に対する反転(vertical_flip)

vertical_flip = Trueで横軸に対しランダムに反転処理を加えます.

datagen = ImageDataGenerator(
           vertical_flip = True
          )
max_img_num = 9
imgs = []
for d in datagen.flow(x, batch_size=1):
    imgs.append(image.array_to_img(d[0], scale=True))
    if (len(imgs) % max_img_num) == 0:
        break
show_imgs(imgs, row=3, col=3)
  • 出力結果(上下反転処理)
    f:id:shirakonotempura:20190109074155p:plain

水平方向の移動(width_shift_range)

width_shift_range = ratioで、[-ratio * width, ratio * width]の範囲でランダムに水平移動を行います.

datagen = ImageDataGenerator(
           width_shift_range = 0.2,
          )
max_img_num = 9
imgs = []
for d in datagen.flow(x, batch_size=1):
    imgs.append(image.array_to_img(d[0], scale=True))
    if (len(imgs) % max_img_num) == 0:
        break
show_imgs(imgs, row=3, col=3)
  • 出力結果(水平移動)
    f:id:shirakonotempura:20190109074220p:plain

上下方向の移動(height_shift_range)

height_shift_range = ratioで、[-ratio * height, ratio * height]の範囲でランダムに上下方向の移動を行います.

datagen = ImageDataGenerator(
           height_shift_range = 0.3
          )
max_img_num = 9
imgs = []
for d in datagen.flow(x, batch_size=1):
    imgs.append(image.array_to_img(d[0], scale=True))
    if (len(imgs) % max_img_num) == 0:
        break
show_imgs(imgs, row=3, col=3)
  • 出力結果(上下移動)
    f:id:shirakonotempura:20190109074247p:plain

画像の拡大・縮小(zoom_range)

zoom_range = [x_lower, x_upper]で画像の拡大・縮小処理を加えることができます.詳細を省きますが、x > 1の場合、画像内の物体が縮小されたように、0 < x < 1の場合、画像内の物体が拡大されます.ですので、[0.7, 1.3]くらい設定してればよいかと思います.

datagen = ImageDataGenerator(
           zoom_range = [0.7, 1.3]
          )
max_img_num = 9
imgs = []
for d in datagen.flow(x, batch_size=1):
    imgs.append(image.array_to_img(d[0], scale=True))
    if (len(imgs) % max_img_num) == 0:
        break
show_imgs(imgs, row=3, col=3)
  • 出力結果(拡大縮小処理)
    f:id:shirakonotempura:20190109074303p:plain

画像のせん断方向への引き延ばし(shear_range)

こちらも詳細は省きます(というか処理が良く分かりません)が、‘shear_range=angle`で画像を対角線方向に向けて引き延ばす処理を加えることができます.

datagen = ImageDataGenerator(
           shear_range = 20
          )
max_img_num = 9
imgs = []
for d in datagen.flow(x, batch_size=1):
    imgs.append(image.array_to_img(d[0], scale=True))
    if (len(imgs) % max_img_num) == 0:
        break
show_imgs(imgs, row=3, col=3)
  • 出力結果(せん断処理)
    f:id:shirakonotempura:20190109074321p:plain

カラーチャンネルの変更(channel_shift_range)

channel_shift_range = x により[-x, x]の範囲でランダムに画素データに値を足します.

datagen = ImageDataGenerator(
           channel_shift_range = 100
          )
max_img_num = 9
imgs = []
for d in datagen.flow(x, batch_size=1):
    imgs.append(image.array_to_img(d[0], scale=True))
    if (len(imgs) % max_img_num) == 0:
        break
show_imgs(imgs, row=3, col=3)
  • 出力結果(カラーチャンネルの変更)
    f:id:shirakonotempura:20190109074336p:plain

適当に組み合わせてみる

最後に上記のパラメータを適当に組み合わせて、16枚の画像を作成してみます.この処理で、1枚の画像が16枚の異なる画像へと水増しされたことになります.
以下では、回転、水平・上下移動、水平反転、カラーチャンネルの変更の処理を加えています.

# # 適当に組み合わせてみる
datagen = ImageDataGenerator(
            rotation_range=30,
            width_shift_range=0.2,
            height_shift_range=0.2,
            horizontal_flip=True,
            channel_shift_range=100
          )
max_img_num = 16
imgs = []
for d in datagen.flow(x, batch_size=1):
    imgs.append(image.array_to_img(d[0], scale=True))
    if (len(imgs) % max_img_num) == 0:
        break
show_imgs(imgs, row=4, col=4)
  • 出力結果(カラーチャンネルの変更)
    f:id:shirakonotempura:20190109074345p:plain

実際の運用に関する補足:

今回は、変換した画像をarray_to_imgで画像データに戻して表示していますが、実際に運用する際は配列データのまま持っていればいいと思います. また、今回は学習データのうち、インプットにあたる部分の操作のみになりますので変換後のデータに対しても元のデータと同じラベルを付ける処理を行う必要があります.まあ、それは普通のfor文でなんとかなるだろうと考えています.

まとめ

画像の水増し操作について一通り操作してみました.KerasのImageDataGeneratorを使えば非常に簡単に画像の水増しをすることができました.実際は良く理解できていないオプションが多く残ってるのですが、とりあえずは理解できたものを中心に使っていきたいと思います.
今回のコードは コチラに保存しておきます.

次回は、前回扱ったネコ科の猛獣の画像識別にデータの水増しを取り入れてみたいと思います.


参考記事

実装にあたっては以下の記事を参考にさせていただきました.ありがとうございます.

  • とりあえず必要な主な変換についてはこちらが簡潔で分かりやすかったです. qiita.com

  • 結構細かく説明がされていますので、処理の中身をもっと知りたい方向け pynote.hatenablog.com