画像処理ソリューション
これを見れば画像処理の入門から基礎~応用まで全てがわかるのを目指して!
   
翻訳(Translate)

プロフィール

Akira

ニックネーム:Akira
東京都の町田事業所に勤務
画像処理ソフトの開発を行っています。リンクフリーです!
詳細プロフィールは こちら
お問い合わせは、こちら↓

【補助HP】
画像処理ソリューションWeb版 【Newブログ】
イメージングソリューション

スポンサーリンク


カテゴリ

最近のコメント

カレンダー

09 | 2017/10 | 11
S M T W T F S
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31 - - - -

趣味のブログ

iPhone萬歳!
iPhoneの情報いろいろ。
ブログ学習帳
ブログ、SEO、アフィリエイト情報など(まだまだこれから)
俺流クラフト日記
ハンドメイド作品の記録(現在、放置中)

スポンサーリンク 最近の記事
(09/18)  計測測定展に光切断のデモを出展しました
(08/17)  ディジタル画像技術事典200に記事が載りました
(06/09)  光切断を画像センシング展で公開
(05/14)  中国(上海)へ行って来ました
(04/12)  韓国へ行って来ました
(03/10)  私の求める新人像
(01/18)  エレクトロテストジャパンにカラー光切断法のデモを出展しました。
(12/23)  ユニークアクセス200万達成!
(12/10)  【カラー光切断法】YouTube動画まとめ
(11/04)  国際画像機器展2014にカラー光切断法を出展します。
(10/05)  第25回コンピュータビジョン勉強会@関東に参加してきました。
(09/08)  フーリエ変換の記事を追加しました。
(08/09)  【画像処理】ランキング低下中
(07/06)  記事の更新が停滞中...
(06/08)  画像センシング展2014でカラー光切断法のデモを行います。
(05/17)  カラー光切断法の動画を公開しました。
(04/30)  ソニーα NEX-5Rで星空撮影
(04/10)  カラー光切断法の取込結果を追加しました
(03/08)  Korea Vision Show 2014へ行ってきました
(02/05)  フーリエ変換シリーズを始めます。
(01/06)  2014年、あけましておめでとうございます。
(12/04)  カラー光切断法を公開(国際画像機器展2013にて)
(11/13)  国際画像機器展2013に出展します
(10/14)  「画像処理のためのC#」はじめます。
(09/16)  【C#,VB.NET】高速描画コントロールをバージョンアップしました。
(09/04)  拡大鏡に輝度値表示、ルーラー機能を追加した個人ツールを公開
(08/05)  7月の拍手Top5
(07/06)  2013年6月人気記事Top5
(05/12)  SONY α NEX-5Rレビュー
(04/24)  SONY α NEX-5RY購入

【OpenCV】輪郭処理(cvFindContours)を使ったラベリング処理

メインページOpenCV

OpenCVには標準的にはcvLabelingのようなラベリングの関数は無いので、

 ラベリングクラス(大阪大学の井村先生によるもの)
 Blob extraction library

を使いましょう!というのが一般的になってきているように思いますが、最初のラベリングクラスでは、画像の幅の画素数が4の倍数で無い場合、うまく動作してくれなかった気がするし、Blob extraction library は英語なので良く分からないし・・・
ということで、OpenCVに標準的にある輪郭処理の関数【cvFindContours】を使ってラベリングの処理ができないか?調べてみました。

結果、OpenCVの関数だけで、こんな感じ↓まで出来ました。

処理前ラベリング処理後
OpenCVでラベリングOpenCVでラベリング

何はともあれ、まずは分かりづらいcvFindContoursの関数です。
関数の定義は

int cvFindContours(
          CvArr* image,                        // 入力画像(8Bitモノクロ)
          CvMemStorage* storage,        // 抽出された輪郭を保存する領域
          CvSeq** first_contour,            // 一番最初の輪郭(ツリー構造を持つ)へのポインタ
          int header_size = sizeog(CvContour),  // シーケンスのヘッダサイズ
          int mode = CV_RETR_LIST,   // 抽出モード
          int method = CV_CHAIN_APPROX_SIMPLE,   // 近似手法
          CvPoint offset = cvPoint(0, 0)  // オフセット
          );

となっているのですが、とにかく分かりづらい抽出モード( mode )の理解から。

まずは mode = CV_RETR_TREE を例に取って説明したいと思います。

処理前の画像↓
OpenCVでラベリング

この画像の輪郭を外側から順に追いかけると、

  の内側に 、  の内側に  と  ・・・ というような構造になっています。

OpenCVでラベリング

この構造をツリーのように階層構造で表すと、

階層
(Level)
輪郭構造
OpenCVでラベリングOpenCVでラベリング

のように、一番外側に白の輪郭(Level = 1)があり、その内側に黒の輪郭(Level = 2)、さらにその内側に白の輪郭(Level = 3)・・・と、一番外側の白の輪郭から始まり、その輪郭の内側に黒の輪郭→白の輪郭→黒の輪郭→白の輪郭・・・とレベルが大きくなるにつれ、さらに内側に輪郭が存在しています。
この構造を保持しているのが CvSeq** first_contour となります。
このfirst_contourには一番最初の輪郭を示すポインタが格納されています。

同じ階層(Level)にある別の輪郭を参照したい場合は
  CvSeq* contour = first_contour->h_next;

とすれば、同じ階層にある輪郭を参照できます。
さらに
 contour = contour->h_next;
とすれば、さらに次の輪郭を参照出来ます。 
もし、contourNULL になったら同じ階層に、同じ親を持つ別の輪郭は無い事を意味しています。

同じ様に
 contour = contour->v_next;
とすれば、現在の輪郭のさらに内側にある輪郭へとポインタが移動します。

このようにh_next、v_nextを使うと、全ての輪郭を構造的に参照することが可能となります。

同様に
mode = CV_RETR_EXTERNAL  の場合、
OpenCVでラベリング

階層
(Level)
輪郭構造
OpenCVでラベリングOpenCVでラベリング

のように、一番外側の白の輪郭のみを取得します。

mode = CV_RETR_LIST  の場合、
OpenCVでラベリング

階層
(Level)
輪郭構造
OpenCVでラベリングOpenCVでラベリング

のように、白の輪郭、黒の輪郭、内側、外側関係なく、同じ階層で輪郭が取得されます。

mode = CV_RETR_CCOMP  の場合、
OpenCVでラベリング

階層
(Level)
輪郭構造
OpenCVでラベリングOpenCVでラベリング

のように、白の輪郭の一つ下のレベルに黒の輪郭を持つ構造となります。
ただし、ここで大事なのは白の輪郭のさらに内側にある白の輪郭(上図の5や6)も同じ階層となるので、ご注意下さい。

※modeの設定は共通して最初の階層の輪郭は白色の輪郭になっているようです。
 そのため、白色の地に黒色の輪郭のある画像を処理すると、最初の輪郭は画像全体となるのでご注意下さい。

ということで、cvFindContours関数を使って輪郭を描画するプログラムはこんな感じ↓になります。

////////////////////////////////////////////////////////////////
//  次の輪郭を描画する。
////////////////////////////////////////////////////////////////
void DrawNextContour(      
                IplImage *img,    //ラベリング結果を描画するIplImage(8Bit3chカラー)
                CvSeq *Contour, //輪郭へのポインタ
                int Level            //輪郭のレベル(階層)
                ){

      // 輪郭を描画する色の設定
      CvScalar ContoursColor;

      if ((Level % 2) == 1){
            //白の輪郭の場合、赤で輪郭を描画
            ContoursColor = CV_RGB( 255, 0, 0 );
      }else{
            //黒の輪郭の場合、青で輪郭を描画
            ContoursColor = CV_RGB( 0, 0, 255 );
      }
             
      //輪郭の描画
      cvDrawContours( img, Contour, ContoursColor, ContoursColor, 0, 2);

      //各種輪郭の特徴量の取得
      GetContourFeature(Contour); //←オリジナル関数です。(詳細は後述)

      if (Contour->h_next != NULL)
            //次の輪郭がある場合は次の輪郭を描画
            DrawNextContour(img, Contour->h_next, Level);

      if (Contour->v_next != NULL)
            //子の輪郭がある場合は子の輪郭を描画
            DrawChildContour(img, Contour->v_next, Level + 1);
}

////////////////////////////////////////////////////////////////
//  子の輪郭を描画する。
////////////////////////////////////////////////////////////////
void DrawChildContour(      
                IplImage *img,    //ラベリング結果を描画するIplImage(8Bit3chカラー)
                CvSeq *Contour, //輪郭へのポインタ
                int Level             //輪郭のレベル(階層)
                ){
             
      // 輪郭を描画する色の設定
      CvScalar ContoursColor;

      if ((Level % 2) == 1){
            //白の輪郭の場合、赤で輪郭を描画
            ContoursColor = CV_RGB( 255, 0, 0 );
      }else{
            //黒の輪郭の場合、青で輪郭を描画
            ContoursColor = CV_RGB( 0, 0, 255 );
      }
             
      //輪郭の描画
      cvDrawContours( img, Contour, ContoursColor, ContoursColor, 0, 2);
             
      //各種輪郭の特徴量の取得
      GetContourFeature(Contour); //←オリジナル関数です。(詳細は後述)

      if (Contour->h_next != NULL)
            //次の輪郭がある場合は次の輪郭を描画
            DrawNextContour(img, Contour->h_next, Level);
      if (Contour->v_next != NULL)
            //子の輪郭がある場合は子の輪郭を描画
            DrawChildContour(img, Contour->v_next, Level + 1);
}
 
////////////////////////////////////////////////////////////////
//  ラベリング処理
////////////////////////////////////////////////////////////////
void cv_Labelling(                     
             IplImage *src_img,       //入力画像(8Bitモノクロ)
             IplImage *dst_img        //出力画像(8Bit3chカラー)
             ) {
             
      CvMemStorage *storage = cvCreateMemStorage (0);
      CvSeq *contours = NULL;
             
      if (src_img == NULL)   
            return; 
         
      // 画像の二値化【判別分析法(大津の二値化)】
      cvThreshold (src_img, src_img, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);

      // 輪郭の検出(戻り値は取得した輪郭の全個数)
      int find_contour_num = cvFindContours (
                     src_img,                     // 入力画像
                     storage,                      // 抽出された輪郭を保存する領域
                     &contours,                  // 一番外側の輪郭へのポインタへのポインタ
                     sizeof (CvContour),      // シーケンスヘッダのサイズ
                     CV_RETR_TREE,       // 抽出モード
                                               
      // CV_RETR_EXTERNAL - 最も外側の輪郭のみ抽出
                                               
      // CV_RETR_LIST - 全ての輪郭を抽出し,リストに追加
                                               
      // CV_RETR_CCOMP - 全ての輪郭を抽出し,
                                                      // 二つのレベルを持つ階層構造を構成する.
                                                      // 1番目のレベルは連結成分の外側の境界線,
                                                      // 2番目のレベルは穴(連結成分の内側に存在する)の境界線.
                                                      // CV_RETR_TREE - 全ての輪郭を抽出し,
                                                      // 枝分かれした輪郭を完全に表現する階層構造を構成する.
                    
CV_CHAIN_APPROX_NONE    // CV_CHAIN_APPROX_SIMPLE:輪郭の折れ線の端点を取得
                                                                   // CV_CHAIN_APPROX_NONE: 輪郭の全ての点を取得
                                                                   // Teh-Chinチェーンの近似アルゴリズム中の一つを適用する
                                                                   // CV_CHAIN_APPROX_TC89_L1
                                                                   // CV_CHAIN_APPROX_TC89_KCOS
                     );

      if (contours != NULL){
            //処理後画像を0(黒)で初期化
            cvSet(dst_img, CV_RGB( 0, 0, 0 ));
            //輪郭の描画
            DrawNextContour(dst_img, contours, 1);
      }

      //メモリストレージの解放
      cvReleaseMemStorage (&storage);
}

という感じで、全ての輪郭を描画する事が出来ます。
最初に DrawNextContour(dst_img, contours, 1); と最初の輪郭を描画して、残りの輪郭は再帰的に描画しています。

輪郭は
  cvDrawContours( img, Contour, ContoursColor, ContoursColor, 0, 2);
のように
線幅2で描画していますが、これを
  cvDrawContours( img, Contour, ContoursColor, ContoursColor, 0, CV_FILLED);
とすると輪郭の内側が塗りつぶされます。

上記サンプルプログラムの途中で出てきたGetContourFeature関数は、輪郭のポインタを用いて、各種特徴量を計算しています。通常はこれらの特徴量を用いてフィルタ処理や検査などを行って下さい。(ここでは特徴量を拾うだけです。)

//各種輪郭の特徴量の取得
void GetContourFeature(CvSeq *Contour){
         //面積
         double Area = fabs(cvContourArea(Contour, CV_WHOLE_SEQ));
         //周囲長
         double Perimeter = cvArcLength(Contour);
         //円形度
         double CircleLevel = 4.0 * CV_PI * Area / (Perimeter * Perimeter);

         //傾いていない外接四角形領域(フィレ径)
         CvRect rect = cvBoundingRect(Contour);
         //輪郭を構成する頂点座標を取得
         for ( int i = 0; i < Contour->total; i++){
                  CvPoint *point = CV_GET_SEQ_ELEM (CvPoint, Contour, i);
         }
}

上記プログラムで注意が必要なのは、輪郭から面積を計算する関数cvContourAreaは、下図の様に輪郭線の内側の面積を計算します。(画素数ではありません。
下図の例では面積は 17.5 となります。
輪郭の内側の穴の面積は考慮されないので、穴の部分を除外したい場合はv_nextで一つ下の階層にある黒の輪郭を全て取得し、黒の面積を白の面積から引いて下さい。
OpenCVでラベリング

【輪郭座標について】
CV_GET_SEQ_ELEMマクロで取得している輪郭を構成する座標はcvFindContours関数の6番目の引数methodの設定できまります。

method = CV_CHAIN_APPROX_NONE  の場合
OpenCVでラベリング

上図のように輪郭を構成している座標全てを取得します。

method = CV_CHAIN_APPROX_SIMPLE  の場合
OpenCVでラベリング

上図のように輪郭を構成している折れ線の角の座標を取得します。

method = CV_CHAIN_APPROX_TC89_L1 
        もしくは     CV_CHAIN_APPROX_TC89_KCOS  の場合
OpenCVでラベリング

上図のように輪郭を構成している座標をTeh-Chinチェーンの近似アルゴリズムに基づいて近似した線の折れ線の角の座標を取得します。
との事ですが、詳細は良く分かりませんでした...(上図も実際の結果と異なるかも?)


一番最初に示したラベリング処理例の結果は、ここに記載したサンプルプログ ラムでは、同じ結果になりません。
 輪郭を描画している関数(cvDrawContours)で線幅を CV_FILLEDにして、輪郭の内側を塗りつぶし、輪郭が白の輪郭の場合は、適当な色で、黒の輪郭の場合は黒で塗りつぶすと、この結果になります。


OpenCVでは他にも、いろいろな特徴量を計算する関数が用意されています。
おそらくcvFindContours関数を使ってラベリング処理をした方が、いろんな使い道が考えられると思います。
 
 

Loading...
スポンサーリンク

この記事に対するコメント
ありがとうございます
ご回答ありがとうございます。
OpenCVソースコード(cvFindNextContour)を見てみましたが・・・良く分かりません。
まぁ、ID付与はどうにかなりそうですので、とりあえずこのまま進めようと思います。
それと、cvReduce関数でも文字の切出しは可能でした。
ただ、今回は切出した文字の面積や輪郭周囲長なども検査に使用するので、分かりやすいcvFindContours関数を使おうと思います。
(他の方に説明するときもわりと理解できる処理ですので)
また、何かありましたらよろしくお願いいたします。
【2010/09/14 13:03】 URL | OPENCV勉強中 #DL0dExLA [ 編集]

Re: 抽出された輪郭の順番について
はじめまして。コメント頂きありがとうございました。
お問い合わせ頂いた内容ですが、正直、私も知らないので、同じようにcvBoundingRectで取得できるRectの左上の座標と輪郭の検出する順番を確認してみましたが、やはり領域の左上の座標が画像の右下から順に入っていくようでした。
OpenCVのソースコード(cvcontours.cpp)も確認してみましたが、最初にcvStartFindContours関数が呼び出され、次にcvFindNextContour関数、最後にcvEndFindContours関数が呼び出されていましたが、特に輪郭の位置を並び替えている様子は無さそうです。
詳しく知りたい場合はcvFindNextContour関数を見ると分かりそうです...(ちょっと複雑です)
もし、文字の領域を切出すだけなら、古典的な方法ですが、cvReduce関数を使って画像の輝度値を縦、横方向に積算して、輝度値の山や谷の部分から領域を分割するのでも出来そうですが、ダメでしょうかね(未評価です)
以上、参考になれば幸いです。
【2010/09/13 23:47】 URL | Akira #- [ 編集]

抽出された輪郭の順番について
はじめまして。OPENCVを勉強中のものです。
ラベリング関係で情報収集していたら、こちらのサイトにたどり着きました。
私もcvFindContours関数を使用し、ラベリングを試みていたのですが、1つ分からないところがあります。
cvFindContoursで抽出された輪郭は、どういった順番で抽出されるのでしょうか?
実は、商品パッケージの文字検査の際に指定した矩形領域内の文字を一度に切出し、それぞれの文字にIDを付与し、各特徴量を利用した欠陥検査をしようとしているところです。
基本となるアルゴリズムは出来たのですが、抽出された各文字(輪郭点列を包含する矩形でROI領域を定義し切出している)にIDを付与する際に、抽出順番がこちらの意図したものと違うので困っています。
具体的には、cvBoundingRectで抽出された矩形を描画する際に、同時にcvPutTextで矩形にIDを付けて、輪郭数の数だけ描画しているのですが、画像右下の矩形からIDが振られていきます。
イメージ的には、左上から輪郭をスキャンしていると思っていたのですが・・・。
cvFindContoursで抽出された輪郭は、どのようにソートされているのか、お分かりでしたらご教授ください。
以上、よろしくお願いいたします。
【2010/09/13 13:25】 URL | OPENCV勉強中 #DL0dExLA [ 編集]


この記事に対するコメントの投稿














管理者にだけ表示を許可する


この記事に対するトラックバック
トラックバックURL
→http://imagingsolution.blog107.fc2.com/tb.php/202-db492bc6
この記事にトラックバックする(FC2ブログユーザー)

現在の閲覧者数: / 合計