#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の一部をトリミングしたものです。
Boatのテンプレートは,img1.ppmを使用したものです。
Grafftiのテンプレートは,img1.ppmの一部をトリミングしたものです。
Leuvenのテンプレートは,img1.ppmの一部をトリミングしたものです。
UBCのテンプレートは,img1.ppmの一部をトリミングし回転たものです。
試してみた特徴点検出と特徴量記述は,両方備わっている,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
◯ img2
◯ img3
◯ img4
◯ img5
◯ img6
Boat
◯ img1
◯ img2
◯ img3
◯ img4
◯ img5
◯ img6
Graffti
◯ img1
◯ img2
◯ img3
◯ img4
◯ img5
◯ img6
Leuven
◯ img1
◯ img2
◯ img3
◯ img4
◯ img5
◯ img6
UBC
◯ img1
◯ img2
◯ img3
◯ img4
◯ img5
◯ img6
#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