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 &quot;opencv/cv.h&quot;
#include &quot;opencv/highgui.h&quot;
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 << &quot;,&quot; << 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(&quot;camera&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow(&quot;reso&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow(&quot;reso org&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow(&quot;reso koma&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);

	char fname[256];
	cv::Mat img_koma[7];
	for ( int i=0; i<7; i++ ) {
		sprintf( fname, &quot;D:\\work\\OpenCV\\src\\mini\\koma%02d.png&quot;, 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 << &quot;:(&quot; << max_pt.x << &quot;, &quot; << max_pt.y << &quot;), score=&quot; << 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(&quot;camera&quot;, img );
		cv::imshow(&quot;reso&quot;, img_reso);
		cv::imshow(&quot;reso org&quot;, 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 &quot;opencv/cv.h&quot;
#include &quot;opencv/highgui.h&quot;
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(&quot;camera1&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow(&quot;camera2&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
	while ( 1 ) {
		cv::Mat img1, img2;
		cap1 >> img1 ;
		cap2 >> img2 ;
		cv::imshow(&quot;camera1&quot;, img1 );
		cv::imshow(&quot;camera2&quot;, 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 &quot;opencv/cv.h&quot;
#include &quot;opencv/highgui.h&quot;
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(&quot;camera1&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
	while ( 1 ) {
		cv::Mat img1;
		cap1 >> img1 ;
		cv::imshow(&quot;camera1&quot;, 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(&quot;Normal&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow(&quot;Gray&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
  	cv::namedWindow(&quot;Canny&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);

	cv::imshow(&quot;Normal&quot;, img);
	cv::imshow(&quot;Gray&quot;, gray_img2);
	cv::imshow(&quot;Canny&quot;, 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 の特徴量検出器を使ってみる はコメントを受け付けていません

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

の続き

パズルゲームを解くのが目的なので、テンプレートマッチングは非常にオーバーヘッドが大きいのですが、試しに。

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

int
main(int argc, char *argv[])
{
	CvCapture *capture = cvCreateCameraCapture(0);

	// テンプレート画像
	cv::Mat tmp_img = cv::imread(argv[2], 1);
	if(!tmp_img.data) return -1;

  	cv::namedWindow(&quot;search image&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
	while ( 1 ) {
  		// 探索画像
  		cv::Mat search_img0 = cvQueryFrame( capture );
  		cv::Mat search_img;
  		search_img0.copyTo( search_img );

	    cv::Mat result_img;
		// 50 個検出する
	  	for ( int i=0; i<50; i++ ) {
			  // テンプレートマッチング
			  cv::matchTemplate(search_img, tmp_img, result_img, CV_TM_CCOEFF_NORMED);
			  // 最大のスコアの場所を探す
			  cv::Rect roi_rect(0, 0, tmp_img.cols, tmp_img.rows);
			  cv::Point max_pt;
			  double maxVal;
			  cv::minMaxLoc(result_img, NULL, &maxVal, NULL, &max_pt);
			  // 一定スコア以下の場合は処理終了
			  if ( maxVal < 0.5 ) break;

			  roi_rect.x = max_pt.x;
			  roi_rect.y = max_pt.y;
			  std::cout << &quot;(&quot; << max_pt.x << &quot;, &quot; << max_pt.y << &quot;), score=&quot; << maxVal << std::endl;
			  // 探索結果の場所に矩形を描画
			  cv::rectangle(search_img0, roi_rect, cv::Scalar(0,255,255), 3);
			  cv::rectangle(search_img, roi_rect, cv::Scalar(0,0,255), CV_FILLED);
		}

  		cv::imshow(&quot;search image&quot;, search_img0);
  		char ch = cv::waitKey(33);
		if ( ch == 27 ) break;
  }
	cvReleaseCapture( &capture );
}

 

ちょうど、駒と同じサイズ(30×30)のテンプレートを用意して、画面上を探索します。
テンプレートマッチングをしたときは、cv::minMaxLoc で最大のスコアを取得するのですが、最大だと1つしか取れないので、50 回ぐらい繰り返します。閾値の「0.5」は適当に決めたものです。

ひとつの駒だけ検出するならば、そこそこのスピードで動くのですが、これを全ての駒(今回は7種類)に対してテンプレートマッチングをすると、ええ、大変遅いですね(苦笑)。

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

int
main(int argc, char *argv[])
{
	CvCapture *capture = cvCreateCameraCapture(0);

	// テンプレート画像
	cv::Mat tmp_imgs[7];
	tmp_imgs[0] = cv::imread(&quot;mini\\koma01.png&quot;, 1);
	tmp_imgs[1] = cv::imread(&quot;mini\\koma02.png&quot;, 1);
	tmp_imgs[2] = cv::imread(&quot;mini\\koma03.png&quot;, 1);
	tmp_imgs[3] = cv::imread(&quot;mini\\koma04.png&quot;, 1);
	tmp_imgs[4] = cv::imread(&quot;mini\\koma05.png&quot;, 1);
	tmp_imgs[5] = cv::imread(&quot;mini\\koma06.png&quot;, 1);
	tmp_imgs[6] = cv::imread(&quot;mini\\koma07.png&quot;, 1);
	// 枠線の色
	cv::Scalar cols[6];
	cols[0] = cv::Scalar(0,0,255);
	cols[1] = cv::Scalar(0,255,0);
	cols[2] = cv::Scalar(255,0,0);
	cols[3] = cv::Scalar(255,0,255);
	cols[4] = cv::Scalar(255,255,0);
	cols[5] = cv::Scalar(255,255,255);
	cols[6] = cv::Scalar(100,100,100);

  	cv::namedWindow(&quot;search image&quot;, CV_WINDOW_AUTOSIZE|CV_WINDOW_FREERATIO);
	while ( 1 ) {

  		// 探索画像
  		cv::Mat search_img0 = cvQueryFrame( capture );
  		cv::Mat search_img;
  		search_img0.copyTo( search_img );

		for ( int j=0; j<7; j++ ) {
			cv::Mat &tmp_img =  tmp_imgs[j];
		    cv::Mat result_img;
			// 50 個検出する
		  	for ( int i=0; i<50; i++ ) {
				  // テンプレートマッチング
				  // cv::matchTemplate(search_img, tmp_img, result_img, CV_TM_SQDIFF_NORMED);
				  cv::matchTemplate(search_img, tmp_img, result_img, CV_TM_CCOEFF_NORMED);
				  // 最大のスコアの場所を探す
				  cv::Rect roi_rect(0, 0, tmp_img.cols, tmp_img.rows);
				  cv::Point max_pt;
				  double maxVal;
				  cv::minMaxLoc(result_img, NULL, &maxVal, NULL, &max_pt);
				  // 一定スコア以下の場合は処理終了
				  if ( maxVal < 0.5 ) break;

				  roi_rect.x = max_pt.x;
				  roi_rect.y = max_pt.y;
				  std::cout << &quot;(&quot; << max_pt.x << &quot;, &quot; << max_pt.y << &quot;), score=&quot; << maxVal << std::endl;
				  // 探索結果の場所に矩形を描画
				  cv::rectangle(search_img0, roi_rect, cols[j], 3);
				  cv::rectangle(search_img, roi_rect, cv::Scalar(0,0,255), CV_FILLED);
			}
		}

  		cv::imshow(&quot;search image&quot;, search_img0);
  		char ch = cv::waitKey(33);
		if ( ch == 27 ) break;
  }
	cvReleaseCapture( &capture );
}


 

そりゃ、全画面に対して毎回テンプレートを検索するのは無駄だし、人がパズルを解いているときはそういうことはしないので、もうちょっと方法を考えないと駄目ですね。

パズルの盤面は普通は格子状になっているので、テンプレートマッチングのようにちょっとずつ動かす必要はありません。盤面上にひとつの駒が検出できれば、その上下左右を見ていくという方法です。
あと、駒の形状を細かくマッチングしていく必要はなくて、他の駒との差異が分かればよいわけで、それぞれの駒の特徴を取り出して、実際の盤面の駒のスコアを比較すればよいわけです。実際、人の目はそうやっているし。

なので、

  1. あらかじめ、駒同士を比較して特徴量を検出する
  2. 盤面から、駒があるであろう場所を検出
  3. 盤面上の駒を、学習済みの特徴量でスコアを計算
  4. スコアが一番高いものを駒とみなす

というロジックになります。

テンプレートマッチングの参照先はこちら↓

画像処理 – OpenCV-CookBook
http://opencv.jp/cookbook/opencv_img.html#id32

カテゴリー: C++, プチロボ | OpenCV のテンプレートマッチを使って駒を検出 はコメントを受け付けていません

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

の続き

テンプレートマッチングの前に、SVM というか、OpenCV で顔認識を試してみます。
ひとまず、opencv.jp の sample コードを見ながらざっとコーディング。

#include <stdlib.h>
#include &quot;opencv/cv.h&quot;
#include &quot;opencv/highgui.h&quot;

int main( int argc, char **argv )
{
	cvNamedWindow( &quot;Face01in&quot;, CV_WINDOW_AUTOSIZE );
	cvNamedWindow( &quot;Face01out&quot;, CV_WINDOW_AUTOSIZE );

	 static CvScalar colors[] = {
	    {{0, 0, 255}}, {{0, 128, 255}},
	    {{0, 255, 255}}, {{0, 255, 0}},
	    {{255, 128, 0}}, {{255, 255, 0}},
	    {{255, 0, 0}}, {{255, 0, 255}}
	  };

	CvCapture *capture;
	if ( argc == 1 ) {
		capture = cvCreateCameraCapture(0);
	} else {
	 	capture = cvCreateFileCapture( argv[1] );
	}
	IplImage *frame = NULL;
	IplImage *frameGray = NULL;

 	const char *cascade_name = &quot;haarcascade_frontalface_default.xml&quot;;
	CvHaarClassifierCascade *cascade =
		(CvHaarClassifierCascade *) cvLoad (cascade_name, 0, 0, 0);

	while ( 1 ) {
		frame = cvQueryFrame( capture );
		if ( !frame ) break;

		if ( frameGray == NULL ) {
			frameGray = cvCreateImage(cvGetSize(frame), IPL_DEPTH_8U, 1);
		}

	 	CvMemStorage *storage = 0;
		storage = cvCreateMemStorage (0);
    	cvClearMemStorage (storage);
	    cvCvtColor (frame, frameGray, CV_BGR2GRAY);
	  	cvEqualizeHist (frameGray, frameGray);

		CvSeq *faces;
		faces = cvHaarDetectObjects (frameGray, cascade, storage, 1.11, 4, 0, cvSize (40, 40));

	  // (5)検出された全ての顔位置に,円を描画する
		for (int i = 0; i < (faces ? faces->total : 0); i++) {
		    CvRect *r = (CvRect *) cvGetSeqElem (faces, i);
		    CvPoint center;
		    int radius;
		    center.x = cvRound (r->x + r->width * 0.5);
		    center.y = cvRound (r->y + r->height * 0.5);
		    radius = cvRound ((r->width + r->height) * 0.25);
		    cvCircle (frameGray, center, radius, colors[i % 8], 3, 8, 0);
		}
		cvReleaseMemStorage (&storage);

		cvShowImage( &quot;Face01in&quot;, frame );
		cvShowImage( &quot;Face01out&quot;, frameGray );

		char ch = cvWaitKey(33);
		if ( ch == 27 ) break;
	}
	cvReleaseCapture( &capture );
	cvDestroyWindow( &quot;Face01in&quot; );
	cvDestroyWindow( &quot;Face01out&quot; );

	return 0;
}

opencv.jp – OpenCV: 物体検出(Object Detection)サンプルコード –
http://opencv.jp/sample/object_detection.html#face_detection

を参考にして、cvHaarDetectObjects 関数で顔検出をします。
あらかじめ、学習済みの「haarcascade_frontalface_default.xml」というファイルを使う訳ですが、いじわる的に下記のように漫画で試してみると…目の大きいキャラは駄目ですねw



人を「顔」として認識するパターンと、学習済みの「顔」のパターンが違うわけで、実際には、^_^ のようなものも「顔」として人は認識できるので、何を以って「顔」とするかという哲学的な領域になります。まあ、10 年前の画像解析の論文を読むとそのあたりも書いてあるのですが、最近はもっと現実解を求めて、大ざっぱに求めるというのが通例です(顔認識カメラとか、笑顔認識カメラとか)。

パズルを解くための視覚的な能力は、オプティカルフロー(オライリーの「OpenCV」の本の 335 ページ目から)にある粗いレベルでパズルのボードを認識した後に、細かいレベルで駒の動きを目で追うというアルゴリズムになりそうですね。

opencv.jp – OpenCV: オプティカルフロー(Optical flow)サンプルコード –
http://opencv.jp/sample/optical_flow.html#optflowHSLK

のあたりに移動方向を計算する方法が載っているのですが、ちとパズルを解く処理のためだけには重いかなと。パズルのボード自体はその時々では動かないので、背景の差分だけでもよいのかなぁと考えているところです。

カテゴリー: C++, プチロボ | OpenCV を使って顔認識 はコメントを受け付けていません

OpenCV を使ってエッジ抽出

プチロボ事始め | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2416
プチロボで4軸構成にしてみる | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2421

の続き

プチロボを使ってアームを作る前に、OpenCV を使って Web カメラを使ってキャプチャを試してみました。

#include <stdlib.h>
#include &quot;opencv/cv.h&quot;
#include &quot;opencv/highgui.h&quot;

int main( int argc, char **argv )
{
	cvNamedWindow( &quot;Sample03in&quot;, CV_WINDOW_AUTOSIZE );
	cvNamedWindow( &quot;Sample03out&quot;, CV_WINDOW_AUTOSIZE );

	CvCapture *capture;
	if ( argc == 1 ) {
		capture = cvCreateCameraCapture(0);
	} else {
	 	capture = cvCreateFileCapture( argv[1] );
	}
	IplImage *frame = NULL;
	IplImage *frameGray = NULL;
	IplImage *frameCanny = NULL;
	while ( 1 ) {
		frame = cvQueryFrame( capture );
		if ( !frame ) break;

		if ( frameGray == NULL ) {
			frameGray = cvCreateImage(cvGetSize(frame), IPL_DEPTH_8U, 1);
			frameCanny = cvCreateImage(cvGetSize(frame), IPL_DEPTH_8U, 1);
		}
		cvCvtColor(frame, frameGray, CV_BGR2GRAY);
		cvCanny( frameGray, frameCanny, 10, 100, 3 );

		cvShowImage( &quot;Sample03in&quot;, frame );
		cvShowImage( &quot;Sample03out&quot;, frameCanny );

		char ch = cvWaitKey(33);
		if ( ch == 27 ) break;
	}
	cvReleaseCapture( &capture );
	cvDestroyWindow( &quot;Sample03in&quot; );
	cvDestroyWindow( &quot;Sample03out&quot; );
	return 0;
}

手順としては、

  1. cvCreateCameraCapture 関数でカメラを開く
  2. cvQueryFrame 関数で1フレーム分取得
  3. その画像に対して、グレースケール化と Canny 変換によりエッジ抽出)
  4. それぞれの画像を cvShowImage 関数で画面に表示

OpenCV は C++ からも使えるのでクラスのほうも試したほうが良いのですが、どうやらサンプルのほうはC言語のほうが多いので、こちらで。

opencv.jp – OpenCV: ビデオ入出力(Video I/O)サンプルコード –
http://opencv.jp/sample/video_io.html#cap_write



単純にアニメキャラの場合は、エッジでも十分取れるのですが、iPhone のゲームの場合は色が取れないのでエッジを抽出しても駒の判別がつかないので、別途色を加える必要がありますね。
キーボードの場合は、視認性が高いのでそのままでも良さそう。

パズルを解かせるには、エッジ抽出をしてからある程度テンプレートマッチングで場所を特定して、それから色や詳細でマッチングということになりそうです。
あと、iPhone や iPad の傾きを調べるにはエッジ抽出が有効そうです。

カテゴリー: C++, プチロボ | OpenCV を使ってエッジ抽出 はコメントを受け付けていません

コードの完成までを見積もるための考察(3)

コード品質を計測して、コードの完成までを見積もるための考察 | Moonmile Solutions Blog
コードの完成までを見積もるための考察(2) | Moonmile Solutions Blog

の続きです。

マネジメント的に 80% の合格ラインを目指すためには、どの値から計算をしていけばよいでしょうか?という問題です。
TOC/CPPM 的には、50% の見積りを最初に出します。そして確率的にプロジェクトバッファを 50% ほど取ることのなるのですが、この「50% の確率」で見積もるのは結構難しいことが分かっています。

どうして難しいかというと、

・楽観的な人に見積をしてもらうと、リスクを含めないで理想的な日数が出てきて過小見積りになる。
・悲観的な人に見積をしてもらうと、リスクを含めた日数がでてくるので過大見積りになる。

というわけで、中央の5分5分の位置というのは結構難しいのです。なので、方法としては、

・楽観的な人の見積と、悲観的な人の見積を足して 2 で割る。

のような見積をしたり、

・楽観的な人の見積の 2 倍を予想期間として見積もる

という方法を取ったりします。実際のところは、後者の「楽観的見積×2」というのが経験的に現実に非常に合います。

ならば、経験的にやりやすい方法かつ現実に即した方法を取るように、楽観的な見積=理想日数を基準にして期間見積を出すとよいのでは?と考えてみます。

(理想日数 + タスク件数 x 不具合率 x 日数) x 80%確率

になればよいわけです。ちなみに楽観的な人の見積を基準にすると、

理想日数 x 2

楽観的と悲観的見積を足して2で割ると

(理想日数 + (理想日数 + リスク発生)) / 2

この中で不確定値が少ないほうが、数式が正確になります。ただし、変数が少なすぎると最初の予想が間違っている場合は、ずれが大きくなりますね。リスク発生による日数が、理想日数の2倍と同じになると、楽観的見積からの計算と同じになります。つまりは、楽観的な見積の約3倍が悲観的な見積が、常識的なラインということでしょうか。こうなると、個人によって見積に差が出るのは当然な気がします。

ある意味、「理想日数 x 2」では、理想日数(最速でコーディングができる日数)の部分は、出せることが多いのですが、「x2」の根拠はどこから?というのがあります。まあ、経験上の x2 なので、根拠があるような無いような数値ですね。

先のシミュレートの場合は、理想日数 200 日間になるので、予想期間が 400 日間になります。シミュレートの結果よりも大幅に多いのは、不具合率が少なすぎるか、不具合修正の日数が過小すぎるか、このシミュレートが現実を表していないか、ということになるのですが。まぁ、現実よりも十分すぎるほど楽観的なモデルなのは確かですが。

さて、不具合率と不具合を直す日数の予想がつけば、それを入力値にしてシミュレーションをすることが可能です。

試しに、不具合率を 0.1, 0.3, 0.5 で計算すると「(理想日数 + タスク件数 x 不具合率 x 日数) x 80%確率」の値は次のようにシミュレートできます。

0.1: 226 日間
0.3: 298 日間
0.5: 420 日間

0.1 というのは、試験 10 件のうち 1 件に不具合が発生がする、という意味なので、0.5 の場合は 2 件に 1 件という値ですね…非常に品質の悪い状態。このシミュレーションでは不具合修正に対しても不具合が発生するという複利的な確率になるので、不具合率が増えると予想期間は自乗に比例して延びます。これを、先の理想日数 x2 と比較すると、このシミュレーションでは 0.5 弱ぐらいがうまくマッチングします。

逆に言えば、不具合率を 0.5 より下回れば、理想日数 x2 となる予想日数よりも早く終わる可能性があるわけです。
また、試験を開始して 0.3 という確率で不具合がでているならば、300 日以内に終わる確率が 80% という予想が立てられます。まぁ、実測値からの予想は、シミュレーションをするよりも、実測の不具合率 x 実測の不具合修正日数から予想を立てるほうが、良いとは思いますが。

ここで「不具合」という言葉を使っていますが、実は、

・設計にあわない不具合(コーディングが間違っている)
・設計そのものが間違っている不具合(業務にそぐわない)

の2種類がありますが、それのどちらも含めます。どちらも「直さなければいけない修正」なので、あらかじめ修正のための日数として取っておくほうが良いと思っています。
これは、設計書に 100% 忠実に沿うようにしても、不具合が 0 件にはならないことを示していて、そうであるならば、

・設計書に 80% 忠実にコーディングをする。

ことによって、設計書に沿うための労力を減らして、その分の余力を 2 種類の不具合対処にまわすというやり方です。

カテゴリー: プロジェクト管理 | コードの完成までを見積もるための考察(3) はコメントを受け付けていません

コードの完成までを見積もるための考察(2)

前回↓なところで、考察したものの実験コードです。

コード品質を計測して、コードの完成までを見積もるための考察 | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2431

あ、あまりにもダサいので、公開を躊躇していたのですが…まあ、完成コードができあがるまでの経過ということで。

/**

 class Task
  + Do()
  + GetStatus() :

CodeTask.exe: CodeTask.cs
	csc /target:exe /reference:System.dll CodeTask.cs

 */
using System;
using System.IO;

// タスク
public class Task
{
	// ランダム値
	private static Random _rnd = new Random(Environment.TickCount);
	// 状態
	private TASK_STATUS _status = TASK_STATUS.START;
	// 期間
	private double _period = 0;

	// タスクを実行
	// 地道に遷移図を直すパターン
	public void Do()
	{
		int r = 0;

		switch ( _status ) {
		case TASK_STATUS.START: // 開始
			_period += 1;
			_status = TASK_STATUS.DONE_CODE; // コード完了
			break;
		case TASK_STATUS.DONE_CODE: // コード完了
			_period += 1;
			_status = TASK_STATUS.DONE_TEST; // 試験完了
			break;
		case TASK_STATUS.DONE_TEST: // 試験完了
			_period += 0;
			r = _rnd.Next(10);
			if ( r < 9 ) {
				_status = TASK_STATUS.TEST_OK;	// 試験合格
			} else {
				_status = TASK_STATUS.TEST_NG;	// 試験失敗
			}
			break;
		case TASK_STATUS.TEST_OK:	// 試験合格
			_period += 0;
			_status = TASK_STATUS.CODE_COMPLETE;	// コード完成
			break;
		case TASK_STATUS.CODE_COMPLETE:	// コード完成
			_period += 0;
			_status = TASK_STATUS.END;	// 完了
			break;

		case TASK_STATUS.TEST_NG:	// 試験失敗
			_period += 1;
			_status = TASK_STATUS.DONE_RECODE;	// コード修正
			break;
		case TASK_STATUS.DONE_RECODE:	// コード修正
			_period += 1;
			_status = TASK_STATUS.DONE_RETEST;	// 再試験完了
			break;
		case TASK_STATUS.DONE_RETEST:	// 再試験完了
			_period += 0;
			r = _rnd.Next(10);
			if ( r < 9 ) {
				_status = TASK_STATUS.TEST_OK;	// 試験合格
			} else {
				_status = TASK_STATUS.TEST_NG;	// 試験失敗
			}
			break;

		case TASK_STATUS.END: // 完了
			_period += 0;
			_status = TASK_STATUS.END; // 完了
			break;
		}
	}
	// 状態を取得
	public TASK_STATUS GetStatus()
	{
		return _status;
	}
	// 期間を取得
	public double GetPeriod()
	{
		return _period;
	}
}

// 人員
public class Person
{
	private Task _task ;

	public bool SetTask( Task task )
	{
		_task = task ;
		return true;
	}
	public bool Do()
	{
		if ( _task == null ) return false;
		_task.Do();
		return true;
	}
}

// タスクの状態
/*
 状態遷移
   開始 -> コード完了-> 試験完了
   試験完了 -> 試験合格 -> コード完成
   試験完了 -> 試験失敗 -> コード修正
   コード修正 -> 再試験完了 -> 試験合格
   コード修正 -> 再試験完了 -> 試験失敗
 */
public enum TASK_STATUS
{
	START	= 0,
	DONE_CODE,
	DONE_TEST,
	TEST_OK,
	CODE_COMPLETE,
	TEST_NG,
	DONE_RECODE,
	DONE_RETEST,
	END
}

public class Program
{
	public static void Main( string[] args )
	{
		double sum0 = 0.0;
		for ( int k=0; k<100; k++ ) {
			double sum = 0.0;
			Person person = new Person();
			for ( int j=0; j<100; j++ ) {
				Task task = new Task();
				person.SetTask( task );
				for ( int i=0; i<100; i++ ) {
					// task.Do();
					person.Do();
					TASK_STATUS st = task.GetStatus();
					// Console.Write(&quot;{0}->&quot;, st.ToString());
					if ( task.GetStatus() == TASK_STATUS.END )
						break;
				}
				sum += task.GetPeriod();
				// Console.WriteLine(&quot;{0}&quot;, task.GetPeriod());
			}
			Console.WriteLine(&quot;period sum,{0}&quot;, sum );
			sum0 += sum ;
		}
		Console.WriteLine(&quot;period ave,{0}&quot;, sum0/100.0 );
	}
}

状態遷移のところは、ひとまず Task クラス内にベタベタに書いています。この部分は本来は状態クラスを分離して、タスク遷移とは切り離しておきたいところ。
タスクを実行するのは、人員クラス(Person クラス)なんですが、このコードだとパラレルに動けるような複数名の場合がシミュレートできません。あと、コーディングする人とテストする人が別々の役割になるような待ち行列状態がシミュレートできません。
そうそう、複数タスクが並列に動く場合もシミュレートできないという…ちょっと、というか、かなり中途半端なプログラムです。

まあ、これを実行してどう分析するかというと、外側のループで 100 回試行しているのでこれを Excel で処理します。

日数で、累積をして表にします。

[img 20111111_01.jpg]

100 個のタスクが終わるまでの日数と累積した確率をグラフにします。

[img 20111111_02.jpg]

オレンジの行が、最頻値になります。このプログラム場合、タスクの日数は2日間(コーディング1日、試験1日)発生バグ率が 10% なので、平均は 2.2 日間になります。100 個のタスクをこなすと、予想通り 220 日間が最頻値になります。不具合の発生率は正規分布に準じることになるので、この最頻値がイコール中央値になりますね。

さて、プロジェクトのマネージメントの立場から言うと(どこの時点までリスクを含めるかというと)、中央値の 220 日を取るのではなく、プロジェクトが 80% の確率で終わる時点を取ります。
確率を累積することになるので、緑の行の 226 日時点になります。

これで何がわかるかというと、

■理想日数 200 日間で終わる確率は 0% である。

当たり前ですが、不具合が発生する以上、理想的なコーディング日数 × タスク数とはなりません。
この場合は、2 日間 × 100 タスクということで、200 日間です。

■中央値は、不具合発生率を掛けた地点になる

このシミュレーションの場合、10 回に 1 回不具合が発生するので、不具合発生率は 10 % になります。
普通は、これを掛けたところの 220 日間が、コーディング完成の予想日数なわけですが、この値は中央値になるので累積の確率は 50 % になります。5分5分の勝負では、プロジェクトマネージメントとしては不満ですね。

■累積確率の 80% ラインを予定日数とする

累積した確率が 80% ラインあるいは、90% ラインにします。
80% にするか、90% にするかは、予定日数を超えた場合の費用によって決定します。
このシミュレーションの場合は、80% ラインが 226 日間、90 % ラインが 230 日間となるので、4 日間分の費用が、リスクを超えたときの損失よりも少なければ、90 % にすればよいわけです。
理想日数が、200 日間なので、15 % ほど費用を割り増しすれば、安全圏が 90 % になるというわけです。

さて、この理想日数から実行するときのスケジュールを立てるには、どうするのかというと…続く。

カテゴリー: プロジェクト管理 | コードの完成までを見積もるための考察(2) はコメントを受け付けていません

コード品質を計測して、コードの完成までを見積もるための考察

コードの品質は、コードの不具合を直すよりも、優先度が高いのだッ!!! … というのを、数式で証明するテストです。
数式というか、シミュレーションですね。プログラミングをするときの不具合の混入率と不具合の修正日数を変数に入れれば、おおまかにコードの完了予定日が計算できればよいかなと。


まずは、状態遷移のモデルを作ります。
状態と作業タスクが、多少ごっちゃになっていますが、

A.コード生成
B.試験
C.不具合発生
D.コード修正
E.再試験
F.合格
G.完成品

という具合です。
ある状態からある状態に移るときに、確率を利用します。必ず次の状態に移るときは「1.0」という具合。
確率の部分を除くと、PERT 図と同じになるので、それぞの状態≒タスクになります。
「合格」や「不具合発生」の場合は、実は現象をあらわすだけので、作業日数は「0.0」とすれば OK です。まぁ、このあたりはおおまかに。

システムダイナミクスの理論を応用する…というか、フィードバックを入れるので、数式で解くよりも適当なプログラミングを行ってシミュレーションするほうが楽になります。というのも、このくらいのフィードバックと変数ならば数式でやってもよいのですが、このタスクをこなす【人員】の問題を入れると途端に難しくなるので、シミュレーションにします。

【人員】の問題というのは、試験、コード修正などを1名で行う場合には、単純な作業日数の累積になるのですが、実際のように複数名で行う場合、コーディングをする人と試験を行う人が違うとか、3名がコーディングと試験の両方をこなして手が空いた人が不具合を修正するとか、というような「待ち行列」の問題(サプライチェーンの問題ともイコールです)が入ってくるので、単純にはいきません。いわゆる「M/M/N」の待ち行列になるわけです。

あと、状態遷移図を見ると分かるとおり、非常に低い確率ではありますが、不具合修正が不具合を呼ぶというループがあります。この事象を適当な回数で切り捨てるために(確率が低いので)、「試験完了まで80%で終わる確率の日数は、何日か?」というような、答えを確率で計算する場合も数式だけだとちょっと難しいのです。まぁ、実際に応用するためにも、厳密な計算値よりも、大雑把ではあるがリスクの少ない数値(確度の高い数値)を拾っておいて、その他に関してはリスク管理として別立てにするとよい、ということころです。

前置きが長くなってしまったのですが、状態遷移図を遷移表に直したのが次のものです。

[img 20111028_03.jpg]

遷移図と遷移表は同値なので、どちらかを使えば OK です。Excel で処理するには、遷移表のほうが便利かなと。

さて、これをどうやってシミュレーションしようかと考えていたのですが、オブジェクト指向的には、マルチエージェントシステム風に組んだほうがよかろうと思っています。この場合は簡単なものなので、専用のシステムは使わず、C# で組むか、Excel VBA で組むかというところですね。継承関係など複雑なものはいらないので、Excel VBA でも十分かもしれません。

という訳で、引き続き。

カテゴリー: プロジェクト管理 | コード品質を計測して、コードの完成までを見積もるための考察 はコメントを受け付けていません