第九天
今日写了一天代码,故打算细写一篇记录。
为什么写拼接
尽管OPENCV已经已经提供了方便强大且全面的stiching类,但是作为初学者~~(尤其是opencv正儿八经的工程都没写过一个的)~~ ,有必要体验一下整个缝合的过程。加之之前书看了一堆,到底怎么用,还是得自己动手才有感觉。
环境:opencv3.4.5+vscode2017+ubuntu18.04,注意opencv要有contrib库,别跟我个憨憨一样再回头装。如果你真的没有。
原理
整体流程有四+1步(主要参考别人博客)
- 从两幅图片中提取特征点,可以用SURF、SIFT、ORB、FLANN等花里胡哨的,我用的是SURF。
- 匹配两幅图像中的特征点,这一步是核心,匹配效果的好坏将严重影响效果。
- 根据配对的特征点求出旋转矩阵,这里只用了仿射变换,但opencv的实现里好像还有相机参数估计和透视变换。
- 将其中一张图片旋转到另一个的坐标系下,并将重合的部分拼接到一起。
- 边缘优化。
这里贴出两个主要参考的博客:
原始大哥的博客
超级大哥的博客,参考了上面,但内容更全
1.提取特征点
我这里用的是SURF,因为它虽然精度差一点,但速度快上几倍,稳定性也更好。
当然我不会讲SURF的原理,因为我也就只是看过一遍,想看的话网上一大把;更重要的是,你其实没必要知道它的详细数学过程,只要知道大概方法和怎么用就行了。
///*------------------------------------------------------SURF特征点检测
int minHEssian = 2000;//hessian阈值,越大筛选越严格,匹配的特征点越少
//初始化SURF类和特征点向量
cv::Ptr<SURF>detector = SURF::create(minHEssian);
std::vector<KeyPoint> keyPoint1,keyPoint2;
//检测特征点,保存在vector中
detector->detect(srcImage1,keyPoint1);
detector->detect(srcImage2,keyPoint2);
//计算特征向量描述符
cv::Ptr<SURF>extractor = SURF::create();
Mat descriptors1,descriptors2;
std::vector< DMatch > matches;
extractor->compute ( srcImage1, keyPoint1, descriptors1);
extractor->compute ( srcImage2, keyPoint2, descriptors2);
吐槽一下:opencv都出到4了,由于版权大家只能用3,书上讲的却都是2,函数名字一个都不对。
2.匹配特征点
上面我们用SURF求得了两幅图片的特征点,现在就该匹配他们了,上代码。
//使用匹配器匹配
cv::Ptr<DescriptorMatcher>matcher = DescriptorMatcher::create("BruteForce");//匹配方法
matcher->match(descriptors2,descriptors1,matches); //前者称为query集,后者成为train集
sort(matches.begin(),matches.end());//排序,误差距离短的在前面
matches.erase(matches.begin()+GOODPOINTNUM,matches.end());//只用前GOODPOINTNUM个匹配点
//绘制匹配点
Mat imgMatches;
cv::drawMatches(srcImage2,keyPoint2,srcImage1,keyPoint1,matches,imgMatches);
cv::imshow("特征点匹配",imgMatches);
这里用了一个noob优化,排序,然后取最相近的前几个点,虽然有用,但效果有限,主要是为了去除明显错误的匹配。效果如下:
网上有很短匹配的优化算法,这里我只是DD尝试版,就不列出了。
3.求出变换矩阵
基础知识:
首先说一下基本思想,就是为什么我们知道对应特征点之后就能把图像匹配过去了
首先我们得明确匹配在一起的特征点的物理意义是什么:它们是我们在不同的图片中发现的“相同物体”——至少算法认为它们是相同的。也就是说,相当于现实里物体的一点,你站了两个不同的角度去拍摄它,尽管在两幅图上的位置和样子不同,但它是同一个点!而仿射变换处理的是什么呢?是一个二维图形的旋转、平移、缩放、翻转(严格的来说仿射只是二维的,但我们可以认为拍摄两个照片的位置非常接近,相机只进行了上述运动)。
但是!!!运动是相对的,对一个图像进行向左的平移,不就等于你的摄像头向右平移吗?因此,既然两幅图是在不同的位置(位姿不同)对统一个东西(匹配的特征点)投影生成的二维图像,那么这两个相机的位置就一定对应了一个仿射变换矩阵,而我们只需要有足够的数据,即特征点(3点就能确定一个平面),就能将其解出。
当然,我们没必要自己去考虑如何用过饱和的数据求出尽可能精确的解,但是有必要知道基本的思路。
代码很简单~~(毕竟不用懂都能写)~~ :
///*----------------------------------------------------------坐标系转换
std::vector<cv::Point2f> imagePoints1, imagePoints2;//findHomography需要Point2f类型
for(int i=0;i<matches.size();i++)
{
imagePoints2.push_back(keyPoint2[matches[i].queryIdx].pt);
imagePoints1.push_back(keyPoint1[matches[i].trainIdx].pt);
}
//获得透视矩阵并进行投影
//图像1到2的映射,3*3转换阵
Mat transMat = cv::findHomography(imagePoints1,imagePoints2,CV_RANSAC);
Mat adjustMat1=(cv::Mat_<double>(3,3)<<1.0,0,srcImage1.cols/2,0,1.0,0,0,0,1.0); //平移变换矩阵,将矩阵沿着长的方向平移一个图像
transMat=adjustMat1*transMat;
cout<<"透视变换矩阵为"<<transMat<<endl;
我中间加了个平移矩阵,是因为,有的点仿射变换后会跑到外边去,这样我能把他移回来(主要是横向)。
仿射变换:
https://www.zhihu.com/question/20666664
https://www.cnblogs.com/happystudyeveryday/p/10547316.html 可以看一下这个里面的平移、旋转等单独矩阵,单拿出来处理图像用也很不错,我就从这里面取的。
http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/imgtrans/warp_affine/warp_affine.html
同时也顺便了解一下透视变换(三维):
https://zhuanlan.zhihu.com/p/36082864
https://zhuanlan.zhihu.com/p/36191127
https://www.cnblogs.com/liekkas0626/p/5262942.html
变换后(有平移,绿色是最佳匹配点):
4.图像配准与拼接
我这里是按照大哥的方法找到最佳匹配点然后将两幅图像在此纵向对齐,你也可以粗暴的直接覆盖到右边,当然不管怎么样都得先扩大一下图像面积,然后将两张图精准的放进去,剩下的就主要数字游戏了。
cv::Point2f PointTrans(const Point2f srcPoint, const Mat& transMat)//对点仿射变换
{
Mat oriPos,tarPos;
oriPos=(cv::Mat_<double>(3,1)<<srcPoint.x,srcPoint