NumPyでの画像のData Augmentationまとめ

Python Advent Calendar 2017 の 18日目 の記事です。
画像のData Augmentationの手法をNumpy(とSciPy)で実装し、まとめてみました。
DeepLearningで画像分類など画像系の問題に取り組むとき、画像の枚数が精度に大きく影響を与えます。そこで、画像を数を増やすために、画像をちょっと傾けたり、反転させたりと画像を加工したりします。加工して数を増やすことを一般にData Augmentationと呼びます。
Data Augmentationは前処理で使うだけでなく、推論時にも使うこともできます。例えば、予測する画像を10枚に増やして、10枚それぞれに対して予測を行い、その平均を最終的な予測結果にすることで精度が向上すると言われています。
そんな有用なData Augmentationですが、Kerasでは、ImageDataGeneratorを使うと、簡単に実装できますが、今回は勉強を兼ねてNumpyを使って実装してみます。
使うデータ
デモ画像として、ネコの画像を使いたいと思います。可愛いですね。

Original Image
まずはじめに、画像をNumpy Arrayに格納します*1。
import numpy as np
from PIL import Image
image = np.array(Image.open('/path/to/imagefile'), dtype=np.float32)
中身を見てみます。
print(image)
# array([[[ 84., 80., 79.],
# [ 85., 81., 80.],
# [ 87., 83., 82.],
# ...,
print(image.shape)
# (224, 224, 3)
高さ, 幅, チャンネル数の順に格納されています。Tensorflowはこの順ですが、Chainerでは、チャンネル数, 高さ, 幅の順で画像を扱うことに注意が必要です。
Data Augmentation
Horizontal Flip
Horizontal Flip は、水平方向に反転させます。
配列の値を逆順にするには、スライスを使って[::-1]とするだけです。水平方向なので、幅の箇所のみを逆順にします。
def horizontal_flip(image):
image = image[:, ::-1, :]
return image


Original(左), Horizontal Flip(右)
実際に使うときは、ランダムに反転させると良いと思います。
def horizontal_flip(image, rate=0.5):
if np.random.rand() < rate:
image = image[:, ::-1, :]
return image
Vertical Flip
Vertical Flip は垂直方向の反転です。
実装は、 Horizontal Flip とほとんど同じです。
def vertical_flip(image, rate=0.5):
if np.random.rand() < rate:
image = image[::-1, :, :]
return image


Original(左), Vertical Flip(右)
Random Crop
Random Crop は1枚の画像からランダムに切り抜きます。ここでは 224 x 224 に切り抜きます。224 x 224 はImageNetの画像分類でよく用いられるサイズです。
def random_crop(image, crop_size=(224, 224)):
h, w, _ = image.shape
# 0~(400-224)の間で画像のtop, leftを決める
top = np.random.randint(0, h - crop_size[0])
left = np.random.randint(0, w - crop_size[1])
# top, leftから画像のサイズである224を足して、bottomとrightを決める
bottom = top + crop_size[0]
right = left + crop_size[1]
# 決めたtop, bottom, left, rightを使って画像を抜き出す
image = image[top:bottom, left:right, :]
return image
9回繰り返した結果です。

Random Crop
Scale Augmentation
Scale Augmentation はスケールを変化させながらCropします。リサイズはSciPyのimresizeを使いました。
最小256 x 256, 最大400 x 400にリサイズを行い、リサイズされた画像から224 x 224の画像をRandom Cropします。
Random Cropの部分は先程の関数がそのまま使えるので、リサイズのサイズをランダムに決めるだけです。
from scipy.misc import imresize
def scale_augmentation(image, scale_range=(256, 400), crop_size=224):
scale_size = np.random.randint(*scale_range)
image = imresize(image, (scale_size, scale_size))
image = random_crop(image, (crop_size, crop_size))
return image

Scale Augmentation
Random Rotation
Random Rotation は画像を回転させます。これもScipyを使いました。0°~180°からランダムに回転させます。回転させるとサイズが変わるので、回転前のサイズにリサイズします。
from scipy.ndimage.interpolation import rotate
def random_rotation(image, angle_range=(0, 180)):
h, w, _ = image.shape
angle = np.random.randint(*angle_range)
image = rotate(image, angle)
image = imresize(image, (h, w))
return image

Random Rotation
回転させる角度を小さくするなり、補完するなりして、黒い部分をなんとかしないと、そのまま使うのは良くないかもしれません。
Cutout
Cutout は2017年8月に発表された論文 Improved Regularization of Convolutional Neural Networks with Cutout で提案された手法です。日本語の解説はこちらの記事が分かりやすかったです。
出典: Improved Regularization of Convolutional Neural Networks with Cutout
Cutout は画像の一部マスクすることによって、より汎化能力をあげます。マスクの画素値は画像の平均とし、マスクをかける場所はランダムに決めますがはみ出すことも許可されます。
マスクの大きさは論文の結果を見ると画像サイズの1/3~1/2が良さそうです*2。今回は1/2にしました。
def cutout(image_origin, mask_size):
# 最後に使うfill()は元の画像を書き換えるので、コピーしておく
image = np.copy(image_origin)
mask_value = image.mean()
h, w, _ = image.shape
# マスクをかける場所のtop, leftをランダムに決める
# はみ出すことを許すので、0以上ではなく負の値もとる(最大mask_size // 2はみ出す)
top = np.random.randint(0 - mask_size // 2, h - mask_size)
left = np.random.randint(0 - mask_size // 2, w - mask_size)
bottom = top + mask_size
right = left + mask_size
# はみ出した場合の処理
if top < 0:
top = 0
if left < 0:
left = 0
# マスク部分の画素値を平均値で埋める
image[top:bottom, left:right, :].fill(mask_value)
return image

Cutout
はみ出すことを許しているので、そこそこいい感じにマスクされているっぽいです。これくらいだと人間の場合、容易にネコと認識できますね。
Random Erasing
Random Erasing もCutoutと同時期に発表された論文 Random Erasing Data Augmentation で提案された手法です。
内容もほとんど同じで、画像にマスクをかけることで汎化性能の向上を目指します。用いるマスクが異なり、 Random Erasing は、
- マスクするかしないか(p)
- 画像全体の何割をマスクするか(s)
- マスクのアスペクト比(r)
を決めます。
それぞれの値は p=0.5, s=0.02~0.4, r=0.3~3 くらいが良いそうです。また、マスクの画素値は、ランダムの方が結果が良かったそうです。
def random_erasing(image_origin, p=0.5, s=(0.02, 0.4), r=(0.3, 3)):
# マスクするかしないか
if np.random.rand() > p:
return image
image = np.copy(image_origin)
# マスクする画素値をランダムで決める
mask_value = np.random.randint(0, 256)
h, w, _ = image.shape
# マスクのサイズを元画像のs(0.02~0.4)倍の範囲からランダムに決める
mask_area = np.random.randint(h * w * s[0], h * w * s[1])
# マスクのアスペクト比をr(0.3~3)の範囲からランダムに決める
mask_aspect_ratio = np.random.rand() * r[1] + r[0]
# マスクのサイズとアスペクト比からマスクの高さと幅を決める
# 算出した高さと幅(のどちらか)が元画像より大きくなることがあるので修正する
mask_height = int(np.sqrt(mask_area / mask_aspect_ratio))
if mask_height > h - 1:
mask_height = h - 1
mask_width = int(mask_aspect_ratio * mask_height)
if mask_width > w - 1:
mask_width = w - 1
top = np.random.randint(0, h - mask_height)
left = np.random.randint(0, w - mask_width)
bottom = top + mask_height
right = left + mask_width
image[top:bottom, left:right, :].fill(mask_value)
return image
if mask_height > h - 1とかで高さや幅を修正すると、決めたサイズとアスペクト比が異なることになりますが、元々ランダムで決めるものなので、まぁいいかなと思います。

Random Erasing
こちらもいい感じにマスクされています。右上の画像はかなりマスクされていますが、40%近く隠されていても認識できるものですね。
さいごに
画像のData Augmentationの手法をNumpy(とSciPy)で実装し、まとめてみました。
いずれも簡単な実装で、そこそこ精度向上が期待されるものですが、どちらかと言うと小手先のテクニックであり、本当に画像が増えているわけではないので、過剰な期待は禁物です。
最近では、GANを用いたData Augmentationや、画像自体を増やすのではなく、ネットワークの中間層からデータを増やす手法などもあったりします。CapsNetの登場により、本質的な認識ができるようになりつつあるので、Data Augmentationが不要になる時代が来るかもしれません。
最後に、今回用いたコードはGithubにあげていますので、よろしければご参考ください。誤りがあればプルリク等で連絡ください。
xkumiyu/numpy-data-augmentation
*1:float32でなくとも処理できますが、後にディープラーニングで使うことを想定してfloat32にしています
*2:cifarの画像は32 x 32
出典: