Skip to content

Instantly share code, notes, and snippets.

@donaldmunro
Last active January 10, 2016 12:08
Show Gist options
  • Save donaldmunro/464124b94b9f850caaeb to your computer and use it in GitHub Desktop.
Save donaldmunro/464124b94b9f850caaeb to your computer and use it in GitHub Desktop.
Benchmark OpenCV Feature/Descriptor Extractors.
/*
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