Pynote

Python、機械学習、画像処理について

OpenCV - findContours() による輪郭抽出

概要

2値画像から findContours() を使用して輪郭抽出を行う方法について紹介する。
輪郭を抽出したあとの処理については下記を参照されたい。

pynote.hatenablog.com

findContours

contours, hierarchy = cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])
  • 引数
    • image: 入力画像 (8bit、1チャンネル)。非0の画素は1とした2値画像として扱われる。
    • mode: 輪郭を検索する方法を指定する。
    • method: 輪郭を近似する方法を指定する。
    • contours: 輪郭。(引数で結果を受け取る場合、指定する。)
    • hierarchy: 階層構造。(引数で結果を受け取る場合、指定する。)
    • offset: 返り値の輪郭の点にオフセットを加えたい場合は指定する。
  • 返り値
    • contours: 抽出された輪郭のリスト。各輪郭は (NumPoints, 1, 2) の numpy 配列。
    • hierarchy: (1, NumContours, 4) の numpy 配列。階層構造のリスト。

OpenCV 3 までは返り値が以下のように3つだったので、OpenCV 3 を使ってる場合は注意。サンプルコードは OpenCV 4 を前提として記載している。

image, contours, hierarchy = cv2.findContours(image, mode, method[, contours[, hierarchy[, offset]]])

サンプルコード

使用する画像

輪郭を抽出する。

import cv2

# 画像を読み込む。
img = cv2.imread("sample.png")

# グレースケールに変換する。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 輪郭を抽出する。
contours, hierarchy = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

輪郭を描画する。

matplotlib で抽出した輪郭を描画する。

import matplotlib.pyplot as plt
from matplotlib.patches import Polygon


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(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="orange", size="20")


fig, ax = plt.subplots(figsize=(8, 8))
draw_contours(ax, img, contours)

plt.show()


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 が入っている。

cv2.RETR_EXTERNAL

使用する画像

輪郭を抽出する。
import cv2

# 画像を読み込む。
img = cv2.imread("sample.png")

# グレースケールに変換する。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 輪郭を抽出する。
contours, hierarchy = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 抽出した輪郭を描画する。
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_title("cv2.RETR_EXTERNAL")
draw_contours(ax, img, contours)

plt.show()
階層構造を出力する。

anytree を使用して、階層構造を見やすい形で出力する。

# anytree がない場合は pip install anytree
from anytree import Node, RenderTree

num_contours = len(hierarchy[0])

# any tree で階層構造を元に木を作成する。

# 頂点を作成する。
root = Node("root")
nodes = {i: Node(f"contour {i}") for i in range(num_contours)}
nodes[-1] = root

# 辺を作成する。
for i, (next_sibling, prev_sibling, first_child, parent) in enumerate(hierarchy[0]):
    nodes[i].parent = nodes[parent]
    print(
        f"contour {i} (next_sibling: {next_sibling}, prev_sibling: {prev_sibling},"
        f" first_child: {first_child}, parent: {parent})"
    )

# 木を出力する。
for pre, fill, node in RenderTree(root):
    print("{}{}".format(pre, node.name))
contour 0 (next_sibling: 1, prev_sibling: -1, first_child: -1, parent: -1)
contour 1 (next_sibling: -1, prev_sibling: 0, first_child: -1, parent: -1)

root
├── contour 0
└── contour 1


cv2.RETR_LIST

contour 0 (next_sibling: 1, prev_sibling: -1, first_child: -1, parent: -1)
contour 1 (next_sibling: 2, prev_sibling: 0, first_child: -1, parent: -1)
contour 2 (next_sibling: 3, prev_sibling: 1, first_child: -1, parent: -1)
contour 3 (next_sibling: 4, prev_sibling: 2, first_child: -1, parent: -1)
contour 4 (next_sibling: 5, prev_sibling: 3, first_child: -1, parent: -1)
contour 5 (next_sibling: 6, prev_sibling: 4, first_child: -1, parent: -1)
contour 6 (next_sibling: 7, prev_sibling: 5, first_child: -1, parent: -1)
contour 7 (next_sibling: -1, prev_sibling: 6, first_child: -1, parent: -1)

root
├── contour 0
├── contour 1
├── contour 2
├── contour 3
├── contour 4
├── contour 5
├── contour 6
└── contour 7


cv2.RETR_CCOMP

contour 0 (next_sibling: 2, prev_sibling: -1, first_child: 1, parent: -1)
contour 1 (next_sibling: -1, prev_sibling: -1, first_child: -1, parent: 0)
contour 2 (next_sibling: 4, prev_sibling: 0, first_child: 3, parent: -1)
contour 3 (next_sibling: -1, prev_sibling: -1, first_child: -1, parent: 2)
contour 4 (next_sibling: 6, prev_sibling: 2, first_child: 5, parent: -1)
contour 5 (next_sibling: -1, prev_sibling: -1, first_child: -1, parent: 4)
contour 6 (next_sibling: -1, prev_sibling: 4, first_child: 7, parent: -1)
contour 7 (next_sibling: -1, prev_sibling: -1, first_child: -1, parent: 6)

root
├── contour 0
│   └── contour 1
├── contour 2
│   └── contour 3
├── contour 4
│   └── contour 5
└── contour 6
    └── contour 7


cv2.RETR_TREE

contour 0 (next_sibling: 2, prev_sibling: -1, first_child: 1, parent: -1)
contour 1 (next_sibling: -1, prev_sibling: -1, first_child: -1, parent: 0)
contour 2 (next_sibling: -1, prev_sibling: 0, first_child: 3, parent: -1)
contour 3 (next_sibling: -1, prev_sibling: -1, first_child: 4, parent: 2)
contour 4 (next_sibling: 6, prev_sibling: -1, first_child: 5, parent: 3)
contour 5 (next_sibling: -1, prev_sibling: -1, first_child: -1, parent: 4)
contour 6 (next_sibling: -1, prev_sibling: 4, first_child: 7, parent: 3)
contour 7 (next_sibling: -1, prev_sibling: -1, first_child: -1, parent: 6)

root
├── contour 0
│   └── contour 1
└── contour 2
    └── contour 3
        ├── contour 4
        │   └── contour 5
        └── contour 6
            └── contour 7


method 引数

method 引数で輪郭点の近似手法を指定する。

使用する画像

import cv2

# 画像を読み込む。
img = cv2.imread("sample.png")

# 各 method での輪郭抽出の結果を描画する。
params = {
    "cv2.CHAIN_APPROX_NONE": cv2.CHAIN_APPROX_NONE,
    "cv2.CHAIN_APPROX_SIMPLE": cv2.CHAIN_APPROX_SIMPLE,
    "cv2.CHAIN_APPROX_TC89_L1": cv2.CHAIN_APPROX_TC89_L1,
    "cv2.CHAIN_APPROX_TC89_KCOS": cv2.CHAIN_APPROX_TC89_KCOS,
}

fig = plt.figure(figsize=(8, 8))
for i, (name, param) in enumerate(params.items(), 1):
    # グレースケールに変換する。
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 輪郭を抽出する。
    contours, hierarchy = cv2.findContours(gray, cv2.RETR_EXTERNAL, param)

    # 抽出した輪郭を描画する。
    ax = fig.add_subplot(2, 2, i)
    ax.set_title(f"method={name}")
    draw_contours(ax, img, contours)

plt.show()