タマふぃるた~

昨日は日曜日だったのでこんなものを作りました。

ビデオカメラからの入力画像をリアルタイム解析して、ある程度丸いものが見つかったらパカっと開けて中でふにふに動いてくれます。

以下技術的な話です。

やってることは至って単純で、OpenCvでカメラ入力画像から輪郭抽出をして、その中で円形に近い領域に対して上半分のピクセルを上にずらして、空いたところにアニメーションを表示しているものです。

ある領域が円形に近いかどうかの判定は以下の2つの条件で行いました。

  • 領域矩形の縦横比が1に近い
  • 領域矩形との面積比がpi/4(=正方形に内接する円)に近い

誤検知を狙って作られたようなひねくれオブジェクトでなければ、大体これで丸いものを見つけられるようです。

ただ、みかんが意外と横に長かったり、環境とスレッショルドの兼ね合いによっては検出される輪郭が大きかったり小さかったりするようで、結構パラメータには余裕をもたせた方が良いかも知れません。

僕の身の周りのものだと、縦横比は3:2くらいまで、面積比は0.675から0.875くらいまであたりの設定で良い感じに検出されてくれました。

例えば電球とか、瓶や缶とかだと理想的なので検出されやすいと思います。

ところで、こういう値のリアルタイム調整にはofxParamEditが便利なんで使ってください。(宣伝)

以下ソースです。(抜粋)

(ofxOpenCvを使ってます)

// 輪郭抽出時のパラメータ(適当に初期化してください)
float area_min_;
float area_max_;
int area_count_max_;
int gray_threshold_;

// 丸いかどうかの判定に使用するパラメータ(適当に初期化してください)
struct BlobDetect {
	float aspect_ratio_;
	float area_ratio_min_;
	float area_ratio_max_;
} blob_detect_;

ofPixels buf_;	// カメラ入力画像データ
ofxCvColorImage cv_color_;
ofxCvGrayscaleImage cv_gray_;
ofxCvContourFinder contour_;
ofxCvContourFinder contour_inv_;
vector<ofxCvBlob*> valid_blobs_;	// 有効な領域だけ保持しとく
Animation anime_;	// ふにふにアニメーション

void update()
{
	// カメラ画像をcvImageに読み込み
	cv_color_.setFromPixels(buf_.getPixels(), buf_.getWidth(), buf_.getHeight());
	// グレースケール変換
	cv_gray_ = cv_color_;
	// 二値化
	cv_gray_.threshold(gray_threshold_);
	// 輪郭抽出
	contour_.findContours(cv_gray_, area_min_, area_max_, area_count_max_, false);

	// 逆に二値化して再度チェック
	cv_gray_ = cv_color_;
	cv_gray_.threshold(gray_threshold_, true);
	contour_inv_.findContours(cv_gray_, area_min_, area_max_, area_count_max_, false);

	// 丸い輪郭だけ抽出
	valid_blobs_.clear();
	for(int i = 0; i < contour_.blobs.size(); ++i) {
		if(isValidBlob(contour_.blobs[i])) {
			valid_blobs_.push_back(&contour_.blobs[i]);
		}
	}
	for(int i = 0; i < contour_inv_.blobs.size(); ++i) {
		if(isValidBlob(contour_inv_.blobs[i])) {
			valid_blobs_.push_back(&contour_inv_.blobs[i]);
		}
	}

	// パカってする
	ofPixelsRef pix = cv_color_.getPixelsRef();
	for(vector<ofxCvBlob*>::iterator it = valid_blobs_.begin(); it != valid_blobs_.end(); ++it) {
		const ofRectangle& rect = (*it)->boundingRect;
		for(int x = rect.x; x < rect.x+rect.width; ++x) {
			for(int y = rect.y; y < rect.y+rect.height/2; ++y) {
				if(y-rect.height/2 < 0) {
					continue;
				}
				pix.setColor(x,y-rect.height/2, pix.getColor(x,y));
				pix.setColor(x,y, ofColor(0,0,0,0));
			}
		}
	}
	// テクスチャ更新
	cv_color_.updateTexture();
}

// その領域が丸いかどうかの判定
bool isValidBlob(ofxCvBlob& blob)
{
	// 縦横比
	const ofRectangle& rect = blob.boundingRect;
	if(rect.width < rect.height) {
		if(rect.height/rect.width > blob_detect_.aspect_ratio_) {
			return false;
		}
	}
	else {
		if(rect.width/rect.height > blob_detect_.aspect_ratio_) {
			return false;
		}
	}
	// 面積
	float area_ratio = blob.area/(rect.width*rect.height);
	if(area_ratio > blob_detect_.area_ratio_max_ || area_ratio < blob_detect_.area_ratio_min_) {
		return false;
	}
	return true;
}

void draw()
{
	ofPushMatrix();
	ofPushStyle();
	cv_color_.draw(0,0);
	for(vector<ofxCvBlob*>::iterator it = valid_blobs_.begin(); it != valid_blobs_.end(); ++it) {
		const ofRectangle& rect = (*it)->boundingRect;
		anime_.draw(rect.x, rect.y, rect.width, rect.height/2);
	}
	ofPopStyle();
	ofPopMatrix();
}

ちなみに、画像をパカっと切り取るところはシェーダーを使うとフレームバッファを消費することになるので、可搬性を考慮してCPUでやりましたが、あんまり気持ちよくはないのでうまい方法があれば教えてください。

Lazy.h

Kinectとかのハードウェアからとってきた値を使うような処理をしていると、値の細かなブレが歓迎されない状況が多いので、ブレを吸収するためのテンプレートクラスを作ってみました。

Lazy.h on gist

組み上げたアプリへの導入が簡単になるよう、=で代入して右辺値で取り出すっていうシンプルな使い方にしていますので、目的の値の宣言をLazy<>で囲むだけで導入できます。

実際、すでに作った処理に対して組み込む作業を自分でもやってますが、かなり楽ちんです。

例えばfloatならLazy<float>、oFでofPointならLazy<ofPoint>にすればOK。

コンストラクタの第一引数か、もしくはsetSize関数でバッファの大きさを指定してやれば、それだけでブレ補正してくれるはずです。この値が大きいほど補正が強くなります。

以下軽く注意。

  1. 左辺値としては=演算子しか実装してないので、それ以外の使い方をしてるところがあればコンパイルエラーがでます。適切に処理してあげてください。
  2. setSize(unsigned int), clear(), reset(const T&)という関数がもともと定義してある場合、おそらく意味が変わっている上にコンパイル時にエラーとならないので、適切に処理してください。
    ※Tは<>内で指定した型
  3. コンストラクタの意味が変わっているので、そこはチェックしてください。
  4. テンプレートなのでどんなクラスでも使えますが、=演算子、+演算子、/(float)演算子が(適切に)実装されていることが必要です。

 

ちなみに、細かい設定はできませんがアニメーションにも使用できます。

以下のコードは0から1までの値を10分割補間して出力します。
※whlieの条件が環境によってはヤバそうですが、サンプルってことでご容赦を。

Lazy<float> value(10);
value = 0;
while(value < 1.0f) {
	printf("%f\n", value);
	value = 1.0f;
}