概要
OpenCV の findContours() を使用して 2 値画像から輪郭抽出を行う方法について解説します。
関連記事
輪郭を抽出したあと、誤検出を除いたり、輪郭の点の数や大きさで目的の輪郭を探す場合、以下の記事を参考にしてください。
OpenCV – 輪郭の特徴分析について – pystyle
cv2.findContours
contours, hierarchy = cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])
公式リファレンス: cv2.findContours
名前 | 型 | デフォルト値 |
---|---|---|
image | ndarray | |
入力画像 (8bit、1 チャンネル)。非 0 の画素は 1 とした 2 値画像として扱われる。 | ||
mode | RetrievalModes (後述) | |
輪郭を検索する方法を指定する。 | ||
method | ContourApproximationModes (後述) | |
輪郭を近似する方法を指定する。 | ||
offset | tuple of int | (0, 0) |
返り値の輪郭の点にオフセットを加算したい場合は指定する。 |
名前 | 説明 | ||
---|---|---|---|
contours | 抽出された輪郭のリスト。各輪郭は (NumPoints, 1, 2) の numpy 配列。 | ||
hierarchy | 階層構造のリスト。(1, NumContours, 4) の numpy 配列。 |
輪郭抽出する手順
以下の画像から月の部分の輪郭を抽出する方法について解説します。
輪郭抽出を行うには、まず検出したい物体は白、それ以外の物体は黒となっている 2 値画像を作成する必要があります。今回は以下のように 2 値化しました。
cv2.cvtColor()
で RGB 画像をグレースケール画像に変換するcv2.threshold()
で 2 値化する
2 値化はいくつかのやり方があります。詳しくは以下の記事を参照してください。
- OpenCV – 画像処理の 2 値化の仕組みと cv2.threshold() の使い方 – pystyle
- OpenCV – inRange で画像を 2 値化する方法について – pystyle
import cv2
import numpy as np
from IPython import display
from matplotlib import pyplot as plt
def imshow(img, format=".jpg", **kwargs):
"""ndarray 配列をインラインで Notebook 上に表示する。"""
img = cv2.imencode(format, img)[1]
img = display.Image(img, **kwargs)
display.display(img)
# 画像を読み込む。
img = cv2.imread("sample.jpg")
# グレースケールに変換する。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2値化する
ret, img_bin = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)
imshow(img_bin)
上の画像のように輪郭抽出したい対象物が白、それ以外は黒として綺麗に 2 値化できるパラメータを探してください。以下は 2 値化の失敗例です。2 値化がうまくいっていない場合、その後の輪郭抽出も失敗します。
2 値化がうまくできたら、findContours()
で 2 値画像の白の領域を囲む輪郭を取得します。この関数は輪郭の一覧を表す contours
と輪郭の階層構造を表す hierarchy
を返します。その後、誤検出の輪郭がいくつかあるので、cv2.contourArea()
で面積を計算し、面積が小さい輪郭は削除します。
抽出した輪郭は cv2.drawContours()
で画像に描画して確認できます。
# 輪郭を抽出する。
contours, hierarchy = cv2.findContours(
img_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
# 面積が小さい輪郭は誤検出として削除する
contours = [x for x in contours if cv2.contourArea(x) > 100]
# 輪郭を描画する。
cv2.drawContours(img, contours, -1, color=(0, 0, 255), thickness=2)
imshow(img)
返り値 contours の構造
contours
は輪郭のリストになっています。各要素は輪郭の点の一覧を表す形状が (点の数, 1, 2)
の numpy 配列です。
# 画像を読み込む。
img = cv2.imread("sample2.jpg")
# グレースケールに変換する。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2値化する
ret, img_bin = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)
# 輪郭を抽出する。
contours, hierarchy = cv2.findContours(
img_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
for i, cnt in enumerate(contours):
print(f"contours[{i}].shape: {cnt.shape}")
contours[0].shape: (4, 1, 2) contours[1].shape: (165, 1, 2) contours[2].shape: (171, 1, 2) contours[3].shape: (6, 1, 2) contours[4].shape: (10, 1, 2)
matplotlib で抽出した輪郭を描画してみます。赤い点が輪郭を構成する点、青い線が輪郭、番号は contours のインデックスを表しています。
def draw_contours(ax, img, contours):
ax.imshow(img)
ax.set_axis_off()
for i, cnt in enumerate(contours):
# 形状を変更する。(NumPoints, 1, 2) -> (NumPoints, 2)
cnt = cnt.squeeze(axis=1)
# 輪郭の点同士を結ぶ線を描画する。
ax.add_patch(plt.Polygon(cnt, color="b", fill=None, lw=2))
# 輪郭の点を描画する。
ax.plot(cnt[:, 0], cnt[:, 1], "ro", mew=0, ms=4)
# 輪郭の番号を描画する。
ax.text(cnt[0][0], cnt[0][1], i, color="r", size="20", bbox=dict(fc="w"))
fig, ax = plt.subplots(figsize=(8, 8))
draw_contours(ax, img, contours)
plt.show()
method 引数
method 引数で輪郭点の近似手法を指定します。基本的には cv2.CHAIN_APPROX_SIMPLE
を指定すればよいでしょう。
cv2.CHAIN_APPROX_NONE
cv2.CHAIN_APPROX_SIMPLE
cv2.CHAIN_APPROX_TC89_L1
cv2.CHAIN_APPROX_TC89_KCOS
mode 引数
mode
引数では、輪郭を検索する方法を指定します。
- cv2.RETR_EXTERNAL: 一番外側の輪郭のみ抽出する
- cv2.RETR_LIST: すべての輪郭を抽出するが、階層構造は作成しない
- cv2.RETR_CCOMP: すべての輪郭を抽出し、2 階層の階層構造を作成する
- cv2.RETR_TREE: すべての輪郭を抽出し、ツリーで階層構造を作成する
cv2.RETR_LIST
、cv2.RETR_CCOMP
、cv2.RETR_TREE
はいずれもすべての輪郭を抽出しますが、返り値の hierarchy
の内容が異なります。
返り値 hierarchy の構造
抽出された輪郭 contours が $N$ 個であった場合、hierarchy は (1, N, 4)
の numpy 配列で、輪郭 contours[i]
の階層情報は hierarchy[0, i]
に格納されています。4 つの要素は、[次のインデックス、前のインデックス、最初の子のインデックス、親のインデックス]
を表しており、次、前、子、親が存在しない場合は -1 が設定されています。
階層構造を把握するため、anytree というライブラリを使用して可視化します。
pip install anytree
でインストールしてください。
import pandas as pd
from anytree import Node, RenderTree # anytree がない場合は pip install anytree
def print_hierarchy(hierarchy):
n_contours = len(hierarchy[0])
# ノードを作成する。
nodes = {i: Node(f"contour {i}") for i in range(n_contours)}
nodes[-1] = Node("root")
# エッジを作成する。
data = []
for i in range(n_contours):
next_sibling, prev_sibling, first_child, parent = hierarchy[0, i]
nodes[i].parent = nodes[parent]
data.append(
{
"next_sibling": next_sibling,
"prev_sibling": prev_sibling,
"first_child": first_child,
"parent": parent,
}
)
data = pd.DataFrame(data)
display.display(data)
# 木を出力する。
for pre, fill, node in RenderTree(nodes[-1]):
print("{}{}".format(pre, node.name))
# 画像を読み込む。
img = cv2.imread("sample3.jpg")
# グレースケールに変換する。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2値化する
ret, img_bin = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)
# 抽出した輪郭を描画する。
params = ["cv2.RETR_EXTERNAL", "cv2.RETR_LIST", "cv2.RETR_CCOMP", "cv2.RETR_TREE"]
for i, param in enumerate(params):
display.display(display.Markdown(f"#### {param}"))
mode = eval(param)
# 輪郭を抽出する。
contours, hierarchy = cv2.findContours(img_bin, mode, cv2.CHAIN_APPROX_SIMPLE)
# 階層構造を出力する。
print_hierarchy(hierarchy)
# 輪郭を描画する。
fig, ax = plt.subplots(figsize=(8, 8))
ax.set_title(param)
draw_contours(ax, img, contours)
plt.show()
cv2.RETR_EXTERNAL
next_sibling | prev_sibling | first_child | parent | |
---|---|---|---|---|
0 | 1 | -1 | -1 | -1 |
1 | -1 | 0 | -1 | -1 |
root ├── contour 0 └── contour 1
cv2.RETR_LIST
next_sibling | prev_sibling | first_child | parent | |
---|---|---|---|---|
0 | 1 | -1 | -1 | -1 |
1 | 2 | 0 | -1 | -1 |
2 | 3 | 1 | -1 | -1 |
3 | 4 | 2 | -1 | -1 |
4 | 5 | 3 | -1 | -1 |
5 | 6 | 4 | -1 | -1 |
6 | 7 | 5 | -1 | -1 |
7 | -1 | 6 | -1 | -1 |
root ├── contour 0 ├── contour 1 ├── contour 2 ├── contour 3 ├── contour 4 ├── contour 5 ├── contour 6 └── contour 7
cv2.RETR_CCOMP
next_sibling | prev_sibling | first_child | parent | |
---|---|---|---|---|
0 | 2 | -1 | 1 | -1 |
1 | -1 | -1 | -1 | 0 |
2 | 4 | 0 | 3 | -1 |
3 | -1 | -1 | -1 | 2 |
4 | 6 | 2 | 5 | -1 |
5 | -1 | -1 | -1 | 4 |
6 | -1 | 4 | 7 | -1 |
7 | -1 | -1 | -1 | 6 |
root ├── contour 0 │ └── contour 1 ├── contour 2 │ └── contour 3 ├── contour 4 │ └── contour 5 └── contour 6 └── contour 7
cv2.RETR_TREE
next_sibling | prev_sibling | first_child | parent | |
---|---|---|---|---|
0 | 2 | -1 | 1 | -1 |
1 | -1 | -1 | -1 | 0 |
2 | -1 | 0 | 3 | -1 |
3 | -1 | -1 | 4 | 2 |
4 | 6 | -1 | 5 | 3 |
5 | -1 | -1 | -1 | 4 |
6 | -1 | 4 | 7 | 3 |
7 | -1 | -1 | -1 | 6 |
root ├── contour 0 │ └── contour 1 └── contour 2 └── contour 3 ├── contour 4 │ └── contour 5 └── contour 6 └── contour 7
コメント