Wikiクローラで収集した画像から、キャラクター部分のみを抽出することを考えてみます。
OpenCVが持つGrabCutアルゴリズムを使って処理をしてみました。
GrabCut
PythonチュートリアルGrabCutのページが既に十分詳しいですが、行っていることは一種のクラスタリングです。
まず明確に背景である部分と前景がすべて含まれている領域がわかっていることを前提とします。後者の領域は矩形で示します。
OpenCVの実装では、GC_INIT_WITH_RECTモードでgrabCutを呼び出す際は指定した領域すべてに前景、それ以外に背景のラベルを与えます。これが初期値です。
混合ガウシアンモデル(Gaussian Mixture Model, GMM)を用いて、前景と背景の画素値分布を学習します。混合ガウシアンモデルは任意の連続関数を複数の正規混合分布で近似する統計的モデリング手法です。
前景、背景それぞれにラベルされたピクセルの色情報をGMMに与え、訓練します。これにより、ピクセルの色情報を与えると前景である確率、背景である確率を出力する予測モデルが構築されます。
これらの予測モデルを用いて、グラフを構築します。グラフはすべてのピクセルがノードとなります。それ以外2つの特殊なノードsource, sinkを用意し、すべての前景ピクセルはsourceに、背景ピクセルはsinkにつなげます。
source/sinkと画素ノードをつなぐエッジの重みは先のモデルから得られる前景らしさ、背景らしさの確率に基づいた値をとります。
さらに各画素の上下左右に隣接するピクセル同士にもエッジをもたせます。エッジの重みは画素の色のノルムとしているようです。
このグラフを元に、sourceに属する画素群とsinkに属する画素群の2つに分割します。分割の基準は、切断されたエッジの重みの総和です。これが最小となる範囲で分割を行います。
できあがった前景・背景ラベル情報を元に、GMMモデル構築から分割までの処理を引数に指定した回数だけ行い、それを最終結果とします。
OpenCVでは、GMMのコンポーネント数が5にハードコーディングされています。ソースコード内部では13xコンポーネント数分のdouble配列をGMMモデル格納領域として用いるので、前景・背景それぞれ用に(1, 65)のnumpy配列を用意し与えます。
適用例
チュートリアルのコードを参考にしつつ、適用してみましょう。
import cv2
import numpy as np
# wiki画像 ホッキョクオオカミ
fname = 'fixed/cool/5/20151225174718cRVL4Dkz.jpg'
img = cv2.imread(fname)
rect = (210, 50, 230, 400) # キャラ全体をカバーする領域 (始点x, y, 幅、高さ)
def img_clip(img): # 画像全体からキャラクターを含む領域だけをクリッピング
h, w, c = img.shape
x0 = int(w * 0.45) # 右半分以上は情報表示部分なのでカット
y0 = int(h * 0.76) # 下1/4も同様にカット
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) # 背景GMMモデル
fgModel = np.zeros((1, 65), np.float64) # 前景GMMモデル
cv2.grabCut(cimg, mask, rect, bgModel, fgModel, 5, cv2.GC_INIT_WITH_RECT) # イテレーション回数5
mask2 = np.where((mask == 2) | (mask == 0), 0, 1).astype(np.uint8) # maskの値が1の部分のみ残して0クリア
out = cimg * mask2[:, :, np.newaxis] # カラーチャンネルの分だけbroadcastしつつ掛け算、結果として1の領域のみ残す
cv2.imshow('output', out)
cv2.waitKey()
実行結果
思ったよりは悪くない結果ですが、目の部分もくり抜かれてしまっています。今回対象にした画像はレアリティ★5のキャラクターで、背景がホログラム風のパターンをしているため、目のハイライトと傾向が似ているのが原因と考えられます。
これについては事前に前景ラベルを陽に指定することで対応できます。その方法については改めて記事にします。