フレンズ画像を生成させてみたい-前景抽出 GrabCut(2) maskの追加

[前回](https://qiita.com/knok/items/63e32adef9cfff095d49)はキャラクターが含まれている領域のみを指定してGrabCutを適用するというアプローチで処理を行い、目の部分以外は割ときれいに分離できたという結果になりました。
GrabCutは「この領域は前景・背景」である情報を用に与えることが出来ます。目の付近は前景であるということを指定すれば、望む結果を得ることができそうです。どのあたりが目の部分に相当するかをきちんと検討してみます。

# 平均画像の作成

全部で24枚の属性クール、レアリティ5の画像があります。サイズは予め正規化して1104x621にすべてリサイズした結果を用意しておきます。これに関しては[githubのリポジトリ](https://github.com/knok/kemono-friends-images)上に[リサイズするスクリプト](https://github.com/knok/kemono-friends-images/blob/master/scripts/resize-images.py)を用意したので、そちらで処理されたデータを想定しています。

この画像群の平均を求めてみましょう。単純に画素の値を合計して、画像枚数で割れば得られます。

```py3
# -*- coding: utf-8 -*-

import glob
import os
import numpy as np
import cv2

imgpath = 'fixed/cool/5'

def load_img_clip(fname):
        img = cv2.imread(fname)
        h, w, c = img.shape
        x0 = int(w * 0.45)
        y0 = int(h * 0.76)
        clip = img[0:y0, 0:x0, :]
        return clip

def main():
    files = glob.glob("%s/*.jpg" % imgpath)
    imgs = []
    for f in files:
        img = load_img_clip(f)
        imgs.append(img)
    avg = np.zeros(imgs[0].shape, dtype=np.float32)
    for i in imgs:
        avg += i
    avg = avg / len(imgs)
    avg = avg.astype(np.uint8)
    cv2.imshow('avarage', avg)
    cv2.waitKey()
    

if __name__ == '__main__':
    main()
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/31714/e52f4a2f-d79d-403e-937a-f76d35264df6.png)

(280, 120)を始点とする80x80ぐらいの領域はほぼ顔パーツであると見て間違いないでしょう。この領域を陽に前景と指定した上で、GrabCutを適用してみます。

# GrabCut+前景情報の明示

一旦領域に対してGrabCutを適用したあと、得られたマスク情報に対し顔の領域部分を1(前景)で塗りつぶして、再度適用します。

```py3
import cv2
import numpy as np

fname = 'save/cool/5/20151225174718cRVL4Dkz.jpg'
img = cv2.imread(fname)

mask_rect = (280, 120, 80, 80) # 顔領域
x1, y1 = mask_rect[0], mask_rect[1]
x2, y2 = x1 + mask_rect[2], y1 + mask_rect[3]

rect = (210, 50, 230, 400)
def img_clip(img):
    h, w, c = img.shape
    x0 = int(w * 0.45)
    y0 = int(h * 0.76)
    clip = img[0:y0, 0:x0, :]
    return clip
cimg = img_clip(img)
mask = np.zeros(cimg.shape[:2], np.uint8)
bgModel = np.zeros((1, 65), np.float64)
fgModel = np.zeros((1, 65), np.float64)
cv2.grabCut(cimg, mask, rect, bgModel, fgModel, 5, cv2.GC_INIT_WITH_RECT) # この時点でmaskに0~2の値(背景、前景、曖昧)が指定される
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype(np.uint8) # 前景以外の値はゼロクリア

cv2.rectangle(mask2, (x1, y1), (x2, y2), 1, -1) # 顔領域を1で塗りつぶす
cv2.grabCut(cimg, mask2, None, bgModel, fgModel, 5, cv2.GC_INIT_WITH_MASK) # マスク情報に基づいたGrabCutを適用
mask = np.where((mask2 == 2) | (mask2 == 0), 0, 1).astype(np.uint8)
out = cimg * mask[:, :, np.newaxis]

cv2.imshow('output', out)
cv2.waitKey()
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/31714/6a36d23d-ac4c-cbdb-bcd6-b2c240a914b1.png)

良い感じになりました。これをすべての画像に適用してみます。

# スクリプト

githubにスクリプトを置きました。

* https://github.com/knok/kemono-friends-images/blob/master/scripts/grabcut.py

先のコードとは異なりマスク部分以外を白抜きにするよう修正しています。コードで示すと以下のような違いになります。

```py3
# 旧コード
# mask = np.where((mask2 == 2) | (mask2 == 0), 0, 1).astype(np.uint8) # 残したい部分を1に、それ以外を0にする
# out = cimg * mask[:, :, np.newaxis] # 掛け算をすることで背景部分を0にする

# 新コード
out[np.where((mask == 0) | (mask == 2))] = 255 # np.whereの条件に一致する部分へ255を代入
```


デフォルトではfixedディレクトリ以下にある画像を再帰的に走査し、grabcutディレクトリ以下に同じサブディレクトリ構造で処理したものを書き出すようにしています。--input/outputオプションでそれぞれ指定できます。

```shell-session
$ python script/grabcut.py
grabcut/cool/3/1435387873008.jpg
grabcut/cool/3/1435387873004.jpg
grabcut/cool/3/1435387873007.jpg
(以下略)
```

結果の例をいくつか示します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/31714/2a81c4cc-2d55-67ee-4847-5f982fd70e53.png)

一見だいたい処理出来ていそうに見えますが、一つ一つ精査してみると若干の問題があります。

## 指定した領域をはみ出ている

一部のキャラは想定した範囲を超えて描画されています。grabCutに与えた範囲(rect)をはみ出た分は消えてしまいます。

## 顔パーツの位置が想定よりずれている

キャラによっては顔の位置が想定よりずれてしまっていることがあります。そのため、はみ出た分の背景が残ってしまう問題が起きています。

例: ![image.png](https://qiita-image-store.s3.amazonaws.com/0/31714/c272d6e3-921b-9795-15bc-ee88b2e2fcc1.png)

## 目以外のパーツが背景判定される

この問題も一部のキャラでしか起きませんが、たまたま背景と同じような傾向にある色合いのパーツがあると背景と判断され、処理によって白抜きになってしまいます。

例: ![image.png](https://qiita-image-store.s3.amazonaws.com/0/31714/570d96db-2f28-57ba-eebc-f1c9a4fd4627.png)

# 対応策

大半の画像に関してはGrabCutでうまくいっているので、少数の例外についてのみ、個別に対応すればなんとかなりそうです。そうでなくとも、使えそうなものだけでも選別すればおそらく200程度の画像はそのまま使えるでしょう。

それ以外の問題として、そもそもキャラクターがアセットの裏に隠れてしまうケースもあり、こうなると完全に手のうちようがありません。

例: ![image.png](https://qiita-image-store.s3.amazonaws.com/0/31714/95152fa4-2a1c-21f1-a1fb-8e991ace79a3.png)

なにかしらの画像補完技術でどうにかできたりするのでしょうか…

# 今後

まず個別対応なり、選別なりをスクリプト化して同じデータを再現できる状況にしたいと思います。
現状だと左側にかなりの余白があるので、画像全体をうまく中心に持っていくような正規化処理も必要です。

それらが出来たところで、改めてGANsに与えて訓練をしてみようと思います。

これ以外に試してみてあまり良い結果の得られなかったBackground Subtractionについても解説できればと思っています。