画像分割,領域結合,輪郭検出
■ 画像の領域分割
画像による認識やトラッキングを行なうために,得られた画像を意味のある領域に分割する事は重要な技術である.OpenCVにはこのような領域分割を行なうための手法として,画像ピラミッドによる画像のセグメント化・平均値シフト法による画像のセグメント化・Watershedアルゴリズムによる領域分割が実装されている.サンプル
画像ピラミッドを用いた画像の領域分割 cvPyrSegmentation
レベルを指定して画像ピラミッドを作成し,その情報を用いて画像のセグメント化を行なう.
サンプルコード
#include <cv.h> #include <highgui.h> int main (int argc, char **argv) { int level = 4; double threshold1, threshold2; IplImage *src_img = 0, *dst_img; CvMemStorage *storage = 0; CvSeq *comp = 0; CvRect roi; // (1)画像の読み込み if (argc >= 2) src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR); if (src_img == 0) exit (-1); // (2)領域分割のためにROIをセットする roi.x = roi.y = 0; roi.width = src_img->width & -(1 << level); roi.height = src_img->height & -(1 << level); cvSetImageROI (src_img, roi); // (3)分割結果画像出力用の画像領域を確保し,領域分割を実行 dst_img = cvCloneImage (src_img); storage = cvCreateMemStorage (0); threshold1 = 255.0; threshold2 = 50.0; cvPyrSegmentation (src_img, dst_img, storage, &comp, level, threshold1, threshold2); // (4)入力画像と分割結果画像の表示 cvNamedWindow ("Source", CV_WINDOW_AUTOSIZE); cvNamedWindow ("Segmentation", CV_WINDOW_AUTOSIZE); cvShowImage ("Source", src_img); cvShowImage ("Segmentation", dst_img); cvWaitKey (0); cvDestroyWindow ("Source"); cvDestroyWindow ("Segmentation"); cvReleaseImage (&src_img); cvReleaseImage (&dst_img); cvReleaseMemStorage (&storage); return 0; }
// (1)画像の読み込み
コマンド引数で指定されたファイル名の画像(入力画像)をオープンし,関数
cvLoadImage()で読み込む.
// (2)領域分割のためにROIをセットする
cvPyrSegmentationは,2(ピラミッドのレベル)で割り切れる画像サイズしか
処理できない(エラーが発生する).そのため,指定したレベルに対応するROIサイズを
入力画像にセットする必要がある.サンプルプログラムでは,指定したレベル数だけ1を左に
シフトし(2(指定したレベル)を計算),その後2の補数を取る事で
ビットマスクを作成し,元画像のwidth, heightとのANDを取り,割り切れる画像サイズ
を計算する.
// (3)分割結果画像出力用の画像領域を確保し,領域分割を実行
入力画像と同じサイズ,チャンネル数,デプスの出力用画像dst_imgを,cvCloneImage()で確保し,
2つの閾値をセットして,領域分割を実行する.
// (4)入力画像と分割結果画像の表示
ウィンドウを生成し,入力画像,分割結果画像を表示し,何かキーが押されるまで待つ.
実行結果例
入力画像と領域分割結果
[左から] 入力画像,領域分割結果(threshold2=25),領域分割結果(threshold2=50),領域分割結果(threshold2=75),領域分割結果(threshold2=100)
平均値シフト法による画像のセグメント化 cvPyrMeanShiftFiltering
平均値シフト法による画像のセグメント化を行う.
サンプルコード
#include <cv.h> #include <highgui.h> int main (int argc, char **argv) { int level = 2; IplImage *src_img = 0, *dst_img; CvRect roi; // (1)画像の読み込み if (argc >= 2) src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR); if (src_img == 0) exit (-1); if (src_img->nChannels != 3) exit (-1); if (src_img->depth != IPL_DEPTH_8U) exit (-1); // (2)領域分割のためにROIをセットする roi.x = roi.y = 0; roi.width = src_img->width & -(1 << level); roi.height = src_img->height & -(1 << level); cvSetImageROI (src_img, roi); // (3)分割結果画像出力用の画像領域を確保し,領域分割を実行 dst_img = cvCloneImage (src_img); cvPyrMeanShiftFiltering (src_img, dst_img, 30.0, 30.0, level, cvTermCriteria (CV_TERMCRIT_ITER + CV_TERMCRIT_EPS, 5, 1)); // (4)入力画像と分割結果画像の表示 cvNamedWindow ("Source", CV_WINDOW_AUTOSIZE); cvNamedWindow ("MeanShift", CV_WINDOW_AUTOSIZE); cvShowImage ("Source", src_img); cvShowImage ("MeanShift", dst_img); cvWaitKey (0); cvDestroyWindow ("Source"); cvDestroyWindow ("MeanShift"); cvReleaseImage (&src_img); cvReleaseImage (&dst_img); return 0; }
// (1)画像の読み込み
コマンド引数で指定されたファイル名の画像(入力画像)をオープンし,関数
cvLoadImage()で読み込む.
// (2)領域分割のためにROIをセットする
cvPyrMeanShiftFilteringも上で示したcvPyrSegmentationと同様,
2(ピラミッドのレベル)で割り切れる画像サイズしか処理できない(エラーが発生する).
そのため,指定したレベルに対応するROIサイズを入力画像にセットする必要がある.
サンプルプログラムでは,指定したレベル数だけ1を左にシフトし(2(指定したレベル)を
計算),その後2の補数を取る事でビットマスクを作成し,元画像のwidth, heightとのANDを取り,
割り切れる画像サイズを計算する.
// (3)分割結果画像出力用の画像領域を確保し,領域分割を実行
入力画像と同じサイズ,チャンネル数,デプスの出力用画像dst_imgを,cvCloneImage()で確保し,
2つの閾値をセットして,領域分割を実行する.
// (4)入力画像と分割結果画像の表示
ウィンドウを生成し,入力画像,分割結果画像を表示し,何かキーが押されるまで待つ.
実行結果例
入力画像とセグメント化結果
[左から] 入力画像,セグメント化結果
Watershedアルゴリズムによる画像の領域分割 cvWatershed
マウスで円形のマーカー(シード領域)の中心を指定し,複数のマーカーを設定する.
このマーカを画像のgradientに沿って広げて行き,gradientの高い部分に出来る境界を元に領域を分割する.
領域は,最初に指定したマーカーの数に分割される.
このサンプルコードは,マウスイベントを処理するための関数(on_mouse)を含んでいる.
サンプルコード
#include <cv.h> #include <highgui.h> IplImage *markers = 0, *dsp_img = 0; /* マウスイベント用コールバック関数 */ void on_mouse (int event, int x, int y, int flags, void *param) { int seed_rad = 20; static int seed_num = 0; CvPoint pt; // (1)クリックにより中心を指定し,円形のシード領域を設定する if (event == CV_EVENT_LBUTTONDOWN) { seed_num++; pt = cvPoint (x, y); cvCircle (markers, pt, seed_rad, cvScalarAll (seed_num), CV_FILLED, 8, 0); cvCircle (dsp_img, pt, seed_rad, cvScalarAll (255), 3, 8, 0); cvShowImage ("image", dsp_img); } } /* メインプログラム */ int main (int argc, char **argv) { int *idx, i, j; IplImage *src_img = 0, *dst_img = 0; // (2)画像の読み込み,マーカー画像の初期化,結果表示用画像領域の確保を行なう if (argc >= 2) src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_ANYDEPTH | CV_LOAD_IMAGE_ANYCOLOR); if (src_img == 0) exit (-1); dsp_img = cvCloneImage (src_img); dst_img = cvCloneImage (src_img); markers = cvCreateImage (cvGetSize (src_img), IPL_DEPTH_32S, 1); cvZero (markers); // (3)入力画像を表示しシードコンポーネント指定のためのマウスイベントを登録する cvNamedWindow ("image", CV_WINDOW_AUTOSIZE); cvShowImage ("image", src_img); cvSetMouseCallback ("image", on_mouse, 0); cvWaitKey (0); // (4)watershed分割を実行する cvWatershed (src_img, markers); // (5)実行結果の画像中のwatershed境界(ピクセル値=-1)を結果表示用画像上に表示する for (i = 0; i < markers->height; i++) { for (j = 0; j < markers->width; j++) { idx = (int *) cvPtr2D (markers, i, j, NULL); if (*idx == -1) cvSet2D (dst_img, i, j, cvScalarAll (255)); } } cvNamedWindow ("watershed transform", CV_WINDOW_AUTOSIZE); cvShowImage ("watershed transform", dst_img); cvWaitKey (0); cvDestroyWindow ("watershed transform"); cvReleaseImage (&markers); cvReleaseImage (&dsp_img); cvReleaseImage (&src_img); cvReleaseImage (&dst_img); return 1; }
// (1)クリックにより中心を指定し,円形のシード領域を設定する
入力画像を表示したウィンドウ上で,マウスクリックのイベントが発生すると,シードの数(seed_num)を
一つ増やし,マーカー画像(markers)上に,既定半径で色をseed_numである塗りつぶし円を描画する.
この,markerがwatershed分割のシードとなる.
何かキー入力を行なうまで,シードを追加する事ができる.
// (2)画像の読み込み,マーカー画像の初期化,結果表示用画像領域の確保を行なう
コマンド引数で指定されたファイル名の画像(入力画像)をオープンし,関数
cvLoadImage()で読み込む.同時に表示用画像として入力画像をコピーする.
マーカー画像として,32ビットシングルチャンネルの画像領域(markers)を確保する.
このマーカー画像では,watershed境界が-1,関係が未知の領域は0,それ以外では属する
領域番号が入っている.
// (3)入力画像を表示しシードコンポーネント指定のためのマウスイベントを登録する
入力画像を表示し,このウィンドウ上でのマウスイベントに対するコールバック関数(on_mouse)を指定する.
// (4)watershed分割を実行する
マーカー画像設定完了のキー入力を待ち,cvWatershed()を実行する.
// (5)実行結果の画像中のwatershed境界(ピクセル値=-1)を結果表示用画像上に表示する
分割結果を保持しているマーカー画像をスキャン(cvPtr2Dで指定した配列要素へのポインタが返ってくる)し,
値が-1のピクセルに対応する結果表示用画像(dst_img)のピクセル値を255(白)にcvSet2D()を用いて設定し,結果を表示する.
実行結果例
入力画像と領域分割結果
[左から] 入力画像,入力画像上に表示したマーカー領域,領域分割結果
■ 画像の輪郭検出
OpenCVでは,輪郭はさまざまなデータ構造により管理される. つまり,全ての輪郭が単なるリストで表現されることもあれば,親子関係を保持したツリー構造で表現されることもある. 輪郭を利用した処理(包含矩形,近似,比較)の解説ついては別の章に譲り, ここでは,輪郭の検出と走査について簡単に説明する.サンプル
輪郭の検出と描画 cvFindContours
画像中から輪郭を検出し,-1〜+1までのレベルにある輪郭を描画する
サンプルコード
#include <cv.h> #include <highgui.h> int main (int argc, char *argv[]) { int i; IplImage *src_img = 0, **dst_img; IplImage *src_img_gray = 0; IplImage *tmp_img; CvMemStorage *storage = cvCreateMemStorage (0); CvSeq *contours = 0; int levels = 0; if (argc >= 2) src_img = cvLoadImage (argv[1], CV_LOAD_IMAGE_COLOR); if (src_img == 0) return -1; src_img_gray = cvCreateImage (cvGetSize (src_img), IPL_DEPTH_8U, 1); cvCvtColor (src_img, src_img_gray, CV_BGR2GRAY); tmp_img = cvCreateImage (cvGetSize (src_img), IPL_DEPTH_8U, 1); dst_img = (IplImage **) cvAlloc (sizeof (IplImage *) * 3); for (i = 0; i < 3; i++) { dst_img[i] = cvCloneImage (src_img); } // (1)画像の二値化 cvThreshold (src_img_gray, tmp_img, 120, 255, CV_THRESH_BINARY); // (2)輪郭の検出 cvFindContours (tmp_img, storage, &contours, sizeof (CvContour), CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE); // (3)輪郭の描画 cvDrawContours (dst_img[0], contours, CV_RGB (255, 0, 0), CV_RGB (0, 255, 0), levels - 1, 2, CV_AA, cvPoint (0, 0)); cvDrawContours (dst_img[1], contours, CV_RGB (255, 0, 0), CV_RGB (0, 255, 0), levels, 2, CV_AA, cvPoint (0, 0)); cvDrawContours (dst_img[2], contours, CV_RGB (255, 0, 0), CV_RGB (0, 255, 0), levels + 1, 2, CV_AA, cvPoint (0, 0)); // (4)画像の表示 cvNamedWindow ("Level:-1", CV_WINDOW_AUTOSIZE); cvShowImage ("Level:-1", dst_img[0]); cvNamedWindow ("Level:0", CV_WINDOW_AUTOSIZE); cvShowImage ("Level:0", dst_img[1]); cvNamedWindow ("Level:1", CV_WINDOW_AUTOSIZE); cvShowImage ("Level:1", dst_img[2]); cvWaitKey (0); cvDestroyWindow ("Level:-1"); cvDestroyWindow ("Level:0"); cvDestroyWindow ("Level:1"); cvReleaseImage (&src_img); cvReleaseImage (&src_img_gray); cvReleaseImage (&tmp_img); for (i = 0; i < 3; i++) { cvReleaseImage (&dst_img[i]); } cvFree (dst_img); cvReleaseMemStorage (&storage); return 0; }
// (1)画像の二値化
読み込んだ画像を二値化する.関数cvFindContours()は,処理対象の画像を二
値画像として扱う(値が0以外のピクセルは"1"、0のピクセルは"0"とする)
ためである.
// (2)輪郭の検出
二値化された画像から,輪郭を検出する.5番目の引数では,輪郭の抽出モードを指定する.
ここでは,CV_RETR_TREE を指定し,
全ての輪郭を抽出し,枝分かれした輪郭を完全に表現する階層構造を構成する.
また,6番目の引数は,輪郭の近似手法を表しており,
CV_CHAIN_APPROX_SIMPLEが指定された場合は,
水平・垂直・斜めの線分が圧縮され,それぞれの端点のみが点列として残る.
その他のモード,近似手法については,リファレンス マニュアルを参照すること.
// (3)輪郭の描画
関数cvDrawContours()を用いて,輪郭を描画する.今回は,以下のように-1,0,1の三つのレベルを指定して輪郭を描画している.
-1:2番目の引数で指定した輪郭と,その子のレベル0までの輪郭(つまり,ひとつ下の階層の輪郭)が描画される 0:2番目の引数で指定した輪郭のみが描画される +1:2番目の引数で指定した輪郭と,同レベルの輪郭(つまり,同じ階層にある輪郭)が描画される
// (4)画像の表示
ウィンドウを生成し,3種類の輪郭抽出画像を表示して,何かキーが押されるまで待つ.
実行結果例
Level:-1 | Level:0 | Level:+1 |