株式会社インデペンデンスシステムズ横浜

システム開発エンジニアの西田五郎が運営しております。Raspberry Pi や Arduino その他新規開発案件のご依頼をお待ちしております。

OpenCV

OpenCVで輪郭抽出から隣接領域の切り出し(その1)輪郭抽出まで

投稿日:2014年11月9日 更新日:

OpenCVで画像内の輪郭抽出からその輪郭の隣接領域(四角形)を求めてその領域を切り出すという処理を作ってみました。以下の画像がその結果の例です。(※実画像サイズは大きめです。)

out1contour

ここでの処理結果画像内の緑色の線が輪郭で、一定以上の面積の輪郭を直線近似した線が青色の線です。一応面積も表示しています。以下が切り出した領域の画像の一部です。
0001

0002

0003

0004

ここではペットボトルのキャップの下が透明になっているのでペットボトル全体の輪郭ではなくキャップ部分と胴体部分を認識したようです。既に飲んだ後で空だったと思います。中身が入った状態で光の反射等があれば別の輪郭を認識したかもしれないです。漢字はそれぞれこのような結果になりました。

(※細かいことですが、このウーロン茶はローソンプライベートブランドのウーロン茶です。たまたま飲んでいただけですが、シンプルなデザインで文字も含まれていて分かりやすいと思ったので使いました。それだけです。)

処理としては以下のような流れになります。

入力画像のグレースケール化

グレースケールから2値可画像の取得

2値化画像から輪郭取得(※今回の説明はここまでです。)

輪郭を直線近似

直線近似した輪郭が一定以上の面積であれば輪郭に隣接する矩形を取得

隣接した矩形を切り出して表示

ここから実際のプログラムの説明です。開発環境とベースになるプログラムは以下のページのプログラムを元にしています。
OpenCVをWin32ベースで利用する(その2)OpenCVの組み込み

今回のプログラム全体は以下からダウンロード出来ます。(※必要な場合は用途に限らずご利用頂いて問題ありませんが、一切無保証です。弊社は一切の責任を負いません。)
ソース一式(※OpenCVを含むため61M程度のサイズです。)

起動すると以下のような画面になります。
0009

今回の処理は「輪郭取得」ボタンでの処理です。この部分のソースは以下です。ソース全体が必要な場合はお手数ですがダウンロードをして参照して下さい。

/*------------------------------------------------
 処理1
-------------------------------------------------*/
void func1(HWND hWnd)
{
	////MessageBox(hWnd, "func1", "debug", MB_OK);
	if (imgMatRead.rows == 0){
		MessageBox(hWnd, "画像ファイルが無効です", "エラー", MB_OK);
		return;
	}

	//入力画像、ここでは毎回ファイルから読み込む
	cv::Mat imgIn = cv::imread((const std::string&)szOpenFileName, 1); //3チャンネルカラー画像で読み込む;

	//グレースケール
	cv::Mat grayImage, binImage;
	cv::cvtColor(imgIn, grayImage, CV_BGR2GRAY);
	
	//2値化(※反転で結果が変わる、基本は背景が黒で物体が白)
	BOOL bInv = checkInv(hWnd);
	if (bInv){
		cv::threshold(grayImage, binImage, 0.0, 255.0, CV_THRESH_BINARY_INV | CV_THRESH_OTSU);
	}
	else{
		cv::threshold(grayImage, binImage, 0.0, 255.0, CV_THRESH_BINARY | CV_THRESH_OTSU);
	}
	cv::imshow("bin", binImage);

	//輪郭の座標リスト
	std::vector< std::vector< cv::Point > > contours;

	//輪郭取得
	////cv::findContours(binImage, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);
	cv::findContours(binImage, contours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);

	// 検出された輪郭線を緑で描画
	for (auto contour = contours.begin(); contour != contours.end(); contour++){
		cv::polylines(imgIn, *contour, true, cv::Scalar(0, 255, 0), 2);
	}	

	//輪郭の数
	int roiCnt = 0;

	//輪郭のカウント	
	int i = 0;

	for (auto contour = contours.begin(); contour != contours.end(); contour++){

		std::vector< cv::Point > approx;
		
		//輪郭を直線近似する
		cv::approxPolyDP(cv::Mat(*contour), approx, 0.01 * cv::arcLength(*contour, true), true);
		
		// 近似の面積が一定以上なら取得
		double area = cv::contourArea(approx);

		if (area > 1000.0){	
			//青で囲む場合			
			cv::polylines(imgIn, approx, true, cv::Scalar(255, 0, 0), 2);
			std::stringstream sst;
			sst << "area : " << area;
			cv::putText(imgIn, sst.str(), approx[0], CV_FONT_HERSHEY_PLAIN, 1.0, cv::Scalar(0, 128, 0));
			
			//輪郭に隣接する矩形の取得
			cv::Rect brect = cv::boundingRect(cv::Mat(approx).reshape(2));
			roi[roiCnt] = cv::Mat(imgIn, brect);

			//入力画像に表示する場合
			//cv::drawContours(imgIn, contours, i, CV_RGB(0, 0, 255), 4);

			//表示
			cv::imshow("label" + std::to_string(roiCnt+1), roi[roiCnt]);
		
			roiCnt++;

			//念のため輪郭をカウント
			if (roiCnt == 99)
			{
				break;
			}
		}

		i++;
	}

	//全体を表示する場合
	//cv::imshow("coun", imgIn);

	imgMatWrite = imgIn;
}

順番に各処理についてです。ここではOpenCVを使ったプログラミングという観点で書きます。画像処理の理論的な内容や数式については書けませんのでご了承ください。必要な場合は専門書等を参照して下さい。

2値化処理について
入力画像をグレースケールに変換して、そのグレースケールの画像を2値化します。2値化でのポイントはしきい値の決定です。要するにどこで白と黒に分けるかということですが、ここでは判別分析法(大津の2値化法)で自動で決定しています。以下の部分です。

BOOL bInv = checkInv(hWnd);
if (bInv){
    cv::threshold(grayImage, binImage, 0.0, 255.0, CV_THRESH_BINARY_INV | CV_THRESH_OTSU);
}
else{
    cv::threshold(grayImage, binImage, 0.0, 255.0, CV_THRESH_BINARY | CV_THRESH_OTSU);
}

画面上のチェックボックスで反転の指定が出来ますがこの意味については輪郭抽出のところで書きます。

ここの処理で以下の画像を処理したところ黄色の図形が消えました。もちろん固定のしきい値を指定する方法で黄色を認識することも出来ます。
contour

0010

輪郭抽出処理
次に輪郭抽出の部分です。以下の部分ですが、2値化した画像を元に輪郭を抽出します。

//輪郭の座標リスト
std::vector< std::vector< cv::Point > > contours;

//輪郭の抽出
cv::findContours(binImage, contours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);

// 検出された輪郭線を緑で描画
for (auto contour = contours.begin(); contour != contours.end(); contour++){
    cv::polylines(imgIn, *contour, true, cv::Scalar(0, 255, 0), 2);
}	

まず輪郭とはですが、画像処理的には背景と物体の見かけの境界線といった定義が出来ます。ではその背景と物体ですがどうやらOpenCV(画像処理一般)では2値化画像の黒が背景で白が物体という認識のようです。(※OpenCV関連の書籍や今回の処理結果から判断しました。)ということで、ここでは白が物体黒が背景ということですが、輪郭は見かけの境界線であり必ずしも物体の輪郭ではないというぐらいの認識で進めたいと思います。(※物体の輪郭を認識したいという前提ですが、適当に画像ソフトで書いた図形群では背景も物体も区別はないだろうということです。)

そうしますと、前出の単純な図形で図形を物体とすると反対になるのでチェックボックスで2値化の反転を使います。以下のような画像になります。これを入力に使います。反転しないで処理すると画像全体を一つの輪郭と認識します。(※黄色の図形は特に存在しないものとして進めます。)
0011

以上を踏まえて、cv::findContours の使い方です。3番目のパラメータが重要です。

詳細は下記の関連リンクのマニュアル等を参照して下さいということですが、CV_RETR_EXTERNAL を指定すると、最も外側の輪郭のみを抽出します。ということは、上記のような画像だと輪郭を抽出することが出来ますが以下のような場合は外側だけ抽出します。
test2input

このような画像で内部の輪郭も抽出する場合は、CV_RETR_EXTERNAL を使わないで CV_RETR_LISTやその他を使います。その場合は輪郭の階層構造が指定された構造で出力されます。階層構造を意識しないで輪郭だけ抽出する場合はCV_RETR_LISTでいいと思います。ここでもこれを使っています。
cv::findContours(binImage, contours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);

0015

0016

0019

cv::findContoursの4番目のパラメータは輪郭の近似手法です。ここでは、CV_CHAIN_APPROX_NONEを指定して全ての輪郭点を完全に格納しています。
cv::findContours(binImage, contours, CV_RETR_LIST, CV_CHAIN_APPROX_NONE);

cv::findContours関連リンク
cv::findContours 構造解析と形状ディスクリプタ opencv 2.2 documentation
【OpenCV】輪郭処理(cvFindContours)を使ったラベリング処理

以上が輪郭抽出の処理です。長くなってきたので続きは次回で書きます。次回は輪郭を直線近似して隣接する矩形の取得処理について書きます。

関連書籍
(※OpenCV 2 プログラミングブックは私も持っています。)

(※ここからはKindle版で英語ですが最新の情報もあるようです。また価格的にはいいかもしれないです。)


AdSense

AdSense

-OpenCV

執筆者:

関連記事

NuGetでOpenCVを導入する

Visual Studio Express 2013 for Windows Desktopの開発環境でNuGetを使ってOpenCVを導入してみました。その方法についてです。 まずは、OpenCV( …

OpenCVでのORBによる特徴点抽出とマッチング(その2)GUIの利用

OpenCVでのORBアルゴリズムによる特徴点抽出とマッチングの処理についての2回目です。前回はVisual Studio Community 2013のVisual C++コンソールアプリでORBに …

OpenCVをWin32ベースで利用する(その5)GUIコントロールの追加

OpenCVをWin32ベースで利用するの5回目です。前回の4回目で一度終了したのですが、画面上の入出力用のコントロール、つまり、ラベル、エディット、コンボ等のコントロールがなかったので追加してみまし …

OpenCVで背景差分とテスト的な動体検知

OpenCVで背景差分を試してみました。あとテスト的な動体検知も試してみました。背景差分とはあらかじめ取得した画像を背景画像として、観測時点の画像とその背景画像との差分を取ることによりその差分を前景領 …

OpenCVで輪郭抽出から隣接領域の切り出し(その3)凸包の取得

OpenCVで画像内の輪郭抽出からその輪郭の隣接領域(四角形)を求めてその領域を切り出すという処理を作ってみました。前回までのプログラムとほぼ同じですが、前回までは輪郭抽出 → 直線近似 → 隣接領域 …