輪郭線のテンプレート探索によるボールの検出
力技の輪郭線マッチを試しました。ニューラルネットワークのクラス分類器は、早いしマジすごいですが、なかなかうまく検出できない物体が結構あって、手動での調節が難しいので、とりあえずなんでもいいから、いじれる物体検出プログラムを作りたいと思って試しました。よく分からないので、とりあえず一番単純そうな方法で行いました。検出対象は、どこを向いていても同じ輪郭が見えるので比較的簡単そうな『ボール』にしました。
検出方法
まず、こんなテンプレートマッチ用のフィルターをテキトウに定義します。
// テキトウな円形検出フィルター #define CIRCLE_FILTER_WIDTH 16 #define CIRCLE_FILTER_HEIGHT 16 const int CIRCLE_FILTER[CIRCLE_FILTER_HEIGHT][CIRCLE_FILTER_WIDTH] = { { -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 }, { -2, -2, -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2, -2, -2 }, { -2, -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2, -2 }, { -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, +1, -1, -1, -1, -1, +1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, -1, -1, -1, -1, -1, -1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, -1, -1, -1, -1, -1, -1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, -1, -1, -1, -1, -1, -1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, -1, -1, -1, -1, -1, -1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, +1, -1, -1, -1, -1, +1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2 }, { -2, -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2, -2 }, { -2, -2, -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2, -2, -2 }, { -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 } }; // 円形かの閾値 #define CIRCLE_FILTER_THRESHOLD 25
これを探索ウィンドウにして、輪郭線抽出した画像内を探索し、各エリアでのフィルターの出力値からボールがあるかを判定してみました。
フィルターの出力値は、フィルターに対応する各ピクセルの色(黒:1 or 白:0)との積の総和としました。これが閾値を超える場合は、そこにボールがあると判定します。
一番下に試したソースコードを載せています。
結果
画像はflickrにあったCCのものを使用しました。
http://www.flickr.com/photos/person/56233211/
http://www.flickr.com/photos/andrewpaulcarr/1333940043/
http://www.flickr.com/photos/nevrlndtink/1362178354/
最初に一般的そうな、失敗するともしないとも微妙な感じの画像で試しました。ダメそうならあきらめるつもりでした。
探索中はプログラムから上のような感じで見えています。輪郭線画像はCannyフィルターで作るようにしました。
最終的にボールが検出されたエリアに、検出したボールと同じ色でマークをつけます。
わりとうまくいっている感じです。
検出できなかったのは、ぼやけているため輪郭線抽出の時点でとり逃したボールと、背景の輪郭線がでてしまったために、-2のフィルターが反応して、出力値が閾値よりも下がってしまったボールのようでした。
まとめ
このフィルタでは背景とかぶる輪郭線はうまく検出できない。逆に二重円の内側の円やスキンヘッドの人の顔の中など、輪郭線がある程度保障されていて特徴のある輪郭線がある物体に対しては、そこそこ使えそう。もう少しまともに使えるようにするためには、ある程度の不一致を許すようにフィルター出力値の計算方法を変える必要がある。また、検出したあとに検出物体の色の特徴など別の手段で検査すると誤検出ははじけそう。
あと、めちゃくちゃ遅い。ダメダ。
ソースコード
OpenCVが必要です。
#include <stdio.h> #include <stdlib.h> #include "highgui.h" #include "cv.h" #include "cxcore.h" #define _DEBUG_IMG 0 // テキトウな円形検出フィルター #define CIRCLE_FILTER_WIDTH 16 #define CIRCLE_FILTER_HEIGHT 16 const int CIRCLE_FILTER[CIRCLE_FILTER_HEIGHT][CIRCLE_FILTER_WIDTH] = { { -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 }, { -2, -2, -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2, -2, -2 }, { -2, -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2, -2 }, { -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, +1, -1, -1, -1, -1, +1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, -1, -1, -1, -1, -1, -1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, -1, -1, -1, -1, -1, -1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, -1, -1, -1, -1, -1, -1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, -1, -1, -1, -1, -1, -1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, +1, -1, -1, -1, -1, +1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2 }, { -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2 }, { -2, -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2, -2 }, { -2, -2, -2, -2, +1, +1, +1, +1, +1, +1, +1, +1, -2, -2, -2, -2 }, { -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 } }; // 円形かの閾値 #define CIRCLE_FILTER_THRESHOLD 25 // 円形? int isCircle(int val) { return CIRCLE_FILTER_THRESHOLD < val ? 1:0; } // ボール検出 int ballDetect(IplImage *base_image) { IplImage *canny_base_image = cvCreateImage(cvGetSize(base_image), IPL_DEPTH_8U, 1); IplImage *canny_image = cvCreateImage(cvGetSize(base_image), IPL_DEPTH_8U, 1); IplImage *temp_image; int count = 0; double scale = 1.3; double current_scale = 1; // 輪郭線抽出 cvCvtColor(base_image, canny_base_image, CV_BGR2GRAY); cvCanny(canny_base_image, canny_base_image, 50.0, 150.0); cvCopy(canny_base_image, canny_image); // 探索 while (CIRCLE_FILTER_WIDTH < canny_image->width && CIRCLE_FILTER_HEIGHT < canny_image->height) { for (int y = 0; y + CIRCLE_FILTER_HEIGHT < canny_image->height; y += 2) { for (int x = 0; x + CIRCLE_FILTER_WIDTH < canny_image->width; x += 2) { // フィルター出力値計算 int filter_value = 0; for (int fy = 0; fy < CIRCLE_FILTER_HEIGHT; ++fy ) { for (int fx = 0; fx < CIRCLE_FILTER_WIDTH; ++fx ) { CvScalar color = cvGet2D( canny_image, y + fy, x + fx ); filter_value += CIRCLE_FILTER[fy][fx] * (color.val[0] == 0 ? 0:1); } } if (isCircle(filter_value)) { // ヒット int rx = cvRound(x * current_scale); int ry = cvRound(y * current_scale); int rw = cvRound(CIRCLE_FILTER_WIDTH * current_scale); int rh = cvRound(CIRCLE_FILTER_HEIGHT * current_scale); CvScalar color_avg; IplImage *ball_image = cvCreateImage( cvSize(rw, rh), IPL_DEPTH_8U, 3 ); #ifdef _DEBUG printf("filter value: %d\n", filter_value); #endif #if _DEBUG_IMG cvRectangle( canny_image, cvPoint(x, y), cvPoint(x + CIRCLE_FILTER_WIDTH, y + CIRCLE_FILTER_HEIGHT), cvScalar(0xcc), 2 ); #endif // 検出範囲コピー cvSetImageROI(base_image, cvRect(rx, ry, rw, rh)); cvCopy(base_image, ball_image); // 色の平均取得 color_avg = cvAvg(ball_image); cvResetImageROI(base_image); // マーク cvRectangle( base_image, cvPoint(rx, ry), cvPoint(rx + rw, ry + rh), color_avg, 2 ); cvReleaseImage(&ball_image); ++count; } } } #if _DEBUG_IMG cvShowImage("BASE", canny_image); cvWaitKey(); #endif temp_image = canny_image; canny_image = cvCreateImage( cvSize( cvRound(canny_image->width / scale), cvRound(canny_image->height / scale) ), IPL_DEPTH_8U, 1 ); // 画像をリサイズ cvResize(canny_base_image, canny_image); cvReleaseImage(&temp_image); current_scale *= scale; } cvReleaseImage(&canny_image); cvReleaseImage(&canny_base_image); return count; } int main(int argc, const char *argv[]) { IplImage *base_image; if (argc == 1) { fprintf(stderr, "usage: balldetect file\n"); return -1; } base_image = cvLoadImage(argv[1], 3); if (! base_image) { fprintf(stderr, "load error: %s\n", argv[1]); return EXIT_FAILURE; } cvNamedWindow("BASE"); ballDetect(base_image); cvShowImage("BASE", base_image); cvWaitKey(); return EXIT_SUCCESS; }