色相特徴のテンプレート探索による青色ボールの検出

輪郭線によるテンプレート探索は背景の輪郭線が濃いとボールを検出できなかったので、別の方法として色相の特徴を使った検出を試してみました。検出対象は、前回いくつか検出できなかった青色のボールとしました。

検出方法

まず前回と同じくテキトウにテンプレートマッチ用のフィルターを定義しました。

// テキトウな円形検出フィルター
#define CIRCLE_FILTER_WIDTH     16
#define CIRCLE_FILTER_HEIGHT    16
const int CIRCLE_FILTER[CIRCLE_FILTER_HEIGHT][CIRCLE_FILTER_WIDTH] = 
{
    { -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3 },
    { -3, -3, -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3, -3, -3 },
    { -3, -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +2, +2, +2, +2, +1, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +2, +2, +2, +2, +2, +2, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +2, +2, +2, +2, +2, +2, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +2, +2, +2, +2, +2, +2, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +2, +2, +2, +2, +2, +2, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +2, +2, +2, +2, +1, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3 },
    { -3, -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3, -3 },
    { -3, -3, -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3, -3, -3 },
    { -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3 }
};
// 円形かの閾値
#define CIRCLE_FILTER_THRESHOLD 110

今回は円の中身があるため、四画っぽい物体を誤検知しないように、少し厳密にしました。
次に、『青色』の範囲を定義しました。

// 青色の範囲
#define HSV_BLUE_LOWER cvScalar(95, 0, 20)
#define HSV_BLUE_UPPER cvScalar(130, 235, 255)

H(色相),S(彩度),V(明度)です。OpenCVではHの範囲が0〜180°で、個人的感覚により95〜130くらいが青としました。この範囲の色を抽出してそれに対してテンプレート探索を行います。
探索方法は前回と同じく、フィルターを探索ウィンドウとして青色抽出した画像内を探索し、各エリアでのフィルターの出力値から青色ボールがあるかを判定します。フィルターの出力値は、フィルターに対応する各ピクセルの色(黒:1 or 白:0)との積の総和としました。これが閾値を超える場合は、そこに青いボールがあると判定します。
一番下に試したソースコードを載せています。

結果

画像は前回同じものを使用しました。
http://www.flickr.com/photos/person/56233211/
f:id:ultraist:20080102120412j:image
輪郭線による方法では、中央に3つある青いボールが検出できませんでした。
f:id:ultraist:20080102221839j:image
探索中はプログラムから上のような感じで見えています。青い領域が白くなり、それ以外の領域が黒くなっています。
f:id:ultraist:20080102221957j: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] = 
{
    { -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3 },
    { -3, -3, -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3, -3, -3 },
    { -3, -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +2, +2, +2, +2, +1, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +2, +2, +2, +2, +2, +2, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +2, +2, +2, +2, +2, +2, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +2, +2, +2, +2, +2, +2, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +2, +2, +2, +2, +2, +2, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +2, +2, +2, +2, +1, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3 },
    { -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3 },
    { -3, -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3, -3 },
    { -3, -3, -3, -3, +1, +1, +1, +1, +1, +1, +1, +1, -3, -3, -3, -3 },
    { -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3 }
};
// 円形かの閾値
#define CIRCLE_FILTER_THRESHOLD 110

// 青色の範囲
#define HSV_BLUE_LOWER cvScalar(95, 0, 20)
#define HSV_BLUE_UPPER cvScalar(130, 235, 255)

// 円形?
int isCircle(int val)
{
    return CIRCLE_FILTER_THRESHOLD < val ? 1:0;
}

// 青色ボール検出
int blueBallDetect(IplImage *base_image)
{
    IplImage *hsv_image = cvCreateImage(cvGetSize(base_image), IPL_DEPTH_8U, 3);
    IplImage *blue_base_image = cvCreateImage(cvGetSize(base_image), IPL_DEPTH_8U, 1);
    IplImage *blue_image = cvCreateImage(cvGetSize(base_image), IPL_DEPTH_8U, 1);
    IplImage *temp_image;

    int count = 0;
    double scale = 1.3;
    double current_scale = 1;

    // 青色領域抽出
    CvScalar blue_lower = HSV_BLUE_LOWER;
    CvScalar blue_upper = HSV_BLUE_UPPER;
    cvCvtColor(base_image, hsv_image, CV_BGR2HSV);
    cvInRangeS(hsv_image, blue_lower, blue_upper, blue_base_image);
    cvCopy(blue_base_image, blue_image);

    // 探索
    while (CIRCLE_FILTER_WIDTH < blue_image->width 
        && CIRCLE_FILTER_HEIGHT < blue_image->height) 
    {
        for (int y = 0; y + CIRCLE_FILTER_HEIGHT < blue_image->height; y += 2) {
            for (int x = 0; x + CIRCLE_FILTER_WIDTH < blue_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(
                            blue_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);
#ifdef _DEBUG
                    printf("filter value: %d\n", filter_value);
#endif
#if _DEBUG_IMG
                    cvRectangle(
                        blue_image, 
                        cvPoint(x, y),
                        cvPoint(x + CIRCLE_FILTER_WIDTH, y + CIRCLE_FILTER_HEIGHT),
                        cvScalar(0xcc), 2
                    );
#endif
                    // マーク
                    cvRectangle(
                        base_image, 
                        cvPoint(rx, ry), 
                        cvPoint(rx + rw, ry + rh),
                        CV_RGB(0xff, 0x00, 0x00), 2
                    );

                    ++count;
                }
            }
        }
#if _DEBUG_IMG
        cvShowImage("BASE", blue_image);
        cvWaitKey();
#endif
        temp_image = blue_image;
        blue_image = cvCreateImage(
            cvSize(
                cvRound(blue_image->width / scale),
                cvRound(blue_image->height / scale)
            ),
            IPL_DEPTH_8U, 1
        );
        cvResize(blue_base_image, blue_image);
        cvReleaseImage(&temp_image);
        current_scale *= scale;
    }
    cvReleaseImage(&blue_image);
    cvReleaseImage(&blue_base_image);

    return count;
}

int main(int argc, const char *argv[])
{
    IplImage *base_image;

    if (argc == 1) {
        fprintf(stderr, "usage: balldetect file\n");
        return EXIT_FAILURE;
    }
    
    base_image = cvLoadImage(argv[1], 3);
    if (! base_image) {
        fprintf(stderr, "load error: %s\n", argv[1]);
        return EXIT_FAILURE;
    }
    cvNamedWindow("BASE");
    
    blueBallDetect(base_image);

    cvShowImage("BASE", base_image);
    cvWaitKey();

    cvReleaseImage(&base_image);

    return EXIT_SUCCESS;
}