基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
首先给出opencv的官方指导手册网站:
- opencv document index
-
https://docs.opencv.org/3.0-last-rst/doc/tutorials/features2d/feature_detection/feature_detection.html?highlight=feature
在这个网站可以查询不同版本下的opencv的各种功能的介绍,以及不同版本下,函数如何进行的修改优化。在进行图像匹配之前,我们首先要进行特征点提取;很多相关资料并没有提及使用的opencv版本,代码比较混杂,甚至有的文章代码抄袭,给读者造成很大混乱。譬如Feature Detection部分,在opencv3.0之前的版本和opencv3.0之后的版本差异比较大;本文主要讲的是3.0之后的版本,与时俱进嘛。
再给出opencv示例图片及视频的位置,D:\opencv\sources\samples\data方便查找使用。
1. 特征检测算法
先给出opencv手册里的Feature Detection特征点获取源码
#include <stdio.h>
#include <iostream>
#include "opencv2/core.hpp"
#include "opencv2/features2d.hpp"
#include "opencv2/xfeatures2d.hpp"
#include "opencv2/highgui.hpp"
using namespace cv;
using namespace cv::xfeatures2d;
void readme();
/* @function main */
int main( int argc, char** argv )
{
if( argc != 3 )
{ readme(); return -1; }
Mat img_1 = imread( argv[1], IMREAD_GRAYSCALE );
Mat img_2 = imread( argv[2], IMREAD_GRAYSCALE );
if( !img_1.data || !img_2.data )
{ std::cout<< " --(!) Error reading images " << std::endl; return -1; }
//-- Step 1: Detect the keypoints using SURF Detector
int minHessian = 400;
Ptr<SURF> detector = SURF::create( minHessian );
std::vector<KeyPoint> keypoints_1, keypoints_2;
detector->detect( img_1, keypoints_1 );
detector->detect( img_2, keypoints_2 );
//-- Draw keypoints
Mat img_keypoints_1; Mat img_keypoints_2;
drawKeypoints( img_1, keypoints_1, img_keypoints_1, Scalar::all(-1), DrawMatchesFlags::DEFAULT );
drawKeypoints( img_2, keypoints_2, img_keypoints_2, Scalar::all(-1), DrawMatchesFlags::DEFAULT );
//-- Show detected (drawn) keypoints
imshow("Keypoints 1", img_keypoints_1 );
imshow("Keypoints 2", img_keypoints_2 );
waitKey(0);
return 0;
}
/* @function readme */
void readme()
{ std::cout << " Usage: ./SURF_detector <img1> <img2>" << std::endl; }
可以看到这段代码非常之简洁,运行效果如图所示。
我们可以看到这段代码唯一有用的参量就是海塞矩阵阈值,海塞矩阵的阈值越大,特征点越少,结果就越精准。在opencv自带的源码中我们是看不到特征点之间的对应关系的,我们需要把两张图中相同的特征点之间连线,表示出这两者之间的关系,这就涉及到opencv中feature2D的二维特征点匹配。
2. 特征点匹配算法:BruteForceMatcher和FlannBasedMatcher
- BruteForceMatcher顾名思义是暴力匹配法;它选择尝试尽可能所有的匹配,从而找到最佳匹配方案。将海塞阈值调至2000之后的效果如图所示:
可见匹配效果非常一般,我们将海塞矩阵阈值调到4000,好起来了,如图所示:
这里给出代码:
#include <stdio.h>
#include <iostream>
#include "opencv2/core.hpp"
#include "opencv2/features2d.hpp"
#include "opencv2/xfeatures2d.hpp"
#include "opencv2/highgui.hpp"
using namespace cv;
using namespace cv::xfeatures2d;
void readme();
/* @function main */
int main(int argc, char** argv)
{
//if (argc != 3)
//{
// readme(); return -1;
//}
Mat img_1 = imread("f:\\box.png", IMREAD_GRAYSCALE);
Mat img_2 = imread("f:\\box_in_scene.png", IMREAD_GRAYSCALE);
if (!img_1.data || !img_2.data)
{
std::cout << " --(!) Error reading images " << std::endl; return -1;
}
//-- Step 1: Detect the keypoints using SURF Detector
int minHessian = 4000;
Ptr<SURF> detector = SURF::create(minHessian);
std::vector<KeyPoint> keypoints_1, keypoints_2;
detector->detect(img_1, keypoints_1);
detector->detect(img_2, keypoints_2);
//-- Draw keypoints
Mat img_keypoints_1; Mat img_keypoints_2;
drawKeypoints(img_1, keypoints_1, img_keypoints_1, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
drawKeypoints(img_2, keypoints_2, img_keypoints_2, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
//-- Show detected (drawn) keypoints
imshow("Keypoints_1", img_keypoints_1);
imshow("Keypoints_2", img_keypoints_2);
//计算特征向量
Ptr<SURF>extractor = SURF::create();
Mat descriptors1, descriptors2;
extractor->compute(img_1, keypoints_1, descriptors1);
extractor->compute(img_2, keypoints_2, descriptors2);
//使用BruteForce进行匹配
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce");
std::vector< DMatch > matches;
matcher->match(descriptors1, descriptors2, matches);
//绘制直线连接关键点
Mat imgMatches;
drawMatches(img_1, keypoints_1, img_2, keypoints_2, matches, imgMatches);
imshow("match", imgMatches);
waitKey(0);
return 0;
}
/* @function readme */
void readme()
{
std::cout << " Usage: ./SURF_detector <img1> <img2>" << std::endl;
}
- FlannBasedMatchers:首先要知道,Flann全称是:Fast Library forApproximate Nearest Neighbors,意思是最近邻近似匹配算法,有点事算法的速度比较快,缺点是找到的解不一定是最优解。代码只需要将第44行改成
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("FlannBased");
效果如图所示:
在来一张简单的:
效果实在是太差了;源码气死人啊;摔。
3.对于基于SURF和SIFT算法的特征点匹配算法的修正
第二部分我们可以看到,对于opencv自带的匹配算法,不管是BruteForceMatcher还是FlannBasedMatcher,匹配的效果可以说非常非常差,对于有一定旋转变换,尺度变化,仿射变换的图片匹配得到的关键点错误很多,甚至只是平移变换的图片,我们的算法也不能精准判断出来;如果我们那这样子的匹配结果直接进行下一步的图像拼接,鬼知道可以拼出什么样子。
于是我们进一步要对匹配点进行筛选,可以看做将不良好的匹配点滤波掉。为了排除因为图像遮挡和背景混乱而产生的无匹配关系的关键点,SIFT的作者Lowe提出了比较最近邻距离与次近邻距离的匹配方式:取一幅图像中的一个关键点,并找出其与另一幅图像中欧式距离最近的前两个关键点,在这两个关键点中,如果最近的距离除以次近的距离得到的比率ratio少于某个阈值T,则接受这一对匹配点,否则舍去。因为对于错误匹配,由于特征空间的高维性,相似的距离可能有大量其他的错误匹配,从而它的ratio值比较高。显然降低这个比例阈值T,匹配点数目会减少,但更加稳定,反之亦然。
在这里我们先给出代码:
//TODO:对opencv自带的匹配算法进行优化
//思路:最近和次近匹配点之间的距离之比为ratio
//将小于某阈值的关键点滤掉即可;
//20190225night
#include <stdio.h>
#include <iostream>
#include "opencv2/core.hpp"
#include "opencv2/features2d.hpp"
#include "opencv2/xfeatures2d.hpp"
#include "opencv2/highgui.hpp"
using namespace cv;
using namespace cv::xfeatures2d;
void readme();
/* @function main */
int main(int argc, char** argv)
{
//if (argc != 3)
//{
// readme(); return -1;
//}
Mat img_1 = imread("leftYS.jpg", IMREAD_GRAYSCALE);
Mat img_2 = imread("rightYS.jpg", IMREAD_GRAYSCALE);
if (!img_1.data || !img_2.data)
{
std::cout << " --(!) Error reading images " << std::endl; return -1;
}
//-- Step 1: Detect the keypoints using SURF Detector
int minHessian = 2000;
Ptr<SURF> detector = SURF::create(minHessian);
std::vector<KeyPoint> keypoints_1, keypoints_2;
detector->detect(img_1, keypoints_1);
detector->detect(img_2, keypoints_2);
//-- Draw keypoints
Mat img_keypoints_1; Mat img_keypoints_2;
drawKeypoints(img_1, keypoints_1, img_keypoints_1, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
drawKeypoints(img_2, keypoints_2, img_keypoints_2, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
//-- Show detected (drawn) keypoints
imshow("Keypoints_1", img_keypoints_1);
imshow("Keypoints_2", img_keypoints_2);
//计算特征向量
Ptr<SURF>extractor = SURF::create();
Mat descriptors1, descriptors2;
extractor->compute(img_1, keypoints_1, descriptors1);
extractor->compute(img_2, keypoints_2, descriptors2);
//使用BruteForce进行匹配
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce");
std::vector<std::vector<DMatch>> matches;
std::vector<DMatch> GoodMatchPoints;
std::vector<Mat> train_desc(1, descriptors1);
matcher->add(train_desc);
matcher->train();
matcher->knnMatch(descriptors2, matches, 2);
for (int i = 0; i < matches.size(); i++)
{
//if (matches[i][0].distance < 0.6*matches[i][1].distance)
GoodMatchPoints.push_back(matches[i][0]);
}
//绘制直线连接关键点
Mat imgMatches;
drawMatches(img_2, keypoints_2, img_1, keypoints_1, GoodMatchPoints, imgMatches);
imshow("match", imgMatches);
waitKey(0);
return 0;
}
/* @function readme */
void readme()
{
std::cout << " Usage: ./SURF_detector <img1> <img2>" << std::endl;
}
在这里我们顺便将匹配过程中的暴力**法也换了一种表现形式,因为之前opencv自带的源码的匹配效果实在是不忍直视;因为我们第58行对于关键点的选择一句注释掉了,相当于没有对关键点进行筛选,此时的匹配效果如下图所示:
不吹不黑,效果比原来的第二部分所给出的源码好的多;当我们将第58行解注释之后再次运行程序,效果如图所示:
可以说非常不错了,肉眼可见的错误匹配点几乎没有。
注意:
- SIFT算法只是把上述代码中的SURF全部替换成SIFT即可,其作用主要是更加精确,代价是耗时更长;
- 第58行的0~1之间的系数即为ratio,具体数值有具体工况决定。设置的ratio越小,那么匹配的关键点越少。
- 修正的代码中,第54行和第63行需要注意,因为对于drawMatches函数来说,两张src图是有先后顺序之分的,关键点少的一张应该在前面,否则会出现容器溢出问题报错,在这里我们把KnnMatch函数处理过的图片放在前面,因为加了一层暴力knn算法的匹配限制,所以第二张图的匹配点变少了,切记一定要把它放在drawMatches函数的前面,这样就可以得到最后的结果。