Help us understand the problem. What is going on with this article?

[OpenCV] いまさら局所特徴量で物体検出!?

More than 3 years have passed since last update.

1.はじめに

OpenCVには,様々な処理が用意されています。画像処理,映像解析,カメラキャリブレーション,特徴点抽出,物体検出,機械学習,コンピュテーショナルフォトグラフィ,3D可視化などが基本モジュールで用意されています。さらに,エクストラモジュールを追加することで,より豊富うな処理が利用できます。[1]
OpenCV 3.x系を中心に話をします。

今回は,OpenCVの局所特徴量がどの程度簡単に使えるのか興味があり,局所特徴量を利用した物体検出を作成しました。
最近世間では,ディープな物体認識で盛り上がっていますが。

特徴点抽出に関する詳しい説明は,検索すれば多数ありますので,ここでは割愛します。
藤吉先生 (中部大学)のスライド「画像局所特徴量SIFTとそれ以降のアプローチ」は,とてもわかり易く説明されています。

2.特徴点検出と特徴量記述

特徴点検出と特徴量記述は,features2dモジュール(基本)とxfeatures2dモジュール(エクストラ)内にあります。
基本モジュール内に用意されているものは,次のものです。

名前 手法 特徴量の表現
cv::GFTTDetector goodFeaturesToTrack (特徴点検出) -
cv::AgastFeatureDetector AGAST (特徴点検出) -
cv::FastFeatureDetector FAST (特徴点検出) -
cv::MSER MSER (特徴点検出) -
cv::BRISK BRISK バイナリ
cv::KAZE KAZE スケール
cv::ORB ORB バイナリ
cv::AKAZE A-KAZE バイナリ

エクストラモジュールには,次のようなものです。

名前 手法 特徴量の表現
cv::xfeatures2d::StarDetector StarDetector (特徴点記述) -
cv::xfeatures2d::MSDDetector MSD (特徴点検出) -
cv::xfeatures2d::LATCH LATCH (特徴量記述) バイナリ
cv::xfeatures2d::LUCID LUCID (特徴量記述) ?
cv::xfeatures2d::BriefDescriptorExtractor BRIEF (特徴量記述) バイナリ
cv::xfeatures2d::DAISY DAISY (特徴量記述) 実数ベクトル
cv::xfeatures2d::FREAK FREAK (特徴量記述) バイナリ
cv::xfeatures2d::SIFT SIFT 実数ベクトル
cv::xfeatures2d::SURF SURF 実数ベクトル

(注)カッコ書きがあるものは,どちらかのみです。

使い方は次のようになります。

キーポイント検出と特徴量記述
cv::Ptr<cv::Feature2D> features = *使用したい特徴の名前*::create();
//(例) A-KAZEの場合
//features = cv::AKAZE::create();

std::vector<cv::KeyPoint> keypoints; //特徴点
cv::Mat descriptors; //特徴量

feature->detectAndCompute(画像,cv::noArray(), keypoints, descriptors);
キーポイントのみ
feature->detect(画像, keypoints);
特徴量記述
//何かしらの手法で特徴点を取得済みであること
feature->compute(画像, keypoints, descriptors);

3.特徴量マッチング

物体検出などを行うためには,得られた特徴量を用いてマッチングを行う必要があります。
特徴量マッチングを行うクラスは,DescriptorMatcherです。
cv::DescriptorMatcher::create(タイプ)のようにタイプを指定すれば特徴量マッチングを行えます。

タイプ 手法
BruteForce L2ノルム・全探索
BruteForce-L1 L1ノルム・全探索
BruteForce-Hamming ハミング距離・全探索
BruteForce-Hamming(2) ハミング距離・全探索
FlannBased flann・最近傍探索

マッチングのメソッド

メソッド 手法
match 最も良い点を探す
knnMatch 上位k個の良い点を探す
radiusMatch 特徴量記述の空間で距離がしきい値以下の点を探す
特徴量のマッチングの例
cv::Ptr<cv::DescriptorMatcher> matcher = cv::DescriptorMatcher::create("タイプ");

std::vector<std::vector<cv::DMatch>> mathes;
matcher->knnMatch(descriptor-A, descriptor-B, matches, 2); //上位2位までの点を探す
//descriptor-A: query画像
//descriptor-B: train画像

また,他の方法として,cv::DescriptorMatcherの代わりに,cv::BFMatcherを使う方法もあります。
これは,全探索を行います。

cv::BFMatcher matcher(タイプ);
std::vector<std::vector<cv::DMatch>> matches;
matcher.knnMatch(descriptor-A, descriptor-B, matches, 2);

こちらの場合の,タイプは次のようなものがあります。

タイプ 手法 使える特徴量
NORM_L1 L1ノルム SIFT,SURFなど
NORM_L2 L2ノルム SIFT,SURFなど
NORM_HAMMING ハミング距離 ORB,BRISK,BRIEFなど
NORM_HAMMIN2 ハミング距離 ORB

4.平面の推定

4.1 ホモグラフィ行列の推定

物体検出を行うため,テンプレートがどのような姿勢でカメラに写り込んでいるかを調べる必要があります。
今回は,RANSACを使用しました。

cv::Mat masks;
cv::Mat H = cv::findHomography(matchPoints-A, matchPoints-B, masks, cv::RANSAC, 3.f);

参考:OpenCVのcv::findHomographyについて

4.2 RANSACで使用した対応点(インライア)を抽出

平面を推定するために,アウトライアを除外し,インライアのみにします。

4.3 カメラ画像への射影を推定

テンプレート画像の四隅の点と求めたホモグラフィ行列をを利用して,射影を推定します。

cv::perspectiveTransform(template_corners, scene_corners, H);

4.4 カメラ画像への描画

検出した特徴点,インライアに引いた線,射影した平面をカメラ画像に描画する。

//特徴点の表示
cv::drawMatches(template, keypoints1, scene, keypoints2, good_matches, dst);
// keypoints1: テンプレートの特徴点
// keypoints2: カメラ画像の特徴点
// good_matches: knnMatch後にさらにしぼった良い点
// dst: 出力画像

//インライアの対応点のみ表示
cv::drawMatches(template, keypoints1, scene, keypoints2, inlierMatches, dst);
// keypoints1: テンプレートの特徴点
// keypoints2: カメラ画像の特徴点
// inlierMatches: インライア
// dst: 出力画像
テンプレートの枠の描画
if (!H.empty()) {
    // 対象物体画像からコーナーを取得 ( 対象物体が"検出"される )
    std::vector<cv::Point2f> obj_corners(4);
    obj_corners[0] = cv::Point2f(.0f, .0f);
    obj_corners[1] = cv::Point2f(static_cast<float>(target.cols), .0f);
    obj_corners[2] = cv::Point2f(static_cast<float>(target.cols), static_cast<float>(target.rows));
    obj_corners[3] = cv::Point2f(.0f, static_cast<float>(target.rows));

    // シーンへの射影を推定
    std::vector<cv::Point2f> scene_corners(4);
    cv::perspectiveTransform(obj_corners, scene_corners, H);

    // コーナー間を線で結ぶ ( シーン中のマップされた対象物体 - シーン画像 )
    float w = static_cast<float>(target.cols);
    cv::line(dst, scene_corners[0] + cv::Point2f(w, .0f), scene_corners[1] + cv::Point2f(w, .0f), cv::Scalar(0, 255, 0), 4);
    cv::line(dst, scene_corners[1] + cv::Point2f(w, .0f), scene_corners[2] + cv::Point2f(w, .0f), cv::Scalar(0, 255, 0), 4);
    cv::line(dst, scene_corners[2] + cv::Point2f(w, .0f), scene_corners[3] + cv::Point2f(w, .0f), cv::Scalar(0, 255, 0), 4);
    cv::line(dst, scene_corners[3] + cv::Point2f(w, .0f), scene_corners[0] + cv::Point2f(w, .0f), cv::Scalar(0, 255, 0), 4);
}

5.実験

Affine Convariant Regions DatasetsのBikes,Boat,Graffti,Leuven,UBCを使用しました。

Bikesのテンプレートは,img1.ppmの一部をトリミングしたものです。
Bikesのテンプレート

Boatのテンプレートは,img1.ppmを使用したものです。
Boatのテンプレート

Grafftiのテンプレートは,img1.ppmの一部をトリミングしたものです。
Grafftiのテンプレート

Leuvenのテンプレートは,img1.ppmの一部をトリミングしたものです。
Leuvenのテンプレート

UBCのテンプレートは,img1.ppmの一部をトリミングし回転たものです。
UBCのテンプレート

試してみた特徴点検出と特徴量記述は,両方備わっている,SIFT,SURF,BRISK,ORB,KAZE,A-KAZEで比較してみました。

実験環境

CPU: Intel(R) Core(TM) i7-6700K
メモリ: 16 GB
OS: Windows10 Pro 64bit
開発環境: Visual Studio 2013
コンパイルオプション: 実行速度の最大化(/O2),OpenMPなし
OpenCVのバージョン: 3.1.0

*特徴量のマッチングは,各特徴量のデフォルト設定を使用しました。

結果

Bikes

◯ img1

bikes_img1.jpeg

◯ img2

bikes_img2.jpeg

◯ img3

bikes_img3.jpeg

◯ img4

bikes_img4.jpeg

◯ img5

bikes_img5.jpeg

◯ img6

bikes_img6.jpeg

Boat

◯ img1

boat_img1.jpeg

◯ img2

boat_img2.jpeg

◯ img3

boat_img3.jpeg

◯ img4

boat_img4.jpeg

◯ img5

boat_img5.jpeg

◯ img6

boat_img6.jpeg

Graffti

◯ img1

graffiti_img1.jpeg

◯ img2

graffiti_img2.jpeg

◯ img3

graffiti_img3.jpeg

◯ img4

graffiti_img4.jpeg

◯ img5

graffiti_img5.jpeg

◯ img6

graffiti_img6.jpeg

Leuven

◯ img1

leuven_img1.jpeg

◯ img2

leuven_img2.jpeg

◯ img3

leuven_img3.jpeg

◯ img4

leuven_img4.jpeg

◯ img5

leuven_img5.jpeg

◯ img6

leuven_img6.jpeg

UBC

◯ img1

ubc_img1.jpeg

◯ img2

ubc_img2.jpeg

◯ img3

ubc_img3.jpeg

◯ img4

ubc_img4.jpeg

◯ img5

ubc_img5.jpeg

◯ img6

ubc_img6.jpeg

6.おわりに

どの局所特徴量も気軽に使えるようになっています。今回は,デフォルトパラメータを使って実験しましたが,調整することも可能です。
局所特徴量ごとに,どんなものに向いているのかなど特徴がありますので,みなさんの目的に応じて適切に選択してください。

おまけ

綾鷹,つくもたんをターゲットに動画を撮ってみました。
特徴量比較動画

コード

局所特徴量抽出,物体検出,描画部分のコードを示して置きます。

void features(cv::Mat &target, cv::Mat &scene, cv::Mat &t_gray, cv::Mat &s_gray, cv::Mat &dst, int num)
{
    // 時間計算のための周波数
    double f = 1000.0 / cv::getTickFrequency();

    int64 time_s; //スタート時間
    double time_detect; // 検出エンド時間
    double time_match; // マッチングエンド時間


    // 特徴点検出と特徴量計算

    cv::Ptr<cv::Feature2D> feature;
    std::stringstream ss;

    switch (num)
    {
    case 0:
        feature = cv::xfeatures2d::SIFT::create();
        ss << "SIFT";
        break;
    case 1:
        feature = cv::xfeatures2d::SURF::create();
        ss << "SURF";
        break;
    case 2:
        feature = cv::ORB::create();
        ss << "ORB";
        break;
    case 3:
        feature = cv::AKAZE::create();
        ss << "A-KAZE";
        break;
    case 4:
        feature = cv::BRISK::create();
        ss << "BRISK";
        break;
    case 5:
        feature = cv::KAZE::create();
        ss << "KAZE";
        break;
    default:
        break;
    }
    std::cout << "--- 計測(" << ss.str() << ") ---" << std::endl;


    //******************************
    // キーポイント検出と特徴量記述
    //******************************
    std::vector<cv::KeyPoint> kpts1, kpts2;
    cv::Mat desc1, desc2;

    feature->detectAndCompute(t_gray, cv::noArray(), kpts1, desc1);

    time_s = cv::getTickCount(); // 時間計測 Start
    feature->detectAndCompute(s_gray, cv::noArray(), kpts2, desc2);
    time_detect = (cv::getTickCount() - time_s)*f; // 時間計測 Stop

    if (desc2.rows == 0){
        std::cout << "WARNING: 特徴点検出できず" << std::endl;
        return;
    }

    //*******************
    // 特徴量マッチング
    //*******************
    auto matchtype = feature->defaultNorm(); // SIFT, SURF: NORM_L2
                                             // BRISK, ORB, KAZE, A-KAZE: NORM_HAMMING
    cv::BFMatcher matcher(matchtype);
    std::vector<std::vector<cv::DMatch >> knn_matches;


    time_s = cv::getTickCount(); // 時間計測 Start
    // 上位2点
    matcher.knnMatch(desc1, desc2, knn_matches, 2);
    time_match = (cv::getTickCount() - time_s)*f; // 時間計測 Stop


    //***************
    // 対応点を絞る
    //***************
    const auto match_par = .6f; //対応点のしきい値
    std::vector<cv::DMatch> good_matches;

    std::vector<cv::Point2f> match_point1;
    std::vector<cv::Point2f> match_point2;

    for (size_t i = 0; i < knn_matches.size(); ++i) {
        auto dist1 = knn_matches[i][0].distance;
        auto dist2 = knn_matches[i][1].distance;

        //良い点を残す(最も類似する点と次に類似する点の類似度から)
        if (dist1 <= dist2 * match_par) {
            good_matches.push_back(knn_matches[i][0]);
            match_point1.push_back(kpts1[knn_matches[i][0].queryIdx].pt);
            match_point2.push_back(kpts2[knn_matches[i][0].trainIdx].pt);
        }
    }

    //ホモグラフィ行列推定
    cv::Mat masks;
    cv::Mat H;
    if (match_point1.size() != 0 && match_point2.size() != 0) {
        H = cv::findHomography(match_point1, match_point2, masks, cv::RANSAC, 3.f);
    }

    //RANSACで使われた対応点のみ抽出
    std::vector<cv::DMatch> inlierMatches;
    for (auto i = 0; i < masks.rows; ++i) {
        uchar *inlier = masks.ptr<uchar>(i);
        if (inlier[0] == 1) {
            inlierMatches.push_back(good_matches[i]);
        }
    }
    //特徴点の表示
    cv::drawMatches(target, kpts1, scene, kpts2, good_matches, dst);

    //インライアの対応点のみ表示
    cv::drawMatches(target, kpts1, scene, kpts2, inlierMatches, dst);

    if (!H.empty()) {

        //
        // 対象物体画像からコーナーを取得 ( 対象物体が"検出"される )
        std::vector<cv::Point2f> obj_corners(4);
        obj_corners[0] = cv::Point2f(.0f, .0f);
        obj_corners[1] = cv::Point2f(static_cast<float>(target.cols), .0f);
        obj_corners[2] = cv::Point2f(static_cast<float>(target.cols), static_cast<float>(target.rows));
        obj_corners[3] = cv::Point2f(.0f, static_cast<float>(target.rows));

        // シーンへの射影を推定
        std::vector<cv::Point2f> scene_corners(4);
        cv::perspectiveTransform(obj_corners, scene_corners, H);

        // コーナー間を線で結ぶ ( シーン中のマップされた対象物体 - シーン画像 )
        float w = static_cast<float>(target.cols);
        cv::line(dst, scene_corners[0] + cv::Point2f(w, .0f), scene_corners[1] + cv::Point2f(w, .0f), cv::Scalar(0, 255, 0), 4);
        cv::line(dst, scene_corners[1] + cv::Point2f(w, .0f), scene_corners[2] + cv::Point2f(w, .0f), cv::Scalar(0, 255, 0), 4);
        cv::line(dst, scene_corners[2] + cv::Point2f(w, .0f), scene_corners[3] + cv::Point2f(w, .0f), cv::Scalar(0, 255, 0), 4);
        cv::line(dst, scene_corners[3] + cv::Point2f(w, .0f), scene_corners[0] + cv::Point2f(w, .0f), cv::Scalar(0, 255, 0), 4);
    }



    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 40), cv::FONT_HERSHEY_SIMPLEX, beta-.1, cv::Scalar(255, 255, 255), 1, CV_AA);
    ss.str("");
    ss << "Detection & Description";
    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 70), cv::FONT_HERSHEY_SIMPLEX, beta - .1, cv::Scalar(0, 255, 255), 1, CV_AA);
    ss.str("");
    ss << "Time: " << time_detect << " [ms]";
    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 95), cv::FONT_HERSHEY_SIMPLEX, beta - .1, cv::Scalar(0, 255, 255), 1, CV_AA);
    ss.str("");
    ss << "Matching";
    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 120), cv::FONT_HERSHEY_SIMPLEX, beta - .1, cv::Scalar(0, 255, 255), 1, CV_AA);
    ss.str("");
    ss << "Time: " << time_match << " [ms]";
    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 145), cv::FONT_HERSHEY_SIMPLEX, beta - .1, cv::Scalar(0, 255, 255), 1, CV_AA);

    ss.str("");
    ss << "--Matches--";
    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 170), cv::FONT_HERSHEY_SIMPLEX, beta - .1, cv::Scalar(255, 255, 0), 1, CV_AA);
    ss.str("");
    ss << "Good Matches: " << good_matches.size();
    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 190), cv::FONT_HERSHEY_SIMPLEX, beta - .1, cv::Scalar(255, 255, 0), 1, CV_AA);

    ss.str("");
    ss << "Inlier: " << inlierMatches.size();
    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 220), cv::FONT_HERSHEY_SIMPLEX, beta - .1, cv::Scalar(255, 255, 0), 1, CV_AA);

    ss.str("");
    auto ratio = .0;
    if (good_matches.size() != .0)
        ratio = inlierMatches.size()*1.0 / good_matches.size();
    ss << "Inlier ratio: " << ratio;
    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 240), cv::FONT_HERSHEY_SIMPLEX, beta - .1, cv::Scalar(255, 255, 0), 1, CV_AA);


    ss.str("");
    ss << "Target KeyPoints: " << kpts1.size();
    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 270), cv::FONT_HERSHEY_SIMPLEX, beta - .1, cv::Scalar(255, 0, 255), 1, CV_AA);
    ss.str("");
    ss << "Scene KeyPoints: " << kpts2.size();
    cv::putText(dst, ss.str(), cv::Point(10, target.rows + 290), cv::FONT_HERSHEY_SIMPLEX, beta - .1, cv::Scalar(255, 0, 255), 1, CV_AA);

    std::cout << "検出時間: " << time_detect << " [ms]" << std::endl;
    std::cout << "照合時間: " << time_match << " [ms]" << std::endl;
    std::cout << "Good Matches: " << good_matches.size() << std::endl;
    std::cout << "Inlier: " << inlierMatches.size() << std::endl;
    std::cout << "Inlier ratio: " << ratio << std::endl;
    std::cout << "target Keypoints: " << kpts1.size() << std::endl;
    std::cout << "scene Keypoints: " << kpts2.size() << std::endl;
    std::cout << "target match_points: " << match_point1.size() << std::endl;
    std::cout << "scene match_points: " << match_point2.size() << std::endl;

}

参考

[1] OpenCV 3.1 Document

hmichu
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away