学習画像の水増し

# はじめに
- 学習画像が少ない場合のために、水増しをする手法があります。
- コントラスト、ガンマ、ブラー、ノイズ等色々あります。
- 今回は、左右反転、ランダムクロップを実施しました。
- 実は、色々実験した所、学習精度が1番良かった組み合わせでした。あくまで、今回の元画像の場合です。
- ソース一式は [ここ](https://github.com/maedamikio/public/tree/master/abe_or_ishihara/learning) です。


# ライブラリ
- `Numpy` `Pillow` を使いました。

```
$ pip install numpy==1.16.5 pillow
```


# 設定
- `CLASSES` に従い、逐次処理が繰り返されます。
- `FACE_PATH` には、顔画像が保存されています。
- `TEST_NUM` に従い、`FACE_PATH` から `TEST_PATH` へ画像が複製されます。
- `TRAIN_PATH` には、`TEST_PATH` へ複製されなかった画像が複製されます。
- `AUGMENT_NUM` に従い、`TRAIN_PATH` から `AUGMENT_PATH` へ水増し画像が作成されます。

```config.py
CLASSES = [
    '安倍乙',
    '石原さとみ',
    '大原優乃',
    '小芝風花',
    '川口春奈',
    '森七菜',
    '浜辺美波',
    '清原果耶',
    '福原遥',
    '黒島結菜'
]

BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATA_PATH = os.path.join(BASE_PATH, 'data')
FACE_PATH = os.path.join(DATA_PATH, 'face')
TRAIN_PATH = os.path.join(DATA_PATH, 'train')
TEST_PATH = os.path.join(DATA_PATH, 'test')
AUGMENT_PATH = os.path.join(DATA_PATH, 'augment')

TRAIN_NUM = 0
TEST_NUM = 100
AUGMENT_NUM = 6000
```

# 顔画像を学習画像とテスト画像に複製

- 顔画像、学習画像、テスト画像のパスを確認します。
- 顔画像の一覧を作成します。
- `query` には、`CLASSES` が順次与えられます。

```save_train_test_from_face.py
def split(query):
    """顔画像の一覧の取得、学習とテストに分割しコピー."""

    face_path = os.path.join(FACE_PATH, query)
    train_path = os.path.join(TRAIN_PATH, query)
    test_path = os.path.join(TEST_PATH, query)

    face_file_list = glob.glob(os.path.join(face_path, '*.jpeg'))
    face_file_list.sort()
```

- 顔画像の一覧をシャッフルします。
- `TEST_NUM` に従い、顔画像のリストを学習画像とテスト画像に分割します。

```save_train_test_from_face.py
    random.shuffle(face_file_list)

    train_file_list = face_file_list[:-TEST_NUM]
    test_file_list = face_file_list[len(train_file_list):]
```
- 学習画像とテスト画像の複製を作成します。
- 元の顔画像は、残しておく方が、やり直しの手間が省けますね。

```save_train_test_from_face.py
    for face_file in train_file_list:
        train_file = os.path.join(train_path, os.path.basename(face_file))
        shutil.copy(face_file, train_file)

    for face_file in test_file_list:
        test_file = os.path.join(test_path, os.path.basename(face_file))
        shutil.copy(face_file, test_file)
```

- 以下の様に、顔画像が学習画像とテスト画像に分割されました。
- 学習画像は、最大 `392` 最小 `269` 枚ですね。少ないかもな。

```
$ python save_train_test_from_face.py
query: 安倍乙, face: 415, train: 315, test: 100
query: 石原さとみ, face: 492, train: 392, test: 100
query: 大原優乃, face: 372, train: 272, test: 100
query: 小芝風花, face: 400, train: 300, test: 100
query: 川口春奈, face: 369, train: 269, test: 100
query: 森七菜, face: 389, train: 289, test: 100
query: 浜辺美波, face: 481, train: 381, test: 100
query: 清原果耶, face: 428, train: 328, test: 100
query: 福原遥, face: 420, train: 320, test: 100
query: 黒島結菜, face: 448, train: 348, test: 100
```


# 学習画像の水増し
- 下記を参考にさせてもらいました。
- [NumPyでの画像のData Augmentationまとめ](https://www.kumilog.net/entry/numpy-data-augmentation)

## 水平方向に反転の関数
- 最初に、`Pillow` から `Numpy` に変換します。
- また、`rate` で反転の確率が与えられます。`0.5` を設定し半々の確率にしています。
- `Numpy` に変換した上で、`fliplr` で水平方向に反転します。
- 最後に、`Numpy` から　`Pillow` に戻します。

```
def horizontal_flip(image, rate=0.5):
    """水平方向に反転."""

    image = np.array(image, dtype=np.float32)

    if np.random.rand() < rate:
        image = np.fliplr(image)

    return Image.fromarray(np.uint8(image))
```

## ランダムクロップの関数
- `image.shape` で、画像の高さと幅を取得します。
- `size` を元にクロップサイズを決めます。`0.8` は、`80%` のサイズでクロップする事を意味します。
- `左上` と `右下` の位置を決めます。
- `top` は、`0` から `height` - `crop_size` の範囲のランダムな値になります。
- 同様に、`left` も決めます。
- `bottom` は、`top` と `crop_size` を足す事で位置を決めます。
- 同様に、`right` も決めます。
- 最後に、`image` からクロップします。

```
def random_crop(image, size=0.8):
    """ランダムなサイズでクロップ."""

    image = np.array(image, dtype=np.float32)

    height, width, _ = image.shape
    crop_size = int(min(height, width) * size)

    top = np.random.randint(0, height - crop_size)
    left = np.random.randint(0, width - crop_size)
    bottom = top + crop_size
    right = left + crop_size
    image = image[top:bottom, left:right, :]

    return Image.fromarray(np.uint8(image))
```

## 水増し処理
- 学習画像と水増し画像のパスを設定します。
- `query` には、`CLASSES` が順次与えられます。

```
def augment(query):
    """学習画像の読み込み、水増し、保存."""

    train_path = os.path.join(TRAIN_PATH, query)
    augment_path = os.path.join(AUGMENT_PATH, query)
```

- 顔画像の一覧のリストを作成します。

```
    train_list = glob.glob(os.path.join(train_path, '*.jpeg'))
    train_list.sort()
```

- 水増し画像の枚数から、顔画像を何枚作成するべきかを確認し、ループ処理の回数を決定します。

```
    loop_num = math.ceil(AUGMENT_NUM / len(train_list))
```

- ループ処理回数と顔画像リストのループの中で以下を実施します。
- 顔画像の読み込み。
- 50% の割合で、水平方向に反転。
- 80% の画像サイズで、ランダムクロップ。
- 顔画像のファイル名に `-0001.jpeg` の付加し、水増し画像を保存。

```
    augment_num = 0
    for num in range(1, loop_num + 1):
        for train_file in train_list:
            if augment_num == AUGMENT_NUM:
                break

            image = Image.open(train_file)

            image = horizontal_flip(image)
            image = random_crop(image)

            augment_file = os.path.join(AUGMENT_PATH, query, os.path.basename(train_file).split('.')[0] + '-{:04d}.jpeg'.format(num))
            image.save(augment_file, optimize=True, quality=95)
            print('query: {}, train_file: {}, augment_file: {}'.format(
                query, os.path.basename(train_file), os.path.basename(augment_file)))

            augment_num += 1

```


# おわりに
- 学習画像の水増しを、`Pillo` と `Numpy` で行いました。
- 作業の過程で、ランダムクロップ以外の、スケールクロップ、カットアプト、ランダムイレース、ランダムローテートも確認しました。今回の顔画像に場合は、精度向上に向いていなかったので、利用していません。
- 次回は、学習画像、テスト画像を扱いやすくするための、データセットを作成する予定です。