Pynote

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

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

findContours()

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

OpenCV 4 では返り値が以下のように2つに変更されたので注意。サンプルコードは OpenCV 3 を前提として記載していますので、適宜変更してください。

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

サンプルコード

使用する画像

画像を読み込む。

import cv2
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Polygon

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

# 画像を表示する。
fig, axes = plt.subplots(figsize=(6, 6))
axes.imshow(img)
axes.axis('off')
plt.show()

グレースケール画像に変換し、findContours() で輪郭を抽出する。

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

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

def draw_contours(ax, img, contours):
    ax.imshow(img)
    ax.axis('off')
    for i, cnt in enumerate(contours):
        cnt = np.squeeze(cnt, axis=1)  # (NumPoints, 1, 2) -> (NumPoints, 2)
        # 輪郭の点同士を結ぶ線を描画する。
        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=(6, 6))
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

使用する画像

輪郭を抽出する。
# 画像を読み込む。
img = cv2.imread('sample2.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

# 階層構造を元に木を作成する。
root = Node('root')
nodes = {i: Node('contour {}'.format(i)) for i in range(len(hierarchy[0]))}
nodes[-1] = root

for i, info in enumerate(hierarchy[0]):
    nodes[i].parent = nodes[info[3]]
    print('contour {} (next: {}, previous: {}, first_child: {}, parent: {})'.format(i, *info))

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

root
├── contour 0
└── contour 1


cv2.RETR_LIST

contour 0 (next: 1, previous: -1, first_child: -1, parent: -1)
contour 1 (next: 2, previous: 0, first_child: -1, parent: -1)
contour 2 (next: 3, previous: 1, first_child: -1, parent: -1)
contour 3 (next: 4, previous: 2, first_child: -1, parent: -1)
contour 4 (next: 5, previous: 3, first_child: -1, parent: -1)
contour 5 (next: 6, previous: 4, first_child: -1, parent: -1)
contour 6 (next: 7, previous: 5, first_child: -1, parent: -1)
contour 7 (next: -1, previous: 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: 2, previous: -1, first_child: 1, parent: -1)
contour 1 (next: -1, previous: -1, first_child: -1, parent: 0)
contour 2 (next: 4, previous: 0, first_child: 3, parent: -1)
contour 3 (next: -1, previous: -1, first_child: -1, parent: 2)
contour 4 (next: 6, previous: 2, first_child: 5, parent: -1)
contour 5 (next: -1, previous: -1, first_child: -1, parent: 4)
contour 6 (next: -1, previous: 4, first_child: 7, parent: -1)
contour 7 (next: -1, previous: -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: 2, previous: -1, first_child: 1, parent: -1)
contour 1 (next: -1, previous: -1, first_child: -1, parent: 0)
contour 2 (next: -1, previous: 0, first_child: 3, parent: -1)
contour 3 (next: -1, previous: -1, first_child: 4, parent: 2)
contour 4 (next: 6, previous: -1, first_child: 5, parent: 3)
contour 5 (next: -1, previous: -1, first_child: -1, parent: 4)
contour 6 (next: -1, previous: 4, first_child: 7, parent: 3)
contour 7 (next: -1, previous: -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 引数で輪郭点の近似手法を指定する。

使用する画像

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

# 各 method での輪郭抽出の結果を描画する。
methods = {'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, axes = plt.subplots(2, 2, figsize=(10, 10))
for ax, (name, method) in zip(axes.ravel(), methods.items()):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, contours, hierarchy = cv2.findContours(gray, cv2.RETR_EXTERNAL, method)
    ax.set_title(name)
    draw_contours(ax, img, contours)
plt.show()