人工知能に関する断創録

このブログでは人工知能のさまざまな分野について調査したことをまとめています(更新停止: 2019年12月31日)

3日で作る高速特定物体認識システム (3) SURFの抽出

3日で作る高速特定物体認識システム (2) SIFT特徴量の抽出(2009/10/24)の続きです。あっ、3日経っちゃいました。

今回は、SIFTとは別の局所特徴量であるSURF(Speeded Up Robust Features)を抽出してみます。SURFのFはFeaturesなのでSURF特徴量とは言わないのかな?SIFTとは抽出方法は違いますが、画像からキーポイントと特徴ベクトルを抽出する点では同じです。抽出速度はSIFTより数倍高速だそうですが、精度は多少落ちるとのこと。リアルタイム処理したいときはこっちのほうがよさそうです。また、OpenCVにもすでに実装されています。SURFの詳しいアルゴリズムは後で論文を読むとしてとりあえず試してみます。

画像からSURFを抽出する

以下のプログラムは、画像からSURFを抽出して特徴点を描画し、特徴量をファイルへ格納するプログラムです。このプログラムはOpenCVについているfind_obj.cpp(C:\OpenCV2.0\samples\cにあります)を参考にしています。このプログラムからSURFを抽出する部分だけ取り出してみました。

surf.exe [画像ファイル名] [SURFファイル名]

という感じで使います。

#include <cv.h>
#include <highgui.h>
#include <iostream>
#include <fstream>

using namespace std;

const int DIM_VECTOR = 128;  // 128次元ベクトル

/**
 * SURF情報をファイルに出力
 * @param[in]   filename            SURFを保存するファイル名
 * @param[in]   imageKeypoints      SURFキーポイント情報
 * @param[in]   imageDescriptors    SURF特徴ベクトル情報
 * @return なし
 */
void writeSURF(const char* filename, CvSeq* imageKeypoints, CvSeq* imageDescriptors) {
    fstream fout;

    fout.open(filename, ios::out);
    if (!fout.is_open()) {
        cerr << "cannot open file: " << filename << endl;
        return;
    }

    // 1行目はキーポイント数と特徴量の次元数を書き込む
    fout << imageKeypoints->total << ' ' << DIM_VECTOR << endl;

    // 2行目からキーポイント情報と特徴ベクトルを書き込む
    for (int i = 0; i < imageKeypoints->total; i++) {
        CvSURFPoint* point = (CvSURFPoint*)cvGetSeqElem(imageKeypoints, i);
        float* descriptor = (float*)cvGetSeqElem(imageDescriptors, i);
        // キーポイント情報(X座標, Y座標, サイズ, ラプラシアン)を書き込む
        fout << point->pt.x << ' ' << point->pt.y << ' ' << point->size << ' ' << point->laplacian << ' ';
        // 特徴ベクトルを書き込む
        for (int j = 0; j < DIM_VECTOR; j++) {
            fout << descriptor[j] << ' ';
        }
        fout << endl;
    }

    fout.close();
}

int main(int argc, char** argv) {
    const char* imageFile = argc == 3 ? argv[1] : "image/accordion_image_0001.jpg";
    const char* surfFile  = argc == 3 ? argv[2] : "image/accordion_image_0001.surf";

    // SURF抽出用に画像をグレースケールで読み込む
    IplImage* grayImage = cvLoadImage(imageFile, CV_LOAD_IMAGE_GRAYSCALE);
    if (!grayImage) {
        cerr << "cannot find image file: " << imageFile << endl;
        return -1;
    }

    // キーポイント描画用にカラーでも読み込む
    IplImage* colorImage = cvLoadImage(imageFile, CV_LOAD_IMAGE_COLOR);
    if (!colorImage) {
        cerr << "cannot find image file: " << imageFile << endl;
        return -1;
    }

    CvMemStorage* storage = cvCreateMemStorage(0);
    CvSeq* imageKeypoints = 0;
    CvSeq* imageDescriptors = 0;
    CvSURFParams params = cvSURFParams(500, 1);

    // 画像からSURFを取得
    cvExtractSURF(grayImage, 0, &imageKeypoints, &imageDescriptors, storage, params);
    cout << "Image Descriptors: " << imageDescriptors->total << endl;

    // SURFをファイルに出力
    writeSURF(surfFile, imageKeypoints, imageDescriptors);

    // 画像にキーポイントを描画
    for (int i = 0; i < imageKeypoints->total; i++) {
        CvSURFPoint* point = (CvSURFPoint*)cvGetSeqElem(imageKeypoints, i);
        CvPoint center;  // キーポイントの中心座標
        int radius;      // キーポイントの半径
        center.x = cvRound(point->pt.x);
        center.y = cvRound(point->pt.y);
        radius = cvRound(point->size * 1.2 / 9.0 * 2.0);
        cvCircle(colorImage, center, radius, cvScalar(0,255,255), 1, 8, 0);
    }

    cvNamedWindow("SURF");
    cvShowImage("SURF", colorImage);
    cvWaitKey(0);

    // 後始末
    cvReleaseImage(&grayImage);
    cvReleaseImage(&colorImage);
    cvClearSeq(imageKeypoints);
    cvClearSeq(imageDescriptors);
    cvReleaseMemStorage(&storage);
    cvDestroyAllWindows();

    return 0;
}

たくさん後始末しなくちゃいけないので大変ですね・・・C++はこれだから嫌い。もし、何か解放し忘れてるのに気が付いたら教えてください。当初は、cvClearSeq()するのを知らず書いてなかったことがありました・・・

SURFを格納するファイルはSIFT特徴量の抽出(2009/10/24)で使ったSIFTファイルと同じ形式です。1行に1つのキーポイントの情報をずらずら出力してます。SURFの理論が詳しくわかってないのですが、サイズはSIFTのスケールのことかな?あとラプラシアンもあまりわかってないですが、1か-1の値を取ります。あとでキーポイントのマッチングを高速化するときに使うのでとりあえず保存しときます。

408 128   <-- キーポイントの数 SURFの次元数
138.821 11.208 14 1 0.00771829 0.00876939 ...     <-- 1つめのキーポイントのX座標, Y座標, サイズ, ラプラシアン, 以下128次元の特徴ベクトル
121.597 17.7526 17 -1 -0.00576097 0.00586741 ...  <-- 2つめのキーポイントの情報
...

SIFTと同様に抽出対象となる画像はグレースケール(モノクロ)で読み込みます。他の関数の使い方は上のサンプルで大体わかると思いますが、キーポイントの情報(imageKeypoints)とキーポイントの特徴ベクトルの情報(imageDescriptors)は別々のCvSeqに格納される仕様になってるようです。ただし、添え字は対応しています。たとえば、imageKeypointsのi番目のキーポイントの特徴ベクトルはimageDescriptorsのi番目に格納されてます。キーポイント情報はCvSURFPointという構造体にまとめられていて、特徴ベクトルはfloatの配列がそのまま入っているようです。SURFのパラメータは、cvSURFParamsで指定します。OpenCVのマニュアル(docフォルダにあるopencv.pdf)によると第1引数は特徴量の次元で0だと64次元、1だと128次元です。次元が大きいほうが正確なのかな?第2引数は閾値でキーポイントのhessianがこの値より大きい点だけ抽出されます。適切な閾値は300-500とのこと。大きくすると抽出される点が少なくなるのがわかります。

いくつかの画像で試してみます。SIFTと比べると背景と物体の間の線(エッジ)が抽出されやすいのかな?陰陽を見ると違いがよくわかります。エッジをキーポイントとするのはよくないそうなのでSIFTに比べて少し精度が落ちるってことでしょうか?もし詳しい人がいたらアドバイスください。







動画からSURFをリアルタイム抽出する

次は、上のプログラムを少し拡張して、Webカメラからキャプチャした動画からSURFをリアルタイムに抽出してみます。このプログラムは、Webカメラを接続してないと動きません。

#include <cv.h>
#include <highgui.h>
#include <iostream>

using namespace std;

int main(int argc, char** argv) {
    CvCapture* capture;

    // カメラを初期化
    if ((capture = cvCreateCameraCapture(0)) == NULL) {
        cerr << "cannot find camera" << endl;
        return -1;
    }

    // ウィンドウを生成
    cvNamedWindow("SURF");

    IplImage* captureImage = cvQueryFrame(capture);
    while (true) {
        CvMemStorage* storage = cvCreateMemStorage(0);
        CvSeq* imageKeypoints = 0;
        CvSeq* imageDescriptors = 0;
        CvSURFParams params = cvSURFParams(500, 1);

        captureImage = cvQueryFrame(capture);

        // グレースケールに変換
        IplImage* grayImage = cvCreateImage(cvGetSize(captureImage), 8, 1);
        cvCvtColor(captureImage, grayImage, CV_BGR2GRAY);

        // フレーム画像からSURFを取得
        cvExtractSURF(grayImage, 0, &imageKeypoints, &imageDescriptors, storage, params);

        // 画像にキーポイントを描画
        for (int i = 0; i < imageKeypoints->total; i++) {
            CvSURFPoint* point = (CvSURFPoint*)cvGetSeqElem(imageKeypoints, i);  // i番目のキーポイント
            CvPoint center;  // キーポイントの中心座標
            center.x = cvRound(point->pt.x);
            center.y = cvRound(point->pt.y);
            cvCircle(captureImage, center, 2, cvScalar(0,255,255), CV_FILLED);
        }
        cvShowImage("SURF", captureImage);

        // ループ内で作成したオブジェクトは始末
        cvReleaseImage(&grayImage);
        cvClearSeq(imageKeypoints);
        cvClearSeq(imageDescriptors);
        cvReleaseMemStorage(&storage);

        // ESCキーが押されたらループを抜ける
        int key = cvWaitKey(30);
        if (key == 27) {
            break;
        }
    }

    // 後始末
    cvReleaseCapture(&capture);
    cvDestroyAllWindows();

    return 0;
}

ループ内で生成したオブジェクトはループ内で後始末します。これ忘れるとものすごい勢いでメモリリークしてメモリを食いつぶしそのうちクラッシュします。これまたC++に慣れてないとときどきやっちゃいます。実行すると下の動画みたいにリアルタイムで抽出できます。静止画ではなく動画を対象にするとSIFTに比べてSURFが高速なのが実感できます。これからは特に注意書きしない限り、特徴点の抽出にはSURFを使っていきます。OpenCVに関数あるから楽だし(ボソッ)。

3日で作る高速特定物体認識システム (4) 特徴点のマッチング(2009/11/2)へ続きます。

参考文献