OpenCV – findContours で画像から輪郭を抽出する方法

概要
OpenCV の findContours() を使用して2値画像から輪郭抽出を行う方法について解説します。 輪郭を抽出したあとに行う、誤検出を除いたり、輪郭の点の数や大きさで目的の輪郭を探す処理は以下の記事を参考にしてください。
OpenCV – 輪郭の特徴分析について – pystyle
cv2.findContours
contours, hierarchy = cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])
- 引数
- image: 入力画像 (8bit、1チャンネル)。非0の画素は1とした2値画像として扱われる。
- mode: 輪郭を検索する方法を指定する。
- method: 輪郭を近似する方法を指定する。
- offset: 返り値の輪郭の点にオフセットを加算したい場合は指定する。
- 返り値
- contours: 抽出された輪郭のリスト。各輪郭は (NumPoints, 1, 2) の numpy 配列。
- hierarchy: 階層構造のリスト。(1, NumContours, 4) の numpy 配列。
輪郭抽出する手順
以下の画像から月の部分の輪郭を抽出する方法について解説します。
輪郭抽出を行うには、まず検出したい物体は白、それ以外の物体は黒となっている2値画像を作成する必要があります。今回は以下のように2値化しました。
cv2.cvtColor()
でRGB 画像をグレースケール画像に変換するcv2.threshold()
で2値化する
2値化はいくつかのやり方があります。詳しくは以下の記事を参照してください。
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, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)
imshow(bin_img)

上の画像のように輪郭抽出したい対象物が白、それ以外は黒として綺麗に2値化できるパラメータを探してください。以下は2値化の失敗例です。2値化がうまくいっていない場合、その後の輪郭抽出も失敗します。

2値化がうまくできたら、findContours() で2値画像の白の領域を囲む輪郭を取得します。この関数は輪郭の一覧を表す contours
と輪郭の階層構造を表す hierarchy
を返します。その後、誤検出の輪郭がいくつかあるので、輪郭の面積が一定未満のものは filter()
で削除します。
抽出した輪郭は cv2.drawContours()
で画像に描画して確認できます。
# 輪郭を抽出する。
contours, hierarchy = cv2.findContours(
bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
# 小さい輪郭は誤検出として削除する
contours = list(filter(lambda x: cv2.contourArea(x) > 100, contours))
# 輪郭を描画する。
cv2.drawContours(img, contours, -1, color=(0, 0, 255), thickness=2)
imshow(img)

contours の構造
contours
は輪郭のリストになっています。各要素は輪郭の点の一覧を表す形状が (点の数, 1, 2)
の numpy 配列です。
# 画像を読み込む。
img = cv2.imread("sample3.jpg")
# グレースケールに変換する。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 2値化する
ret, bin_img = cv2.threshold(gray, 20, 255, cv2.THRESH_BINARY)
# 輪郭を抽出する。
contours, hierarchy = cv2.findContours(
bin_img, 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

hierarchy の構造
抽出された輪郭 contours が $N$ 個であった場合、hierarchy は (1, N, 4)
の numpy 配列で、輪郭 contours[i]
の階層情報は hierarchy[0, i]
に格納されています。4つの要素は、[次のインデックス、前のインデックス、最初の子のインデックス、親のインデックス]
を表しており、次、前、子、親が存在しない場合は -1 が設定されています。
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
の内容が異なります。
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

参考文献
-
前の記事
OpenCV – inRange で画像を2値化する方法について 2020.08.13
-
次の記事
OpenCV – 輪郭の特徴分析について 2020.08.16