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("search image", 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 << "(" << max_pt.x << ", " << max_pt.y << "), score=" << 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("search image", 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("mini\\koma01.png", 1);
	tmp_imgs[1] = cv::imread("mini\\koma02.png", 1);
	tmp_imgs[2] = cv::imread("mini\\koma03.png", 1);
	tmp_imgs[3] = cv::imread("mini\\koma04.png", 1);
	tmp_imgs[4] = cv::imread("mini\\koma05.png", 1);
	tmp_imgs[5] = cv::imread("mini\\koma06.png", 1);
	tmp_imgs[6] = cv::imread("mini\\koma07.png", 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("search image", 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 << "(" << max_pt.x << ", " << max_pt.y << "), score=" << 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("search image", 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 "opencv/cv.h"
#include "opencv/highgui.h"

int main( int argc, char **argv )
{
	cvNamedWindow( "Face01in", CV_WINDOW_AUTOSIZE );
	cvNamedWindow( "Face01out", 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 = "haarcascade_frontalface_default.xml";
	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( "Face01in", frame );
		cvShowImage( "Face01out", frameGray );

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

	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 "opencv/cv.h"
#include "opencv/highgui.h"

int main( int argc, char **argv )
{
	cvNamedWindow( "Sample03in", CV_WINDOW_AUTOSIZE );
	cvNamedWindow( "Sample03out", 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( "Sample03in", frame );
		cvShowImage( "Sample03out", frameCanny );

		char ch = cvWaitKey(33);
		if ( ch == 27 ) break;
	}
	cvReleaseCapture( &capture );
	cvDestroyWindow( "Sample03in" );
	cvDestroyWindow( "Sample03out" );
	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("{0}->", st.ToString());
					if ( task.GetStatus() == TASK_STATUS.END )
						break;
				}
				sum += task.GetPeriod();
				// Console.WriteLine("{0}", task.GetPeriod());
			}
			Console.WriteLine("period sum,{0}", sum );
			sum0 += sum ;
		}
		Console.WriteLine("period ave,{0}", 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 でも十分かもしれません。

という訳で、引き続き。

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

SqlBulkCopy では、Decimal 型で桁落ちをする

SqlBulkCopy のスピードは 20 倍ぐらい早い | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/2234

なところで、SQL Server へのインサートを高速化したのですが、実はちょっとした落とし穴があります(落とし穴、というよりも「仕様」っぽいのですが)。

SQL Server で Decimal 型を使うと、Oracle の nameric 型のように精度を指定できます。たとえば decimal(18,4) と設定すると、小数点以下第4桁までの精度になります。

この場合、「0.12345」という値を入れようとすると、クエリでは「0.1235」のように最後の桁で四捨五入されるのですが、SqlBulkCopy を使うと「0.1234」のように切り捨てになる、という違いがあります。

CREATE TABLE [dbo].[t_dec](
	[dec] [decimal](18, 4) NULL,
	[dbl] [float] NULL
) ON [PRIMARY]

のようにテーブルを作成しておいて、データベースに書き込みます。

''' <summary>
''' BulkCopy で書き込み
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
''' <remarks></remarks>
Private Sub Button3_Click(sender As System.Object, e As System.EventArgs) Handles Button3.Click

	Dim dec As Decimal = 0.1234567890123456789012345679D
	Dim dbl As Decimal = 0.12345678901234559
	Dim dt As New DataTable

	dt.Columns.Add("dec", GetType(Decimal))
	dt.Columns.Add("dbl", GetType(Double))
	Dim row As DataRow = dt.NewRow
	row("dec") = dec
	row("dbl") = dbl
	dt.Rows.Add(row)

	Dim cn As New SqlConnection(CNSTR)
	Dim bc As New SqlBulkCopy(cn)
	cn.Open()
	bc.DestinationTableName = "t_dec"
	bc.WriteToServer(dt)
	cn.Close()
	bc.Close()

	' decimal(18,4) で指定していると、dec = 0.1234 となり切り捨てとなる

	Dim dt2 As New DataTable
	Dim da As New SqlDataAdapter("SELECT * FROM t_dec", cn)
	da.Fill(dt2)
	DataGridView1.DataSource = dt2


End Sub

''' <summary>
''' SqlCommand で書き込み
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
''' <remarks></remarks>
Private Sub Button4_Click(sender As System.Object, e As System.EventArgs) Handles Button4.Click

	Dim dec As Decimal = 0.1234567890123456789012345679D
	Dim dbl As Decimal = 0.12345678901234559

	Dim cn As New SqlConnection(CNSTR)
	Dim cmd As New SqlCommand("INSERT INTO t_dec values ( @DEC, @DBL )", cn)
	cmd.Parameters.Add(New SqlParameter("@DEC", dec))
	cmd.Parameters.Add(New SqlParameter("@DBL", dbl))

	cn.Open()
	cmd.ExecuteNonQuery()
	cn.Close()

	' decimal(18,4) で指定していると、dec = 0.1235 となり四捨五入される

	Dim dt2 As New DataTable
	Dim da As New SqlDataAdapter("SELECT * FROM t_dec", cn)
	da.Fill(dt2)
	DataGridView1.DataSource = dt2
End Sub

こうすると、SqlCommand の場合は「0.1235」のように四捨五入されるのですが、SqlBulkCopy の場合は「0.1234」のように切り捨てられます。

なので、仕方がないので(?)、四捨五入されるように「0.00005D」を加算します(末尾の「D」は、decimal 型の印です)

''' <summary>
''' decimal の精度に合わせて 0.00005 を加算する
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
''' <remarks></remarks>
Private Sub Button5_Click(sender As System.Object, e As System.EventArgs) Handles Button5.Click

	Dim dec As Decimal = 0.1234567890123456789012345679D
	Dim dbl As Decimal = 0.12345678901234559
	Dim dt As New DataTable

	dt.Columns.Add("dec", GetType(Decimal))
	dt.Columns.Add("dbl", GetType(Double))
	Dim row As DataRow = dt.NewRow
	row("dec") = dec + 0.00005D	' 四捨五入させる
	row("dbl") = dbl
	dt.Rows.Add(row)

	Dim cn As New SqlConnection(CNSTR)
	Dim bc As New SqlBulkCopy(cn)
	cn.Open()
	bc.DestinationTableName = "t_dec"
	bc.WriteToServer(dt)
	cn.Close()
	bc.Close()

	' BulkCopy だと切り捨てになるので、
	' decimal(18,4) の場合は 0.00005D を加えて、
	' dec = 0.1235 のように四捨五入にする

	Dim dt2 As New DataTable
	Dim da As New SqlDataAdapter("SELECT * FROM t_dec", cn)
	da.Fill(dt2)
	DataGridView1.DataSource = dt2

End Sub

さて、これを組み込みかどうかを思案中。

カテゴリー: 開発, VB | SqlBulkCopy では、Decimal 型で桁落ちをする はコメントを受け付けていません

データベースアクセスのパフォーマンスチューニング例

2000件のデータ登録で2時間程掛かっている処理を、10分程度に短縮する方法を紹介…ができたらいいなぁ。ってのを、考察も交えて紹介します。

■擬似コード

・20回ほどの SELECT 文が呼び出されている(らしい)。
・1件のデータ登録で、20件のデータ挿入がある。

for i = 1 to 2000 
	
	何かと SELECT 処理がある。
	前の SELECT の結果を利用して、次のパラメータとする...らしい。
	時には if 文などがある。
	SELECT1
	SELECT2
	...
	SELECT20

	帳票用のテーブルから該当レコードを DELETE する。
	DELETE1
	DELETE2
	...
	DELETE20

	帳票用のテーブルに 20 件データ出力する
	INSERT1
	INSERT2
	...
	INSERT20
next

この1回のループが4秒ぐらいなので、4秒×2000回 = 8000秒 ≒ 2時間超 という具合。

■考えられる対策

1.
データベースに SELECT を何度も呼び出しているが、これが毎回 SqlCommand 呼出となっている。なので、毎回データベースに接続する手間が掛かっているのでは?これを、できれば一回の SqlCommand 呼出にする。ただし、SELECT1 から SELECT20 までの間は if 文が入ったり、動的に SQL 文を作っていたりするので、必ずしもひとつにまとめられるとは限らない。
出来る限り、まとまることにする。

2.
帳票テーブルに関しては、DELETE のあとに INSERT している。INSERT を単独で呼び出すと遅くなるので(総計4万件になる)SqlBulkCopy を使って一括挿入する。

3.
その前に DELETE を何度も呼び出しているが、これは無駄なので、該当する ID をまとめるなどして1回だけの DELETE 呼出にする。

4.
実は、所々 DataTable を使っているので VB 側の処理が重たくなっている可能性もある。このあたりは、実測した後に、どちらのチューニングに重きをおくのか?あるは、アクセスチューニングによって、どのくらいの効果が見込めるのか、をあらかじめ考察しておく。

■実験用テーブル

プロジェクト案件テーブル
 親子の案件があるので、親案件(parent_id)が自分自身を示す。
 (これが今回の SQL を複雑にしている原因でもあるので、実験テーブルにいれておく)

CREATE TABLE [dbo].[t_anken](
	[id] [int] NOT NULL,
	[name] [varchar](50) NOT NULL,
	[parent_id] [int] NULL,
	[userid] [varchar](10) NOT NULL,
	[updatedate] [datetime] NOT NULL
) ON [PRIMARY]

出力用の帳票テーブル

CREATE TABLE [dbo].[t_report](
	[id] [int] NOT NULL,
	[name] [varchar](50) NOT NULL,
	[data] [varchar](500) NOT NULL,
	[userid] [varchar](10) NOT NULL,
	[updatedate] [datetime] NOT NULL
) ON [PRIMARY]

■データ挿入用コード

20万件のデータ挿入は10秒弱ぐらいで終わります。

private string toMD5(string s)
{
	//文字列をbyte型配列に変換する
	byte[] data = System.Text.Encoding.UTF8.GetBytes(s);

	//MD5CryptoServiceProviderオブジェクトを作成
	System.Security.Cryptography.MD5CryptoServiceProvider md5 =
		new System.Security.Cryptography.MD5CryptoServiceProvider();
	//または、次のようにもできる
	//System.Security.Cryptography.MD5 md5 =
	//    System.Security.Cryptography.MD5.Create();

	//ハッシュ値を計算する
	byte[] bs = md5.ComputeHash(data);

	//byte型配列を16進数の文字列に変換
	System.Text.StringBuilder result = new System.Text.StringBuilder();
	return BitConverter.ToString(bs).ToLower().Replace("-", "");
}

// 接続文字列
const string CNSTR =
	@"Data Source=.\sqlexpress;Initial Catalog=stress;Integrated Security=True;Pooling=False";
const int ANKENMAX = 200000;

private void button1_Click(object sender, EventArgs e)
{
	SqlConnection cn = new SqlConnection(CNSTR);

	// テーブル内容を削除
	SqlCommand cmd = new SqlCommand();
	cmd.CommandText = 
		"DELETE FROM t_anken \n" + 
		"DELETE FROM t_report";
	cmd.Connection = cn;
	cn.Open();
	cmd.ExecuteNonQuery();
	cn.Close();

	// データ作成
	SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM t_anken WHERE 1 = 0", cn);
	DataTable dt = new DataTable();
	da.Fill(dt);
	for (int i = 0; i < ANKENMAX; i++)
	{
		DataRow row = dt.NewRow();
		row["id"] = i;
		row["name"] = toMD5(i.ToString() + DateTime.Now.ToString());
		row["parent_id"] = DBNull.Value;
		row["userid"] = "masuda";
		row["updatedate"] = DateTime.Now;
		dt.Rows.Add(row);
	}
	SqlBulkCopy bc = new SqlBulkCopy(cn);
	bc.DestinationTableName = "t_anken";
	cn.Open();
	bc.WriteToServer(dt);
	cn.Close();
	bc.Close();

	MessageBox.Show(string.Format("t_anken に {0} 件データ挿入しました", ANKENMAX));
}

続きは明日以降で

カテゴリー: 開発, C# | データベースアクセスのパフォーマンスチューニング例 はコメントを受け付けていません

プチロボで4軸構成にしてみる

キーボードをぽちぽち打つのであれば、3軸でもいいような気もするのだが、土台に2軸、間接に1軸の3軸の自由度にだけだと指先がキーボードに届かなくて困るのでは?と思い、指先部分に1軸加える。

何で腕の部分を作ろうかと思案した挙句、そうだレゴがあるじゃないか、という具合で作ってみたのがこれ↓

手元にはレゴ(大)(苦笑)しかなくて、レゴ(大)で組み合わせてみたものの、レゴ(大)の重さがサーボには重すぎて…倒れてしまう、ってな感じで後でやり直すことにしよう。

写真を見ても分かる通り、一番向こうのキーに届かないし、腕(というか指の関節)を最大に伸ばした時にはコードが届かなそうだし。

ちなみに、両面テープでくっ付けたので、その後しばらくするとカタンと落ちてしまうのが悲しい。

カテゴリー: プチロボ | プチロボで4軸構成にしてみる はコメントを受け付けていません

プチロボ事始め

Amazon.co.jp: プチロボ改(ロボット素材キット): おもちゃ
http://www.amazon.co.jp/dp/B001IWNE0C/

amazon からプチロボが届いたので、サーボモータを繋げてみる。
2足ロボットを組み立てる予定はない(苦笑)なので、9個のサーボをどう組み合わせるかというと。

例えば、

■キーボードを打たせる

最初は、iPhone か iPad ですりすりさせる、というのを考えたいたのだが、このサーボにはフィードバックがないので(パソコンからの送信のみ)、スライドという動作が結構大変そうなことが判明。

なので、昔の万博で出ていたようなキーボードを打たせるところから始めようと思う。ロボットの腕を制御するときに3つの自由度があればよいのだが、サーボが動いたとき(あるいはモーター位置決めをしたとき)の後のフィードバックがないと、どこに腕を置いたのかがわからなくなる。

なので、

  1. 指を降ろす。
  2. キーが打てたかを、視覚的に確認する。

というフィードバックが必要になる。ある程度キーボードの配置を覚えれば、ブランドタッチ(って何がなんだか)が可能なのだが、初心者のうちはキートップの文字を見ながら指を降ろしていくことになる。この動作が、

  1. 視覚的に、キートップの印字を識別。
  2. 目的のキー目指して、指を降ろす。
  3. 目的のキーが押せるか判別する。

ってな、視覚的なフィードバックが必要になる。

視覚的な部分は通常の WEB カメラを使って、差分検出か最初はマーキングあたりで指先を検出。最終的にはキートップのマークをテンプレートマッチングさせて、未知のキーまで押せるように、かな。適当な学習器を挟み込んで、ローレゾの WEB カメラとサーボの誤差に対応させる予定。

カテゴリー: プチロボ | 4件のコメント