LoginSignup
7
8

More than 1 year has passed since last update.

OpenCVのfindContoursの階層構造まとめ

Posted at

環境

作業はJupyter Notedbook上で実行しています。
OpenCVのバージョンは(4.5.3)です。

cv2.findContoursとは

OpenCVの関数で、同じ値をもつ連続した点をつなげて輪郭を検出することができます。以下が公式のガイドです。

必須となる引数は以下の3つとなります。

引数 内容
image 輪郭を検出したい画像。精度のためにエッジ検出した二値画像の入力が推奨されている。
mode 輪郭検出の階層構造を4つの中から指定できる。
method 輪郭の近似手法を4つの中から指定できる。

戻り値は検出した輪郭の座標と、階層構造の情報を保存した配列です。

modemethodは以下の通りになります。

mode 概要 method 概要
CV_RETR_LIST 検出した全ての輪郭に階層構造を与えない CV_CHAIN_APPROX_NONE 全ての輪郭点を格納
CV_RETR_EXTERNAL 最も外側の輪郭のみ抽出し、それらに階層構造を与えない CV_CHAIN_APPROX_SIMPLE 水平・垂直・斜めの成分を圧縮して端点として格納
CV_RETR_CCOMP 検出した全ての輪郭に2階層構造を与える CV_CHAIN_APPROX_TC89_L1 Teh-Chinチェーン近似アルゴリズム(未確認)
CV_RETR_TREE 検出した全ての輪郭に完全な階層構造を与える CV_CHAIN_APPROX_TC89_KCOS Teh-Chinチェーン近似アルゴリズム(未確認)

今回はmethodはわかりやすくかつデータ量の少ないCV_CHAIN_APPROX_SIMPLEを使うことにし、modeについて違いを確認していきたいと思います。

modeの確認

準備

以下のように必要なライブラリをインポートし、元の画像とcv2.Cannyによってエッジ処理を行った画像を表示してみます。

import numpy as np
import cv2
import matplotlib.pyplot as plt
%matplotlib inline

org_image = cv2.cvtColor(cv2.imread('image.jpg', 1), cv2.COLOR_BGR2GRAY)
org_image = cv2.resize(org_image, (256, 256))
edge = cv2.Canny(org_image, 50, 150)
#描画
fig, ax = plt.subplots(1, 2, figsize=(20, 20), sharex=True, sharey=True)
ax[0].imshow(org_image, cmap='gray')
ax[0].set_title('orginal')
ax[1].imshow(edge, cmap='gray')
ax[1].set_title('edged')
ax[0].set_xticks([])
ax[0].set_yticks([])
plt.show()

スクリーンショット (120).png

このエッジ処理後の画像を使って確認していきます。

RETR_LIST

RETR_LISTは特に階層構造を与えずに全ての輪郭を検出します。輪郭と階層構造をそれぞれcontourshierachyに格納し、hierachyの中身を確認します。

contours, hierachy = cv2.findContours(edge, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
hierachy
#--->
#array([[[ 1, -1, -1, -1],
#        [ 2,  0, -1, -1],
#        [ 3,  1, -1, -1],
#        [ 4,  2, -1, -1],
#        [ 5,  3, -1, -1],
#        [ 6,  4, -1, -1],
#        [ 7,  5, -1, -1],
#        [ 8,  6, -1, -1],
#        [ 9,  7, -1, -1],
#        [-1,  8, -1, -1]]], dtype=int32)

このhierachyの中に格納されているリストの意味は次の通りです。

1 2 3 4
同階層内の次の輪郭の番号(次がなければ-1) 同階層内の前の輪郭の番号(次がなければ-1) 子の輪郭の番号(下の階層がなければ-1) 親の輪郭の番号(上の階層がなければ-1)

RETR_LISTでは階層構造なく検出されるので、3, 4番目はすべて-1になっています。

また、cv2.drawContours()関数を使ってcontoursに保存した輪郭を描画することができます。

fig, ax = plt.subplots(2, 5, sharex=True, sharey=True, figsize=(15,15))
ax = ax.flatten()
for i in range(len(contours)):
    img = cv2.drawContours(cv2.cvtColor(org_image, cv2.COLOR_GRAY2RGB), #描画する画像
                           contours,                                    #輪郭を保存したリスト
                           i,                                           #リストの何番目を描くか
                           (0,255,0),                                   #色の指定
                           2)                                           #線の太さの指定
    ax[i].imshow(img)
    ax[i].set_title('contour{}'.format(i))
ax[0].set_xticks([])
ax[0].set_yticks([])
plt.tight_layout()
plt.show()

スクリーンショット (121).png

線の内側と外側で2つずつ、輪郭であってほしい部分がしっかりと検出されていることがわかります。

RETR_EXTERNAL

RETR_EXTERNALは一番外側の輪郭のみを階層構造なしに返します。

contours, hierachy = cv2.findContours(edge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
hierachy
#--->
#array([[[ 1, -1, -1, -1],
#        [-1,  0, -1, -1]]], dtype=int32)

RETR_LISTと同じように3, 4番目の値はすべて-1になっていますが、格納されている数が少なくなっています。描画すると以下のようになっています。

スクリーンショット (122).png

上の直線はしっかりと外側と認識されており、また、下の四角の中は輪郭検出されていません。

RETR_CCOMP

RETR_CCOMPはすべての輪郭を、物体の外側の輪郭を第1階層、内側の輪郭を第2階層、というふうに2階層構造にして返します。

contours, hierachy = cv2.findContours(edge, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
hierachy
#--->
#array([[[ 2, -1,  1, -1],
#        [-1, -1, -1,  0],
#        [ 4,  0,  3, -1],
#        [-1, -1, -1,  2],
#        [ 6,  2,  5, -1],
#        [-1, -1, -1,  4],
#        [ 8,  4,  7, -1],
#        [-1, -1, -1,  6],
#        [-1,  6,  9, -1],
#        [-1, -1, -1,  8]]], dtype=int32)

0番目の輪郭について、以下のように考えられます。

  • 4番目の要素が-1なので親がいません、すなわち第1階層の輪郭です
  • 3番目の要素が1なので、1番目の輪郭が子にいることがわかります
  • 2番目の要素が-1なので、最初に出てきた第1階層の輪郭です
  • 1番目の要素が2なので、次の第1階層の輪郭は2番目の輪郭です

同じように1番目の輪郭について、以下のように考えられます。

  • 4番目の要素が0なので、これは0番目の輪郭の子の輪郭です
  • 3番目の要素が-1なので、この輪郭に子はいません
  • 2番目の要素が-1なので、0番目の子の輪郭の中で最初に出てきた第2階層の輪郭です
  • 1番目の要素が-1なので、0番目の輪郭の子はもうありません

このようにして階層構造を考えることができます。例えば以下のように第1階層の輪郭、すなわち物体の外側の輪郭のみをみることができます。

#0番目の輪郭から見て、1番目の要素を追って描画する
next_c = 0
fig, ax = plt.subplots(1, 5, sharex=True, sharey=True, figsize=(20, 20))
for i in range(len(contours)):
    img = cv2.drawContours(cv2.cvtColor(org_image, cv2.COLOR_GRAY2RGB), contours, next_c, (0,255,0), 2)
    ax[i].imshow(img)
    ax[i].set_title('contour{}'.format(i))
    next_c = hierachy[0][next_c][0]
    if next_c == -1: #第1階層が最後のとき脱出
        break
ax[0].set_xticks([])
ax[0].set_yticks([])
plt.tight_layout()
plt.show()

スクリーンショット (124).png

輪郭は全部で10個検出されましたが、そのうち外側の5つのみを描画することができました。

RETR_TREE

RETR_TREEはすべての輪郭を入れ子構造を完全に保存した階層構造を持たせて返します。例の画像では下の四角たちが入れ子構造になります。

contours, hierachy = cv2.findContours(edge, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
hierachy
#--->
#array([[[ 8, -1,  1, -1],
#        [-1, -1,  2,  0],
#        [-1, -1,  3,  1],
#        [-1, -1,  4,  2],
#        [ 6, -1,  5,  3],
#        [-1, -1, -1,  4],
#        [-1,  4,  7,  3],
#        [-1, -1, -1,  6],
#        [-1,  0,  9, -1],
#        [-1, -1, -1,  8]]], dtype=int32)

入れ子構造は以下の図のようになっています。

スクリーンショット (128).png

四角の中に四角があるような部分はすべて一番外側の四角の下にあるように階層構造が作られています。

うまく利用すれば、物体ごとにマスクした画像を複製することもできそう。

使用例:背景の削除

'RETR_EXTERNAL'を使って背景の削除を試します。

エッジ処理はCannyだけだと輪郭が複雑すぎてしまうのでdilateを使って太くしておきます。

image = cv2.cvtColor(cv2.imread('image2.png', 1), cv2.COLOR_BGR2RGB)
gray_image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
edge_image = cv2.Canny(gray_image, 50, 70)
kernel = np.ones((3, 3), np.uint8)
edge_image = cv2.dilate(edge_image, kernel, iterations=3)
plt.imshow(edge_image, cmap='gray')

スクリーンショット (129).png

次に外側の輪郭で、囲って面積が生まれるようなものを取り出し、マスク画像を作ります。

contours, hierachy = cv2.findContours(edge_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
out = []
for c in contours:
    if cv2.contourArea(c) > 0:
        out.append(c)
mask = np.ones(edge_image.shape)
for i in range(len(out)):
    cv2.fillConvexPoly(mask, out[i], (0))
plt.imshow(mask, cmap='gray')

スクリーンショット (130).png

最後にマスクと元画像を組み合わせると背景が黒塗りされた状態の画像(すなわち背景情報を消した画像)を作ることができます。

mask = np.ones(edge_image.shape)
for i in range(len(contours)):
    cv2.fillConvexPoly(mask, contours[i], (0))
mask = cv2.dilate(mask, None, iterations=3)
mask = cv2.erode(mask, None, iterations=3)
mask_stack = np.dstack([mask]*3).astype('float32')
image = (image/255.).astype('float32')
sub_image = cv2.subtract(image, mask_stack)
plt.imshow(sub_image, cmap='gray')

スクリーンショット (131).png

Cannyの閾値やdilate, erodeのイテレーション回数、カーネルサイズを動かすことで切り取り方をすこしかえることができます。
(ここをフォルダ内の画像すべてにうまい具合に適用する、というのがすごい難しそう。多分うまくいかない画像がたくさん出てくるので、よく観察しなければならないかもしれないです。例えばマスク画像であまりに黒い面積が生まれるならその画像は見ない、とかしたほうがいいような気かしました。今後実験してみたいです。)

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8