目標
この章では
- 画像から前景を抽出する GrabCut アルゴリズムを見ていく
- このためのインタラクティブなアプリケーションを作成する。
理論
GrabCut アルゴリズムは、英国マイクロソフトリサーチケンブリッジの Carsten Rother、Vladimir Kolmogorov、Andrew Blake によって考案された。彼らの論文 "GrabCut": interactive foreground extraction using iterated graph cuts で発表されている。ユーザーの操作を最小限に抑えて前景を抽出するアルゴリズムが必要とされ、その成果が GrabCut であった。
ユーザーの視点から見た動作は? まずユーザーは前景領域の周囲に矩形を描く (前景領域は矩形の内側に完全に収まっている必要がある)。次にアルゴリズムが反復的にセグメンテーションを行い、最良の結果を得る。これで完了。ただし場合によっては、セグメンテーションがうまくいかず、たとえば一部の前景領域が背景としてマークされたり、その逆が起きたりすることがある。その場合、ユーザーは細かな修正を行う必要がある。誤った結果がある画像上に、いくつかストロークを描けばよい。ストロークは基本的に 「おい、この領域は前景のはずだ。お前は背景とマークしたが、次の反復で直せ」 と伝えるものであり、背景の場合はその逆になる。すると次の反復で、より良い結果が得られる。
下の画像を見てほしい。まず選手とサッカーボールが青い矩形で囲まれている。次に、白いストローク (前景を示す) と黒いストローク (背景を示す) で最終的な修正が加えられている。そして良い結果が得られる。
image
では裏側では何が起きているのか?
- ユーザーが矩形を入力する。この矩形の外側はすべて確実な背景とみなされる (前述で矩形がすべてのオブジェクトを含むべきだと述べた理由はこれである)。矩形の内側はすべて未知である。同様に、前景や背景を指定するユーザー入力はハードラベリングとみなされ、処理の過程で変化しない。
- コンピュータは、与えられたデータに基づいて初期ラベリングを行う。前景と背景のピクセルにラベル付けする (あるいはハードラベル付けする)
- 次に、前景と背景をモデル化するために混合ガウスモデル (GMM) が使用される。
- 与えられたデータに基づいて、GMM は学習を行い、新たなピクセル分布を作成する。すなわち、未知のピクセルは、色の統計量という観点で他のハードラベル付けされたピクセルとの関係に応じて、前景候補または背景候補のいずれかにラベル付けされる (これはクラスタリングとちょうど同じである)。
- このピクセル分布からグラフが構築される。グラフのノードはピクセルである。さらに2つのノード、ソースノードとシンクノードが追加される。すべての前景ピクセルはソースノードに接続され、すべての背景ピクセルはシンクノードに接続される。
- ピクセルをソースノード/終端ノードに接続するエッジの重みは、そのピクセルが前景/背景である確率によって定義される。ピクセル間の重みは、エッジ情報またはピクセルの類似度によって定義される。ピクセルの色に大きな差がある場合、それらの間のエッジは低い重みを持つことになる。
- 次に、ミニカット (mincut) アルゴリズムを用いてグラフをセグメンテーションする。これはグラフを、最小のコスト関数でソースノードとシンクノードに分離するように2つに切断する。コスト関数は、切断されるすべてのエッジの重みの総和である。切断後、ソースノードに接続されているすべてのピクセルが前景となり、シンクノードに接続されているものが背景となる。
- この処理は、分類が収束するまで繰り返される。
これは下の画像で図示されている (画像提供: http://www.cs.ru.ac.za/research/g02m1682/)
image
デモ
では OpenCV を使って grabcut アルゴリズムを実行する。OpenCV にはこのための関数 cv.grabCut() がある。まずその引数を見ていく:
- img - 入力画像
- mask - これはマスク画像で、どの領域が背景、前景、あるいは背景候補/前景候補かなどを指定する。これは次のフラグ cv.GC_BGD, cv.GC_FGD, cv.GC_PR_BGD, cv.GC_PR_FGD によって行うか、あるいは単に 0,1,2,3 を画像に渡す。
- rect - 前景オブジェクトを含む矩形の座標で、(x,y,w,h) の形式で与える
- bdgModel, fgdModel - これらはアルゴリズムが内部的に使用する配列である。サイズ (1,65) の np.float64 型のゼロ配列を2つ作成するだけでよい。
- iterCount - アルゴリズムを実行する反復回数。
- mode - cv.GC_INIT_WITH_RECT または cv.GC_INIT_WITH_MASK、あるいはそれらを組み合わせたものにすべきで、矩形を描いているのか最終的な修正ストロークを描いているのかを決定する。
まず矩形モードで見ていこう。画像を読み込み、同様のマスク画像を作成する。fgdModel と bgdModel を作成する。矩形のパラメータを与える。すべて単純明快である。アルゴリズムを5回反復させよう。矩形を使用するので、モードは cv.GC_INIT_WITH_RECT にすべきである。そして grabcut を実行する。これはマスク画像を変更する。新しいマスク画像では、上で指定したように背景/前景を示す4つのフラグでピクセルがマークされる。そこで、すべての 0 ピクセルと 2 ピクセルを 0 (すなわち背景) にし、すべての 1 ピクセルと 3 ピクセルを 1 (すなわち前景ピクセル) にするようにマスクを変更する。これで最終的なマスクが完成する。あとはそれを入力画像に乗算するだけで、セグメンテーションされた画像が得られる。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
assert img is not None, "file could not be read, check with os.path.exists()"
mask = np.zeros(img.shape[:2],np.uint8)
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)
rect = (50,50,450,290)
cv.grabCut(img,mask,rect,bgdModel,fgdModel,5,cv.GC_INIT_WITH_RECT)
mask2 = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask2[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()
Mat imread(const String &filename, int flags=IMREAD_COLOR_BGR)
Loads an image from a file.
void grabCut(InputArray img, InputOutputArray mask, Rect rect, InputOutputArray bgdModel, InputOutputArray fgdModel, int iterCount, int mode=GC_EVAL)
Runs the GrabCut algorithm.
以下の結果を参照:
image
おっと、メッシの髪がなくなってしまった。髪のないメッシを誰が好むだろうか? これを取り戻す必要がある。そこで、1 ピクセル (確実な前景) で細かな修正を加える。同時に、不要なグラウンドの一部やロゴが画像に写り込んでしまっている。これらを取り除く必要がある。そこで 0 ピクセル (確実な背景) の修正を加える。前述したように、前のケースの結果マスクをこのように変更する。
実際に行ったことは次のとおりである。ペイントアプリケーションで入力画像を開き、画像に別のレイヤーを追加した。ペイントのブラシツールを使って、見落とされた前景 (髪、靴、ボールなど) を白で、不要な背景 (ロゴやグラウンドなど) を黒で、この新しいレイヤー上にマークした。それから残りの背景をグレーで塗りつぶした。次にそのマスク画像を OpenCV で読み込み、新たに追加したマスク画像の対応する値で、得られた元のマスク画像を編集した。下のコードを確認してほしい:
newmask =
cv.imread(
'newmask.png', cv.IMREAD_GRAYSCALE)
assert newmask is not None, "file could not be read, check with os.path.exists()"
mask[newmask == 0] = 0
mask[newmask == 255] = 1
mask, bgdModel, fgdModel =
cv.grabCut(img,mask,
None,bgdModel,fgdModel,5,cv.GC_INIT_WITH_MASK)
mask = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img = img*mask[:,:,np.newaxis]
plt.imshow(img),plt.colorbar(),plt.show()
以下の結果を参照のこと。
image
以上である。ここでは rect モードで初期化する代わりに、直接 mask モードに入ることもできる。マスク画像中の矩形領域を 2 ピクセルまたは 3 ピクセル (背景候補/前景候補) でマークするだけでよい。それから、2番目の例で行ったように、確実な前景 (sure_foreground) を 1 ピクセルでマークする。そして mask モードで grabCut 関数を直接適用する。
演習
- OpenCV のサンプルには、grabcut を使ったインタラクティブなツールである grabcut.py というサンプルが含まれている。確認してほしい。また、その使い方に関するこの youtube 動画 も見てほしい。
- ここでは、これをマウスで矩形やストロークを描くインタラクティブなサンプルにしたり、ストローク幅を調整するトラックバーを作成したりできる。