輪郭線のテンプレート探索によるボールの検出

力技の輪郭線マッチを試しました。ニューラルネットワークのクラス分類器は、早いしマジすごいですが、なかなかうまく検出できない物体が結構あって、手動での調節が難しいので、とりあえずなんでもいいから、いじれる物体検出プログラムを作りたいと思って試しました。よく分からないので、とりあえず一番単純そうな方法で行いました。検出対象は、どこを向いていても同じ輪郭が見えるので比較的簡単そうな『ボール』にしました。

検出方法

まず、こんなテンプレートマッチ用のフィルターをテキトウに定義します。

// テキトウな円形検出フィルター
#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/

f:id:ultraist:20080102120412j:image
最初に一般的そうな、失敗するともしないとも微妙な感じの画像で試しました。ダメそうならあきらめるつもりでした。
f:id:ultraist:20080102122044j:image
探索中はプログラムから上のような感じで見えています。輪郭線画像はCannyフィルターで作るようにしました。
f:id:ultraist:20080102122246j:image
最終的にボールが検出されたエリアに、検出したボールと同じ色でマークをつけます。
わりとうまくいっている感じです。
検出できなかったのは、ぼやけているため輪郭線抽出の時点でとり逃したボールと、背景の輪郭線がでてしまったために、-2のフィルターが反応して、出力値が閾値よりも下がってしまったボールのようでした。

得意そうな画像

得そうな画像が分かったので試しました。
f:id:ultraist:20080102123827j:image
結果は、全て検出できました。
(画像は改変がNGのため消しました)

苦手そうな画像

f:id:ultraist:20080102220254j:image
色でみるときれいに分けれていますが、輪郭線はひどいことになりそうです。
だめそうで、実際ダメでした。結果はひとつも検出できなかったので載せません。

まとめ

このフィルタでは背景とかぶる輪郭線はうまく検出できない。逆に二重円の内側の円やスキンヘッドの人の顔の中など、輪郭線がある程度保障されていて特徴のある輪郭線がある物体に対しては、そこそこ使えそう。もう少しまともに使えるようにするためには、ある程度の不一致を許すようにフィルター出力値の計算方法を変える必要がある。また、検出したあとに検出物体の色の特徴など別の手段で検査すると誤検出ははじけそう。
あと、めちゃくちゃ遅い。ダメダ。

ソースコード

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;
}