Benchmark OpenCV Feature/Descriptor Extractors.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Copyright (c) 2016, Donald Munro | |
Permission to use, copy, modify, and/or distribute this software for any | |
purpose with or without fee is hereby granted, provided that the above | |
copyright notice and this permission notice appear in all copies. | |
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | |
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | |
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | |
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | |
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | |
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | |
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
*/ | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <iostream> | |
#include <string> | |
#include <locale> | |
#include <chrono> //benchmark nano seconds (probably doesn't have ns resolution in Windoze) | |
#include <opencv2/core/base.hpp> | |
#include <opencv2/core/core.hpp> | |
#include <opencv2/core/mat.hpp> | |
#include <opencv2/imgproc.hpp> | |
#include <opencv2/highgui/highgui.hpp> | |
#include <opencv2/imgcodecs.hpp> | |
#include <opencv2/calib3d.hpp> | |
#include <opencv2/features2d/features2d.hpp> | |
#include "opencv2/opencv_modules.hpp" | |
#ifdef HAVE_OPENCV_XFEATURES2D | |
#include <opencv2/xfeatures2d.hpp> | |
#endif | |
using namespace std; | |
using namespace cv; | |
#include "optionparser.h" //http://optionparser.sourceforge.net/index.html (file http://optionparser.sourceforge.net/optionparser.h) | |
bool create(string &detector_name, string &extractor_name, Ptr<FeatureDetector> &detector, | |
Ptr<DescriptorExtractor> &descriptor, Ptr<DescriptorMatcher> &matcher, stringstream &errs); | |
void match(Mat &img1, Mat &img2, Ptr<FeatureDetector> detector, Ptr<DescriptorExtractor>, Ptr<DescriptorMatcher> matcher, | |
string name); | |
void help(); | |
vector<Point2f> Points(vector<KeyPoint> keypoints); | |
string type(InputArray a); | |
struct Arg : public option::Arg | |
//============================= | |
{ | |
static void printError(const char *msg1, const option::Option &opt, const char *msg2) | |
//----------------------------------------------------------------------------------- | |
{ | |
fprintf(stderr, "%s", msg1); | |
fwrite(opt.name, (size_t) opt.namelen, 1, stderr); | |
fprintf(stderr, "%s", msg2); | |
help(); | |
} | |
static option::ArgStatus Required(const option::Option &option, bool msg) | |
//----------------------------------------------------------------------- | |
{ | |
if (option.arg != 0) | |
return option::ARG_OK; | |
if (msg) printError("Option '", option, "' requires an argument\n"); | |
return option::ARG_ILLEGAL; | |
} | |
static option::ArgStatus Unknown(const option::Option &option, bool msg) | |
//---------------------------------------------------------------------- | |
{ | |
if (msg) printError("Unknown option '", option, "'\n"); | |
return option::ARG_ILLEGAL; | |
} | |
}; | |
const char *FEATURE_EXTRACTOR_DESC = | |
"-f <arg>, \t--feature=<arg> \tSpecify Feature Detector where <arg> can be:\n AKAZE, BRISK, ORB, SIFT, SURF"; | |
const char *DESCRIPTOR_EXTRACTOR_DESC = | |
"-d <arg>, \t--descriptor=<arg> \tSpecify Descriptor Extractor (can be unspecified). <arg> can be:\n BRIEF, LATCH, SIFT, SURF, FREAK"; | |
enum optionIndex { UNKNOWN, HELP, FEATURE_EXTRACTOR, DESCRIPTOR_EXTRACTOR }; | |
const option::Descriptor usage[] = | |
{ | |
{ UNKNOWN, 0, "", "", Arg::Unknown, "USAGE: features [options] image1 image2\nExample features -f ORB -d LATCH lena1.png lena2.png\n\nOptions:" }, | |
{ HELP, 0, "h", "help", Arg::None, "-h or --help \tPrint usage and exit." }, | |
{ FEATURE_EXTRACTOR, 0, "f","feature", Arg::Required, FEATURE_EXTRACTOR_DESC }, | |
{ DESCRIPTOR_EXTRACTOR, 0, "d", "descriptor", Arg::Required, DESCRIPTOR_EXTRACTOR_DESC }, | |
{ 0, 0, 0, 0, 0, 0 }, | |
}; | |
const float inlier_threshold = 2.5f; // Distance threshold to identify inliers | |
const float nn_match_ratio = 0.8f; // Nearest neighbor matching ratio | |
const double ransac_thresh = 2.5f; | |
void help() | |
//--------- | |
{ | |
cout << "features <options> image1 image2" << endl; | |
option::printUsage(cout, usage); | |
} | |
int main(int argc, char **argv) | |
//----------------------------- | |
{ | |
argc-=(argc>0); argv+=(argc>0); // skip program name argv[0] if present | |
option::Stats stats(usage, argc, argv); | |
std::vector<option::Option> options(stats.options_max); | |
std::vector<option::Option> buffer(stats.buffer_max); | |
option::Parser parse(usage, argc, argv, &options[0], &buffer[0]); | |
// option::Parser parse(usage, argc, argv, options, buffer); | |
if (parse.error()) | |
return 1; | |
if ( (options[HELP]) || (argc == 0) || (parse.nonOptionsCount() < 2) ) | |
{ | |
help(); | |
return 0; | |
} | |
string feature_detector = "", descriptor_extractor = ""; | |
for (int i = 0; i < parse.optionsCount(); ++i) | |
{ | |
option::Option& opt = buffer[i]; | |
switch (opt.index()) | |
{ | |
case FEATURE_EXTRACTOR: | |
if (opt.arg) | |
feature_detector = string(opt.arg); | |
break; | |
case DESCRIPTOR_EXTRACTOR: | |
if (opt.arg) | |
descriptor_extractor = string(opt.arg); | |
break; | |
case HELP: | |
case UNKNOWN: | |
return 1; | |
} | |
} | |
if (feature_detector.empty()) | |
{ | |
cout << "Please specify a feature detector using the -f <arg> or --feature=<arg> option" << endl; | |
help(); | |
return 0; | |
} | |
else | |
std::transform(feature_detector.begin(), feature_detector.end(), feature_detector.begin(), ::toupper); | |
if (! descriptor_extractor.empty()) | |
std::transform(descriptor_extractor.begin(), descriptor_extractor.end(), descriptor_extractor.begin(), ::toupper); | |
Mat img1 = cv::imread(parse.nonOption(0), IMREAD_GRAYSCALE); | |
Mat img2 = cv::imread(parse.nonOption(1), IMREAD_GRAYSCALE); | |
Ptr<FeatureDetector> detector; | |
Ptr<DescriptorExtractor> descriptor; | |
Ptr<DescriptorMatcher> matcher; | |
stringstream errs; | |
bool isok = create(feature_detector, descriptor_extractor, detector, descriptor, matcher, errs); | |
cout << errs.str() << endl; | |
if (! isok) | |
return 1; | |
cout << "Feature Detector " << feature_detector << endl << "Descriptor Extractor " << descriptor_extractor << endl; | |
string name = feature_detector + ((! descriptor_extractor.empty()) ? "-" + descriptor_extractor : ""); | |
match(img1, img2, detector, descriptor, matcher, name); | |
} | |
bool create(string &detector_name, string &extractor_name, Ptr<FeatureDetector> &detector, | |
Ptr<DescriptorExtractor> &descriptor, Ptr<DescriptorMatcher> &matcher, stringstream &errs) | |
//-------------------------------------------------------------------------------------- | |
{ | |
if (detector_name == "AKAZE") | |
{ | |
detector = AKAZE::create(); | |
descriptor = Ptr<DescriptorExtractor>(); | |
if (! extractor_name.empty()) | |
{ | |
errs << "WARNING: Using built in AKAZE descriptor extractor instead of " << extractor_name; | |
extractor_name = "AKAZE"; | |
} | |
matcher = Ptr<DescriptorMatcher>(new BFMatcher(NORM_HAMMING)); | |
return true; | |
} | |
else if (detector_name == "ORB") | |
{ | |
detector = ORB::create(); | |
matcher = Ptr<DescriptorMatcher>(new BFMatcher(NORM_HAMMING)); | |
if (extractor_name.empty()) | |
{ | |
//descriptor = cv::xfeatures2d::BriefDescriptorExtractor::create(32, true); | |
//extractor_name = "BRIEF"; | |
extractor_name = "ORB"; | |
descriptor = Ptr<DescriptorExtractor>(); | |
return true; | |
} | |
} | |
else if (detector_name == "BRISK") | |
{ | |
detector = BRISK::create(); | |
matcher = Ptr<DescriptorMatcher>(new BFMatcher(NORM_HAMMING)); | |
if (extractor_name.empty()) | |
{ | |
//descriptor = cv::xfeatures2d::BriefDescriptorExtractor::create(32, true); | |
//extractor_name = "BRIEF"; | |
extractor_name = "BRISK"; | |
descriptor = Ptr<DescriptorExtractor>(); | |
return true; | |
} | |
} | |
else if (detector_name == "SURF") | |
{ | |
#ifdef HAVE_OPENCV_XFEATURES2D | |
detector = cv::xfeatures2d::SURF::create(); | |
matcher = Ptr<DescriptorMatcher>(new BFMatcher(NORM_L2)); | |
if (extractor_name.empty()) | |
{ | |
descriptor = Ptr<DescriptorExtractor>(); | |
extractor_name = "SURF"; | |
return true; | |
} | |
#else | |
errs << "SURF requires OpenCV built with -DOPENCV_EXTRA_MODULES_PATH"; | |
return false; | |
#endif | |
} | |
else if (detector_name == "SIFT") | |
{ | |
#ifdef HAVE_OPENCV_XFEATURES2D | |
detector = cv::xfeatures2d::SIFT::create(); | |
matcher = Ptr<DescriptorMatcher>(new BFMatcher(NORM_L2)); | |
if (extractor_name.empty()) | |
{ | |
descriptor = Ptr<DescriptorExtractor>(); | |
extractor_name = "SIFT"; | |
return true; | |
} | |
#else | |
errs << "SIFT requires OpenCV built with -DOPENCV_EXTRA_MODULES_PATH"; | |
return false; | |
#endif | |
} | |
else | |
{ | |
errs << "Invalid or unsupported feature detector " << detector_name; | |
return false; | |
} | |
if (extractor_name.empty()) | |
{ | |
errs << "No descriptor extractor specified and no default descriptor extractor"; | |
return false; | |
} | |
if (extractor_name == "BRIEF") | |
#ifdef HAVE_OPENCV_XFEATURES2D | |
descriptor = cv::xfeatures2d::BriefDescriptorExtractor::create(32, true); | |
#else | |
errs << "BRIEF requires OpenCV built with -DOPENCV_EXTRA_MODULES_PATH"; | |
#endif | |
else if (extractor_name == "LATCH") | |
#ifdef HAVE_OPENCV_XFEATURES2D | |
descriptor = cv::xfeatures2d::LATCH::create(); | |
#else | |
errs << "LATCH requires OpenCV built with -DOPENCV_EXTRA_MODULES_PATH"; | |
#endif | |
else if (extractor_name == "SIFT") | |
{ | |
#ifdef HAVE_OPENCV_XFEATURES2D | |
descriptor = cv::xfeatures2d::SiftDescriptorExtractor::create(0, 3, 0.04, 15 /*edgeThreshold = 10 */, 1.6); | |
matcher = Ptr<DescriptorMatcher>(new BFMatcher(NORM_L2)); | |
#else | |
errs << "SIFT requires OpenCV built with -DOPENCV_EXTRA_MODULES_PATH"; | |
#endif | |
} | |
else if (extractor_name == "SURF") | |
{ | |
#ifdef HAVE_OPENCV_XFEATURES2D | |
descriptor = cv::xfeatures2d::SurfDescriptorExtractor::create(); | |
matcher = Ptr<DescriptorMatcher>(new BFMatcher(NORM_L2)); | |
#else | |
errs << "SURF requires OpenCV built with -DOPENCV_EXTRA_MODULES_PATH"; | |
#endif | |
} | |
else if (extractor_name == "FREAK") | |
{ | |
#ifdef HAVE_OPENCV_XFEATURES2D | |
descriptor = cv::xfeatures2d::FREAK::create(); | |
matcher = Ptr<DescriptorMatcher>(new BFMatcher(NORM_HAMMING)); | |
#else | |
errs << "FREAK requires OpenCV built with -DOPENCV_EXTRA_MODULES_PATH"; | |
#endif | |
} | |
else | |
{ | |
errs << "Invalid or unsupported descriptor extractor " << extractor_name; | |
return false; | |
} | |
return true; | |
} | |
void match(Mat &img1, Mat &img2, Ptr<FeatureDetector> detector, Ptr<DescriptorExtractor> descriptor, | |
Ptr<DescriptorMatcher> matcher, string name) | |
//-------------------------------------------------------------------------------------------------- | |
{ | |
Mat d1, d2; | |
vector<KeyPoint> k1, k2; | |
auto start = std::chrono::high_resolution_clock::now(); | |
if (descriptor.empty()) | |
{ | |
cout << "Empty descriptor" << endl; | |
detector->detectAndCompute(img1, noArray(), k1, d1); | |
detector->detectAndCompute(img2, noArray(), k2, d2); | |
} | |
else | |
{ | |
detector->detect(img1, k1); | |
descriptor->compute(img1, k1, d1); | |
detector->detect(img2, k2); | |
descriptor->compute(img2, k2, d2); | |
} | |
// auto find_time = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count(); | |
// cout << "Find Time: " << find_time << "ns " << (find_time / 1000000.0) << "ms" << endl; | |
vector<vector<DMatch>> matches; | |
matcher->knnMatch(d1, d2, matches, 2); | |
vector<KeyPoint> matched1, matched2, inliers1, inliers2; | |
vector<DMatch> good_matches; | |
for (size_t i = 0; i < matches.size(); i++) | |
{ | |
DMatch first = matches[i][0]; | |
float dist1 = matches[i][0].distance; | |
float dist2 = matches[i][1].distance; | |
if ( (dist1 < nn_match_ratio * dist2) && | |
(first.queryIdx >= 0) && (first.trainIdx >= 0)/* FREAK seems to freak out occasionally and provide negative indices */ | |
) | |
{ | |
matched1.push_back(k1[first.queryIdx]); | |
matched2.push_back(k2[first.trainIdx]); | |
} | |
} | |
// auto match_time = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count(); | |
// cout << "Find Time (match Inclusive): " << match_time << "ns " << (match_time / 1000000.0) << "ms" << endl; | |
Mat homography, inliers; | |
if (matched1.size() >= 4) | |
{ | |
homography = findHomography(Points(matched1), Points(matched2), RANSAC, ransac_thresh, inliers); | |
for (unsigned i = 0; i < matched1.size(); i++) | |
{ | |
Mat col = Mat::ones(3, 1, CV_64F); | |
col.at<double>(0) = matched1[i].pt.x; | |
col.at<double>(1) = matched1[i].pt.y; | |
col = homography * col; | |
col /= col.at<double>(2); | |
double dist = sqrt(pow(col.at<double>(0) - matched2[i].pt.x, 2) + | |
pow(col.at<double>(1) - matched2[i].pt.y, 2)); | |
// cout << dist << " " << inlier_threshold << endl; | |
if (dist < inlier_threshold) | |
{ | |
int new_i = static_cast<int>(inliers1.size()); | |
inliers1.push_back(matched1[i]); | |
inliers2.push_back(matched2[i]); | |
good_matches.push_back(DMatch(new_i, new_i, 0)); | |
} | |
} | |
} | |
auto total_time = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now() - start).count(); | |
cout << "Total Time: " << total_time << "ns " << (total_time / 1000000.0) << "ms" << endl; | |
double inlier_ratio = inliers1.size() * 1.0 / matched1.size(); | |
cout << name << " Matching Results" << endl; | |
cout << "*******************************" << endl; | |
cout << "# Keypoints 1: \t" << k1.size() << endl; | |
cout << "# Keypoints 2: \t" << k2.size() << endl; | |
cout << "Descriptor size: \t" << d1.rows << "x" << d1.cols << " of " << type(d1) << | |
" byte size = " << ((d1.isContinuous()) ? d1.total() * d1.elemSize() : d1.step[0] * d1.rows) << endl; | |
cout << "# Matches: \t" << matched1.size() << endl; | |
cout << "# Inliers: \t" << inliers1.size() << endl; | |
cout << "# Inliers Ratio: \t" << inlier_ratio << endl; | |
cout << endl; | |
// cv::drawKeypoints(img1 ,k1, img1, cv::Scalar(255,255,255), cv::DrawMatchesFlags::DRAW_OVER_OUTIMG); | |
// namedWindow("1", CV_WINDOW_AUTOSIZE); | |
// imshow("1", img1); | |
// cv::drawKeypoints(img2 ,k2, img2, cv::Scalar(255,255,255), cv::DrawMatchesFlags::DRAW_OVER_OUTIMG); | |
// namedWindow("2", CV_WINDOW_AUTOSIZE); | |
// imshow("2", img2); | |
Mat res; | |
drawMatches(img1, inliers1, img2, inliers2, good_matches, res); | |
imwrite(name + ".png", res); | |
namedWindow(name, CV_WINDOW_AUTOSIZE); | |
imshow(name, res); | |
cvWaitKey(300000); | |
} | |
vector<Point2f> Points(vector<KeyPoint> keypoints) | |
//------------------------------------------------ | |
{ | |
vector<Point2f> res; | |
for(unsigned i = 0; i < keypoints.size(); i++) { | |
res.push_back(keypoints[i].pt); | |
} | |
return res; | |
} | |
string type(InputArray a) | |
//----------------------- | |
{ | |
int numImgTypes = 35; // 7 base types, with five channel options each (none or C1, ..., C4) | |
int enum_ints[] = {CV_8U, CV_8UC1, CV_8UC2, CV_8UC3, CV_8UC4, | |
CV_8S, CV_8SC1, CV_8SC2, CV_8SC3, CV_8SC4, | |
CV_16U, CV_16UC1, CV_16UC2, CV_16UC3, CV_16UC4, | |
CV_16S, CV_16SC1, CV_16SC2, CV_16SC3, CV_16SC4, | |
CV_32S, CV_32SC1, CV_32SC2, CV_32SC3, CV_32SC4, | |
CV_32F, CV_32FC1, CV_32FC2, CV_32FC3, CV_32FC4, | |
CV_64F, CV_64FC1, CV_64FC2, CV_64FC3, CV_64FC4}; | |
string enum_strings[] = {"CV_8U", "CV_8UC1", "CV_8UC2", "CV_8UC3", "CV_8UC4", | |
"CV_8S", "CV_8SC1", "CV_8SC2", "CV_8SC3", "CV_8SC4", | |
"CV_16U", "CV_16UC1", "CV_16UC2", "CV_16UC3", "CV_16UC4", | |
"CV_16S", "CV_16SC1", "CV_16SC2", "CV_16SC3", "CV_16SC4", | |
"CV_32S", "CV_32SC1", "CV_32SC2", "CV_32SC3", "CV_32SC4", | |
"CV_32F", "CV_32FC1", "CV_32FC2", "CV_32FC3", "CV_32FC4", | |
"CV_64F", "CV_64FC1", "CV_64FC2", "CV_64FC3", "CV_64FC4"}; | |
int typ = a.type(); | |
for(int i=0; i<numImgTypes; i++) | |
if (typ == enum_ints[i]) return enum_strings[i]; | |
return "unknown image type"; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment