OpenCVには標準的にはcvLabelingのようなラベリングの関数は無いので、
- ラベリングクラス(大阪大学の井村先生によるもの)
- Blob extraction library
を使いましょう!というのが一般的になってきているように思いますが、最初のラベリングクラスでは、画像の幅の画素数が4の倍数で無い場合、うまく動作してくれなかった気がするし、Blob extraction library は英語なので良く分からないし・・・
※追記)現在、はconnectedComponentsが使えます。
ということで、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関数を使って輪郭を描画するプログラムはこんな感じ↓になります。
// Labelling.cpp : コンソール アプリケーションのエントリ ポイントを定義します。
//
#include "stdafx.h"
//プロジェクトのプロパティ⇒C/C++⇒全般 の追加のインクルードディレクトリに
// 『C:\OpenCV2.2\include』を追加のこと
#include "opencv2\\opencv.hpp"
#ifdef _DEBUG
//Debugモードの場合
#pragma comment(lib,"C:\\OpenCV2.2\\lib\\opencv_core220d.lib")
#pragma comment(lib,"C:\\OpenCV2.2\\lib\\opencv_imgproc220d.lib")
#pragma comment(lib,"C:\\OpenCV2.2\\lib\\opencv_highgui220d.lib")
#else
//Releaseモードの場合
#pragma comment(lib,"C:\\OpenCV2.2\\lib\\opencv_core220.lib")
#pragma comment(lib,"C:\\OpenCV2.2\\lib\\opencv_imgproc220.lib")
#pragma comment(lib,"C:\\OpenCV2.2\\lib\\opencv_highgui220.lib")
#endif
// 関数宣言
void GetContourFeature(CvSeq*);
void DrawChildContour(IplImage*, CvSeq*,int);
void DrawNextContour(IplImage*, CvSeq*, int);
void cv_Labelling(IplImage*, IplImage*);
//各種輪郭の特徴量の取得
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);
}
}
void DrawChildContour( //子の輪郭を描画する。
IplImage *img, //ラベリング結果を描画するIplImage(8Bit3chカラー)
CvSeq *Contour, //輪郭へのポインタ
int Level //輪郭のレベル(階層)
){
// 領域の色
CvScalar color;
// 輪郭を描画する色の設定
CvScalar ContoursColor;
if ((Level % 2) == 1){
//白の輪郭の場合
// 領域の色
color = CV_RGB( rand()&255, rand()&255, rand()&255 );
// 輪郭の色
ContoursColor = CV_RGB( 255, 0, 0 );
}else{
//黒の輪郭の場合(内側の場合)
// 領域の色
color = CV_RGB(0, 0, 0);
// 輪郭の色
ContoursColor = CV_RGB( 0, 0, 255 );
}
//輪郭の描画
cvDrawContours( img, Contour, color, color, 0, CV_FILLED); // 領域
cvDrawContours( img, Contour, ContoursColor, ContoursColor, 0, 2); // 輪郭
//輪郭を構成する頂点座標を取得
for ( int i = 0; i < Contour->total; i++){
CvPoint *point = CV_GET_SEQ_ELEM (CvPoint, Contour, i);
//cvDrawCircle(img, *point, 3, CV_RGB(0, 255, 0));
}
//各種輪郭の特徴量の取得
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 DrawNextContour( //次の輪郭を描画する。
IplImage *img, //ラベリング結果を描画するIplImage(8Bit3chカラー)
CvSeq *Contour, //輪郭へのポインタ
int Level //輪郭のレベル(階層)
){
// 領域の色
CvScalar color;
// 輪郭を描画する色の設定
CvScalar ContoursColor;
if ((Level % 2) == 1){
//白の輪郭の場合
// 領域の色
color = CV_RGB( rand()&255, rand()&255, rand()&255 );
// 輪郭の色
ContoursColor = CV_RGB( 255, 0, 0 );
}else{
//黒の輪郭の場合(内側の場合)
// 領域の色
color = CV_RGB(0, 0, 0);
// 輪郭の色
ContoursColor = CV_RGB( 0, 0, 255 );
}
//輪郭の描画
cvDrawContours( img, Contour, color, color, 0, CV_FILLED); // 領域
cvDrawContours( img, Contour, ContoursColor, ContoursColor, 0, 2); // 輪郭
//輪郭を構成する頂点座標を取得
for ( int i = 0; i < Contour->total; i++){
CvPoint *point = CV_GET_SEQ_ELEM (CvPoint, Contour, i);
// 頂点座標の描画
//cvDrawCircle(img, *point, 3, CV_RGB(0, 255, 0));
}
//各種輪郭の特徴量の取得
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, //入力画像(8Bitモノクロ)
IplImage *dst //出力画像(8Bit3chカラー)
) {
CvMemStorage *storage = cvCreateMemStorage (0);
CvSeq *contours = NULL;
if (src == NULL)
return;
// 画像の二値化【判別分析法(大津の二値化)】
cvThreshold (src, src, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
// 輪郭の検出(戻り値は取得した輪郭の全個数)
int find_contour_num = cvFindContours (
src, // 入力画像
storage, // 抽出された輪郭を保存する領域
&contours, // 一番外側の輪郭へのポインタへのポインタ
sizeof (CvContour), // シーケンスヘッダのサイズ
CV_RETR_TREE, // 抽出モード
// CV_RETR_EXTERNAL - 最も外側の輪郭のみ抽出
// CV_RETR_LIST - 全ての輪郭を抽出し,リストに追加
// CV_RETR_CCOMP - 全ての輪郭を抽出し,二つのレベルを持つ階層構造を構成する.1番目のレベルは連結成分の外側の境界線,2番目のレベルは穴(連結成分の内側に存在する)の境界線.
// CV_RETR_TREE - 全ての輪郭を抽出し,枝分かれした輪郭を完全に表現する階層構造を構成する.
CV_CHAIN_APPROX_SIMPLE // CV_CHAIN_APPROX_SIMPLE:輪郭の折れ線の端点を取得
// CV_CHAIN_APPROX_NONE: 輪郭の全ての点を取得
// CV_CHAIN_APPROX_TC89_L1 :Teh-Chinチェーンの近似アルゴリズム中の一つを適用する
// CV_CHAIN_APPROX_TC89_KCOS
);
if (contours != NULL){
//処理後画像を0(黒)で初期化
cvZero(dst);
//輪郭の描画
DrawNextContour(dst, contours, 1);
}
//メモリストレージの解放
cvReleaseMemStorage (&storage);
}
int _tmain(int argc, _TCHAR* argv[])
{
//画像データの読込(グレースケールで読込)
IplImage* src = cvLoadImage("sample.bmp", CV_LOAD_IMAGE_GRAYSCALE);
if (src == NULL){
return 0;
}
//表示ウィンドウの作成
cvNamedWindow("src");
cvNamedWindow("dst");
//処理後画像データの確保
IplImage* dst = cvCreateImage(cvGetSize(src), src->depth, 3);
// 入力画像の表示
cvShowImage ("src", src);
// ラベリング処理(※入力画像(src)はcvFindContoursにより変更されます。)
cv_Labelling(src, dst);
// ラベリング画像の表示
cvShowImage ("dst", dst);
// キー入力待ち
cvWaitKey (0);
// 全てのウィンドウの削除
cvDestroyAllWindows();
// 画像データの解放
cvReleaseImage(&src);
cvReleaseImage(&dst);
return 0;
}
サンプルプログラムのダウンロードはこちらより
(OpenCV2.2対応。Visual Studio 2010 C++ Expressにより作成)
上記、サンプルプログラムではcvFindContours関数で輪郭情報を取得し、オリジナルのDrawNectContour関数を用いて再帰的に輪郭、および領域を描画しています。
また、GetContourFeature関数で、面積、周囲長、円形度、フィレ径、輪郭の頂点座標の計算だけをしています。この部分は必要に応じて改良してみて下さい。
ここで注意が必要なのは、輪郭から面積を計算する関数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チェーンの近似アルゴリズムに基づいて近似した線の折れ線の角の座標を取得します。
との事ですが、詳細は良く分かりませんでした...(上図も実際の結果と異なるかも?)
OpenCVでは他にも、いろいろな特徴量を計算する関数が用意されています。
おそらくcvFindContours関数を使ってラベリング処理をした方が、いろんな使い道が考えられると思うので、上記のサンプルプログラムを目的に応じて改良してみて下さい。