昨日は日曜日だったのでこんなものを作りました。
ビデオカメラからの入力画像をリアルタイム解析して、ある程度丸いものが見つかったらパカっと開けて中でふにふに動いてくれます。
以下技術的な話です。
やってることは至って単純で、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でやりましたが、あんまり気持ちよくはないのでうまい方法があれば教えてください。