【Python】OpenCV findContours()の引数解説

前回はOpenCVで画像から物体の輪郭を検出する方法を紹介しました。

https://www.learning-nao.com/?p=2006

その中で、findContours()関数を紹介しましたが、引数についての説明は省略していました。

今回は、その補足説明として、 findContours() で渡す引数の内容はどういったものがあるのかを見ていこうと思います。

findContours()

findContours()で必要な引数をおさらいしておきます。

contours, hierarchy = cv2.findContours(入力画像、抽出モード、近似手法)

今回説明する対象は2つ目の引数である抽出モードについてです。

1つ目の戻り値であるcontoursは検出した輪郭のリスト、2つ目の戻り値は輪郭の階層情報を保持します。

輪郭の階層

検出された輪郭は階層(親子関係)を持つ場合があります。この階層の中でどのように輪郭を取得するかを決めるのが cv2. findContours()の引数にある「抽出モード」です。

抽出モードを操作することで、条件にあてはまる輪郭を取得することができます。階層情報は2つめの戻り値であるhierarchyに格納されます。

親子関係

輪郭の親子関係とはどのようなものなのでしょうか。一般に、輪郭の内部に別の輪郭を検出した場合に親子関係が発生します。基本的には外側の輪郭が親、内側の輪郭が子の関係になります。

以下の図を使って輪郭の階層構造を整理してみます。

階層構造を持つ図形

この図の輪郭に番号を付けていきます。

階層構造に付番する

整数で示された輪郭(1~3)はすべて親の輪郭です。3-1、4は3を親とする子の輪郭になります。

このとき、1~3の番号は順不同です。

hierarchyの中身

上記の情報は2つ目の戻り値であるhierarchyに含まれています。hierarchyは配列で、4つの値を含んでいます。

Next, Previous, First_Child, Parent

例えば、以下のような配列です。

[[[ 1 -1 -1 -1]
[ 2 0 -1 -1]
[ 3 1 -1 -1]
[ 4 2 -1 -1]
[-1 3 -1 -1]]]

Nextは、同じ階層の次の輪郭を示します。上の配列を見ると、最初(0番目)の配列の1つ目の要素は1になっており、これが次の輪郭を表しています。最後の配列では次の輪郭は存在しません。そのため最初の要素は-1になります。

PreviousはNextの逆で、同じ階層の前の輪郭を示します。最初の輪郭には前の輪郭は存在しないので-1となります。

First_Childは、その輪郭から見た最初の子のインデックス位置を示します。子がない場合は-1になります。上記の例は子を持たない抽出方法(後述)を実施した結果のため、全ての輪郭で値が-1になっています。

Parentはその輪郭に対する親の輪郭のインデックス位置を示します。親がいない場合は-1になります。例では親子関係を持っていないため全て-1となっています。

輪郭検出の手法

さて、 cv2.findContours()の引数にある抽出モードには、いくつか種類があります。

  • cv2.RETR_EXTERNAL
  • cv2.RETR_LIST
  • cv2.RETR_CCOMP
  • cv2.RETR_TREE

これらの違いは、輪郭にどのように親子関係を持たせ、どの階層の輪郭を取り出すかというところにあります。

  • cv2.RETR_EXTERNAL
  • この手法では、子の階層にある輪郭はすべて無視されます。なので、上の図でいうところの3-1および3-2の輪郭は描画されません。

    cv2.RETR_EXTERNALの実行結果

    緑の線が太いのでわかりづらいですが、3に該当する輪郭の内側(3-1)は輪郭として検出されていません。また、その内側(4)も検出されていません。

    階層構造は以下のように保持されています。

    EXTERNAL: [[[ 1 -1 -1 -1]
    [ 2 0 -1 -1]
    [-1 1 -1 -1]]]

  • cv2.RETR_LIST
  • この手法では親子関係を構築せず、すべて親の輪郭として検出します。

    そのため、hierarchyのFirst_ChildおよびParentはすべて-1になります。

     cv2.RETR_LISTの実行結果

    LIST: [[[ 1 -1 -1 -1]
    [ 2 0 -1 -1]
    [ 3 1 -1 -1]
    [ 4 2 -1 -1]
    [-1 3 -1 -1]]]

  • cv2.RETR_CCOMP
  • この手法では全ての輪郭を検出し、2つのレベルの階層に分類します。以下の図で、青字で()内に示した数字が各輪郭の階層を示します。

    cv2.RETR_CCOMPの階層構造

    例えば3-1は3の子であるため、階層としては2になります。一方、4も3の子ではありますが、階層としては振りなおされ1となります。4の内側にさらに輪郭が検出されれば、その階層は2となります。

    CCOMP: [[[ 1 -1 -1 -1]
    [ 3 0 2 -1]
    [-1 -1 -1 1]
    [ 4 1 -1 -1]
    [-1 3 -1 -1]]]

    2、3個目のFirst_Child, Parentから親子関係が見て取れます。

    出力される画像は以下のようになります。

    cv2.RETR_CCOMPの実行結果

  • cv2.RETR_TREE
  • この手法では、全ての階層構造を保持します。cv2.RETR_CCOMPでは2つのレベルの階層構造として保持していましたが、ここでは親、子、孫、ひ孫、、といったように目で見たままの階層構造を持ちます。

    cv2.RETR_TREEの階層構造

    この図におけるcv2.RETR_CCOMPとの違いは、4の階層が3となっている点です。

    TREE: [[[ 3 -1 1 -1]
    [-1 -1 2 0]
    [-1 -1 -1 1]
    [ 4 0 -1 -1]
    [-1 3 -1 -1]]]

    First_ChildおよびParentの値を見ると、1~3つ目の配列に親子関係があることがわかります。

    出力される画像は以下のようになります。

    cv2.RETR_TREEの実行結果

    Pythonのコード

    ここまでの結果を出力するまでのコードは以下の通りです。

    import cv2
    
    #入力画像
    image = cv2.imread('input/contour.jpg')
    
    #画像のサイズ縮小
    height = image.shape[0]
    width = image.shape[1]
    image = cv2.resize(image,(round(width/4), round(height/4)))
    
    image_copy1 = image.copy()
    image_copy2 = image.copy()
    image_copy3 = image.copy()
    image_copy4 = image.copy()
    
    #グレースケール化
    image_gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
    
    #閾値処理
    ret,thresh = cv2.threshold(image_gray,95,255,cv2.THRESH_BINARY)
    
    #輪郭検出 (cv2.RETR_EXTERNAL)
    contours1, hierarchy1 = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    print(f"EXTERNAL: {hierarchy1}")
    contours2, hierarchy2 = cv2.findContours(thresh, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    print(f"LIST: {hierarchy2}")
    contours3, hierarchy3 = cv2.findContours(thresh, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
    print(f"CCOMP: {hierarchy3}")
    contours4, hierarchy4 = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    print(f"TREE: {hierarchy4}")
    
    #輪郭の描画
    image_1 = cv2.drawContours(image_copy1, contours1, -1, (0, 255, 0), 2, cv2.LINE_AA)
    image_2 = cv2.drawContours(image_copy2, contours2, -1, (0, 255, 0), 2, cv2.LINE_AA)
    image_3 = cv2.drawContours(image_copy3, contours3, -1, (0, 255, 0), 2, cv2.LINE_AA)
    image_4 = cv2.drawContours(image_copy4, contours4, -1, (0, 255, 0), 2, cv2.LINE_AA)
    
    #実行結果
    cv2.imshow('cv2.RETR_EXTERNAL', image_1)
    cv2.imshow('cv2.RETR_LIST', image_2)
    cv2.imshow('cv2.RETR_CCOMP', image_3)
    cv2.imshow('cv2.RETR_TREE', image_4)
    
    
    cv2.imshow('Original', image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

    OpenCVの学習におすすめ書籍

    PythonでOpenCV始めてみようという方におすすめなのが以下書籍です。実装例も豊富なので、1からコードを書かずとも学習を進めることができます。

    オライリーの1冊は読み物というより辞書としての利用におすすめです。お値段結構しますが、細かい情報までしっかりと詰め込まれています。

    まとめ

    findContours()の引数について解説しました。

    で、結局どれがいいの?と聞かれると困ってしまいますが、cv2.RETR_EXTERNALやcv2.RETR_LISTあたりがよく使われているのでしょうか?

    findContours() の引数の意味は何だ?という疑問の解消に繋がれば幸いです。

    OpenCVの基礎を身につけるためのロードマップは以下を参考にしてください。

    ではでは👋