OpenCVで輪郭を抽出する方法です。単なる白黒画像である二値化画像から注目している領域を抽出することで、そのあとの解析につなげることができるので非常に大切なプロセスになります。
開発環境
- OpenCV 4.2.0
- Python 3.7.9
画像の二値化と輪郭検出
画像を二値化することで、関心領域(ROI)とそうでない部分を視覚的に分けることができますが、しかしあくまでも視覚的に分離させただけです。このままでは、実際にどのような領域が抽出されたのかの情報は全く得られていません。輪郭検出の処理はここからそれぞれの関心領域がどのような座標の領域なのかという情報を取得する作業になります。
なお、OpenCVでは黒い背景の画像に関心領域を白い領域とした二値化画像を用いて輪郭検出を行います。
サンプル画像
以下のコードでは次の画像(sample-contour.tiff)をサンプル画像として用います。
この画像を「C:\BioTech-Lab」フォルダに保存して、輪郭の検出を試してみましょう。
輪郭を検出する / 検出された輪郭を描画する
二値化されたグレースケール画像に対してfindContours関数を用いることでその輪郭を取得することができます。また、検出した輪郭はdrawContours関数で描画することができます。まずはそれぞれの関数に適当なパラメーターを設定して輪郭を検出し、その検出された輪郭を描画してみましょう。
import cv2
img = cv2.imread(r'C:\BioTech-Lab\sample-contour.tiff', 0)
# 輪郭の検出
contours, hierarchy = cv2.findContours(img, 3, 1)
# グレースケールからカラー画像への変換
img_color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
# 検出された輪郭を描画
cv2.drawContours(img_color, contours, -1, (255, 255, 0), 3)
cv2.imshow('preview', img_color)
cv2.waitKey(0)
cv2.destroyAllWindows()
グレースケール画像としてサンプル画像を読み込み、6行目でfindContours関数を用いてその輪郭を検出しています。この時、findContours関数の引数によって、検出する輪郭の階層レベルや実際にデータとして格納する座標情報について指定します。なおfindContours関数に渡す画像はBGRカラー画像ではエラーとなってしまうので、必ずグレースケール画像である必要があります。検出された輪郭のリストはcontours変数に格納され、その階層情報がhierarchy変数に格納されます。
輪郭の階層情報は右図のような親子関係(包含関係)のことを表します。findContours関数ではmode引数で指定することで「外側の輪郭だけを抽出する」「階層構造を維持してすべての輪郭を抽出する」「階層構造を維持せずにすべての輪郭を抽出する」などを選択することができます。
検出された輪郭のリストは12行目でdrawContours関数を用いて描画することができます。method引数で輪郭のリストの中から実際に描画する輪郭の番号を指定することができ、-1を指定することですべての輪郭を描画します。なおここでは分かりやすいように赤色で描画しているので、あらかじめ先ほどのグレースケール画像を9行目でBGRカラー画像に変換しています。
輪郭を表す構造
OpenCVにおいて輪郭を表す構造は次元が(座標数, 1, 2)のndarrayであるContourで与えられます。
例えば次の4つの座標がこの順番で含まれている輪郭(contour)を考えてみましょう。
- (10, 10), (15, 10), (17, 16), (9, 20)
この時、この輪郭には4つの座標が含まれているのでそのcontourの次元は(4, 1, 2)となります。この輪郭に含まれている4つの座標はそれぞれ次のように対応します。
- contour[0, 0, :] → (10, 10)
- contour[1, 0, :] → (15, 10)
- contour[2, 0, :] → (17, 16)
- contour[3, 0, :] → (9, 20)
また、この輪郭に含まれる点のX座標、Y座標は次のようになります。ただし、1番最初の点を0番目とします。
- n番目の点のX座標 → contour[n, 0, 0]
- n番目の点のY座標 → contour[n, 0, 1]
例えば、contourの3番目の点のX座標はcontour[2, 0, 0]となり16です。
findContours関数で返される輪郭のリストはこの輪郭を表す構造(Contour)のリストとなります。
階層情報を表す構造
輪郭の親子関係を表す階層情報は次元が(1, 輪郭数, 4)のndarrayで与えられます。
このndarrayの第2軸で各々の輪郭に対応した階層情報が格納されます。この時、第2軸のindexはfindContours関数で返される輪郭(Contour)のリストの番号に対応しています。
さらに第3軸は次の4つの要素が順番に格納されています。
- [同一階層の次の輪郭のindex, 同一階層の前の輪郭のindex, 子輪郭のindex, 親輪郭のindex]
なお、ここでいう「同一階層」とは同じ親輪郭に含まれる階層のレベルのことを表します。また、対応する輪郭が存在しない場合はそのインデックスとして-1が格納されます。
輪郭の階層情報の例①
例えば、次のような画像をimgとして読み込み、その輪郭を抽出して階層情報を見てみましょう。
contours, hierarchy = cv2.findContours(img, 3, 1)
print(hierarchy)
[[[-1 -1 1 -1] [-1 -1 2 0] [-1 -1 -1 1]]]
取得される輪郭は次の赤線で示す3つとなり、findContours関数では外側の輪郭から順番に格納されていきます。では、先ほど得られた階層情報の結果を解釈していきましょう。
1番目の輪郭(index 0)の階層情報:[-1 -1 1 -1]
- 1番外側の輪郭についての階層情報です。(この輪郭のindexは0)
- この輪郭と同一階層の次の輪郭のindexは-1なので、次の同一階層の輪郭は存在しません。
- この輪郭と同一階層の前の輪郭のindexは-1なので、前の同一階層の輪郭は存在しません。
- この輪郭の子輪郭のindexは1なので、index 1の輪郭が子輪郭になります。
- この輪郭の親輪郭のindexは-1なので、親輪郭は存在しません。
2番目の輪郭(index 1)の階層情報:[-1 -1 2 0]
- 1つ内側の丸い輪郭についての階層情報です。(この輪郭のindexは1)
- この輪郭と同一階層の次の輪郭のindexは-1なので、次の同一階層の輪郭は存在しません。
- この輪郭と同一階層の前の輪郭のindexは-1なので、前の同一階層の輪郭は存在しません。
- この輪郭の子輪郭のindexは2なので、index 2の輪郭が子輪郭になります。
- この輪郭の親輪郭のindexは0なので、index 0の輪郭が親輪郭になります。
3番目の輪郭(index 2)の階層情報:[-1 -1 -1 1]
- 最も内側の三角の輪郭についての階層情報です。(この輪郭のindexは2)
- この輪郭と同一階層の次の輪郭のindexは-1なので、次の同一階層の輪郭は存在しません。
- この輪郭と同一階層の前の輪郭のindexは-1なので、前の同一階層の輪郭は存在しません。
- この輪郭の子輪郭のindexは-1なので、子輪郭は存在しません。
- この輪郭の親輪郭のindexは1なので、index 1の輪郭が親輪郭になります。
輪郭の階層情報の例②
では、続いて次のようなサンプル画像(img)の輪郭を抽出して、その階層情報を表示させてみましょう。
contours, hierarchy = cv2.findContours(img, 3, 1)
print(hierarchy)
[[[ 2 -1 1 -1] [-1 -1 -1 0] [ 3 0 -1 -1] [ 4 2 -1 -1] [-1 3 5 -1] [-1 -1 6 4] [-1 -1 -1 5]]]
今回は赤枠で示すように合計で7個の輪郭を抽出しました。この階層情報のうちいくつかをピックアップしてみていきましょう。
1番目の輪郭(index 0)の階層情報:[2 -1 1 -1]
- 左下のギザギザな形の輪郭についての階層情報です。
- この輪郭と同一階層の次の輪郭のindexは2なので、index 2の輪郭が次の同一階層の輪郭になります。(ここでは最も外側の階層の輪郭)
- この輪郭と同一階層の前の輪郭のindexは-1なので、前の同一階層の輪郭は存在しません。
- この輪郭の子輪郭のindexは1なので、index 1の輪郭が子輪郭になります。
- この輪郭の親輪郭のindexは-1なので、親輪郭は存在しません。
3番目の輪郭(index 2)の階層情報:[3 0 -1 -1]
- 左上の楕円のいずれか一方の輪郭についての階層情報です。
- この輪郭と同一階層の次の輪郭のindexは3なので、index 3の輪郭が次の同一階層の輪郭になります。(ここでは最も外側の階層の輪郭)
- この輪郭と同一階層の前の輪郭のindexは0なので、index 0の輪郭が前の同一階層の輪郭になります。(ここでは最も外側の階層の輪郭)
- この輪郭の子輪郭のindexは-1なので、子輪郭は存在しません。
- この輪郭の親輪郭のindexは-1なので、親輪郭は存在しません。
findContours関数で検出される輪郭の順番は実際に確認してみないと分かりませんが、例えば上の例では3番目と4番目の輪郭の階層情報がそれぞれ[ 3 0 -1 -1]、[ 4 2 -1 -1]となっていて親輪郭と子輪郭がすべて-1なので、これらは「階層を含まないもの」、つまり「左上の楕円に対応する」ということが推定できます。
輪郭の抽出モード
findContours関数ではmode引数に輪郭の抽出モードをRetrievalModesで指定します。ここではそれぞれどのような違いがあるのかを説明していきます。
ここではサンプル画像をimgとして読み込み、その輪郭を抽出します。
RETR_EXTERNAL:一番外側の輪郭のみを検出する
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, 1)
print(hierarchy)
[[[ 1 -1 -1 -1] [ 2 0 -1 -1] [ 3 1 -1 -1] [-1 2 -1 -1]]]
輪郭の抽出モードをRETR_EXTERNALとすることで、図に示すように外側の輪郭のみを抽出します。抽出された輪郭は4個のみで、階層情報はすべて[〇 〇 -1 -1]となっているのが分かります。
RETR_LIST:すべての輪郭を抽出するが、階層構造は作成しない
contours, hierarchy = cv2.findContours(img, cv2.RETR_LIST, 1)
print(hierarchy)
[[[ 1 -1 -1 -1] [ 2 0 -1 -1] [ 3 1 -1 -1] [ 4 2 -1 -1] [ 5 3 -1 -1] [ 6 4 -1 -1] [-1 5 -1 -1]]]
輪郭の抽出モードをRETR_LISTとすることで、図に示すようにすべての輪郭を抽出しますが、階層情報はすべて[〇 〇 -1 -1]となっていて、すべて同一の階層の輪郭として扱われていることが分かります。
RETR_CCOMP:すべての輪郭を抽出し、2階層の階層構造を作成する
contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, 1)
print(hierarchy)
[[[ 2 -1 1 -1] [-1 -1 -1 0] [ 3 0 -1 -1] [ 4 2 -1 -1] [ 5 3 -1 -1] [-1 4 6 -1] [-1 -1 -1 5]]]
輪郭の抽出モードをRETR_CCOMPとすることで、図に示すようにすべての輪郭を抽出しますが、階層情報は「白い領域が上位(外側)の階層で、その中に黒い領域ある」という2階層として認識されます。
ここでは、分かりやすくするために少し複雑な画像を考えてみましょう。以下の左側の画像(sample-contour2.tiff)を輪郭の抽出モードをRETR_CCOMPとして輪郭抽出し、その上位(外側)の階層の輪郭だけを描画します。すると右側の赤線のように、白い領域だけが輪郭として描画されます。
import cv2
img = cv2.imread(r'C:\BioTech-Lab\sample-contour2.tiff', 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, 1)
img_color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
# 外側の輪郭だけ赤く描画する
for i in range(len(contours)):
if hierarchy[0, i, 3] == -1:
cv2.drawContours(img_color, contours, i, (0, 0, 255), 3)
cv2.imshow('preview', img_color)
cv2.waitKey(0)
cv2.destroyAllWindows()
RETR_TREE:すべての輪郭を抽出し、ツリーで階層構造を作成する
contours, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, 1)
print(hierarchy)
[[[ 2 -1 1 -1] [-1 -1 -1 0] [ 3 0 -1 -1] [ 4 2 -1 -1] [-1 3 5 -1] [-1 -1 6 4] [-1 -1 -1 5]]]
輪郭の抽出モードをRETR_TREEとすることで、図に示すようにすべての輪郭を抽出し、その階層情報もすべて取得します。
輪郭の近似手法
findContours関数ではmethod引数に輪郭の近似手法をContourApproximationModesで指定します。例えば長方形の領域の輪郭に対しては、method=CHAIN_APPROX_NONEとするとそのすべての座標を取得するのに対して、method=CHAIN_APPROX_SIMPLEとすると、頂点の4つの座標のみを取得するので大幅に使用メモリーを抑えることができます。
それでは、サンプル画像をCHAIN_APPROX_NONEとCHAIN_APPROX_SIMPLEで輪郭抽出し、抽出された点の座標をプロットしてみましょう。
import cv2
img = cv2.imread(r'C:\BioTech-Lab\sample-contour.tiff', 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
# contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
img_color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
cv2.drawContours(img_color, contours, -1, (0, 0, 255), 3)
for contour in contours:
for i in range(contour.shape[0]):
cv2.circle(img_color, tuple(contour[i, 0, :]), 3, (255, 255, 0), thickness=-1)
cv2.imshow('preview', img_color)
cv2.waitKey(0)
cv2.destroyAllWindows()
CHAIN_APPROX_SIMPLEに設定することで格納する座標の数を減らせていることが分かります。ただし、特に斜めの線などは近似処理での揺らぎの影響があるので、必ずしも理論通り頂点のみにはならないことに注意が必要です。
コメント