PyTorch 入力画像と教師画像の両方にランダムなデータ拡張を実行する方法

# 概要
DeepLearningのタスクの1つであるセマンティックセグメンテーション(Semantic Segmentation)では、分類や検出のタスクと異なって、教師データが画像形式になっている。そのためデータ拡張する場合（クロップや反転など）、入力画像と教師データそれぞれに**同じように**画像処理を行う必要がある。

この記事では入力画像と教師データの両方に同様の**ランダムなデータ拡張**を実行する方法を紹介する記事。


# セマンティックセグメンテーションとは
セマンティックセグメンテーションについては以下が参考になります。
[U-NetでPascal VOC 2012の画像をSemantic Segmentationする (TensorFlow)](https://qiita.com/tktktks10/items/0f551aea27d2f62ef708)

今回はサンプル画像としてVOCデータセットの画像を使用する。
解像度はどちらも500x281になっている。

・入力画像
![2007_000032.jpg](https://qiita-image-store.s3.amazonaws.com/0/250063/f8f86509-30c0-aeed-7d27-ee8e62788cd8.jpeg)
・教師画像
![2007_000032.png](https://qiita-image-store.s3.amazonaws.com/0/250063/a4fd8cbf-1537-59bc-b82d-af8076e605e9.png)



# ランダムなデータ拡張
今回はPyTorchで予め用意されている``torchvision.transforms.RandomCrop(size, padding=0, pad_if_needed=False)``を使って画像に対してクロップを実行する。

関数の中では、乱数でクロップする位置を決めて、指定した`size`でクロップしている。（最終的には内部で``torchvision.transforms.functional.crop(img, i, j, h, w)``がコールされている。）


詳細な使い方やパラメータについてはPyTorchのリファレンスを参照してください。
[PyTorch TORCHVISION.TRANSFORMS](https://pytorch.org/docs/stable/torchvision/transforms.html)



# 課題
``torchvision.transforms.RandomCrop``は内部で乱数を発生させているため、実行するたびに結果が異なってしまう。
よって、以下のように実行すると入力画像と教師画像が異なる位置がクロップされてしまう。


```python:
from PIL import Image
from torchvision import transforms

trans_crop = transforms.RandomCrop((224,224))

img = Image.open(img_path) # 入力画像
target = Image.open(target_path) # 教師画像

img = trans_crop(img) # 入力画像を(224,224)でランダムクロップ
target = trans_crop(target) # 教師画像を(224,224)でランダムクロップ

img.show()
target.show()
```
![input.png](https://qiita-image-store.s3.amazonaws.com/0/250063/4c7dd428-d56b-241b-3368-d3aab7981e80.png)
![target.png](https://qiita-image-store.s3.amazonaws.com/0/250063/bfefddf5-0869-edd2-af48-904db6465478.png)

このように入力画像と教師画像が一致しないため学習ができなくなってしまう。


# 解決策１ 乱数シードを固定する

PyTorchのクロップ関数の内部では``random.randint()``で乱数を発生させているので、``random.seed()``を使って乱数シードを設定すれば同じ結果が得られる。


```python:
from PIL import Image
from torchvision import transforms
import random

trans = transforms.RandomCrop((224,224))

img = Image.open(img_path)
target = Image.open(target_path)

seed = random.randint(0, 2**32) # 乱数で乱数シードを決定

random.seed(seed) # 乱数シードを固定
img = trans(img)

random.seed(seed) # こっちでも乱数シードを固定
target = trans(target)

img.show()
target.show()
```
![input2.png](https://qiita-image-store.s3.amazonaws.com/0/250063/c9e1ede1-bee5-3f6b-48a6-02e7ccc308a5.png)
![target2.png](https://qiita-image-store.s3.amazonaws.com/0/250063/7beffe6a-dd56-1802-ca1c-109a1dc73310.png)

この実装のよくない点はPyTorchの中で「``random.randint()``で乱数を発生させている」ということを前提としていること。
PyTorchの中で乱数の発生のさせ方が変わると急に上手く動作しなくなったりする。




# 解決策2 transforms.RandomCrop.get_params(img, output_size))を使う

``transforms.RandomCrop.get_params(img, output_size))``は乱数で決めたクロップする位置とサイズを返してくれる関数。

``torchvision.transforms.RandomCrop``の中でもこの関数でクロップ位置を決めた後、``torchvision.transforms.functional.crop(img, i, j, h, w)``でクロップしている。

なので、これと同じような処理の流れを自分で実装すれば解決できる。


```python:
from PIL import Image
from torchvision import transforms
from torchvision.transforms import functional as tvf
import random

trans = transforms.RandomCrop((224,224))

img = Image.open(img_path)
target = Image.open(target_path)

# クロップ位置を乱数で決定
i, j, h, w = transforms.RandomCrop.get_params(img, output_size=(224,224))

img = tvf.crop(img, i, j, h, w) # 入力画像を(224,224)でクロップ
target = tvf.crop(target, i, j, h, w) # 教師画像を(224,224)でクロップ
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/250063/473424a2-0109-6e59-22d0-f0d94ddc901a.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/250063/73b938b4-0137-711d-74c2-16d5c3bd3a5b.png)

--------------