【Python・OpenCV】輪郭を検出するには(cv2.findContours)

※当サイトではアフィリエイト広告を利用しています

Python プログラミング 画像処理

【Python・OpenCV】輪郭を検出するには(cv2.findContours)

はじめに

二値化画像から輪郭を検出するためのcv2.findContours関数を取り上げます。
この関数を使用することで、画像内の輪郭を検出し、その形状や位置に関する情報を抽出することができます。

cv2.findContours関数は引数で設定した内容によって、戻り値の扱いが異なります。
戻り値の解析方法についても説明します。

(広告) OpenCV関連書籍をAmazonで探す

輪郭とは

画像中の「輪郭」とは、物体や領域の境界線を指します。
具体的には、異なる色や明るさを持つ領域が接する境界部分です。
輪郭は、物体の形状や構造を認識するために非常に重要です。
例えば、以下のようなケースで輪郭が現れます。

  1. 物体の外周
    • 画像に写っている物体の外周の部分が輪郭になります。たとえば、白い背景に黒い円が描かれている画像では、黒い円の周囲が輪郭です。
  1. 異なる色の領域の境界
    • 一つの画像の中に、異なる色や明るさを持つ複数の領域が存在する場合、その境界部分が輪郭として検出されます。
      たとえば、赤い四角形と青い四角形が接している場合、その接している線が輪郭です。
  1. 物体の内部の境界
    • 物体の内部に異なる色やパターンがある場合、その境界も輪郭と見なされます。
      たとえば、円の中にさらに小さな円がある場合、大きな円の外周と小さな円の外周の両方が輪郭になります。
  1. エッジ(輪郭の一種)

輪郭の例

以下に、典型的な輪郭の例をいくつか挙げます:

  • 単一の形状
    黒い背景に白い四角形が描かれている場合、白い部分の最も外側の画素が輪郭になります。(緑の画素)
  • テキストの輪郭
    画像内のテキストの各文字も、輪郭として検出されます。文字の外周や内部の空白部分が輪郭となります。(緑の画素)

cv2.findContours関数

cv2.findContours関数の引数と戻り値について説明します。

contours, hierarchy = cv2.findContours(入力画像, mode, method[, contours[, hierarchy[, offset]]])

引数

名称説明
入力画像(必須)・画像のモーメント。入力画像。8ビット単一チャンネルの画像である必要があります。通常、二値化やエッジ検出後の画像を使用します。
・元の画像に変更を加えたくない場合は、コピーした画像を関数に渡すことをお勧めします。
numpy.ndarrayオブジェクト
mode(必須・輪郭の検索モード
RetrievalModesで定義される値を取ります。
・int型
method(必須)・輪郭の近似方法
ContourApproximationModesで定義される値を取ります。
・int型
contours(オプション・検出された輪郭が格納される。
List[numpy.ndarray]オブジェクト。
hierarchy(オプション・輪郭の階層情報が格納されるNumPy配列。
・各要素は [Next, Previous, First_Child, Parent] の形式。
numpy.ndarrayオブジェクト
offset(オプション・オプションのオフセット。すべての輪郭点をこの値だけシフトします。
・int型のタプル。(x, y)の形式。

modeは下の表の通りです。

RetrievalModes説明
cv2.RETR_EXTERNAL最も外側の輪郭のみを検出します。
・親輪郭のみが返され、子輪郭は無視されます。
・このモードでは、hierarchyは通常使用しません。
cv2.RETR_LIST・すべての輪郭を検出しますが、階層構造は構築しません。
・すべての輪郭が同じレベルで扱われます。
・このモードでは、hierarchyは通常使用しません。
cv2. RETR_CCOMP・2レベルの階層構造を構築します。
・外部輪郭と穴の輪郭を区別します。
hierarchyは[Next, Previous, First_Child, Parent]の形式で情報を持ちます。
cv2.RETR_TREE・完全な階層構造を構築します。親子関係を含むすべての輪郭を検出します。
hierarchyは[Next, Previous, First_Child, Parent]の形式で完全な階層情報を持ちます。
cv2.RETR_FLOODFILL・一つの連結した領域を一つの輪郭として扱い、その領域内の全てのピクセルを一つのcontourとして返します。
・主にフラッドフィルアルゴリズムと組み合わせて使用されます。
・複雑な輪郭追跡処理を行わないため、高速に処理できる可能性があります。
・このモードでは、hierarchyは通常使用しません。

methodは下の表の通りです。

ContourApproximationModes説明
cv2.CHAIN_APPROX_NONEすべての輪郭点を保存します。
・多くのメモリを消費しますが、最も正確に輪郭を表現します。
cv2.CHAIN_APPROX_SIMPLE・輪郭を圧縮し、端点のみを保存します。
直線の場合、始点と終点のみを保存し、メモリを節約します。
cv2.CHAIN_APPROX_TC89_L1 ・Teh-Chin のアルゴリズムを基にしています。
・複雑な輪郭を単純化するのに効果的です。
・L1 ノルム(マンハッタン距離)を使用して輪郭点の重要性を評価します。
・直線的な特徴をより強調する傾向があります。
・建築物や人工物の輪郭検出(直線的な特徴が多い場合)や文字認識(OCR)の前処理に利用される場合があります。
cv2.CHAIN_APPROX_TC89_KCOS・Teh-Chin のアルゴリズムを基にしています。
・複雑な輪郭を単純化するのに効果的です。
・コサイン値を使用して輪郭点の重要性を評価します。
・曲線的な特徴をより良く保持する傾向があり、より滑らかな近似を生成することがあります。
・自然物や有機的な形状の輪郭検出や医療画像分析などに利用される場合があります。

戻り値

OpenCVのバージョンによって戻り値の形式が若干異なります。バージョン4.0以降は以下の通りです。

contours, hierarchyの形式のタプル

名称説明
contours・検出された輪郭が格納される。
・各輪郭は点の配列(numpy.ndarray)として表現されます。
List[numpy.ndarray]オブジェクト。
hierarchy・輪郭の階層情報が格納されるNumPy配列。
・4つの要素を持つ配列。
numpy.ndarrayオブジェクト

OpenCVのバージョン3.4.20以前では、戻り値が3つの場合があります(image, contours, hierarchy)。
この場合、最初の返り値(image)は通常無視されます。

輪郭データ(contours)

contoursは検出された輪郭のListです。各輪郭は点の配列(numpy.ndarray)として表現されます。

階層データ(hierarchy)

hierarchyは次の形式で情報を持つNumPy配列で、各要素は輪郭のインデックス(または-1)を示します

  • hierarchy[i][0]: 同じ階層の次の輪郭のインデックス(存在しない場合は-1)
  • hierarchy[i][1]: 同じ階層の前の輪郭のインデックス(存在しない場合は-1)
  • hierarchy[i][2]: 最初の子輪郭のインデックス(存在しない場合は-1)
  • hierarchy[i][3]: 親輪郭のインデックス(存在しない場合は-1)
例1: 単純な階層構造

以下の画像を考えてみます。
画像には、白い背景に外側に大きな円(A)と、その中に小さな円(B)が含まれています。

単純な階層構造 説明用

cv2.findContours関数の戻り値として、次のようなhierarchyが得られたとします。
引数はmode : cv2.RETR_TREE、method : cv2.CHAIN_APPROX_NONEの場合の結果となります。

hierarchy = 
[[[-1 -1  1 -1]
  [-1 -1  2  0]
  [-1 -1 -1  1]]]

背景が白なので、背景も輪郭として認識されるため、検出される輪郭は背景の矩形、および円(A)、(B)の3つになります。
検出結果は下の図の通り、背景の輪郭線:赤、円(A)の輪郭先:オレンジ、円(B)の輪郭先:黄色 となりました。

単純な階層構造の検出結果

戻り値のhierarchyの内容のそれぞれの意味は以下となります。

  • [-1, -1, 1, -1]: インデックス0(最初)の輪郭は背景の矩形に対応。同じ階層に輪郭はないので次の輪郭なし、前の輪郭なし、最初の子輪郭がインデックス1、親はなし。
  • [-1, -1, 2, -1]: インデックス1の輪郭(A)に対応。同じ階層の次の輪郭なし、同じ階層の前の輪郭なし、子輪郭はインデックス2、親はなし。
  • [-1, -1, -1, 1]: インデックス2の輪郭(B)に対応。同じ階層の次の輪郭なし、前の輪郭なし、子輪郭なし、親がインデックス1。

上記の検出を行ったコードは下記となります。

import cv2

# 画像読み込み(グレースケール)
image = cv2.imread("hierarchy_test1.png", cv2.IMREAD_GRAYSCALE)

# 輪郭の色
colors = [(0, 0, 255),        # 赤
          (0, 128, 255),      # オレンジ
          (0, 250, 250),      # 黄色
          (0, 255, 0),        # 緑
          (255, 0, 0),        # 青
          (255, 0, 128),      # 紫
          (255, 0, 255),      # ピンク
          (128, 128, 0),      # 青緑
          (255, 255, 0),      # cyan
          (47, 107, 85),      # darkolivegreen
          (45, 82, 160),      # sienna
          (226, 43, 138),     # blueviolet
          (143, 143, 188),    # rosybrown
          (50, 0, 128),       # maroon
          (169, 169, 169)]    # darkgray

# 輪郭線の幅
line_width = 5

# 二値化
_, binary = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)

# 輪郭を検出
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

# 階層構造を解析
for i, contour in enumerate(contours):
    print(f'Contour {i}:')
    print(f'  Next: {hierarchy[0][i][0]}')
    print(f'  Previous: {hierarchy[0][i][1]}')
    print(f'  First Child: {hierarchy[0][i][2]}')
    print(f'  Parent: {hierarchy[0][i][3]}')

print('\n')
print("hierarchy = ")
print(hierarchy)

result = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
color_count = 0
for contour in contours:
    for point in contour:
        cv2.line(result, point[0], point[0], colors[color_count], line_width)
    color_count += 1

cv2.imwrite("hierarchy_test_result1.png", result)
例2: 複数のオブジェクトが存在する階層構造

複数のオブジェクトがある場合、階層構造は少し複雑になります。
下図のような、7つの四角形のオブジェクトを持つ画像の場合を考えてみます。
このケースは背景が黒なので、背景は輪郭として認識されません。

この場合、cv2.findContours関数の戻り値のhierarchyは次のようになります。
引数はmode : cv2.RETR_TREE、method : cv2.CHAIN_APPROX_NONEの場合の結果となります。

hierarchy = 
[[[-1 -1  1 -1]
  [ 5 -1  2  0]
  [-1 -1  3  1]
  [ 4 -1 -1  2]
  [-1  3 -1  2]
  [ 6  1 -1  0]
  [-1  5 -1  0]]]

かっこの数字は、後から記入したもので、検出した輪郭のインデックスを示しています。
7つの輪郭が検出できています。

戻り値のhierarchyの内容のそれぞれの意味は以下となります。

  • [-1, -1, 1, -1]: インデックス0(最初)の輪郭は最も大きな矩形に対応(輪郭線の色:赤)。同じ階層に輪郭はないので次の輪郭なし、前の輪郭なし、最初の子輪郭がインデックス1、親はなし。
  • [5, -1, 2, 0]: インデックス1の輪郭に対応(輪郭線の色:オレンジ)。同じ階層の次の輪郭がインデックス5、同じ階層の前の輪郭なし、子輪郭はインデックス2、親はインデックス0。
  • [-1, -1, 3, 1]: インデックス2の輪郭に対応(輪郭線の色:黄)。同じ階層の次の輪郭なし、前の輪郭なし、子輪郭はインデックス3、親がインデックス1。
  • [4, -1, -1, 2]: インデックス3の輪郭に対応(輪郭線の色:緑)。同じ階層の次の輪郭はインデックス4、前の輪郭なし、子輪郭はなし、親がインデックス2。
  • [-1, 3, -1, 2]: インデックス4の輪郭に対応(輪郭線の色:青)。同じ階層の次の輪郭はなし、前の輪郭はインデックス3、子輪郭はなし、親がインデックス2。
  • [6, 1, -1, 0]: インデックス5の輪郭に対応(輪郭線の色:紫)。同じ階層の次の輪郭はインデックス6、前の輪郭はインデックス1、子輪郭はなし、親がインデックス0。
  • [-1, 5, -1, 0]: インデックス6の輪郭に対応(輪郭線の色:マゼンタ)。同じ階層の次の輪郭なし、前の輪郭はインデックス5、子輪郭はなし、親がインデックス0。

hierarchyの内容はわかりにくく見えますが、それぞれの値が実際の階層構造に相違なく表現されていることがわかります。

上記の検出を行ったコードは下記となります。

import cv2

# 画像読み込み(グレースケール)
image = cv2.imread("hierarchy_test2.png", cv2.IMREAD_GRAYSCALE)

# 輪郭の色
colors = [(0, 0, 255),        # 赤
          (0, 128, 255),      # オレンジ
          (0, 255, 255),      # 黄色
          (0, 255, 0),        # 緑
          (255, 0, 0),        # 青
          (255, 0, 128),      # 紫
          (255, 0, 255),      # ピンク
          (128, 128, 0),      # 青緑
          (255, 255, 0),      # cyan
          (47, 107, 85),      # darkolivegreen
          (45, 82, 160),      # sienna
          (226, 43, 138),     # blueviolet
          (143, 143, 188),    # rosybrown
          (50, 0, 128),       # maroon
          (169, 169, 169)]    # darkgray

# 輪郭線の幅
line_width = 2

# 二値化
_, binary = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)

# findContours関数の呼び出し後、contoursを描画
# cv2.RETR_TREE
print(f"cv2.RETR_TREE")
contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)

for i, contour in enumerate(contours):
    print(f'Contour {i}:')
    print(f'  Next: {hierarchy[0][i][0]}')
    print(f'  Previous: {hierarchy[0][i][1]}')
    print(f'  First Child: {hierarchy[0][i][2]}')
    print(f'  Parent: {hierarchy[0][i][3]}')

print('\n')
print("contours = ")
print(contours)

result = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
color_count = 0
for contour in contours:
    for point in contour:
        cv2.line(result, point[0], point[0], colors[color_count], line_width)
    color_count += 1

cv2.imwrite("RETR_TREE_result.png", result)

おわりに

cv2.findContours関数は、画像処理において非常に強力なツールです。
modeとmethodの組み合わせ方によって、様々な種類の輪郭を検出することができます。

一方で、hierarchyの構造は複雑なため理解しづらいと感じるかもしれません。
本記事がhierarchyの構造理解のお役に立てれば、筆者としてもとてもうれしいです。

本記事のサンプルコードでは、各輪郭を異なる色で描画するためにcv2.line関数を使っていますが、cv2.drawContours関数を使うと、もっと簡単に輪郭を描画することができます。
cv2.drawContours関数や、contoursの様々な解析については別の記事で、紹介したいと思います。

ご質問や取り上げて欲しい内容などがありましたら、コメントをお願いします。
最後までご覧いただきありがとうございました。

参考リンク

■(広告)OpenCVの参考書としてどうぞ!■

-Python, プログラミング, 画像処理
-