OpenCVを使ったPythonでの画像処理について、Watershedアルゴリズムにすでに触れました。そこではWatershedアルゴリズムで物体のセグメンテーションを行い対象の輪郭の検出を行いました。
ただし、そこでは、OpenCVがWatershedアルゴリズムにシードを適用するのに、多くの作業がMarkersを設定することに必要でした。
ここでは、ちょっと実践的に扱えるように、自分自身でシードを適用するようにカスタムシードを扱ってみようと思います。
ここでの最終的なコードはjupyter notebookではなくテキストエディタ(Atom)でターミナルから実行しようと思います。jupyter notebookで実行すると、カーネルが止まる可能性があるからです。Anaconda NavigatorにあるJupyter Labを使ってもいいかもしれません。(説明にはjupyter notebookも必要に応じて併用します)
では順にやっていきましょう。ここではある程度まとまりのある区切りごとにコードを書いていきます。最後に全体のコードを示すことにします。
ここで作成するコードの実行例
ここで作成するものとはどんなものが実行されるのか、最初に示して起きましょう。
コードを実行すると、次のような二つのウインドウに画像を表示します。
最初は左側の画像は何の点も無い写真で、右側は全面同色の画像なのですが、マウスでクリックするとその部分で検出される物体をセグメントし、右側のウインドウに着色していきます。左側の画像はそのクリックした部分に色の丸印が付きます。色をこちらで任意に変更して異なる部分をクリックしていくことで、上記のような表示になるというものです。
この作業ができるコードを、ここでは作っていきます。
ライブラリのインポートと画像の読み込み
まずお決まりのライブラリのインポートとここで利用する画像の読み込みをします。(ここではファイル名はimg_segments.pyにして実行していきます)
import cv2
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
# %matplotlib inline
road = cv2.imread('images/tokyo.jpg')
road_copy = np.copy(road)
# plt.imshow(road)
# plt.show()
OpenCVライブラリ、NumPy、matplotlibをインポートしています。from matplotlib import cmもここではインポートしています。これはカラーマップを後ほど利用するからです。
imread()を使ってtokyo.jpgという画像ファイルを読み込んでいます。これを、np.copy()を使ってコピーします。こちらをセグメントしていくのでこうしています。
コメントアウトした部分は読み込んだ画像を表示して確認するのに使いますが、ここでの最終的な作業としては必要ありません。%matplotlib inlineはjupyter notebookを使う時に記述します。画像を表示してみるには、ここではplt.show()まで書いてターミナルから実行してみます。
画像はこんな画像です。
ビューワーが起動して上記のように表示されます。色はRGBに変換していませんので実行例の画像とは色合いが違っています。
結果を表示するスペースを作成
最初に示した実行例のように、画像をクリックするスペースと、領域を区別したスペースで結果を最終的に表示する必要があります。
ここでは、この結果を表示するスペースを設定しておきます。
# road.shape
# road.shape[:2]
marker_image = np.zeros(road.shape[:2],dtype=np.int32)
segments = np.zeros(road.shape,dtype=np.uint8)
# segments.shape
コメントアウトした部分にはなりますが、読み込んだ画像の型をshapeで取り出しています。
読み込んだ画像は450行、600列、つまりheightが450px、widthが600px、3カラーチャンネルの画像データということになります。下側は[:2]でスライスして縦横のサイズを取り出しているわけです。
marker_imageは画像をクリックする時に使います。元の画像の大きさだけがわかればいいので、np.zeros(road.shape[:2],dtype=np.int32)として元画像のサイズに合わせています。zeros()を全てのデータを0の配列にしています。
segmentsはクリックした部分の領域の検出結果を表示するのに使います。 こちらも、np.zeros(road.shape,dtype=np.uint8)で全てのデータを0の配列にしていますが、結果を表示するのでカラーチャンネルまで含めています。セグメント結果のカラー表示のために、np.uint8で8ビットとして、符号なし整数の0から255の値のデータタイプにしています。
segmentsの型もshapeで確認すると、当然このようになります。
マーカーの色を設定する
最初の実行例の左側の画像ではクリックしたポイントに色の付いた丸印が表示されています。この色は、セグメントしたい部分に合わせて、こちらで変えることができるようにします。
そこでこのマーカーの色を設定できるようにしなくてはいけないのですが、そこでmatplotlibのカラーマップを利用します。最初に、from matplotlib import cm でインポートしたのは、これを利用するためです。
matplotlibのカラーマップのリファレンスはこちらで確認することができます。
この中に、Qualitative colormapsという項目があります。
この中のtab10のカラーマップを使って、10色のマーカーを適用することにしていきます。
カラーマップtab10の内容を見てみましょう。
# cm.tab10(0)
# cm.tab10(1)
tab10でのパラメーターは0から9の10個の数字から選ぶことになります。0と1でデータを見てみます。(コメントアウトを解除して実行しています)
それぞれの内訳を見ると0から1の間の数値でカラーを示しています。
これをここで扱うために、256色のRGBカラーに変換する必要があります。
tab(10)を例にすると、次の手順で変換することができます。
# np.array(cm.tab10(0))
# np.array(cm.tab10(0))[:3]
# np.array(cm.tab10(0))[:3]*255
np.array()でデータを配列にします。[:3]でスライスして最初の3つのカラーデータだけのリストにします。これに255を掛けることでRGBカラーの数値に変えることができます。
RGBでの数値に変わっているのがわかります。
この流れを関数として定義して、色を数値で指定できるようにします。
def create_rgb(i):
x = np.array(cm.tab10(i))[:3]*255
return tuple(x)
create_rgb()という関数を定義します。引数1は色を示す数字が入ることになります。変数xとして先ほどRGBに変換したtab10の配列を代入し、これをカラーデータを示すようにtuple()でタプル化して返します。
これを使って、色の一覧を作ります。
colors = []
for i in range(10):
colors.append(create_rgb(i))
colorsという空のリストを用意します。
for-in文を使って、0から9の数値をrange(10)から順に取り出し、先ほど定義してcreage_rgb(i)に入れてカラーデータを作り、append()でcolorsリストに入れていきます。これで色のリストが完成しました。
# colors
色のリストを確認すると次のようになりました。
10個のRGBカラーデータができているのがわかります。
コールバック関数の設定とwatershedアルゴリズムの領域分割
次に、コールバック関数を定義して、マウスをクリックした時のイベントを設定していきます。コールバック関数とは、他の関数に引数として渡す関数のことで、ここで定義した関数を、画像を表示する際に利用する関数に引数として渡すことになります。
まずマーカーの初期値を与えておきます。
n_markers = 10
current_marker = 1
marks_updated = False
n_markersですが、ここで使うマーカーは10色なので10を代入しています。current_markerはマーカーの初期値として1を設定しておきます。marks_updatedですが、マーカーは最初は更新しないので、ここではまずFalseにしておきます。
ではコールバック関数を定義していきましょう。
def mouse_callback(event, x, y, flags, param):
global marks_updated
if event == cv2.EVENT_LBUTTONDOWN:
cv2.circle(marker_image, (x, y), 10, (current_marker), -1)
cv2.circle(road_copy, (x, y), 10, colors[current_marker], -1)
marks_updated = True
mouse_callback()という関数を定義します。変数としてマウスの操作を示すevent、マウスの座標を示すx、y、他に必要となるフラグやパラメーターをflags、paramsとして与えています。
markers_updatedをグローバル変数として読み込みます。
if文を使ってマウスが左クリックされたかどうかを、cv2.EVENT_LBUTTONDOWNで判定します。
左クリックされた時に、cv2.circle()に画像とマウスの座標と円の半径、マーカーの色、線の太さを渡して円を描きます。
上側のcv2.circle()はマーカーのトラッキングで、画像に最初の方で処理したmarker_imageを渡し、マウスの位置座標、半径を10、色をcurrent_marker、線の太さを-1にして塗りつぶしにしています。
下側のcv2.circle()は実際の画像(元画像のコピーのroad_copy)への表示です。色は選択している色で他の処理は同じ操作です。
マーカーを描いたら、marks_updated = Trueで更新します。
では描画するコードを書いていきましょう。
cv2.namedWindow('Road Image')
cv2.setMouseCallback('Road Image', mouse_callback)
while True:
cv2.imshow('WaterShed Segments', segments)
cv2.imshow('Road Image', road_copy)
k = cv2.waitKey(1) & 0xFF
if k == 27:
break
elif k == ord('c'):
road_copy = road.copy()
marker_image = np.zeros(road.shape[0:2], dtype=np.int32)
segments = np.zeros(road.shape,dtype=np.uint8)
elif k > 0 and chr(k).isdigit():
current_marker = int(chr(k))
if marks_updated:
marker_image_copy = marker_image.copy()
cv2.watershed(road, marker_image_copy)
segments = np.zeros(road.shape,dtype=np.uint8)
for color_ind in range(n_markers):
segments[marker_image_copy == (color_ind)] = colors[color_ind]
marks_updated = False
cv2.destroyAllWindows()
cv.nameWindow()で、描画する画像の窓に名前をつけています。
cv2.setMouseCallback()にウインドウの名前と、先ほど上で定義した関数をコールバック関数として割り当ててマウスボタンの操作の動きと繋げています。
while文を使って、描画を実行しています。
最初の実行例で示したように、2つのウインドウを表示します。imshow()でセグメント表示する画像と元の画像をそれぞれ表示しています。
k = cv2.waitKey(1) & 0xFF として、1ミリ秒キーイベントを待ち、次の処理をどうするかを次のif文で条件分けしていきます。
if k == 27: であれば、breakで処理を終了させます。この27は[esc]キーを意味し、[esc]キーを押せば処理を終了させる操作になります。別のところでもすでに触れていることですが、0xFF == ord(‘q’) とすると[q]キーを押せば終了となるquitの意味でよく使われます。
このあたりの数字の意味はASCII制御文字を参照してください。
次に、elif k == ord(‘c’): であれば、[C]キーを押した時に処理をクリアする操作をします。マーカーの色を全て消して初期画面にする操作です。それぞれの画像を初期値に戻しているコードを記述しています。
elif k > 0 and chr(k).isdigit(): で0から9の数字が入力された時の処理をしています。chr()で押された数字をASCIIコードから文字に変換しています。そしてisdigit()で連結することで数字であることを判定しています。ここで入力された数字を使ってcurrent_marker = int(chr(k))で値を変えて、マーカーの色(色のリストのインデックス)を変更します。
これで何かキーが押された時のif文の処理は終わりです。
次は、画像のどこかをクリックした時に、選択したマーカーでwatershedアルゴリズムを呼び出す処理です。
if marks_updated: で成立しているならマウスがクリックされています。この時点のマーカーであるmarker_imageをコピーし、watershed()を使って元画像のroadにこのマーカーをwatershedアルゴリズムで適用させて対象の領域を抽出します。
segmentsをnp.zeros()で初期化します。for文を使ってn_markersから順に色のインデックスを取り出します。このインデックスとマーカーの値が同じところに対して、該当する色をsegmentsに表示します。そして、marks_updatedをFalseに戻します。
これでwatershedアルゴリズムの処理が終わります。
ここまでがwihle文の中の一連の処理です。
最後にcv2.destroyAllWindows()で全てのwindowを閉じます。
コードはここまでになります。
全体のコード(再掲)
全体のコードをまとめて示すと以下のようになります。
import cv2
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm
road = cv2.imread('images/tokyo.jpg')
road_copy = np.copy(road)
marker_image = np.zeros(road.shape[:2],dtype=np.int32)
segments = np.zeros(road.shape,dtype=np.uint8)
def create_rgb(i):
x = np.array(cm.tab10(i))[:3]*255
return tuple(x)
colors = []
for i in range(10):
colors.append(create_rgb(i))
n_markers = 10
current_marker = 1
marks_updated = False
def mouse_callback(event, x, y, flags, param):
global marks_updated
if event == cv2.EVENT_LBUTTONDOWN:
cv2.circle(marker_image, (x, y), 10, (current_marker), -1)
cv2.circle(road_copy, (x, y), 10, colors[current_marker], -1)
marks_updated = True
cv2.namedWindow('Road Image')
cv2.setMouseCallback('Road Image', mouse_callback)
while True:
cv2.imshow('WaterShed Segments', segments)
cv2.imshow('Road Image', road_copy)
k = cv2.waitKey(1) & 0xFF
if k == 27:
break
elif k == ord('c'):
road_copy = road.copy()
marker_image = np.zeros(road.shape[0:2], dtype=np.int32)
segments = np.zeros(road.shape,dtype=np.uint8)
elif k > 0 and chr(k).isdigit():
current_marker = int(chr(k))
if marks_updated:
marker_image_copy = marker_image.copy()
cv2.watershed(road, marker_image_copy)
segments = np.zeros(road.shape,dtype=np.uint8)
for color_ind in range(n_markers):
segments[marker_image_copy == (color_ind)] = colors[color_ind]
marks_updated = False
cv2.destroyAllWindows()
これを実行すると、画像のウインドウとsegments側の黒いウインドウが立ち上がります。
画像をクリックすると丸印が表示され、segmentsのウインドウが青くないます。
0から9までの数字の押して、画像をクリックすると数字に対応した色の丸印が表示され、segmets側に対象の領域が抽出された色に変わります。最初に示した実行例のような画像になるはずです。
[C]キーを押すと処理がクリアされて最初の2つのウインドウに戻ります。[esc]キーを押すと処理が終了してウインドウが閉じます。
一連の動きを確認して見てください。
最後に
OpenCVを使ったPythonでの画像処理について、Watershedアルゴリズムで物体のセグメンテーションを行った対象の輪郭の検出はこちらですでに触れました。
ですが、そこではOpenCVがWatershedアルゴリズムにシードを適用するのに、多くの作業がMarkersを設定することに必要でしたので、ここでは、マーカーを自分で動的に変更して扱えるような処理をしてみました。