Edited at

[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

  • ![boat_img6.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/1356f585-da33-a1e1-9b9a-de52a4673102.jpeg)
  • ![graffiti_img1.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/e3f2c316-2ba3-8ec7-c04c-5f2fa85f0d95.jpeg)
  • ![graffiti_img2.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/b5907add-e28d-2784-2da8-bce21dc50b0b.jpeg)
  • ![graffiti_img3.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/af55cfc5-4869-0912-8897-193a3c6ebf0f.jpeg)
  • ![graffiti_img4.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/641f2ba9-f30d-d1a2-d067-bc4c3f8aed8f.jpeg)
  • ![graffiti_img5.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/0bc97808-a844-8a05-3686-a73c76dc1783.jpeg)
  • ![graffiti_img6.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/32b226de-6bba-6eb8-5a30-4846f94fde2b.jpeg)
  • ![leuven_img1.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/3696a4db-0b9d-d73d-a5fa-cb082c188d30.jpeg)
  • ![leuven_img2.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/2fac5d9b-c9dd-62e5-3f45-e5e107a3601b.jpeg)
  • ![leuven_img3.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/5c71388f-5d50-7885-d004-02a63a60b91e.jpeg)
  • ![leuven_img4.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/d127e1fa-7b74-57e9-206d-0be7a71c9d63.jpeg)
  • ![leuven_img5.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/af602256-42dc-536b-10dc-760a472577dd.jpeg)
  • ![leuven_img6.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/2472c577-1f9e-92cd-1bb8-6c25a712d0b6.jpeg)
  • ![ubc_img1.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/0fd361d8-dbb6-2746-b567-2ac3bcc03b5a.jpeg)
  • ![ubc_img2.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/3c3d9be1-5621-aba3-a04b-b37fe12132b2.jpeg)
  • ![ubc_img3.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/9a99bcf4-3a1a-2fdd-3887-87d5daa22963.jpeg)
  • ![ubc_img4.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/fc141d5c-2de4-e6d8-3319-238d5cb5b1f1.jpeg)
  • ![ubc_img5.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/3da58467-3f23-3cb8-3890-25af0bebffa2.jpeg)
  • ![ubc_img6.jpeg](https://qiita-image-store.s3.amazonaws.com/0/113616/1c242178-7865-7edc-e3ac-dda4f71197a6.jpeg)
  • 6.おわりに
  • おまけ
  • コード
  • 参考