平面投影就是以序列图像中的一幅图像的坐标系为基准,将其图像都投影变换到这个基准坐标系中,使相邻图像的重叠区域对齐,称由此形成的拼接为平面投影拼接;图像拼接的关键两步是:配准(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 findHomographyvector<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 maskvector<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 findHomographyvector<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 maskvector<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和computeMat 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

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

  1. OpenCV图像特征提取

    Camera系列文章 传感器融合是将多个传感器采集的数据进行融合处理,以更好感知周围环境:这里首先介绍Camera的相关内容,包括摄像头及图像知识基本介绍,OpenCV图像识别(特征提取,目标分类等) ...

  2. OpenCV基础入门系列基本操作——贰

    系列博文第二篇,关于OpenCV4的一些基本操作和使用. 博文主要以实例展示不同的函数使用方法. OpenCV基础入门系列基本操作--壹 前言 下述为本博文需要用到的各类头文件以及全局变量等 读者可根 ...

  3. [图像识别]10.OpenCV的特征点检测 SIFT和SURF算法

    回顾,上节课你学了什么? R=cv.cornerHarris(img,blockSize,ksize,k) corners=cv.goodFeaturesToTrack(img,maxCorners, ...

  4. python读取视频流做人脸识别_基于OpenCV和Keras实现人脸识别系列——二、使用OpenCV通过摄像头捕获实时视频并探测人脸、准备人脸数据...

    基于OpenCV和Keras实现人脸识别系列手记: 项目完整代码参见Github仓库. 本篇是上面这一系列手记的第二篇. 在Opencv初接触,图片的基本操作这篇手记中,我介绍了一些图片的基本操作,而 ...

  5. opencv基础:罗德里格斯旋转公式(Rodrigues' rotation formula)推导 rodrigues()函数原理

    参考:1-https://openhome.cc/Gossip/WebGL/Rodrigues.html(比较好的理解流程) 2-https://www.cnblogs.com/wtyuan/p/12 ...

  6. OpenCV基础入门【C++及python语言】

    OpenCV基础入门[C++语言] OpenCV-Python 中文教程 OpenCV官方教程中文版(For Python) OpenCV2-Python-Tutorials 部分文件参考: http ...

  7. OpenCV基础(基于Opencv4.4+VS2019)

    OpenCV基础(基于Opencv4.4+VS2019) 1.OpenCV介绍 OpenCV是计算机视觉开源库,主要算法涉及图像处理和机器学习相关方法. 是Intel公司贡献出来的,俄罗斯工程师贡献大 ...

  8. 图像特征提取与匹配——SIFT算法

    图像特征提取与匹配--SIFT算法 尺度不变特征转换(Scale-invariant feature transform 或 SIFT)是一种用来侦测与描述影像中局部性特征的算法,它在空间尺度中寻找极 ...

  9. OpenCV 基础实战一图像的读取和显示

    OpenCV 基础实战一图像的读取和显示 该系列主要是完成功能 图像的读取和显示 该系列主要是完成功能 文章主要是基于python3(pycharm) +Opencv3 做的讲解和实现,对于代码的原理 ...

最新文章

  1. oracle数据库增加新字段
  2. 运行在浏览器中的深度学习框架,开源了
  3. Codeforces - 662A 思路巧妙的异或
  4. Silverlight/Windows8/WPF/WP7/HTML5周学习导读(9月24日-9月30日)
  5. 简化存货核算方法和物料分类帐对比
  6. purge table table_name的一点测试!
  7. why my custom callback is not called
  8. Spring LDAP
  9. Put-Me-Down项目Postmortem2
  10. 计算机基础知识ppt操作题,计算机一级ppt操作题
  11. 电压跟随器Voltage Follower
  12. 单片机节日彩灯实训报告_单片机设计节日彩灯控制器课程设计报告
  13. ROS与Arduino:ros_arduino_bridge功能包的使用解读
  14. android 跑马灯带图片,Android自定义跑马灯效果(适合任意布局)
  15. 基于FBX SDK的FBX模型解析与加载 -(一)
  16. 2022还不知道登陆邮箱账号怎么填写?个人邮箱登录注册流程看详解
  17. 锥智科技完成5000万元融资 官网启用拼音域名zhuizhikeji.com
  18. 很详细的SpringBoot整合UEditor教程
  19. w ndoWs10开机时间长,Windows10开机速度变慢,用这5个小妙招,让电脑开机提速
  20. 吐血推荐收藏的学位论文排版教程(完整版)

热门文章

  1. 大公司都有哪些开源项目~~~简化版
  2. Android Studio 无法启动模拟器的一种可能是你装的是Ghost版的系统
  3. 2021年上海高考小三门成绩查询,高考小三门怎么算分 上海2021高考小三门
  4. vbs 解析 json jsonp 方法
  5. python学习--关注容易被忽略的知识点--(四)函数式编程
  6. 数据比赛大杀器----模型融合(stackingblending)(转载)
  7. 2008年下半年软考在即 51CTO将实时提供试题和参考答案
  8. tensor转换为图片_pytorch 实现张量tensor,图片,CPU,GPU,数组等的转换
  9. GDAL库调试(包括跨语言调试)
  10. Arcgis执行Raster Project时报Error001143 : Background server threw an exception