平面投影就是以序列图像中的一幅图像的坐标系为基准,将其图像都投影变换到这个基准坐标系中,使相邻图像的重叠区域对齐,称由此形成的拼接为
平面投影拼接
;图像拼接的关键两步是:配准(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,不但可以提纯,还可以计 算出3×3的转换矩阵。这个转换矩阵十分重要。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;
- 拼接程序
//开始拼接
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
全景拼接关键技术 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