メインページ > OpenCV
OpenCVには標準的にはcvLabelingのようなラベリングの関数は無いので、
●ラベリングクラス(大阪大学の井村先生によるもの) ●Blob extraction library
を使いましょう!というのが一般的になってきているように思いますが、最初のラベリングクラスでは、画像の幅の画素数が4の倍数で無い場合、うまく動作してくれなかった気がするし、Blob extraction library は英語なので良く分からないし・・・ ということで、OpenCVに標準的にある輪郭処理の関数【cvFindContours】を使ってラベリングの処理ができないか?調べてみました。
結果、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 を例に取って説明したいと思います。
処理前の画像↓

この画像の輪郭を外側から順に追いかけると、
1 の内側に 3、 3 の内側に 5 と 6 ・・・ というような構造になっています。

この構造をツリーのように階層構造で表すと、
階層 (Level) | 輪郭構造 |  |  |
のように、一番外側に白の輪郭(Level = 1)があり、その内側に黒の輪郭(Level = 2)、さらにその内側に白の輪郭(Level = 3)・・・と、一番外側の白の輪郭から始まり、その輪郭の内側に黒の輪郭→白の輪郭→黒の輪郭→白の輪郭・・・とレベルが大きくなるにつれ、さらに内側に輪郭が存在しています。 この構造を保持しているのが CvSeq** first_contour となります。 このfirst_contourには一番最初の輪郭を示すポインタが格納されています。
同じ階層(Level)にある別の輪郭を参照したい場合は CvSeq* contour = first_contour->h_next;
とすれば、同じ階層にある輪郭を参照できます。 さらに contour = contour->h_next; とすれば、さらに次の輪郭を参照出来ます。 もし、contourがNULL になったら同じ階層に、同じ親を持つ別の輪郭は無い事を意味しています。
同じ様に contour = contour->v_next; とすれば、現在の輪郭のさらに内側にある輪郭へとポインタが移動します。
このようにh_next、v_nextを使うと、全ての輪郭を構造的に参照することが可能となります。
同様に mode = CV_RETR_EXTERNAL の場合、

階層 (Level) | 輪郭構造
|  |  |
のように、一番外側の白の輪郭のみを取得します。
mode = CV_RETR_LIST の場合、  階層 (Level) | 輪郭構造
|  |  | のように、白の輪郭、黒の輪郭、内側、外側関係なく、同じ階層で輪郭が取得されます。
mode = CV_RETR_CCOMP の場合、  階層 (Level) | 輪郭構造
|  |  | のように、白の輪郭の一つ下のレベルに黒の輪郭を持つ構造となります。 ただし、ここで大事なのは白の輪郭のさらに内側にある白の輪郭(上図の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で一つ下の階層にある黒の輪郭を全て取得し、黒の面積を白の面積から引いて下さい。

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

上図のように輪郭を構成している座標全てを取得します。
method = CV_CHAIN_APPROX_SIMPLE の場合  上図のように輪郭を構成している折れ線の角の座標を取得します。
method = CV_CHAIN_APPROX_TC89_L1 もしくは CV_CHAIN_APPROX_TC89_KCOS の場合  上図のように輪郭を構成している座標をTeh-Chinチェーンの近似アルゴリズムに基づいて近似した線の折れ線の角の座標を取得します。 との事ですが、詳細は良く分かりませんでした...(上図も実際の結果と異なるかも?)
※一番最初に示したラベリング処理例の結果は、ここに記載したサンプルプログ ラムでは、同じ結果になりません。 輪郭を描画している関数(cvDrawContours)で線幅を CV_FILLEDにして、輪郭の内側を塗りつぶし、輪郭が白の輪郭の場合は、適当な色で、黒の輪郭の場合は黒で塗りつぶすと、この結果になります。 | OpenCVでは他にも、いろいろな特徴量を計算する関数が用意されています。 おそらくcvFindContours関数を使ってラベリング処理をした方が、いろんな使い道が考えられると思います。
Loading...
|