OpenCV
Open Source Computer Vision
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
AKAZE local features matching

Prev Tutorial: Detection of planar objects
Next Tutorial: AKAZE and ORB planar tracking

Original author Fedor Morozov
Compatibility OpenCV >= 3.0

Introduction

In this tutorial we will learn how to use AKAZE [10] local features to detect and match keypoints on two images. We will find keypoints on a pair of images with given homography matrix, match them and count the number of inliers (i.e. matches that fit in the given homography).

You can find expanded version of this example here: https://github.com/pablofdezalc/test_kaze_akaze_opencv

Data

We are going to use images 1 and 3 from Graffiti sequence of Oxford dataset.

Homography is given by a 3 by 3 matrix:

7.6285898e-01 -2.9922929e-01 2.2567123e+02
3.3443473e-01 1.0143901e+00 -7.6999973e+01
3.4663091e-04 -1.4364524e-05 1.0000000e+00

You can find the images (graf1.png, graf3.png) and homography (H1to3p.xml) in opencv/samples/data/.

Source Code

  • Downloadable code: Click here
  • Code at glance:
    #include <iostream>
    using namespace std;
    using namespace cv;
    const float inlier_threshold = 2.5f; // Distance threshold to identify inliers with homography check
    const float nn_match_ratio = 0.8f; // Nearest neighbor matching ratio
    int main(int argc, char* argv[])
    {
    CommandLineParser parser(argc, argv,
    "{@img1 | graf1.png | input image 1}"
    "{@img2 | graf3.png | input image 2}"
    "{@homography | H1to3p.xml | homography matrix}");
    Mat img1 = imread( samples::findFile( parser.get<String>("@img1") ), IMREAD_GRAYSCALE);
    Mat img2 = imread( samples::findFile( parser.get<String>("@img2") ), IMREAD_GRAYSCALE);
    Mat homography;
    FileStorage fs( samples::findFile( parser.get<String>("@homography") ), FileStorage::READ);
    fs.getFirstTopLevelNode() >> homography;
    vector<KeyPoint> kpts1, kpts2;
    Mat desc1, desc2;
    Ptr<AKAZE> akaze = AKAZE::create();
    akaze->detectAndCompute(img1, noArray(), kpts1, desc1);
    akaze->detectAndCompute(img2, noArray(), kpts2, desc2);
    BFMatcher matcher(NORM_HAMMING);
    vector< vector<DMatch> > nn_matches;
    matcher.knnMatch(desc1, desc2, nn_matches, 2);
    vector<KeyPoint> matched1, matched2;
    for(size_t i = 0; i < nn_matches.size(); i++) {
    DMatch first = nn_matches[i][0];
    float dist1 = nn_matches[i][0].distance;
    float dist2 = nn_matches[i][1].distance;
    if(dist1 < nn_match_ratio * dist2) {
    matched1.push_back(kpts1[first.queryIdx]);
    matched2.push_back(kpts2[first.trainIdx]);
    }
    }
    vector<DMatch> good_matches;
    vector<KeyPoint> inliers1, inliers2;
    for(size_t 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));
    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));
    }
    }
    Mat res;
    drawMatches(img1, inliers1, img2, inliers2, good_matches, res);
    imwrite("akaze_result.png", res);
    double inlier_ratio = inliers1.size() / (double) matched1.size();
    cout << "A-KAZE Matching Results" << endl;
    cout << "*******************************" << endl;
    cout << "# Keypoints 1: \t" << kpts1.size() << endl;
    cout << "# Keypoints 2: \t" << kpts2.size() << endl;
    cout << "# Matches: \t" << matched1.size() << endl;
    cout << "# Inliers: \t" << inliers1.size() << endl;
    cout << "# Inliers Ratio: \t" << inlier_ratio << endl;
    cout << endl;
    imshow("result", res);
    return 0;
    }
    Brute-force descriptor matcher.
    Definition features2d.hpp:1247
    Designed for command line parsing.
    Definition utility.hpp:890
    Class for matching keypoint descriptors.
    Definition types.hpp:849
    int trainIdx
    train descriptor index
    Definition types.hpp:856
    int queryIdx
    query descriptor index
    Definition types.hpp:855
    XML/YAML/JSON file storage class that encapsulates all the information necessary for writing or readi...
    Definition persistence.hpp:261
    n-dimensional dense array class
    Definition mat.hpp:829
    _Tp & at(int i0=0)
    Returns a reference to the specified array element.
    std::string String
    Definition cvstd.hpp:151
    std::shared_ptr< _Tp > Ptr
    Definition cvstd_wrapper.hpp:23
    #define CV_64F
    Definition interface.h:79
    void drawMatches(InputArray img1, const std::vector< KeyPoint > &keypoints1, InputArray img2, const std::vector< KeyPoint > &keypoints2, const std::vector< DMatch > &matches1to2, InputOutputArray outImg, const Scalar &matchColor=Scalar::all(-1), const Scalar &singlePointColor=Scalar::all(-1), const std::vector< char > &matchesMask=std::vector< char >(), DrawMatchesFlags flags=DrawMatchesFlags::DEFAULT)
    Draws the found matches of keypoints from two images.
    void imshow(const String &winname, InputArray mat)
    Displays an image in the specified window.
    int waitKey(int delay=0)
    Waits for a pressed key.
    CV_EXPORTS_W bool imwrite(const String &filename, InputArray img, const std::vector< int > &params=std::vector< int >())
    Saves an image to a specified file.
    int main(int argc, char *argv[])
    Definition highgui_qt.cpp:3
    Definition core.hpp:107
    STL namespace.

Explanation

  • Load images and homography
CommandLineParser parser(argc, argv,
"{@img1 | graf1.png | input image 1}"
"{@img2 | graf3.png | input image 2}"
"{@homography | H1to3p.xml | homography matrix}");
Mat img1 = imread( samples::findFile( parser.get<String>("@img1") ), IMREAD_GRAYSCALE);
Mat img2 = imread( samples::findFile( parser.get<String>("@img2") ), IMREAD_GRAYSCALE);
Mat homography;
FileStorage fs( samples::findFile( parser.get<String>("@homography") ), FileStorage::READ);
fs.getFirstTopLevelNode() >> homography;

We are loading grayscale images here. Homography is stored in the xml created with FileStorage.

  • Detect keypoints and compute descriptors using AKAZE
vector<KeyPoint> kpts1, kpts2;
Mat desc1, desc2;
Ptr<AKAZE> akaze = AKAZE::create();
akaze->detectAndCompute(img1, noArray(), kpts1, desc1);
akaze->detectAndCompute(img2, noArray(), kpts2, desc2);

We create AKAZE and detect and compute AKAZE keypoints and descriptors. Since we don't need the mask parameter, noArray() is used.

  • Use brute-force matcher to find 2-nn matches
BFMatcher matcher(NORM_HAMMING);
vector< vector<DMatch> > nn_matches;
matcher.knnMatch(desc1, desc2, nn_matches, 2);

We use Hamming distance, because AKAZE uses binary descriptor by default.

  • Use 2-nn matches and ratio criterion to find correct keypoint matches
    vector<KeyPoint> matched1, matched2;
    for(size_t i = 0; i < nn_matches.size(); i++) {
    DMatch first = nn_matches[i][0];
    float dist1 = nn_matches[i][0].distance;
    float dist2 = nn_matches[i][1].distance;
    if(dist1 < nn_match_ratio * dist2) {
    matched1.push_back(kpts1[first.queryIdx]);
    matched2.push_back(kpts2[first.trainIdx]);
    }
    }

If the closest match distance is significantly lower than the second closest one, then the match is correct (match is not ambiguous).

  • Check if our matches fit in the homography model
vector<DMatch> good_matches;
vector<KeyPoint> inliers1, inliers2;
for(size_t 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));
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));
}
}

If the distance from first keypoint's projection to the second keypoint is less than threshold, then it fits the homography model.

We create a new set of matches for the inliers, because it is required by the drawing function.

  • Output results
Mat res;
drawMatches(img1, inliers1, img2, inliers2, good_matches, res);
imwrite("akaze_result.png", res);
double inlier_ratio = inliers1.size() / (double) matched1.size();
cout << "A-KAZE Matching Results" << endl;
cout << "*******************************" << endl;
cout << "# Keypoints 1: \t" << kpts1.size() << endl;
cout << "# Keypoints 2: \t" << kpts2.size() << endl;
cout << "# Matches: \t" << matched1.size() << endl;
cout << "# Inliers: \t" << inliers1.size() << endl;
cout << "# Inliers Ratio: \t" << inlier_ratio << endl;
cout << endl;
imshow("result", res);

Here we save the resulting image and print some statistics.

Results

Found matches

Depending on your OpenCV version, you should get results coherent with:

Keypoints 1: 2943
Keypoints 2: 3511
Matches: 447
Inliers: 308
Inlier Ratio: 0.689038