opencv基础--特征提取与配准(SIFT系列)

平面投影就是以序列图像中的一幅图像的坐标系为基准,将其图像都投影变换到这个基准坐标系中,使相邻图像的重叠区域对齐,称由此形成的拼接为平面投影拼接;图像拼接的关键两步是:配准(registration)和融合(blending)。配准的目的是根据几何运动模型将图像注册到同一个坐标系中;融合则是将配准后的图像合成一张大的拼接图像。

一、sift和surf算法实现两幅图像拼接的过程是一样的,主要分为四个部分:
1.特征点提取和描述
2.特征点配对,找到两幅图像中匹配点的位置
3.通过配对点,生成变换矩阵,并对图像1应用变换矩阵生成对图像2的映射图像
4.图像2拼接到映射图像上,完成拼接
关于sift特征描述:https://blog.csdn.net/counte_rking/article/details/78834644

二、一个图像的特征点由两部分构成:关键点(KeyPoint)和描述子(Descriptor)。关键点指的是该特征点在图像中的位置,有些还具有方向、尺度信息;描述子通常是一个向量,按照认为设计的方式,描述关键点周围像素的信息。通常描述子是按照外观相似的特征应该有的相似的描述子设计的。因此,在匹配的时候,只要两个特征点的描述子在向量空间的距离相近,就可以认为它们是同一个特征点。
特征点的匹配通常需要以下三个步骤:
a.提取图像中的关键点,这部分是查找图像中具有某些特征(不同的算法有不同的)像素
b.根据得到的关键点位置,计算特征点的描述子
c.根据特征点的描述子进行匹配

三、每个特征描述子都是独特的,具有排他性,尽可能减少彼此间的相似性。其中描述子的可区分性和其不变性都是矛盾的,一个具有众多不变性的特征描述子,其区分局部图像内容的能力就比较稍弱;而如果一个很容易区分不同局部图像的特征描述子,其鲁棒性往往比较低。所以,在设计特征描述子的时候,就需要综合考虑这三个特性,找到三者之间的平衡。
特征描述子的不变性主要体现在两个方面:
尺度不变性Scale Invarient
指的是同一个特征,在图像的不同尺度空间保持不变。匹配在不同图像中的同一个特征点经常会有图像的尺度问题,不同尺度的图像中特征点的距离变得不同,物体的尺寸变得不同,而仅仅改变特征点的大小就有可能造就强度不匹配。如果描述子无法保持尺度不变性,那么同一个特征点在放大或者缩小的图像间,就不能很好的匹配。为了保持尺度的不变性,在计算特征点的描述子的时候,通常将图像变换到统一的尺度空间,再加上尺度因子。
旋转不变性Rotation Invarient
指的是同一个特征,在成像视角旋转后,特征仍然能够保持不变。和尺度不变性类似,为了保持旋转不变性,在计算特征点描述子的时候要加上关键点的方向信息。
为了有个更直观的理解,下面给出SIFT,SURF,BRIEF描述子计算方法对比
**加粗样式**
从上表可以看出,SIFT,SURF和BRIEF描述子都是一个向量,只是维度不同。其中,SIFT和SURF在构建特征描述子的时候,保存了特征的方向和尺度特征,这样其特征描述子就具有尺度和旋转不变性;而BRIEF描述子并没有尺度和方向特征,不具备尺度和旋转不变性。
1.获取检测器的实例
在OpenCV3中重新的封装了特征提取的接口,可统一的使用Ptr detector = FeatureDetector::create()来得到特征提取器的一个实例,所有的参数都提供了默认值,也可以根据具体的需要传入相应的参数。
2.在得到特征检测器的实例后,可调用的detect方法检测图像中的特征点的具体位置,检测的结果保存在vector向量中。
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->keypointssrc[matches[i].trainIdx].pt;
        dstPoints[i] = leftPattern->keypointsdst[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.图像变换。一般情况下8参数的透视投影变换最适合描述图像之间的坐标关系,其中8参数的矩阵为[m0,m1,m2;m3,m4,m5; m6,m7,1];最后选择了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;
  1. 拼接程序
//开始拼接
	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输出图像如下(3186762):
在这里插入图片描述
output图像为(2112
765):
在这里插入图片描述
warpPerspective(image01, tempP, homo, Size(image01.cols * 3, image01.rows)); 3倍列数情况下为(4278792)。
在这里插入图片描述
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

全景拼接关键技术 https://www.cnblogs.com/wyuzl/p/7746360.html

图像配准之特征点匹配的思考https://blog.csdn.net/zcg1942/article/details/80105382?utm_source=blogxgwz1

医疗配准:http://www.sohu.com/a/250660422_394300

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值