基于opencv3.1的特征检测、特征点匹配、图像拼接(二)

基于opencv3.1的特征检测、特征点匹配、图像拼接(二)

首先给出opencv的官方指导手册网站:

  1. opencv document index
  2. 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; }

可以看到这段代码非常之简洁,运行效果如图所示。
基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
我们可以看到这段代码唯一有用的参量就是海塞矩阵阈值,海塞矩阵的阈值越大,特征点越少,结果就越精准。在opencv自带的源码中我们是看不到特征点之间的对应关系的,我们需要把两张图中相同的特征点之间连线,表示出这两者之间的关系,这就涉及到opencv中feature2D的二维特征点匹配。

2. 特征点匹配算法:BruteForceMatcher和FlannBasedMatcher

  1. BruteForceMatcher顾名思义是暴力匹配法;它选择尝试尽可能所有的匹配,从而找到最佳匹配方案。将海塞阈值调至2000之后的效果如图所示:
    基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
    可见匹配效果非常一般,我们将海塞矩阵阈值调到4000,好起来了,如图所示:
    基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
    这里给出代码:
#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;
}
  1. FlannBasedMatchers:首先要知道,Flann全称是:Fast Library forApproximate Nearest Neighbors,意思是最近邻近似匹配算法,有点事算法的速度比较快,缺点是找到的解不一定是最优解。代码只需要将第44行改成
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("FlannBased");

效果如图所示:
基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
在来一张简单的:
基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
效果实在是太差了;源码气死人啊;摔。

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行对于关键点的选择一句注释掉了,相当于没有对关键点进行筛选,此时的匹配效果如下图所示:
基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
不吹不黑,效果比原来的第二部分所给出的源码好的多;当我们将第58行解注释之后再次运行程序,效果如图所示:
基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
基于opencv3.1的特征检测、特征点匹配、图像拼接(二)
可以说非常不错了,肉眼可见的错误匹配点几乎没有。
注意:

  1. SIFT算法只是把上述代码中的SURF全部替换成SIFT即可,其作用主要是更加精确,代价是耗时更长;
  2. 第58行的0~1之间的系数即为ratio,具体数值有具体工况决定。设置的ratio越小,那么匹配的关键点越少。
  3. 修正的代码中,第54行和第63行需要注意,因为对于drawMatches函数来说,两张src图是有先后顺序之分的,关键点少的一张应该在前面,否则会出现容器溢出问题报错,在这里我们把KnnMatch函数处理过的图片放在前面,因为加了一层暴力knn算法的匹配限制,所以第二张图的匹配点变少了,切记一定要把它放在drawMatches函数的前面,这样就可以得到最后的结果。