【VS2015&Opencv3.4.1基于Surf特征描述子的图像配准学习备忘笔记二】

一,Sift和Surf算法实现两幅图像拼接的过程是一样的,主要分为四部分:
1. 特征点提取和描述
2. 特征点配对,找到两幅图像中匹配点的位置
3. 通过配对点,生成变换矩阵,并对图像1应用变换矩阵生成对图像2的映射图像
4. 图像2拼接到映射图像上,完成拼接


二,一个图像的特征点由两部分构成:关键点(Keypoint)和描述子(Descriptor)。 关键点指的是该特征点在图像中的位置,有些还具有方向、尺度信息;描述子通常是一个向量,按照人为的设计的方式,描述关键点周围像素的信息。通常描述子是按照外观相似的特征应该有相似的描述子设计的。因此,在匹配的时候,只要两个特征点的描述子在向量空间的距离相近,就可以认为它们是同一个特征点。

特征点的匹配通常需要以下三个步骤:

  • 提取图像中的关键点,这部分是查找图像中具有某些特征(不同的算法有不同的)的像素
  • 根据得到的关键点位置,计算特征点的描述子
  • 根据特征点的描述子,进行匹配

三, 每一个特征描述子都是独特的,具有排他性,尽可能减少彼此间的相似性。其中描述子的可区分性和其不变性是矛盾的,一个具有众多不变性的特征描述子,其区分局部图像内容的能力就比较稍弱;而如果一个很容易区分不同局部图像内容的特征描述子,其鲁棒性往往比较低。所以,在设计特征描述子的时候,就需要综合考虑这三个特性,找到三者之间的平衡。

特征描述子的不变性主要体现在两个方面:

  • 尺度不变性 Scale Invarient
    指的是同一个特征,在图像的不同的尺度空间保持不变。匹配在不同图像中的同一个特征点经常会有图像的尺度问题,不同尺度的图像中特征点的距离变得不同,物体的尺寸变得不同,而仅仅改变特征点的大小就有可能造成强度不匹配。如果描述子无法保证尺度不变性,那么同一个特征点在放大或者缩小的图像间,就不能很好的匹配。为了保持尺度的不变性,在计算特征点的描述子的时候,通常将图像变换到统一的尺度空间,再加上尺度因子。
  • 旋转不变性 Rotation Invarient
    指的是同一个特征,在成像视角旋转后,特征仍然能够保持不变。和尺度不变性类似,为了保持旋转不变性,在计算特征点描述子的时候,要加上关键点的方向信息。

为了有个更直观的理解,下面给出SIFT,SURF,BRIEF描述子计算方法对比

从上表可以看出,SIFT,SURF和BRIEF描述子都是一个向量,只是维度不同。其中,SIFT和SURF在构建特征描述子的时候,保存了特征的方向和尺度特征,这样其特征描述子就具有尺度和旋转不变性;而BRIEF描述子并没有尺度和方向特征,不具备尺度和旋转不变性。

  1. 获取检测器的实例
    在OpenCV3中重新的封装了特征提取的接口,可统一的使用Ptr<FeatureDetector> detector = FeatureDetector::create()来得到特征提取器的一个实例,所有的参数都提供了默认值,也可以根据具体的需要传入相应的参数。
  2. 在得到特征检测器的实例后,可调用的detect方法检测图像中的特征点的具体位置,检测的结果保存在vector<KeyPoint>向量中。
  3. 有了特征点的位置后,调用compute方法来计算特征点的描述子,描述子通常是一个向量,保存在Mat中。
  4. 得到了描述子后,可调用匹配算法进行特征点的匹配。上面代码中,使用了opencv中封装后的暴力匹配算法BFMatcher,该算法在向量空间中,将特征点的描述子一一比较,选择距离(上面代码中使用的是Hamming距离)较小的一对作为匹配点。

特征描述子的匹配方法:

  • 暴力匹配方法(Brute-Froce Matcher)
  • 计算某一个特征点描述子与其他所有特征点描述子之间的距离,然后将得到的距离进行排序,取距离最近的一个作为匹配点。这种方法简单粗暴,
  • 交叉匹配
    针对暴力匹配,交叉过滤的是想很简单,再进行一次匹配,反过来使用被匹配到的点进行匹配,如果匹配到的仍然是第一次匹配的点的话,就认为这是一个正确的匹配。举例来说就是,假如第一次特征点A使用暴力匹配的方法,匹配到的特征点是特征点B;反过来,使用特征点B进行匹配,如果匹配到的仍然是特征点A,则就认为这是一个正确的匹配,否则就是一个错误的匹配。OpenCV中BFMatcher已经封装了该方法,创建BFMatcher的实例时,第二个参数传入true即可,BFMatcher bfMatcher(NORM_HAMMING,true)

  • KNN匹配
    K近邻匹配,在匹配的时候选择K个和特征点最相似的点,如果这K个点之间的区别足够大,则选择最相似的那个点作为匹配点,通常选择K = 2,也就是最近邻匹配。对每个匹配返回两个最近邻的匹配,如果第一匹配和第二匹配距离比率足够大(向量距离足够远),则认为这是一个正确的匹配,比率的阈值通常在2左右。
    OpenCV中的匹配器中封装了该方法,上面的代码可以调用bfMatcher->knnMatch(descriptors1, descriptors2, knnMatches, 2);具体实现的代码如下:
    const float minRatio = 1.f / 1.5f;
    const int k = 2;

    vector<vector<DMatch>> knnMatches;
    matcher->knnMatch(leftPattern->descriptors, rightPattern->descriptors, knnMatches, k);

    for (size_t i = 0; i < knnMatches.size(); i++) {
        const DMatch& bestMatch = knnMatches[i][0];
        const DMatch& betterMatch = knnMatches[i][1];

        float  distanceRatio = bestMatch.distance / betterMatch.distance;
        if (distanceRatio < minRatio)
            matches.push_back(bestMatch);
    }const  float minRatio =  1.f  /  1.5f;
    const  int k =  2;

    vector<vector<DMatch>> knnMatches;
    matcher->knnMatch(leftPattern->descriptors, rightPattern->descriptors, knnMatches, 2);

    for (size_t i =  0; i < knnMatches.size(); i++) {
        const DMatch& bestMatch = knnMatches[i][0];
        const DMatch& betterMatch = knnMatches[i][1];
        float distanceRatio = bestMatch.distance  / betterMatch.distance;
        if (distanceRatio < minRatio)
            matches.push_back(bestMatch);
    }

将不满足的最近邻的匹配之间距离比率大于设定的阈值(1/1.5)匹配剔除。

针对错误匹配的点有如下两种优选方法:

  • 汉明距离小于最小距离的两倍
    选择已经匹配的点对的汉明距离小于最小距离的两倍作为判断依据,如果大于该值则认为是一个错误的匹配,过滤掉;小于该值则认为是一个正确的匹配。其实现代码如下:
    // 匹配对筛选
    double min_dist = 1000, max_dist = 0;
    // 找出所有匹配之间的最大值和最小值
    for (int i = 0; i < descriptors1.rows; i++)
    {
        double dist = matches[i].distance;
        if (dist < min_dist) min_dist = dist;
        if (dist > max_dist) max_dist = dist;
    }
    // 当描述子之间的匹配大于2倍的最小距离时,即认为该匹配是一个错误的匹配。
    // 但有时描述子之间的最小距离非常小,可以设置一个经验值作为下限
    vector<DMatch> good_matches;
    for (int i = 0; i < descriptors1.rows; i++)
    {
        if (matches[i].distance <= max(2 * min_dist, 30.0))
            good_matches.push_back(matches[i]);
    }
  • RANSAC
    另外还可采用随机采样一致性(RANSAC)来过滤掉错误的匹配,该方法利用匹配点计算两个图像之间单应矩阵,然后利用重投影误差来判定某一个匹配是不是正确的匹配。OpenCV中封装了求解单应矩阵的方法findHomography,可以为该方法设定一个重投影误差的阈值,可以得到一个向量mask来指定那些是符合该重投影误差的匹配点对,以此来剔除错误的匹配,代码如下:
const int minNumbermatchesAllowed = 8;
    if (matches.size() < minNumbermatchesAllowed)
        return;

    //Prepare data for findHomography
    vector<Point2f> srcPoints(matches.size());
    vector<Point2f> dstPoints(matches.size());

    for (size_t i = 0; i < matches.size(); i++) {
        srcPoints[i] = rightPattern->keypoints[matches[i].trainIdx].pt;
        dstPoints[i] = leftPattern->keypoints[matches[i].queryIdx].pt;
    }

    //find homography matrix and get inliers mask
    vector<uchar> inliersMask(srcPoints.size());
    homography = findHomography(srcPoints, dstPoints, CV_FM_RANSAC, reprojectionThreshold, inliersMask);

    vector<DMatch> inliers;
    for (size_t i = 0; i < inliersMask.size(); i++){
        if (inliersMask[i])
            inliers.push_back(matches[i]);
    }
    matches.swap(inliers);const  int minNumbermatchesAllowed =  8;
    if (matches.size() < minNumbermatchesAllowed)
        return;

    //Prepare data for findHomography
    vector<Point2f>  srcPoints(matches.size());
    vector<Point2f>  dstPoints(matches.size());

    for (size_t i =  0; i < matches.size(); i++) {
        srcPoints[i] = rightPattern->keypoints[matches[i].trainIdx].pt;
        dstPoints[i] = leftPattern->keypoints[matches[i].queryIdx].pt;
    }

    //find homography matrix and get inliers mask
    vector<uchar>  inliersMask(srcPoints.size());
    homography =  findHomography(srcPoints, dstPoints, CV_FM_RANSAC, reprojectionThreshold, inliersMask);

    vector<DMatch> inliers;
    for (size_t i =  0; i < inliersMask.size(); i++){
        if (inliersMask[i])
            inliers.push_back(matches[i]);
    }
    matches.swap(inliers);

四, 1.选图,两张图的重叠区域不能太小,最少不少于15%,这样才能保证有足够的角点匹配。    

2.角点检测。这一步OpenCV提供了很多种方法,譬如Harris角点检测,而监测出的角点用CvSeq存储,这是一个双向链表。  

3.角点提纯。在提纯的时候,需要使用RANSAC提纯。OpenCV自带了一个函数,FindHomography,不但可以提纯,还可以计      算出3x3的转换矩阵。这个转换矩阵十分重要。OpenCV中的findHomgrophy函数中得到的透视矩阵是img1到img2的投影矩阵,    即findHomography(image1Points, image2Points, CV_RANSAC, 2.5f, inlier_mask);得到的是图像1到图像2的变换矩阵,即以图像2的坐标系为基准参考坐标系的,

4.角点匹配。经过提纯后的角点,则需要匹配。     

5.图像变换。这一步我曾经尝试过很多办法,最后选择了FindHomography输出的变换矩阵,这是一个透视变换矩阵。经过这个透视变换后的图像,可以直接拿来做拼接。    

6.图象拼接。完成上面步骤之后,其实这一步很容易。难的是信息融合,目前主要是渐进渐出法,是越靠近拼接边缘时,待拼接图像像素点的权值越大,拼接图像的像素值得权值越小,最终结果取加权和。


五,

1. 特征子查找与变化矩阵的计算程序

Ptr<SurfFeatureDetector> detector = SurfFeatureDetector::create(800);
	Mat image01 = imread("1.png");

	Mat image02 = imread("2.png");

	imshow("原始测试图像", image01);

	imshow("基准图像", image02);



	//灰度图转换

	Mat srcImage1, srcImage2;

	cvtColor(image01, srcImage1, CV_RGB2GRAY);

	cvtColor(image02, srcImage2, CV_RGB2GRAY);
	vector<cv::KeyPoint> key_points_1, key_points_2;

	Mat dstImage1, dstImage2;
	detector->detectAndCompute(srcImage1, Mat(), key_points_1, dstImage1);
	detector->detectAndCompute(srcImage2, Mat(), key_points_2, dstImage2);//可以分成detect和compute

	Mat img_keypoints_1, img_keypoints_2;
	drawKeypoints(srcImage1, key_points_1, img_keypoints_1, Scalar::all(-1), DrawMatchesFlags::DEFAULT);
	drawKeypoints(srcImage2, key_points_2, img_keypoints_2, Scalar::all(-1), DrawMatchesFlags::DEFAULT);

	Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("FlannBased");
	vector<DMatch>mach;

	matcher->match(dstImage1, dstImage2, mach);

	sort(mach.begin(), mach.end()); //特征点排序	
	double Max_dist = 0;
	double Min_dist = 100;
	for (int i = 0; i < dstImage1.rows; i++)
	{
		double dist = mach[i].distance;
		if (dist < Min_dist)Min_dist = dist;
		if (dist > Max_dist)Max_dist = dist;
	}
	cout << "最短距离" << Min_dist << endl;
	cout << "最长距离" << Max_dist << endl;

	vector<DMatch>goodmaches;
	for (int i = 0; i < dstImage1.rows; i++)
	{
		if (mach[i].distance < 2 * Min_dist)
			goodmaches.push_back(mach[i]);
	}
	Mat img_maches;
	drawMatches(srcImage1, key_points_1, srcImage2, key_points_2, goodmaches, img_maches);

	vector<Point2f> imagePoints1, imagePoints2;

	for (int i = 0; i<10; i++)

	{

		imagePoints1.push_back(key_points_1[mach[i].queryIdx].pt);

		imagePoints2.push_back(key_points_2[mach[i].trainIdx].pt);

	}



	Mat homo = findHomography(imagePoints1, imagePoints2, CV_RANSAC);
	cout << "变换矩阵为:" << endl;
	cout << homo<<endl;

2. 拼接程序

//开始拼接
	Mat tempP;
	warpPerspective(image01, tempP, homo, Size(image01.cols * 2, image01.rows));
	Mat matchP(image01.cols * 2, image01.rows, CV_8UC3);
	tempP.copyTo(matchP);
	image02.copyTo(matchP(Rect(0, 0, image02.cols, image02.rows)));
	imshow("compare", tempP);
	imshow("compare1", matchP);
	//imwrite("1.png", tempP);
	//waitKey(0);

	//优化拼接线
	double lefttop[3] = { 0,0,1 };
	double leftbottom[3] = { 0,image01.rows,1 };
	double transLT[3];
	double transLB[3];
	Mat _lefttop = Mat(3, 1, CV_64FC1, lefttop);
	Mat _leftbottom = Mat(3, 1, CV_64FC1, leftbottom);
	Mat _transLT = Mat(3, 1, CV_64FC1, transLT);
	Mat _transLB = Mat(3, 1, CV_64FC1, transLB);
	_transLT = homo*_lefttop;
	_transLB = homo*_leftbottom;
	double weight = 1;
	int leftline = MIN(transLT[0], transLB[0]);
	double width = image02.cols - leftline;
	for (int i = 0; i < image02.rows; i++)
	{
		uchar* src = image02.ptr<uchar>(i);
		uchar* trans = tempP.ptr<uchar>(i);
		uchar* match = matchP.ptr<uchar>(i);
		for (int j = leftline; j < image02.cols; j++)
		{
			//如果遇到图像trans中无像素的黑点,则完全拷贝img1中的数据
			if (trans[j * 3] == 0 && trans[j * 3 + 1] == 0 && trans[j * 3 + 2] == 0)
			{
				weight = 1;
			}
			else {
				weight = (double)(width - (j - leftline)) / width;
			}
			//img1中像素的权重,与当前处理点距重叠区域左边界的距离成正比  三通道
			match[j * 3] = src[j * 3] * weight + trans[j * 3] * (1 - weight);
			match[j * 3 + 1] = src[j * 3 + 1] * weight + trans[j * 3 + 1] * (1 - weight);
			match[j * 3 + 2] = src[j * 3 + 2] * weight + trans[j * 3 + 2] * (1 - weight);
		}
	}


	imshow("output", matchP);
	imwrite("y.png",  matchP);
	waitKey(0);
	return 0;

还有一种通过opencv自带的函数进行拼接,该函数默认使用surf特征子,两次帅选特征子。

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/stitching/stitcher.hpp>
using namespace std;
using namespace cv;
bool try_use_gpu = false;
vector<Mat> imgs;
string result_name = "dst1.jpg";
int main(int argc, char * argv[])
{
    Mat img1 = imread("34.jpg");
    Mat img2 = imread("35.jpg");

    imshow("p1", img1);
    imshow("p2", img2);

    if (img1.empty() || img2.empty())
    {
        cout << "Can't read image" << endl;
        return -1;
    }
    imgs.push_back(img1);
    imgs.push_back(img2);


    Stitcher stitcher = Stitcher::createDefault(try_use_gpu);
    // 使用stitch函数进行拼接
    Mat pano;
    Stitcher::Status status = stitcher.stitch(imgs, pano);
    if (status != Stitcher::OK)
    {
        cout << "Can't stitch images, error code = " << int(status) << endl;
        return -1;
    }
    imwrite(result_name, pano);
    Mat pano2 = pano.clone();
    // 显示源图像,和结果图像
    imshow("全景图像", pano);
    if (waitKey() == 27)
        return 0;
}

最后再说一下warpPerspective这个函数,很多博客大多都是理论性介绍相关理论参数,看过之后缺乏实际感性认识,那本文中的图片测试:warpPerspective(image01, tempP, homo, Size(image01.cols * 2, image01.rows));  

2倍列数下tempP输出图像如下(3186*762):

output图像为(2112*765):

warpPerspective(image01, tempP, homo, Size(image01.cols * 3, image01.rows));  3倍列数情况下为(4278*792)。

output图像为(3168*765):

顺便发现一个很有意思的事情,把部分拼接代码改一下:

//优化拼接线
    double lefttop[3] = { 0,0,0 };
    double leftbottom[3] = { 0,image01.rows,0 };

输出的output图如下:

参考博客:

http://www.cnblogs.com/wangguchangqing/p/4333873.html

https://blog.csdn.net/dcrmg/article/details/52629856

https://blog.csdn.net/Winder_Sky/article/details/79891154

https://blog.csdn.net/lhanchao/article/details/52974129

https://www.cnblogs.com/skyfsm/p/7411961.html

猜你喜欢

转载自blog.csdn.net/qq_35054151/article/details/82766051