データ指向設計でプロジェクトを成功に導けるのか?

思考を廻しておくために、ちょっと続きを

オブジェクト指向設計とプロジェクトの成功とは無関係なのか? | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2527

「○○設計」や「○○手法」を使う場合、因果関係は薄いよからスタートすると、「じゃあ、何を使えば成功率は高くなるのか?」ってな話になります。まぁ、直接的な利益追求の話でいえば、直近のプロジェクトが云々ということになり、では先行きの学習のほうは?ということなりますが、まぁ、直近のプロジェクトを優先してみるという仮定で。

明確な形になっているかどうかは別として、要求定義→設計→実装→試験な、感じでソフトウェア開発工程が流れていくのは確かなことで、これがサイクル的に極めて早いか(アジャイル的に)、遅いか(ウォーターフォール的に)の違いはあるものの、程よく分けておくと議論がしやすくなります。無勝手流の場合は別として。

その中で、設計をする会社・人と実装をする会社・人が別のことが多い訳で、Web系以外はなんとなくわかれているのですね、これが。この分け方の理由としては、

  • 新人だから、設計できないので、実装から始める。
  • 外注だから、お客と話せないので、実装からスタートする。
  • 金銭上の関係で、実装となる。
  • 業務理解に乏しいので、実装側となる。

というような「設計」はできないけど「実装」はできる、という現実があります。

そんな場合、設計も実装も混在した形でアジャイル式に設計そして実装をやると、混乱が生まれる、と言いますか先に書いた記事のようにマイナス要因が多くなるので却下します。
できることならば、「設計」から「実装」への向きが変わらないほうが、スムースでマイナス要因が少ないわけです。

となれば、一方向になるように設計をする(画面設計も含めて)をするのがベターな話で、思考錯誤的にオブジェクト指向設計に落とし込んだり、便利機能を盛り込んであれこれとライブラリ化したものを組み合わせてしまうよりも、そりゃあ、データベースの構造をきっちりと作りこんだ後に、CRUD に従ってデータベースアクセスをきっちりする画面、関数構造を作っていけば、おおむね一応方向になります…ってのが、私の考えです。

「おおむね」と言うのは、関数として CRUD を揃えることは可能なのですが、あれこれと便利画面を用意するとこのあたり破綻します。あれこれできる GUI 画面は一見お客に便利なように見えますが、その分、実装側に負担が大きくなるために、この「一方向」の流れが崩されてしまいます。一方向の流れが崩れると例外が発生するので、要件→設計→実装の流れに齟齬が生まれはじめ、混沌としたやり取りが発生します。そこから、不具合なり見落としがでてきて、工数に時間が掛かったり試験による品質が悪くなったりしてしまいます。

なので、先の記事のようにマイナス要因を排除するという方針に従うと、あれとして便利画面を「入れません」ってのがデータ指向による設計の基本です。決まりきった方法でしかアクセスしないという方針を、ユーザーが使う画面に押し付けるという流れですね。

いや、そりゃちょっと昨今のソフトウェアでそれはないでしょ、ってことになるので、全部をデータ指向で突き進む訳にもいかないのです。

ならば、部分的にオブジェクト指向を導入してもよいですね、ってのが最近の結論です。全体的にオブジェクト指向の設計を入れたほうが隠蔽化とか汎用性とかが良いのですが、汎用性についてはデータ指向も負けていないわけで、コーディングの手順(手続き)や画面操作は多くなるものの正確にできるという点では、プログラミング(あるいは設計時)の能力に依存しないのです(実行時のパフォーマンス云々はちょっと問題ありかもしれませんが)。

あくまで部分的に最適なものを適用する。勿論、部分的な適用であるから最適化には限界があります。すぐに、天井があって、コーディング速度や開発効率などの上限にぶつかります。このあたりが、直近のプロジェクトの利益率を最大化する、という仮定の上にある話で、この「直近の」を外さない限り、このあたりの話は堂々巡りになってしまうのかなと。逆に「直近の」を外した場合はどうなるかというと、単一プロジェクトから複数プロジェクトの話になるので、もう少し選択肢が広がります、ってな話です。

カテゴリー: 設計, プロジェクト管理 | データ指向設計でプロジェクトを成功に導けるのか? はコメントを受け付けていません

オブジェクト指向設計とプロジェクトの成功とは無関係なのか?

ちょっとリハビリテーションがてら徒然に。

結論から言うと、オブジェクト指向設計を導入したからといって、プロジェクトが成功するとは限らず、逆にプロジェクトが失敗したからといって、オブジェクト指向設計を導入しなかったから、というには言えません。当たり前ですが…因果関係はそれほど強くない。

「○○設計」なり「○○手法」というものがあって、それを導入したからと言って必ず成功するとは限らない。言えるのは、成功する確率が上がるということしか言えない。逆パターンもあれば、導入すると失敗するとは言えないけれども、失敗する確率が上がる(成功する確率が下がる)パターンもある。これは、何を言うのかというと、「○○手法」を導入する場面、場所(人員も含む)、規模など諸々の条件があって、それが有効に働く場合と、働かない場合、むしろマイナスに働いてしまう場合もある、ということ。

が、かといって「○○設計」なり「○○手法」なりを導入しなくても良いかと云えば、そうでもなく、それは導入するということに対して、現時点のプロジェクトの成功だけはなく、次のプロジェクトの成功確率を上げるなり、他社との競争力を上げるなり、個人的なプログラミング能力をあげるなり、その場の流行であったり(これはモチベーションに関わる問題であたり)するわけで、単純にプロジェクトの成功/失敗だけによる結果だけで見るのは早計であるのです。

さて、長い前置きはそれとして、現実問題として「オブジェクト指向」を導入したほうがよいのか、あるいはオブジェクト指向的なアプローチを利用したほうがよいのか、という問題に話を始めると、

  • 「オブジェクト指向」を導入するならば、良いアプローチをせよ

ということに尽きる。逆に言えば、

  • 「良いアプローチ」ができそうでなければ、そのプロジェクトに「オブジェクト指向」を導入すると失敗する確率が高くなる

ということになる。

プロジェクトマネージメント的な言い方をすると、大きなプラスを求めるのではななく、大きなマイナス(あるいは小さなマイナス)を早期に発見・対処することによって、マイナス分を減らして、全体としてプラスに傾くようにするやり方が妥当である、ということになる。

具体的に云えば、

  • 関数的なアプローチしかできない人員の場合には、「オブジェクト指向」アプローチを避ける。
  • データ指向優先で、システム構成が決まっている場合は、「オブジェクト指向」を避ける。
  • システム設計ができていない場合は、「オブジェクト指向」を避ける。
  • 共通化部分が極端に少ない場合は、「オブジェクト指向」を避ける。

という判断があって、この「避けるべき条件」が積み重なるイコール、先行きプロジェクトが危うくなる条件がある、勝ってしまう場合には、オブジェクト指向を避けて、もともとある手続き型なアプローチや、データ指向型の設計を優先させる。

ちなみに、.NET で Windows フォームを使ってアプリケーションを作る場合、最低限 UI 部分はオブジェクト指向的、かつクラスライブラリを使う部分はオブジェクト指向的になるのだが、実際にプログラマがコーディングしなければいけない手続き部分は、さほどオブジェクト指向的にはならず、VB6 の頃からある手続き型のコーディング(関数型のコーディング)になる。

逆に「良いアプローチ」がどのようなポイントを示すのかというと、

  • システム設計段階で、オブジェクト指向的なアプローチがある。
  • NUnit などの自動化のテストケースを積極的に利用する用意がある。
  • オブジェクト指向のコード粒度や複雑度の話が理解でき、実行できる。

というものがある。

要件定義からシステム設計に落とすとき(システム設計というのは、ユーザーの要件をどのようにコンピューターが実行するのか、という物理設計を示している)、ここで手続きの流れのまま設計から実装まで突き進んでしまうのか、一度、共通的な手順、今後の拡張、クラスの強度などを考え直すうえで、システム構造をクラスとして考え直す、という流ればあれば、オブジェクト指向的なアプローチを導入する価値はある。

このあたり、要件定義→設計という流れの中で、これがウォーターフォール的に進むのか、イテレーション的に進むのか、アジャイル的に進むのか、というプロジェクトの進行の問題もある。数年前、このあたりを「アーキテクト」という言い方をしていたような気もするのだが、それはバズワードだったのか?、はここでは問わない。

ひとつ言えるのは、完璧な設計は存在しないし、完璧にオブジェクト指向を進めることは不可能であるので(完璧に近づくことが可能だにしても)、何らかの形で「後戻り」なり「再設計」が必要になる。ならば、現実的に考えて、「再設計あり」でプロジェクトを進めるのも大切であろうということ。逆に、再設計されそうな部分を「予知」しておくことも大切ということ。
このあたりは、職人的な勘というか、動物的な勘というか、そういう匂いがある場所を監視しておく(常に気にしておく)こととなる。体系的に学ぶことも可能だと思うのだが…それは別の機会にでも。

NUnit などの自動化のテストケースを導入する場合、手続き型の関数アプローチでも可能ではあるのだが、ものが大きくなれば、オブジェクト指向的なアプローチでクラスを分けたほうがやりやすくなる。と言うのも、自動化をする場合、対象のメソッドを動かすための前提条件を設定することになる。この前提条件が多ければ多いほど、テストケースを作るのは大変になる。なので、どのデータを設定したあとに、どのメソッドを動かすのか、という範囲が狭まればテストケースを作るのが楽になり、結果的にコーディングをしている時間が短くなり、プロジェクトに時間の余裕が出てくる。ゆえに、プロジェクトの成功確率は高くなる。

逆に言えば、オブジェクト指向、特に隠蔽性なり疎結合の話が理解できていないと、クラス構造に分けたとしてもクラス同士の連携が多くなりすぎて、自動化のテストケースのメリットは薄れてしまう。テストケースが、テストデータに密接に繋がってしまう故に、使い捨てのデータ(テストデータのためのデータ準備)が大変になってしまう。

で、まぁ、今回の業務的なアプローチ(プログラミングを行うという意味で)としては、大きな失敗を避けることはできたものの、いまいち「効果」が実感できるところまでいかなかった、というのが不満ではあります。この手の新規アプローチの導入に関しては、「失敗を避ける」というのが第一であって、実は「成功させる」というのは二の次なんですね。導入の説明としては「成功」へ導くわけですが、実はプロジェクトが失敗してしまうと、二度とそのアプローチに手がでなくなるという大きなマイナスがあるので、なんらかの形で「失敗させない」ことが優先事項になります。
なので、結果的に言えば、成功する失敗するの要因は、オブジェクト指向の導入や、NUnit の導入によるものではなくて、「失敗しない要因を揃えていく」あるいは「失敗しそうなアプローチを避ける(避けさせる)」というのが、今回の仕事だったりして。

カテゴリー: 設計, プロジェクト管理 | オブジェクト指向設計とプロジェクトの成功とは無関係なのか? はコメントを受け付けていません

相関係数を計算を自作する(完成)

マスク画像を使えるように、テンプレートマッチの関数を自作します。
templmatch.cpp 内の元ネタを使っても良かったのですが、相加平均を計算するところが、1/w*h で固定値になっているので、マスク画像を渡すと大幅に書き換えになってしまう…というのと、行列全体をフーリエ演算対象にしてしまっているので、そもそもマスク画像を渡すことができない。ってのが自作の理由です。

相関係数を計算を自作する(途中) | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2509

前回は、RGB チャンネルを平坦に加算していたので誤検出多くなっていましたが、元の関数と同じようにチャンネル毎に相加平均を計算するという方式に変えました。すると、cv::matchTemplate と同じ結果が得られています(厳密にチェックした訳ではないので、違いはでてくるかも)。

このあたり、CV_TM_CCOEFF_NORMED を指定したときの検証用の関数を作る必要がありますね…先は長そう。

// 実画像の位置(dx,dy)に対して、相関係数を計算する
double crossCorr( const cv::Mat& img, const cv::Mat& templ, const cv::Mat& mask, int dx, int dy )
{
	int cnt = 0;
	// 対象ドット数を取得する
	for ( int y=0; y<templ.rows; ++y ) {
		for ( int x=0; x<templ.cols; ++x ) {
			if ( mask.at<char>(y,x) == 0 )
				continue;
			cnt++;
		}
	}

	// 分子(numerator)
	double numerator = 0.0;
	// 分母(denominator)
	double denominator_templ = 0.0;
	double denominator_img   = 0.0;

	// チャンネル毎(RGB)に計算する
	for ( int k=0; k<3; ++k ) {
		double templMean = 0.0;	// テンプレートの相加平均
		double imgMean = 0.0;	// 実画像の相加平均

		for ( int y=0; y<templ.rows; ++y ) {
			for ( int x=0; x<templ.cols; ++x ) {
				if ( mask.at<char>(y,x) == 0 )
					continue;

				cv::Vec3b v1 = templ.at<cv::Vec3b>(y,x);
				cv::Vec3b v2 = img.at<cv::Vec3b>(y+dy,x+dx);
				templMean += v1[k];
				imgMean += v2[k];
			}
		}
		templMean /= (double)cnt;
		imgMean   /= (double)cnt;

		for ( int y=0; y<templ.rows; ++y ) {
			for ( int x=0; x<templ.cols; ++x ) {
				if ( mask.at<char>(y,x) == 0 )
					continue;
				cv::Vec3b v1 = templ.at<cv::Vec3b>(y,x);
				cv::Vec3b v2 = img.at<cv::Vec3b>(y+dy,x+dx);
				numerator += (v1[k] - templMean)*(v2[k] - imgMean);
				denominator_templ += (v1[k] - templMean)*(v1[k] - templMean);
				denominator_img += (v2[k] - imgMean)*(v2[k] - imgMean);
			}
		}
	}
	// 分母を計算する
	double denominator = sqrt(denominator_templ)*sqrt(denominator_img);
	// 相関係数を計算する
	double corr = numerator/denominator;

	return corr;
}

// 実画像のすべてに対して相関係数を計算する
void MatchTemplate( const cv::Mat& img, const cv::Mat& templ, cv::Mat& result, const cv::Mat& mask )
{
	cv::Size corrsize(img.cols-templ.cols, img.rows-templ.rows);
	result.create(corrsize, CV_64F);

	for ( int y=0; y<corrsize.height; ++y ) {
		for ( int x=0; x<corrsize.width; ++x ) {
			double corr = crossCorr( img, templ, mask,x,y );
			result.at<double>(y,x) = corr ;
		}
	}
}

テンプレートマッチング法を使うときに、OpenCV の cv::matchTemplate では矩形に縛られるので、マッチングしたい画像の背景にあるものに影響を受けてしまう…と考えるわけで、教師画像を

のように、背景を抜くためのマスク画像を作ればよいのではないか?という想定です。

で、実際に実験をしてみると、

■マスク画像を使わない場合

■マスク画像を使う場合

マスク画像を使わない場合は、いちごの駒を誤検出していますが、マスク画像を使った場合はみかんの駒だけを検出している…というか、左下の白い領域をぼろぼろと誤検出していますね orz 。
おそらく、マッチングの評価関数に相関係数を使っているので、この白い領域の色ムラがちょうどみかんの教師画像の色ムラとあっているのではないかとと想像できます(相加平均からの差になるので、実画像の色のムラがたまたまマッチングしていると、こういう具合になってしまうかと)。これは、RGB 値の相関係数を使うときの欠点ではなかと。

あと、興味深いマッチングの仕方として、

のように、上記のような教師画像の色がうまく背景の画像にマッチングしてしまっている例があります。みかんの色がオレンジなので、まんなかにオレンジがある領域をさがしてしまうためです。これはマスク画像を使ったときの欠点ですね。

同じ画像でマスクを使わないと次の図になります。

右上のみかんの駒だけが検出されます。これは背景画像をマッチングさせているからです。

マスク画像を使いたいのは、次のように背景が切り替わった場合にも対応させたいためです。

背景差分を取って背景画像のほうを切り取るのもひとつの方法なのですが、検索対象となる教師画像のほうの背景画像を切り取ってしまうほうがより有効ではないかなと。
まあ、この図の場合、みかんだけでなくキウイ(かな?)もマッチングしてしまっている訳で、これも調節に必要ありですが。

カテゴリー: 開発, C++, OpenCV | 3件のコメント

相関係数を計算を自作する(途中)

結局、自作してみる… templmatch.cpp 内にある crossCorr 関数で相関平均を計算しているのだが、フーリエ解析を使っているためか値が違う。

後でコードを見直そう。

// 実画像の位置(dx,dy)に対して、相関係数を計算する
double crossCorr( const cv::Mat& img, const cv::Mat& templ, const cv::Mat& mask, int dx, int dy )
{
	int cnt = 0;

	double templMean = 0.0;	// テンプレートの相加平均
	double imgMean   = 0.0;	// 実画像の相加平均 
	
	for ( int y=0; y<templ.rows; ++y ) {
		for ( int x=0; x<templ.cols; ++x ) {
			if ( mask.at<char>(y,x) == 0 ) 
				continue;
			cv::Vec3b v1 = templ.at<cv::Vec3b>(y,x);
			cv::Vec3b v2 = img.at<cv::Vec3b>(y+dy,x+dx);
			templMean += v1[0] + v1[1] + v1[2];
			imgMean += v2[0] + v2[1] + v2[2];
			cnt += 3;
		}
	}
	templMean = templMean/(double)cnt;
	imgMean = imgMean/(double)cnt;

	// 分子(numerator)
	double numerator = 0.0;
	for ( int y=0; y<templ.rows; ++y ) {
		for ( int x=0; x<templ.cols; ++x ) {
			if ( mask.at<char>(y,x) == 0 ) 
				continue;

			cv::Vec3b v1 = templ.at<cv::Vec3b>(y,x);
			cv::Vec3b v2 = img.at<cv::Vec3b>(y+dy,x+dx);
			numerator += (v1[0] - templMean)*(v2[0] - imgMean);
			numerator += (v1[1] - templMean)*(v2[1] - imgMean);
			numerator += (v1[2] - templMean)*(v2[2] - imgMean);
		}
	}
	// 分母(denominator)
	double denominator_templ = 0.0;
	double denominator_img   = 0.0;
	for ( int y=0; y<templ.rows; ++y ) {
		for ( int x=0; x<templ.cols; ++x ) {
			if ( mask.at<char>(y,x) == 0 ) 
				continue;
			cv::Vec3b v1 = templ.at<cv::Vec3b>(y,x);
			cv::Vec3b v2 = img.at<cv::Vec3b>(y+dy,x+dx);

			denominator_templ += (v1[0] - templMean)*(v1[0] - templMean);
			denominator_templ += (v1[1] - templMean)*(v1[1] - templMean);
			denominator_templ += (v1[2] - templMean)*(v1[2] - templMean);
			denominator_img += (v2[0] - imgMean)*(v2[0] - imgMean);
			denominator_img += (v2[1] - imgMean)*(v2[1] - imgMean);
			denominator_img += (v2[2] - imgMean)*(v2[2] - imgMean);
		}
	}
	double denominator = sqrt(denominator_templ)*sqrt(denominator_img);

	return numerator/denominator;
}

// 実画像のすべてに対して相関係数を計算する
void MatchTemplate( const cv::Mat& img, const cv::Mat& templ, cv::Mat& result, const cv::Mat& mask )
{
	cv::Size corrsize(img.cols-templ.cols, img.rows-templ.rows);
	result.create(corrsize, CV_64F); 

	for ( int y=0; y<corrsize.height; ++y ) {
		for ( int x=0; x<corrsize.width; ++x ) {
			double corr = crossCorr( img, templ, mask,x,y );
			result.at<double>(y,x) = corr ;
		}
	}
}

ちなみに、Mat:at<> を使うとデバッグ実行ではひどく遅いのだが、Release ビルドをすると結構速い。
おそらく、mat.hpp に定義されている

template<typename _Tp> inline _Tp& Mat::at(int i0, int i1)
{
    CV_DbgAssert( dims <= 2 && data && (unsigned)i0 < (unsigned)size.p[0] &&
        (unsigned)(i1*DataType<_Tp>::channels) < (unsigned)(size.p[1]*channels()) &&
        CV_ELEM_SIZE1(DataType<_Tp>::depth) == elemSize1());
    return ((_Tp*)(data + step.p[0]*i0))[i1];
}

で設定されている CV_DbgAssert の関数が遅いわけで、これはリリースモードでは外れるようになっている。
この部分を、CV_DbgAssert を使わないように展開してやれば、早くなるのではないかなぁと。

カテゴリー: 開発, 雑談, C++, OpenCV | 相関係数を計算を自作する(途中) はコメントを受け付けていません

特定点でテンプレートマッチングを行うための考察

テンプレートマッチング法を使うと、教師画像を実画像から簡単に検索できるのだが、相関係数(CV_TM_CCOEFF_NORMED)を使って探索をすると結構遅い…というか、これが目で駒を追うやり方なのかどうかが疑問だったりします。

なので、以下は、ちょっと考察。

なにも、教師画像のすべてをマッチングさせる必要はなくて、教師画像の特徴を掴んで、実画像とマッチングさせればよいのです。これを特徴量という言い方をするのですが、この特徴量の定義として考えられるのが、

  • 教師画像の、色で検出する。
  • 教師画像の、形で検出する。
  • 教師画像の、白黒画像で検出する。
  • 教師画像の、角で検出する。

というやり方があります。

画像処理 (3) 顕著性マップ
http://fussy.web.fc2.com/algo/algo12-3.htm

をつらつらと読んで考えたのが、教師画像での顕著性のある場所が、実画像のマッチングする枠と比較してどうなのか?ということで、教師画像の特徴を掴むという行為は、実は、「色」だったり「形」だったり、「白黒」画像の「白」の位置だったり、「角」っこであったりするわけです。そう、「あったり」する訳なので、実は、それぞれの場面によって異なるのが普通ではないかと。

そうなると、教師画像を1枚だけ見て、どこに特徴があるのか?という行為/計算では足りなくて、実は、

  • 複数の教師画像から、それぞれの相違のある箇所(=特徴量)を見出す。
  • 教師画像と実画像をいくつか比較して、正解しやすい特徴量を学習する。

というパターンになります。

で、このどちらのパターンであっても、特徴量を検出する場合には、色/形状/輝度/角などの検出を決め打ちするのではなくて、その場面によって(実画像からどのような教師画像を見つけるのかなど)、特徴量の検出パターンをどれかに決めるという手順が必要ではないかと。

例えば、今、手元で実験している3マッチのアクションパズルは、果物を使っているので形状でも認識できそうですが、ちょっと前のバージョンでは形状が全く同じなので、でしか判別できません。


また、二角取りのように麻雀パイを使う場合には、マッチングの分類を「萬子」「索子」「筒子」「その他」に分けて検出しないとスピードが上がりません。と、言いますかマッチングパズルをしているときに、そういう見方を私はしています。

なので、画像をマッチングで検出する場合には、

  1. 教師画像の特徴を検出する(色、形、輝度、角など)。
  2. 教師画像の特徴点を決定する(背景画像の除去、不必要なノイズは検出させない)
  3. 教師画像同士で、1,2 が妥当であるか検証する

という前処理があった後

  1. 初回は、実画像の全箇所をテンプレートマッチする?
  2. あるいは、ランダムにマッチング箇所を決定する?

    あるいは、顕著性マップなどから、注目点と特定する?

  3. 次回以降は、前回の箇所から探索をする。
  4. 見つからない場合は、4 に戻る。

  5. 検出箇所を、教師画像と再検証させる。

という探索が必要になるのかなと。

実画像(検索の対象画像)をランダムに検索しようとするのは、目的が動画(リアルタイム)からのピックアップにあるので、実画像からの駒の検出は、計算時間を 30fps の間に収めたいわけです。時間にすると 33 msec というところですね。
この短い間に計算を終えるためには、あらかじめ、検出すべき特徴を頭に叩き込んでおいて実画像から素早くピックアップしなければいけません。例えるならば、サッカー選手が首を振った瞬間に、チームカラーで味方と敵の選手の位置を把握するようなものです。なので、あらかじめ教師画像から特徴量を決めておき、検出する箇所も数か所から数十か所に絞り込んでおいて、実画像とマッチングさせます。この場合、誤検出は出るでしょうが、それは 6 の再検出あるいは、特徴自体を数種類で取り込むことで検出率を上げるようにします(検出率を上げるという確率の方法を取り、100% の完全性は求めないところがミソです)。

で、そんなこんなで、特徴箇所だけでマッチングをするためには、cv::matchTemplate 関数にマスク値か探索位置を渡す必要があるので、コードを改造するしかないのかなぁ、と思ってソースを追っていたところ。

templmatch.cpp の 235 行目から cv::matchTemplate 関数の記述があって、sum 部分は 269 行目から、2 次元の繰り返し部分は、318 行目からになっているので、それなりに改造できそうかなと。

    for( i = 0; i < result.rows; i++ )
    {
        float* rrow = (float*)(result.data + i*result.step);
        int idx = i * sumstep;
        int idx2 = i * sqstep;

        for( j = 0; j < result.cols; j++, idx += cn, idx2 += cn )
        {
			// ここで mask を渡しておいて、
			// if ( mask(i,j) == 0 ) continue ;
			// のように skip させれば ok ?
			// 加えて、sum を計算しているところも mask を見るように修正?

            double num = rrow[j], t;
            double wndMean2 = 0, wndSum2 = 0;

            if( numType == 1 )
            {
				// CV_TM_CCOEFF CV_TM_CCOEFF_NORMED のループ
                for( k = 0; k < cn; k++ )
                {
                    t = p0[idx+k] - p1[idx+k] - p2[idx+k] + p3[idx+k];
                    wndMean2 += CV_SQR(t);
                    num -= t*templMean[k];
                }

                wndMean2 *= invArea;
            }

            if( isNormed || numType == 2 )
            {
                for( k = 0; k < cn; k++ )
                {
                    t = q0[idx2+k] - q1[idx2+k] - q2[idx2+k] + q3[idx2+k];
                    wndSum2 += t;
                }

                if( numType == 2 )
                    num = wndSum2 - 2*num + templSum2;
            }

            if( isNormed )
            {
				// CV_TM_CCOEFF_NORMED の箇所
                t = sqrt(MAX(wndSum2 - wndMean2,0))*templNorm;
                if( fabs(num) < t )
                    num /= t;
                else if( fabs(num) < t*1.125 )
                    num = num > 0 ? 1 : -1;
                else
                    num = method != CV_TM_SQDIFF_NORMED ? 0 : 1;
            }

            rrow[j] = (float)num;
        }
    }

これを流用してマスク画像を渡す方式にするのか、x,y 座標を含めた 1 次元配列を渡すのがよいのか、思案中。
2 次元の mask を取るほうが楽みたいだが。

カテゴリー: 開発, 雑談, OpenCV | 特定点でテンプレートマッチングを行うための考察 はコメントを受け付けていません

jsMath を使う

試しに jsMath で数式を書いてみる。

\int^1_\kappa
\left[\bigl(1-w^2\bigr)\bigl(\kappa^2-w^2\bigr)\right]^{-1/2} dw
= \frac{4}{\left(1+\sqrt{\kappa}\,\right)^2} K
\left(\left(\frac{1-\sqrt{\kappa}}{1+\sqrt{\kappa}}\right)^{\!\!2}\right)

jsMath: Web ページで数式を表示する一つの方法
http://math3.ifdef.jp/jsmath/welcome.htm

あたりを参考に

相関係数とか、

\sum_{i=1}^n \left( x_i – \overline{x} \right) \left( y_i – \overline{y} \right)
\over{
\sqrt{ \sum^n_{i=1} \left( x_i – \overline{x} \right)^2 }
\sqrt{ \sum^n_{i=1} \left( y_i – \overline{y} \right)^2 }
}

ソースは、数式をダブルクリックすると見れます。

カテゴリー: 雑談 | 1件のコメント

OpenCVテンプレートマッチングと低解像度化で、駒を検出する

OpenCV のテンプレートマッチを使って駒を検出 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2468

の続きです。

先のテンプレートマッチングが遅かった原因は、2 つあって、

  • cv::matchTemplate を呼び出して、MaxMin を検索した後に、再び cv::matchTemplate を呼び出しているのが無駄。
  • 元画像をそのままマッチング対象にしているので、低解像度にすれば早くなる?

ってところです。

前者の cv::matchTemplate の多重呼び出しは完全にコーディングミスですね。50 個の max を取るのに、いちいち cv::matchTemplate を呼び出す必要はありません。一回だけ呼び出して、その結果の画像を使って 50 個の max を cv::minMaxLoc で取得すれば良いのです、これで結構速くなります。

後者の低解像度化のほうは、以前から考えていて、高解像度のままマッチングをして検出しようとすると、細かい部分に敏感になってしまうという現象が発生します。細かいところというのは、取得画像のノイズであったり、微妙な手振れであったり、教師画像(テンプレートマッチで見つける画像)の違いによってスコアが大きく異なる、という現象です。このために、平滑化が行われることが多いのですが、わざわざ高解像度であるものを平滑化してしてしまうのはどうかなぁ、と思っていたので、実験しています。平滑化を行うのではなく、単純に低解像化します。低解像度にするときは、となりのドットの平均値を取る…ようなことはせず、単純に間引きます。間引いてしまうと、実はノイズに敏感になってしまうという不利が働く可能性があるのですが、そのあたりが高速化を優先して…というか、実際に目から入る情報をそのまま使う、という方針でいきます。

で、ざっと書いたコードがこんな感じ。

#include "stdafx.h"
#include <iostream>
#include "opencv/cv.h"
#include "opencv/highgui.h"
using namespace std;

/// 低解像度クラス
class RowReso
{
private:
	cv::Mat *_org_img;
	cv::Mat *_reso_img;
	cv::Mat *_reso_org;

	int _reso ;
	int _reso_width ;
	int _reso_height ;

public:
	RowReso()
	{
		_org_img = NULL;
		_reso_img = NULL;
		_reso_org = NULL;
	}
	~RowReso()
	{
		if ( _reso_img != NULL ) delete _reso_img;
		if ( _reso_org != NULL ) delete _reso_org;
	}

	// 初期化
	void Initialize( cv::Mat& img, int reso )
	{
		int width  = img.cols / reso;
		int height = img.rows / reso;

		_org_img = &img ;
		_reso_img = new cv::Mat(height, width, CV_MAKETYPE(img.depth(),img.channels()));
		_reso = reso ;
		_reso_width = width ;
		_reso_height = height ;
	}
	// 低解像度を作成
	cv::Mat& Do()
	{
		for ( int y=0; y<_reso_height; ++y ) {
			for ( int x=0; x<_reso_width; ++x ) {
				int x1 = (_reso+1)/2 + _reso*x;
				int y1 = (_reso+1)/2 + _reso*y;
				cv::Vec3b &v = _org_img->at<cv::Vec3b>(y1,x1);
				// cout << x << "," << y << endl;
				_reso_img->at<cv::Vec3b>(y,x) = v;
			}
		}
		return *_reso_img;
	}
	// 確認用に元の画像の大きさに戻す
	cv::Mat& GetOriginalSize()
	{
		if ( _reso_org == NULL ) {
			_reso_org = new cv::Mat(
				_org_img->rows, _org_img->cols,
				CV_MAKETYPE(_org_img->depth(),_org_img->channels()));
		}
		for ( int y=0; y<_reso_height; ++y ) {
			for ( int x=0; x<_reso_width; ++x ) {
				cv::Vec3b &v = _reso_img->at<cv::Vec3b>(y,x);
				for ( int y1=0; y1<_reso; ++y1 ) {
					for ( int x1=0; x1<_reso; ++x1 ) {
						_reso_org->at<cv::Vec3b>(y*_reso+y1,x*_reso+x1) = v ;
					}
				}
			}
		}
		return *_reso_org;
	}
};

int main2(int argc, char **argv );

int main(int argc, char **argv )
{
	if ( argc == 2 ) {
		main2( argc, argv );
		return 0;
	}
	cv::VideoCapture cap;
	cap.open(0);
	cap.set( CV_CAP_PROP_FRAME_WIDTH, 640 );
	cap.set( CV_CAP_PROP_FRAME_HEIGHT, 480 );

  	cv::namedWindow("camera", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow("reso", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow("reso org", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow("reso koma", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);

	char fname[256];
	cv::Mat img_koma[7];
	for ( int i=0; i<7; i++ ) {
		sprintf( fname, "D:\\work\\OpenCV\\src\\mini\\koma%02d.png", i+1 );
		img_koma[i] = cv::imread(fname);
	}

	// 初回だけ読み込む
	cv::Mat img;
	cap >> img ;

	int reso = 3 ;
	RowReso Reso, ResoKoma[7];
	Reso.Initialize( img, reso );
	cv::Mat img_reso_komas[7];
	for ( int i=0; i<7; i++ ) {
		ResoKoma[i].Initialize( img_koma[i], reso );
		// 低解像度の教師画像
		img_reso_komas[i] = ResoKoma[i].Do();
	}

	// 枠線の色
	cv::Scalar cols[7];
	cols[0] = cv::Scalar(0,0,255);
	cols[1] = cv::Scalar(0,255,255);
	cols[2] = cv::Scalar(255,0,255);
	cols[3] = cv::Scalar(255,0,0);
	cols[4] = cv::Scalar(0,255,0);
	cols[5] = cv::Scalar(255,255,0);
	cols[6] = cv::Scalar(255,255,255);

	while ( 1 ) {
		cap >> img ;

		cv::Mat &img_reso = Reso.Do();
		cv::Mat &img_reso_org = Reso.GetOriginalSize();

		cv::Mat img_search, img_result ;
		img_reso.copyTo( img_search );

		for ( int j=0; j<7; j++ ) {
			cv::Mat &img_reso_koma = img_reso_komas[j];

			// テンプレートマッチング
			cv::matchTemplate(img_search, img_reso_koma, img_result, CV_TM_CCOEFF_NORMED);

			// 50 個検出する
	  		for ( int i=0; i<50; i++ ) {
				  // 最大のスコアの場所を探す
				  cv::Point max_pt;
				  double maxVal;
				  cv::minMaxLoc(img_result, NULL, &maxVal, NULL, &max_pt);
				  // 一定スコア以下の場合は処理終了
				  if ( maxVal < 0.5 ) break;

				  cv::Rect roi_rect(0, 0, img_reso_koma.cols, img_reso_koma.rows);
				  roi_rect.x = max_pt.x ;
				  roi_rect.y = max_pt.y ;
				  cv::Rect roi_rect_org( roi_rect.x * reso , roi_rect.y * reso ,
					  img_reso_koma.cols*reso, img_reso_koma.rows*reso );

				  // std::cout << i << ":(" << max_pt.x << ", " << max_pt.y << "), score=" << maxVal << std::endl;
				  // 探索結果の場所に矩形を描画
				  cv::rectangle(img_reso_org, roi_rect_org, cols[i], 3);
				  // cv::rectangle(img_search, roi_rect, cv::Scalar(0,0,0), CV_FILLED);

				  // 検出済みは 0.0 で塗りつぶし
				  for ( int y=0; y<img_reso_koma.rows; y++ ) {
					  for ( int x=0; x<img_reso_koma.cols; x++ ) {
						  int xx = max_pt.x + x - img_reso_koma.cols/2;
						  int yy = max_pt.y + y - img_reso_koma.rows/2;
						  if ( 0 <= xx && xx < img_result.cols-1 ) {
							  if ( 0 <= yy && yy < img_result.rows-1 ) {
								  img_result.at<int>(yy,xx) = 0;
							  }
						  }
					  }
				  }
				  // koma.push_back( roi_rect );
			}
		}
		cv::imshow("camera", img );
		cv::imshow("reso", img_reso);
		cv::imshow("reso org", img_reso_org);

		char ch = cv::waitKey(30);
		if ( ch == 27 ) break;
	}
	return 0;
}

RowReso クラスは、単純に cv:Mat の中身を間引きしているだけです。
低解像化する率は「3」という奇数を取ります。中央の点をサンプリングしたかったためなのですが、本当は左上の点でもよいのかもしれません。これは後で実験します。

多少、カクカクとしますが、ほどよくマッチングができています。
7 つの駒を、低解像度の画素数(640×480 の 1/3 なので、210×160 = 34000)で検索するので、24 万回のマッチングの計算をしています。低解像度にしたので、9 倍ほど早くなっているはずです。教師画像も 1/3 サイズになっているので、マッチング自体の速度アップも寄与していると思います。

で、検出の精度はどうかというと、良いような悪いような、という感じですね。右のほうに黒の枠がでているので、ここで誤検出しています。また、ところどこ抜けがでているので、検出できない駒もあります。これは 0.5 の足切りになってしまった箇所です。
加えて、実際に実行してみると分かるのですが、検出の色がちかちかと変わります。検出している駒のマッチングで、複数マッチしているものがあるわけです。

このあたりの誤検出は想定のうちで、低解像度によっておおまかな駒の位置がわかったら、高解像度のほうで駒の検出をやり直します。このあたり、人間の目でも、アクションパズルをする場合、大まかに色か形で目で追って、その後でじっと凝視して本当にそれが認識した駒とあっているかどうか?を確認するという認識手順になる…と思うのでそれに準じます。

あと、テンプレートマッチの回数自体は、初回のみ(あるいはパズルが一旦消えた、あるいは iPhone が大きく動いた)ときに必要で、続くフレームのほうでは、先に認識した駒の位置から類推をさせることで、マッチングの範囲を極端に減らすことが可能です。低解像の駒は 10×10 程度なので、これに 2 倍の幅を持たせて 20×20 x 盤面7×7 = 2万回 のマッチングで良くなるはずです。

ってな訳で後日。

カテゴリー: C++, OpenCV | 4件のコメント

UVC対応カメラを複数台つなげるときは帯域に注意する

2 台の Web カメラを繋げて、次のプログラムを実行します。

/* OpenCV 2.3 でビルドする */
#include <iostream>
#include "opencv/cv.h"
#include "opencv/highgui.h"
using namespace std;

int main( int argc, char **argv )
{
	cv::VideoCapture cap1, cap2;
	cap1.open(0);
	cap2.open(1);

	/* USB の転送量が間に合わないのでサイズを 320x240 に変更する */
	/* USB ボードを変えると、640x480 でも ok ? */
	cap1.set( CV_CAP_PROP_FRAME_WIDTH, 320 );
	cap1.set( CV_CAP_PROP_FRAME_HEIGHT, 240 );
	cap2.set( CV_CAP_PROP_FRAME_WIDTH, 320 );
	cap2.set( CV_CAP_PROP_FRAME_HEIGHT, 240 );

  	cv::namedWindow("camera1", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow("camera2", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
	while ( 1 ) {
		cv::Mat img1, img2;
		cap1 >> img1 ;
		cap2 >> img2 ;
		cv::imshow("camera1", img1 );
		cv::imshow("camera2", img2 );
		char ch = cv::waitKey(30);
		if ( ch == 27 ) break;
	}
	return 0;
}

 

結論から言えば、UVC 対応の WEB カメラを複数台つなげることができます。
実は、最初なかなかつながらなくて、AMCAP(DirectShowのサンプル)を動かしても認識しないし、片方を止めれば片方が認識するし、なぜか、3台繋いだ状態でも、同時には1台しか表示させることができない…ってな訳で、半日ほど悩みました。

複数接続させた例は、Linux だったり、何故だろう、Windows 7 では動かないのか?とか。

で、あれこれと調べた挙句。

AOISAKURA – 日記(2010-07-25)
http://www.aoisakura.jp/tdiary/?date=20100725

なところで、「帯域」の話があったので、なるほど USB の帯域の問題であったか orz ということです。
高速にキャプチャする場合には、帯域が被らないように USB ボードを買えばよいのですが、まぁ、そこまで掛けるのもアレなので、できるだけ遠くにある(かな?)USB ポートに差せば大丈夫でした…なんだかなぁ。

解像度を 640×480 にすると、片方の画像がみだれてたり、片方が止まってしまったりします。この現象もあちこちのブログであったので、おそらく帯域の問題でしょう。USB 2.0 の場合、最初の接続がシビアになっているので、なんか接続できない場合は、他のプログラムでひとつずつカメラを繋げてみて、その後、複数台に接続するとうまくいきます。
当然、USB ハブを通した場合は、ハブの性能に引きずられるのでハブを通さないほうがよいかと。USB 機器が繋がっている場合には、外して試してみるとか。現状、320×240 では 2 台でも大丈夫そうですね。このハブには、無線マウスとキーボードが繋がっていますが、普通に動いています。

カテゴリー: 開発, C++, OpenCV | 1件のコメント

OpenCV で UVC(USB video device class)対応のカメラを使う

と、電気屋さんで web カメラが安売り(投げ売り)されていたので、OpenCV のステレオ用に(というか、別の方向からも撮影したいし)ということで、2 個ほど買ってきました。

ELECOM 製の UCAM-DLG200H というやつです。

さて、これをパソコンに繋げて動かそうとしたのですが、動かない。
結論から言うと、UVC 対応の WEB カメラは、OpenCV 2.2 では動きません…ってな訳です。

opencv.jp からダウンロードできる安定版は、現時点で v2.2 (http://opencv.jp/download)なので、

OpenCV2.3rcからOpenCV2.3の変更点(ChangeLog) | OpenCV.jp
http://opencv.jp/misc/changelog_from_23rc

なところから、v2.3 をダウンロードして使います。

ちょっと古めの Web カメラだと、ドライバーをインストールしないと使えないので、それには OpenCV が対応しているのですが、最近よく使われている(というか安めの Web カメラは UVC 対応が普通らしい)カメラは DirectShow 経由でないとうまくいかないようです。

で、OpenCV v2.2 頃では、VideoInput として扱われていた DirectShow が、v2.3 では、標準のカメラとして取り込まれたという具合だそうです。

/* OpenCV 2.3 でビルドする */
#include <iostream>
#include "opencv/cv.h"
#include "opencv/highgui.h"
using namespace std;

#ifndef CV_WINDOW_FREERATIO
#define CV_WINDOW_FREERATIO 0
#endif

int main( int argc, char **argv )
{
	cv::VideoCapture cap1;
	cap1.open(0);
  	cv::namedWindow("camera1", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
	while ( 1 ) {
		cv::Mat img1;
		cap1 >> img1 ;
		cv::imshow("camera1", img1 );
		char ch = cv::waitKey(30);
		if ( ch == 27 ) break;
	}
	return 0;
}

で、サンプル用に作った makefile 。面倒なので、ライブラリん環境は全てインポートしています。
実行時には、OpenCV の DLL をカレントディレクトリにコピーすると楽です。

all: camera21.exe camera22.exe camera23.exe

CVINCPATH23=C:\OpenCV2.3\build\include
CVLIBPATH23=C:\OpenCV2.3\build\x86\vc10\lib

CVINCPATH22=C:\OpenCV2.2\include
CVLIBPATH22=C:\OpenCV2.2\lib

CVINCPATH21=C:\OpenCV2.1\include
CVLIBPATH21=C:\OpenCV2.1\lib

CVLIB21= \
	$(CVLIBPATH21)\cv210.lib \
	$(CVLIBPATH21)\cvaux210.lib \
	$(CVLIBPATH21)\cxcore210.lib \
	$(CVLIBPATH21)\cxts210.lib \
	$(CVLIBPATH21)\highgui210.lib \
	$(CVLIBPATH21)\ml210.lib

CVLIB22= \
	$(CVLIBPATH22)\opencv_calib3d220.lib \
	$(CVLIBPATH22)\opencv_contrib220.lib \
	$(CVLIBPATH22)\opencv_core220.lib \
	$(CVLIBPATH22)\opencv_features2d220.lib \
	$(CVLIBPATH22)\opencv_flann220.lib \
	$(CVLIBPATH22)\opencv_gpu220.lib \
	$(CVLIBPATH22)\opencv_highgui220.lib \
	$(CVLIBPATH22)\opencv_imgproc220.lib \
	$(CVLIBPATH22)\opencv_legacy220.lib \
	$(CVLIBPATH22)\opencv_ml220.lib \
	$(CVLIBPATH22)\opencv_objdetect220.lib \
	$(CVLIBPATH22)\opencv_video220.lib

CVLIB23= \
	$(CVLIBPATH23)\opencv_calib3d230.lib \
	$(CVLIBPATH23)\opencv_contrib230.lib \
	$(CVLIBPATH23)\opencv_core230.lib \
	$(CVLIBPATH23)\opencv_features2d230.lib \
	$(CVLIBPATH23)\opencv_flann230.lib \
	$(CVLIBPATH23)\opencv_gpu230.lib \
	$(CVLIBPATH23)\opencv_haartraining_engine.lib \
	$(CVLIBPATH23)\opencv_highgui230.lib \
	$(CVLIBPATH23)\opencv_imgproc230.lib \
	$(CVLIBPATH23)\opencv_legacy230.lib \
	$(CVLIBPATH23)\opencv_ml230.lib \
	$(CVLIBPATH23)\opencv_objdetect230.lib \
	$(CVLIBPATH23)\opencv_video230.lib

CVINCPATH=$(CVINCPATH23)
CVLIBPATH=$(CVLIBPATH23)
CVLIB=$(CVLIB23)

camera21.obj: camera01.cpp
	cl /EHsc /c /I$(CVINCPATH21) /Focamera21.obj camera01.cpp
camera21.exe: camera21.obj
	cl /Fecamera21.exe camera21.obj $(CVLIB21)

camera22.obj: camera01.cpp
	cl /EHsc /c /I$(CVINCPATH22) /Focamera22.obj  camera01.cpp
camera22.exe: camera22.obj
	cl /Fecamera22.exe camera22.obj $(CVLIB22)

camera23.obj: camera01.cpp
	cl /EHsc /c /I$(CVINCPATH23) /Focamera23.obj camera01.cpp
camera23.exe: camera23.obj
	cl /Fecamera23.exe camera23.obj $(CVLIB23)

UVC 対応カメラを接続した状態で、camera21.exe, camera22.exe を実行するとエラーになりますが、camera23.exe は実行できます、という訳でめでたしめでたし…と思いきや…続く。

カテゴリー: 開発, C++, OpenCV | OpenCV で UVC(USB video device class)対応のカメラを使う はコメントを受け付けていません

OpenCV の特徴量検出器を使ってみる

プチロボ事始め | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2416
プチロボで4軸構成にしてみる | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2421
OpenCV を使ってエッジ抽出 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2452
OpenCV を使って顔認識 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2460
OpenCV のテンプレートマッチを使って駒を検出 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2468

の続き

試しに特徴量を検出してみると、下記な感じ。

#include <iostream>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/highgui/highgui.hpp>

using namespace std ;

int main(int argc, char *argv[])
{
	cv::Mat img = cv::imread(argv[1], 1);
  	if(!img.data) return -1;

	// グレースケールに変換
	cv::Mat gray_img, canny_img ;
	cv::Mat gray_img2, canny_img2;
	cv::cvtColor(img, gray_img, CV_BGR2GRAY);
	cv::normalize(gray_img, gray_img, 0, 255, cv::NORM_MINMAX);
	cv::Canny( gray_img, canny_img, 10, 100, 3 );

	// keypoints を解放するとエラーになるので、対策として new しておく
  	vector<cv::KeyPoint> *keypoints = new vector<cv::KeyPoint>();
  	vector<cv::KeyPoint> *keypoints2 = new vector<cv::KeyPoint>();

  	// 固有値に基づく特徴点検出
  	// maxCorners=80, qualityLevel=0.01, minDistance=5, blockSize=3
  	cv::GoodFeaturesToTrackDetector detector(30, 0.01, 5, 3);
  	detector.detect(gray_img, *keypoints);
  	detector.detect(canny_img, *keypoints2);

	// キーポイントにマークを付ける
	cv::cvtColor(gray_img, gray_img2, CV_GRAY2BGR);
 	cv::Scalar color(0,200,255);
 	for( vector<cv::KeyPoint>::iterator itk = keypoints->begin();
 		 itk != keypoints->end();
 		 ++itk)
 	{
    	circle(gray_img2, itk->pt, 1, color, -1);
    	circle(gray_img2, itk->pt, itk->size, color, 1, CV_AA);
 		if( itk->angle >= 0 ) {
      		cv::Point pt2(itk->pt.x + cos(itk->angle)*itk->size, itk->pt.y + sin(itk->angle)*itk->size);
      		cv::line(gray_img2, itk->pt, pt2, color, 1, CV_AA);
    	}
 	}
	// キーポイントにマークを付ける
	cv::cvtColor(canny_img, canny_img2, CV_GRAY2BGR);
 	for( vector<cv::KeyPoint>::iterator itk = keypoints2->begin();
 		 itk != keypoints2->end();
 		 ++itk)
 	{
    	circle(canny_img2, itk->pt, 1, color, -1);
    	circle(canny_img2, itk->pt, itk->size, color, 1, CV_AA);
 		if( itk->angle >= 0 ) {
      		cv::Point pt2(itk->pt.x + cos(itk->angle)*itk->size, itk->pt.y + sin(itk->angle)*itk->size);
      		cv::line(canny_img2, itk->pt, pt2, color, 1, CV_AA);
    	}
 	}

	// 画面に表示
  	cv::namedWindow("Normal", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow("Gray", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow("Canny", CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);

	cv::imshow("Normal", img);
	cv::imshow("Gray", gray_img2);
	cv::imshow("Canny", canny_img2);
  	cv::waitKey(0);

  	// エラーになるので解放しない
  	// delete keypoints ;

  	return 0;
}


 

普通にグレースケールにした場合と、エッジ検出後に特徴量を計算しています。
が、ここで使っている「特徴量」というのは、ひとつの画像の特徴的な部分を抽出するという意味で、HMM 法や(多分)SVM 法による他との比較による「特徴量」とは意味が違う…のだと思うんだけど、ちと専門用語になるので、詳細は割愛。

さて、先の駒のように「みかん」と「ダイヤモンド」では形状が違うので、グレースケールにしても大丈夫なわけですが、駒がだけで異なる場合はちょっと難しい…というか違いを検出するのは無理。


なので、特徴量を検出する場合は、

  1. 異なる駒同士の違いを検出するための「特徴量」を計算する。
  2. 盤面上の駒と比較してスコアを出す計算式が必要

になります。

いわゆる、自分自身を検出した時にだけスコアが高くなるような計算式を用意すればよいわけで。

と思っていたら、

Tercel::Diary: 形状マッチングで文字認識をさせようとして失敗してみた
http://tercel-sakuragaoka.blogspot.com/2011/05/blog-post.html

なところで、cvMatchShapes を使っているので、ちょっと試してみる。

カテゴリー: C++, プチロボ | OpenCV の特徴量検出器を使ってみる はコメントを受け付けていません