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

目次

概要

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 配列。

輪郭抽出する手順

以下の画像から月の部分の輪郭を抽出する方法について解説します。

sample.png

輪郭抽出を行うには、まず検出したい物体は白、それ以外の物体は黒となっている 2 値画像を作成する必要があります。今回は以下のように 2 値化しました。

  1. cv2.cvtColor() で RGB 画像をグレースケール画像に変換する
  2. cv2.threshold() で 2 値化する

2 値化はいくつかのやり方があります。詳しくは以下の記事を参照してください。

In [1]:
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() で画像に描画して確認できます。

In [3]:
# 輪郭を抽出する。
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 配列です。

sample2.jpg

In [4]:
# 画像を読み込む。
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 のインデックスを表しています。

In [5]:
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_LISTcv2.RETR_CCOMPcv2.RETR_TREE はいずれもすべての輪郭を抽出しますが、返り値の hierarchy の内容が異なります。

sample3.jpg

返り値 hierarchy の構造

抽出された輪郭 contours が $N$ 個であった場合、hierarchy は (1, N, 4) の numpy 配列で、輪郭 contours[i] の階層情報は hierarchy[0, i] に格納されています。4 つの要素は、[次のインデックス、前のインデックス、最初の子のインデックス、親のインデックス] を表しており、次、前、子、親が存在しない場合は -1 が設定されています。 階層構造を把握するため、anytree というライブラリを使用して可視化します。 pip install anytree でインストールしてください。

In [6]:
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))
In [7]:
# 画像を読み込む。
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

参考

コメント

コメントする

目次