OpenCVを使ったPythonでの画像処理について、ここではWatershedアルゴリズムを扱っていきます。
「Watershed」とは「分水嶺」のことで、地理用語の一つです。雨が降ると雨水は地形に合わせて異なる方向に流れますが、その境界のことを分水嶺といいます。特に山岳地帯では山稜が境界になります。水の流れ、水位と地形との関係で境界ができるイメージです。
この概念の画像処理に使ったのがWatershedアルゴリズムです。画像の輝度勾配を地形に見立てて水位を増して地形の輪郭を浮き立たせるような処理をします。画像の接触している物体の境界を分離して認識してくれるアルゴリズムです。
境界を分離する画像の読み込み
ここではよく事例として使われているのと同じように、コインの画像を使ってやっていこうと思います。
jupyter notebookを使ってコードを書いて行きます。
まずは決まり切ったコードから。ライブラリのインポートです。
import numpy as np
import cv2
import matplotlib.pyplot as plt
%matplotlib inline
画像を表示するための関数を定義しておきます。
def display(img,cmap=None):
fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111)
ax.imshow(img,cmap=cmap)
このあたりは何度もこれまでやってきていることですね。
早速、用意した画像を読み込んで表示してみましょう。
coins = cv2.imread('images/coin.jpg')
display(coins)
imread()で用意したコインの画像を読み込んでいます。display()を使ってこれを表示します。
実際に表示してみましょう。
coin.jpgというファイルを用意して開いてみました。互いに接した5つのコイン(10円硬貨)が表示されているのがわかります。これは5つのコインと背景の6つのセグメントに別れているという認識になりますが、これは人が目で見た場合はすぐにわかります。
これをこれまでに扱ってきた画像処理で扱ってみるとどうなるかやってみましょう。
素朴な境界の分離方法
これまでに扱ってきた画像処理方法ですが、単純にしきい値を使ってfindContours()でコインの輪郭を捉えるという形で境界を分離してみましょう。
順番としては次のような処理の流れになります。
- 中央値フィルターでぼかし処理
- グレースケールに変換
- しきい値処理
- 輪郭の検出
では順番にやっていきましょう。
中央値フィルターを使ったぼかしの適用
この硬貨の画像では、光の加減や硬貨の模様、背景と細かい部分があります。細部が多すぎます。 これを中央値フィルターのmedianBlur()を使って画像を少しぼかします。
ぼかしについてはこちらで扱いました。
これは、あとでしきい値を設定するときに役立ちます。
コードを書いて行きましょう。
coins_blur = cv2.medianBlur(coins,25)
display(coins_blur)
medianBlur()に画像を渡して、ぼかしの処理をします。25はフィルタのカーネルサイズです。画像サイズが大きいのが25にしています。
display()で表示してみましょう。
多少、ぼかし処理がされているがわかると思います。
グレースケールに変換
cvtColor()を使って、この画像をグレースケールに変換しておきます。
gray_coins = cv2.cvtColor(coins_blur,cv2.COLOR_BGR2GRAY)
display(gray_coins,cmap='gray')
表示するとこうなります。
画像が白黒になりました。
しきい値処理
次に、しきい値処理をして画像を2値化します。
しきい値処理はこちらで扱いました。
threshold()を使って処理をします。
ret, coins_thresh = cv2.threshold(gray_coins,160,255,cv2.THRESH_BINARY_INV)
display(coins_thresh,cmap='gray')
画像をthreshold()に渡して、中間の値を160、2値化の最大値を255とします。127が本来は中間ですが、硬貨の微妙な影が影響しないように160に調整しました。パラメータをTHRESH_BINARY_INVにして硬貨の部分を白、背景を黒で表示するようにしています。
表示するとこうなります。
硬貨の部分が白く表示されているのがわかります。
輪郭の検出
次に輪郭を取り出してみましょう。
輪郭の検出についてはこちらで扱いました。
findContours()を使って輪郭を捉えていきまます。
image, contours, hierarchy = cv2.findContours(coins_thresh.copy(), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
findContours()に2値化した画像を渡します。ここでは輪郭を画像に表示するのでコピーして画像を渡しました。2レベルの階層構造なのでRETRメソッドをRETR_CCOMPに、近似方法のフラグはメモリを消費しないCHAIN_APPROX_SIMPLEを使っています。
for i in range(len(contours)):
if hierarchy[0][i][3] == -1:
cv2.drawContours(coins, contours, i, (255, 0, 0), 10)
display(coins)
輪郭の検出の項目でやったのと同じように、for-in文を使って輪郭の階層のインデックスをそれぞれ調べて行きます。一番外側の輪郭は親となるような階層をもたないので、hierarchy[0][i][3] が -1の場合がそれに該当します。このときの効果の輪郭データをdrawContours()で描画していきます。元の画像、輪郭データを渡し、硬貨の輪郭データのインデックスを指定します。線の色を赤にして太さを10にして表示してみます。
硬貨の輪郭が赤色で示されているのがわかります。ただし、硬貨が接している部分や内側の方の輪郭がこれでは捉えられていません。これまで扱って来た知識では不十分な結果となってしまいました。
そこで、Watershedアルゴリズムを使ってみることになります。
Watershedアルゴリズム
それでは硬貨の周りに輪郭を描くために、Watershedアルゴリズムをやって行きましょう。次のような処理の流れで行います。
- 画像の読み込み
- 中央値フィルターでぼかし処理
- グレースケールに変換
- しきい値処理(OTSUの2値化と逆バイナリ)
- ノイズ除去
- 背景の把握
- 前景の把握
- 未知の領域の把握
- 確実な前景にラベルマーカー処理
- マーカーにWatershedアルゴリズムを適用
- マーカー上の輪郭を検出
では順にやって行きましょう。
画像の読み込み、ぼかし処理、グレースケール変換
最初の3つの処理を合わせてやっていきましょう。
img = cv2.imread('images/coin.jpg')
img = cv2.medianBlur(img,35)
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
画像の読み込み、ぼかし処理、グレースケール変換は先ほどの処理と同じです。
しきい値処理(大津の二値化と逆バイナリ)
次に閾値処理を行います。
threshold()を使って処理していきます。
ret, thresh = cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)
display(thresh,cmap='gray')
threshold()にグレースケールの画像を渡します。先ほどの処理と違うのは、ここでは大津の二値化の処理を加えているところです。
cv2.THRESH_BINARY_INVにcv2.THRESH_OTSUというパラメータを加えています。これが大津の二値化の処理で、二つの特徴のある画素のグループに分けた時に、同じグループはなるべく集めて、異なるグループはなるべく離れるような分け方が良いと考えることで閾値をとるという考え方から来ている処理です。この時、比較的明るいグループと比較的暗いグループのふたつに別れることになります。今回の処理にあっていますね。
この処理の場合、2値化の閾値の中間値を0にして処理をすることになります。これまでと同じような値にすると、うまくグループが二つに別れません。
ここまでを実行すると、こうなります。
硬貨と背景の部分が白と黒に別れました。
ノイズ除去
物体の輪郭(境界線)を綺麗にとる為に、モルフォロジー勾配の処理を使ってノイズ除去を行います。
モルフォロジー変換についてはこちらで扱いました。
コードはこうなります。
kernel = np.ones((3,3),np.uint8)
opening = cv2.morphologyEx(thresh,cv2.MORPH_OPEN,kernel, iterations = 2)
display(opening,cmap='gray')
前景の白の部分を綺麗にする為に、カーネルをnp.ones()で設定しています。モルフォロジー変換のOpening処理をcv2.MORPH_OPENを使って行なっています。処理回数はここでは2回にしました。
表示するとこうなります。
それほど違いはわかりませんね。この処理は場合によっては必要ありません。処理する画像によるでしょう。
画像の背景の把握
次にこの画像の背景部分である黒い領域をしっかりと把握させておく必要があります。白い部分がしっかり埋まっていれば、他が背景ということで把握できます。
それにはモルフォロジー変換のdilate()で膨張処理を行なって白い部分が隙間なく埋まっているようにします。
sure_bg = cv2.dilate(opening,kernel,iterations=3)
display(sure_bg,cmap='gray')
これを表示するとこうなります。
画像の前景の把握
この前景の部分を把握するには、distanceTransform()を使った距離変換を利用します。これは、2値画像を入力としたとき、その各ピクセルが最も近い値0までの距離を計算します。
カラーが0、つまり黒のピクセル画素と、白である1のピクセル画素の距離を画像全ての位置で計算すれば、0から遠いところは明るく、近いところは暗くなるというイメージです。
次のようなコードで処理をします。
dist_transform = cv2.distanceTransform(opening,cv2.DIST_L2,5)
ret, sure_fg = cv2.threshold(dist_transform,0.7*dist_transform.max(),255,0)
cv2.distanceTransform()の第1引数に2値画像を渡します。距離関数にDIST_L2、カーネルを5を与えて計算します。このパターンが正確な距離推定ができるとされています。このあたりの考え方は専門的な範囲になると思うので、関心のある方はドキュメントなどを見てください。
この距離変換した画像をthreshold()で2値化します。閾値をこの距離変換した値の最大値の70%にしています。最大値を255、閾値を超えないものは0にしています。
それぞれをdisplay()で表示します。
display(dist_transform,cmap='gray')
display(sure_fg,cmap='gray')
実行するとこうなります。
黒い部分から一番遠いところが白く明るくなっているのがわかります。それぞれのコインの真ん中部分がこれに該当します。これで5つの貨幣が前景の画像として捉えられているのがわかります。
さらに、こちらのしきい値処理された画像では,コインだと確信できる領域を検出しています。これで5つのセグメントに別れているのがわかります。
これで、背景の部分と前景のはっきりと確実視できる部分がわかりました。
未知の領域の把握
この背景の部分にある白い領域と前景のはっきりと確実視できる白い部分との間にグレーな領域があります。これはまだ未知の領域で、未確定な部分です。
この領域をsubstract()を使って把握していきます。
sure_fg = np.uint8(sure_fg)
unknown = cv2.subtract(sure_bg,sure_fg)
display(unknown,cmap='gray')
substract()は、同じ出力配列同士の要素毎の差を求めることができます。背景と前景の画像は同じサイズなのでここに渡します。
表示するとこうなります。
これで背景でもなく前景でも無い部分を捉えることができました。
確実な前景にラベルマーカー処理
どの領域が貨幣で、どの領域が背景であるかはわかっているので、マーカを作成してその中の領域にラベルをつけていきます。
ここでマーカは入力画像と同じサイズのものを与えます。前景か背景か確信が持てる領域は正の値で任意のラベルを指定します。ただし、それぞれの領域には異なるラベルを与え、どちらの領域か分からない領域に関してはラベル値として0を指定します。
ここで、cv2.connectedComponents() 関数を利用します。
ret, markers = cv2.connectedComponents(sure_fg)
markers = markers+1
markers[unknown==255] = 0
display(markers,cmap='gray')
cv2.connectedComponents() 関数は画像の背景に0というラベルを与え、それ以外の物体に対して1から順にラベルをつけていく処理をします。ここでは背景と5つのオブジェクトです。背景に対して0ラベルを与えるとwatershedアルゴリズムで処理をする時に未知の領域と扱ってしまうのでmarkerに1を加えて、unknownの領域に対しては0を与えています。
表示するとこうなります。
前景の確実な部分、未知の部分、背景の部分が違いがわかるようになりました。未知の部分は背景よりも黒くなっています。
これでマーカの準備が出来ました。
マーカーにWatershedアルゴリズムを適用
watershed()を使って元画像にマーカーを適用していきます。
markers = cv2.watershed(img,markers)
display(markers)
元画像のimgとマーカーのmarkerを渡して適用し、display()で表示します。
それぞれの物体(硬貨)が区別できるようにラベリングできているのがわかります。
マーカー上の輪郭を検出
物体の区別ができたので、これを元に輪郭抽出を行なっていきます。
image, contours, hierarchy = cv2.findContours(markers.copy(), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
for i in range(len(contours)):
if hierarchy[0][i][3] == -1:
cv2.drawContours(coins, contours, i, (255, 0, 0), 10)
display(coins)
これは、markersのコピーを渡している以外は、最初にやった輪郭の検出と同じ処理です。
表示してみましょう。
それぞれの物体の輪郭が赤く描かれているのがわかります。上でやった方法と違って、接した境界部分や内側のところの輪郭も検出できています。
最後に
OpenCVを使ったPythonでの画像処理について、Watershedアルゴリズムを扱いました。
Watershedアルゴリズムは、山岳地帯などでの水の流れ、水位と地形との関係で境界ができるイメージの概念を使ったアルゴリズムで、画像の輝度勾配を地形に見立てて水位を増して地形の輪郭を浮き立たせるような処理をし、接触している物体の境界を分離して認識することができます。
ここでは、これまで扱ってきた手法での物体の輪郭の検出と、Watershedアルゴリズムを使った輪郭の検出との違いを見てきました。
ここで行ったことは、マーカーを適用するまでにかなりの作業を必要としたので、もっと独自にシードを設定して動的に処理する方法を次で見ていきます。