OpenCVのGrabCut使ってみた

[MYJlabアドベントカレンダー](https://qiita.com/advent-calendar/2019/myjlab)の9日目です。
## これは何か
n番煎じですが、画像の前景領域を抽出するためにOpenCVのGrabCutを使ってみた記事です。

## 背景
僕のプログラミング以外の唯一の趣味はギターなのですが、ギターを見た目から簡単に検索できるようなサービスを作ろうと考えています。その過程でギター単体を抜き出した画像が必要になりそうなので、背景を削除できる手法何かないかなーと探していました。  
特に良さげなものが見つからなければセグメンテーション用のモデルでなんとかできないかなと考えている時に、OpenCVのGrabCut簡単にできるみたいなので、試してみました。

## 実装
仕組みについては[Pythonチュートリアル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_grabcut/py_grabcut.html)が詳しいです。  
行なっている作業は以下の通りです。

1. 画像を読み込む
2. ほしい前景を含む矩形を指定
3. GrabCutを実行
4. GrabCutの結果を元画像に反映させる

今回は、ギターの画像をフリー素材から持ってきました

* https://unsplash.com/photos/YStboKiFPVw

### 画像を読み込む
上の３つの画像は画像サイズが大きいのであらかじめ小さくしておきます。

```python
def resize_image(filepath, width, height):
    img = Image.open(filepath)
    img.thumbnail((width, height),resample=Image.BICUBIC)
    return img
IMG_BASE_PATH = './img/'
filename = 'guitar.jpeg'
WIDTH = 1000
HEIGHT = 1000
file_path = IMG_BASE_PATH + filename
pillow_img = resize_image(file_path, WIDTH, HEIGHT)
resized_image_path = f'{IMG_BASE_PATH}resized_{filename}'
pillow_img.save(resized_image_path)
```

リサイズが終わったらOpenCVを使ってもう一度読み込みます。本当はPillowで読み込んだ画像をOpenCVの画像に変換する方法があると思いますが、今回はこれでよしとしてください。

```python
img = cv2.imread(resized_image_path)
```

### 欲しい前景囲む矩形を指定
GrabCutは対話的な方法で前景を抽出する手法です。なので、使う人間がどこに欲しい前景が含まれているかを指定してあげる必要があります。今回はめんどくさいので画像のほぼ全部を矩形で囲んであげます。

```python
# 引数はx座標, y座標, 幅, 高さ
rect = (1,1, pillow_img.size[0],pillow_img.size[1])
```

ここでx座標とy座標で0を指定するとGrabCut実行時に以下のエラーが出ました。

```
---------------------------------------------------------------------------
error                                     Traceback (most recent call last)
<ipython-input-46-73e975b54ddd> in <module>
      1 rect = (0,0, pillow_img.size[0],pillow_img.size[1])
----> 2 cv2.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)

error: OpenCV(4.1.2) /io/opencv/modules/imgproc/src/grabcut.cpp:386: error: (-215:Assertion failed) !bgdSamples.empty() && !fgdSamples.empty() in function 'initGMMs'
```
チュートリアルをみてみると
> ユーザはまず初めに初期値として前景領域の周りに長方形を描きます(前景物体はこの長方形から飛び出してはいけません)．

今回の場合は飛び出してはいないけど全ての領域を囲っているのでダメなのかなって感じです。

### GrabCutを実行
mask用のnumpy配列とGrabCutに必要な配列を作成します。

```python
mask = np.zeros(img.shape[:2],np.uint8)

bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
```

イテレーション数はチュートリアルのままの5で実行します

```python
cv2.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
```

### GrabCutの結果を元画像に反映させる
反映させる前にGrabCutの結果をみてみます。
GrabCutは画像のピクセルごとに4つの分類を行います。チュートリアルによればこの4つの分類は以下の意味を持ちます
> マスク画像中の画素値が0の画素は背景，1の画素は前景，2の画素は背景らしい，3の画素は前景らしい画素を意味します

マスクの0,2を箇所を黒く塗りつぶすことで前景画像のみを取得できます。

```python
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]
```

## 結果
### 処理前
![guitar3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167075/2a76786c-8b60-4924-de92-5863bc5a4b48.jpeg)

### 処理後
![segmented_guitar3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167075/d5d1e4b8-57cf-82be-58e4-090df956e1ba.jpeg)

概ねいい感じに切り取れてますが、左下の背景と床の光沢が残ってしまいました。本当は修正することも可能ですが、今回はこの精度で問題ないのでここまでで終わりにします。

##最後に
OpenCVのGrabCutを使用してみました。ニューラルネットを使用せずにここまでできることに驚いています。
画像サイズによっては速度が異常に遅かったりするので今後はそこをどうにか解決できないかを探っていきたいです。

## 参考にしたサイト
* http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_grabcut/py_grabcut.html
* https://qiita.com/knok/items/63e32adef9cfff095d49
* https://note.nkmk.me/python-opencv-bgr-rgb-cvtcolor/
* https://ky-orihara.hatenablog.com/entry/2019/03/12/021106