最新のdata augmentationのCutPasteをOpenCV-Pythonで実装する

# CutPaste
まずは今回紹介するCutPasteについて知らない人向けにどのようなものか説明します．
## CutPasteってなに？
Google Cloud AIのリサーチチームが開発したdata augmentationの手法で、Cutoutの画像版のような感じ．論文は[こちら](https://arxiv.org/abs/2104.04015)です．

## 具体的な手法は？
画像のランダムな部分を長方形で切り抜き（Cut）、ランダムな位置に、回転や並進を加えて貼り付ける（Paste）手法です．
これを
![812F4408-6C2D-4184-A857-AFED1F560E66.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565117/227f8bf7-e965-7bad-5578-0e45deb7783c.jpeg)
こうする
![sample_cutpaste.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565117/936a4a64-bcb2-9fd2-7a14-d6ad4b1add5a.jpeg)
真ん中下らへんの部分が左上の方に付け加えられていることがわかるかと思います．イメージとしてはこのような感じです．

## 何に使われるの？
異常検知のdata augmentationに使用されます．異常検知の多くはデータラベルに偏りがあり、この問題の解消としてdata augmentationが行われます．しかし、論文にも書いてあるのですが、この手法はセマンティックな異常検知にはあまり向いていなく、欠陥検知といった細かい部分の異常検知に特化しているようです．論文の実験ではMVTechデータが使われているのですが、このような工業製品の異常検知にとても優れた性能を示しています．工業製品の異常検知では画像のほとんどが同じような配置をしているためこの手法が役に立つのですね．

# 実装
今回の実装に参考にした文献などはこちら
> [PythonのOpenCVで画像の貼り付け - Qiita](https://qiita.com/a2kiti/items/9672fae8e90c2da6f352)
 [Python, OpenCVで幾何変換（アフィン変換・射影変換など） - note.nkmk.me](https://note.nkmk.me/python-opencv-warp-affine-perspective/)
 [ピクセル毎の論理演算 AND NOT OR XOR - OpenCV画像解析入門](https://cvtech.cc/bitwise/)
 [OpenCV-Python チュートリアル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_tutorials.html)

## Cut
まずは画像のランダムな位置からランダムな大きさの画像（以下パッチと呼ぶ）をcutします．

```Python:cut_patch
def cut_patch(img):
    img_height, img_width, _ = img.shape
    top = random.randrange(0, round(img_height))
    bottom = top + random.randrange(round(img_height*0.05),
                                            round(img_height*0.15))
    left = random.randrange(0, round(img_width))
    right = left + random.randrange(round(img_width*0.05),
                                            round(img_width*0.15))
    if (bottom - top) % 2 == 1:
        bottom -= 1
    if (right - left) % 2 == 1:
        right -= 1
    return img[top:bottom, left:right, :]
```
まず、元となる画像のサイズを取得します．
top、bottom、left、rightはそれぞれパッチの大きさを決めるパラメータです．
top、leftは画像全体のどの値でも取ります．ランダム性を最大にするためです．bottom、rightをtop、leftに基づいて決めますが、この時それぞれ画像の5%から15%の長さの値を取るようにしています．ここは必要に応じて変更しましょう．今回はあまり大きなパッチが必要でないのでこの値設定にしています．
if文のところでは、cutとpasteを別の関数にしているため、座標が偶数でないと都合が悪くなってしまい、その処理を行っています．

## Paste
次にpasteです．
### 最初の準備
まず必要な値を用意します．値が多くてすみません．．

```Python:paste_patch
def paste_patch(img, patch, rot, ratio):
    img_height, img_width, _ = img.shape
    patch_height, patch_width, _ = patch.shape
    trans_x = random.randrange(-width_half, width_half)
    trans_y = random.randrange(-height_half, height_half)
    img_h_center = round(img_height / 2)
    img_w_center = round(img_width / 2)
    patch_h_center = round(patch_height / 2)
    patch_w_center = round(patch_width / 2)
    top = round((img_height - patch_height) / 2)
    bottom = round((img_height + patch_height) / 2)
    right = round((img_width - patch_width) / 2)
    left = round((img_width + patch_width) / 2)
```

* rot: パッチの回転（度数）
* ratio: パッチの大きさの倍率
* img_height, img_width: 元画像の縦と横
* patch_height, img_width: パッチの縦と横
* trans_x, trans_y: パッチの平行移動距離
* img_h_center, img_w_center: 元画像の真ん中の座標
* patch_h_center, patch_w_center: パッチの真ん中の座標
* top, bottom, left, right: パッチの座標（cutとpasteを同じ関数に入れるなら必要ない）

#### pasteの流れ

1. 元画像と同じ大きさの仮画像の真ん中にパッチを貼り付ける
1. 引数のrot度回転させる
1. ratio倍にする
1. trans_x、trans_y分平行移動させる

この流れで行います．

### 真ん中に貼り付ける
何はともあれまずは真ん中に貼り付けます．

```Python
tmp_img = np.zeros((img_height, img_width, 3), np.uint8)
tmp_img[top:bottom, left:right, :] = patch
```

元画像のmaskを取得するためにtmp_imgを用意してあげます．用意してあげたら座標を指定し、patchを貼り付けましょう．

### 回転
続いて回転させます．

```Python
M = cv2.getRotationMatrix2D((img_w_center, img_h_center), rot, ratio)
tmp_img = cv2.warpAffine(tmp_img, M, (img_width, img_height))
```

* cv2.getRotationMatrix2D

回転行列を返します．第一引数が回転の原点、第二引数が回転の角度、第三引数が拡大・縮小倍率になります．現在真ん中にパッチが貼られているので画像の真ん中を回転の原点としています． 

* cv2.warpAffine

アフィン変換を行う関数です．第一引数に画像、第二引数に変換行列（回転行列や移動行列など）、第三引数に出力画像のサイズを指定します．

ここではまずMに回転行列を代入し、warpAffineで指定してあげることで回転と拡大・縮小を行なってあげています．

### 平行移動
最後に平行移動をします．

```Python
M = np.float32([[1, 0, trans_x], [0, 1, trans_y]])
tmp_img = cv2.warpAffine(tmp_img, M, (img_width, img_height))
```

平行移動でもMに移動行列を与え、warpAffineでアフィン変換を行なっています．

### マスク処理
ここまでパッチを回転、拡大・縮小、平行移動させてきました．このパッチが含まれているtmp_imgをマスク化し、元画像とくっつけていきます．

```Python
imggray = cv2.cvtColor(tmp_img, cv2.COLOR_BGR2GRAY)
ret, mask = cv2.threshold(imggray, 10, 255, cv2.THRESH_BINARY)
mask_inv = cv2.bitwise_not(mask)
```

マスク化するためには閾値処理を行う必要があります．

* threshold

閾値処理の関数です．第一引数に二値化させた画像、第二引数に閾値、第三引数に最大値、第四引数に処理フラグを必要とします．

* bitwise_not

ピクセルごとの処理演算を行います．現在マスク画像はパッチが1、背景が0の状態なのでそれを逆にするためにNOT演算を行います．mask_invが元画像に穴を開けるためのmaskなので逆にします．

### マスクを利用して元画像に穴を開け、パッチを抽出

```Python
back = cv2.bitwise_and(img, img, mask=mask_inv)
cut = cv2.bitwise_and(tmp_img, tmp_img, mask = mask)
```

これで元画像（back）にパッチの部分に穴を開け、パッチ（cut）に座標がピクセル単位のアノテーションが代入されます．

### 合体
合体させましょう．

```Python
paste = cv2.add(back, cut)
return paste
```

返り値はCutPasteが行われた画像です．

# まとめ
これで終わりです．正直これではまだ不十分（パッチの形のランダム性やアノテーションの抽出の実装がない）なため改良の余地はありますが、最低限の機能を持たせることができました．追加の実装をしたときにまた編集したいと思います．

# コード全体
```Python:CutPaste.py
def cut_patch(img):
    img_height, img_width, _ = img.shape
    top = random.randrange(0, round(img_height))
    bottom = top + random.randrange(round(img_height*0.05),
                                            round(img_height*0.15))
    left = random.randrange(0, round(img_width))
    right = left + random.randrange(round(img_width*0.05),
                                            round(img_width*0.15))
    if (bottom - top) % 2 == 1:
        bottom -= 1
    if (right - left) % 2 == 1:
        right -= 1
    return img[top:bottom, left:right, :]

def paste_patch(img, patch, rot, ratio):
    img_height, img_width, _ = img.shape
    patch_height, patch_width, _ = patch.shape
    width_half = round(img_width / 2)
    height_half = round(img_height / 2)
    trans_x = random.randrange(-width_half, width_half)
    trans_y = random.randrange(-height_half, height_half)
    patch_h_center = round(patch_height / 2)
    patch_w_center = round(patch_width / 2)
    img_h_center = round(img_height / 2)
    img_w_center = round(img_width / 2)
    top = round((img_height - patch_height) / 2)
    bottom = round((img_height + patch_height) / 2)
    left = round((img_width - patch_width) / 2)
    right = round((img_width + patch_width) / 2)
    # paste on center
    tmp_img = np.zeros((img_height, img_width, 3), np.uint8)
    tmp_img[top:bottom, left:right, :] = patch
    # rotation and expansion
    M = cv2.getRotationMatrix2D((img_w_center, img_h_center), rot, ratio)
    tmp_img = cv2.warpAffine(tmp_img, M, (img_width, img_height))
    # translation
    M = np.float32([[1, 0, trans_x], [0, 1, trans_y]])
    tmp_img = cv2.warpAffine(tmp_img, M, (img_width, img_height))
    # make mask of patch
    imggray = cv2.cvtColor(tmp_img, cv2.COLOR_BGR2GRAY)
    ret, mask = cv2.threshold(imggray, 10, 255, cv2.THRESH_BINARY)
    mask_inv = cv2.bitwise_not(mask)
    # cut the mask from original image
    back = cv2.bitwise_and(img, img, mask=mask_inv)
    cut = cv2.bitwise_and(tmp_img, tmp_img, mask = mask)
    # paste(combine original and patch)
    paste = cv2.add(back, cut)
    return paste
```