今日写了一天代码,故打算细写一篇记录。
尽管OPENCV已经已经提供了方便强大且全面的stiching类,但是作为初学者~~(尤其是opencv正儿八经的工程都没写过一个的)~~ ,有必要体验一下整个缝合的过程。加之之前书看了一堆,到底怎么用,还是得自己动手才有感觉。
环境:opencv3.4.5+vscode2017+ubuntu18.04,注意opencv要有contrib库,别跟我个憨憨一样再回头装。如果你真的没有。
整体流程有四+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,函数名字一个都不对。
上面我们用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点就能确定一个平面),就能将其解出。
当然,我们没必要自己去考虑如何用过饱和的数据求出尽可能精确的解,但是有必要知道基本的思路。
代码很简单~~(毕竟不用懂都能写)~~ :
///*----------------------------------------------------------坐标系转换
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
我这里是按照大哥的方法找到最佳匹配点然后将两幅图像在此纵向对齐,你也可以粗暴的直接覆盖到右边,当然不管怎么样都得先扩大一下图像面积,然后将两张图精准的放进去,剩下的就主要数字游戏了。
cv::Point2f PointTrans(const Point2f srcPoint, const Mat& transMat)//对点仿射变换
{
Mat oriPos,tarPos;
oriPos=(cv::Mat_<double>(3,1)<<srcPoint.x,srcPoint.y,1.0); //Mat矩阵赋值
tarPos=transMat*oriPos;
float tarX=tarPos.at<double>(0,0)/tarPos.at<double>(2,0);
float tarY=tarPos.at<double>(1,0)/tarPos.at<double>(2,0); //用变换之后的矩阵除以第三行的比例系数
return Point2f(tarX,tarY);
}
///*图像同面
Mat imageTrans1,imageTrans2;
int mborder=MAX(transCorners.posRD.x,transCorners.posRU.x);
cv::warpPerspective(srcImage1,imageTrans1,transMat,cv::Size(2*srcImage1.cols,srcImage1.rows)) ;
cv::circle(imageTrans1,transCorners.posRD,10,CV_RGB(255,0,0),-1);
cv::circle(imageTrans1,transCorners.posRU,10,CV_RGB(255,0,0),-1);
cv::circle(imageTrans1,transCorners.posLD,10,CV_RGB(255,0,0),-1);
cv::circle(imageTrans1,transCorners.posLU,10,CV_RGB(255,0,0),-1);
cv::circle(imageTrans1,Best_point_trs,10,CV_RGB(0,255,0),1);
cv::imshow("Image1AfterTrans",imageTrans1);
///* 将图二按照最佳匹配点的位置拼过去
Mat ROIMat=srcImage2(cv::Rect(Point(Best_point_src2.x,0),Point(srcImage2.cols,srcImage2.rows)));
ROIMat.copyTo(imageTrans1(cv::Rect(Best_point_trs.x,0,srcImage2.cols-Best_point_src2.x +1, srcImage2.rows)));
cv::imshow("直接拼接",imageTrans1);
拼出来挺丑的,主要是特征点匹配没优化。
这部分由于OPencv用的不熟被自己恶心了好久,以后还是要多练。
上图的边缘处过于"泾渭分明"了,主要是光照和糟糕的匹配点造成的,根据大哥和超级大哥的做法,进行了图片过渡处理,就是在重叠处从左至右富裕两个图片不同的权重比,越向左图一的权重越大,反之(就好比调两个图的透明度一样)。也就是说,中间处二者的图像rgb值各乘50%然后相加。
void RimTransition(Mat &src_dealt, Mat &src_ori, Mat &dst)//坐标转换的、另一张初始的、处理到哪里
{
using cv::Vec3b;
double k=1;//渐变过渡的系数,初始在最左边,故为1.
int X_start=Best_point_trs.x-Best_point_src2.x+1;
int X_end = MAX(transCorners.posRD.x,transCorners.posRU.x);//最右边到哪了
for (int i=X_start;i<=X_end;i++) //外循环是列
{
k=(double)(X_end-i)/(X_end-X_start);
for (int j=0;j<dst.rows;j++)//内循环是行
{
if(i==400)
int a=2;
if(src_dealt.at<Vec3b>(j,i)==Vec3b(0,0,0))
dst.at<Vec3b>(j,i) = src_ori.at<Vec3b>(j,i);
else
dst.at<Vec3b>(j,i) = k*src_dealt.at<Vec3b>(j,i)+(1-k)*src_ori.at<Vec3b>(j,i);
}
}
return;
}
///* 按照比例系数过渡,从图像一变到图像二
Mat src2move =Mat::zeros(imageTrans1.rows, imageTrans1.cols, CV_8UC3);
Mat tempRoi=srcImage2(cv::Rect(Point(0,0),Point(srcImage2.cols,srcImage2.rows)));
tempRoi.copyTo(src2move(cv::Rect(Best_point_trs.x-Best_point_src2.x,0,srcImage2.cols,srcImage2.rows)));
Mat rimPotimize = imageTrans1.clone();
cv::imshow("图二平移后",src2move);
RimTransition(imageTrans1,src2move,rimPotimize);
cv::imshow("边缘过渡",rimPotimize);
效果好了很多,但是有重影
如果要取得好的效果,需要调调第一步的阈值,和特征点匹配对数,我这里测试的结果是越严格越好,而且很明显即便调高阈值,排名靠前的仍会有一些错误匹配,因此多一个点少一个点匹配效果天壤之别,这就是不优化的下场…
最后,总的代码:
#include
#include
#include
#define GOODPOINTNUM 10
using cv::xfeatures2d::SURF;
using cv::Mat,cv::Point2f ,cv::Point;
using cv::KeyPoint,cv::DescriptorMatcher,cv::DMatch;
using std::cout,std::endl;
Point2f Best_point_src;
Point2f Best_point_trs;
Point2f Best_point_src2;
typedef struct
{
Point2f posLU;
Point2f posLD;
Point2f posRU;
Point2f posRD;
}fourCorners;
fourCorners srCorners,transCorners;
cv::Point2f PointTrans(const Point2f srcPoint, const Mat& transMat)//对点仿射变换
{
Mat oriPos,tarPos;
oriPos=(cv::Mat_<double>(3,1)<<srcPoint.x,srcPoint.y,1.0); //Mat矩阵赋值
tarPos=transMat*oriPos;
float tarX=tarPos.at<double>(0,0)/tarPos.at<double>(2,0);
float tarY=tarPos.at<double>(1,0)/tarPos.at<double>(2,0); //用变换之后的矩阵除以第三行的比例系数
return Point2f(tarX,tarY);
}
void RimTransition(Mat &src_dealt, Mat &src_ori, Mat &dst)//坐标转换的、另一张初始的、处理到哪里
{
using cv::Vec3b;
double k=1;//渐变过渡的系数,初始在最左边,故为1.
int X_start=Best_point_trs.x-Best_point_src2.x+1;
int X_end = MAX(transCorners.posRD.x,transCorners.posRU.x);//最右边到哪了
for (int i=X_start;i<=X_end;i++) //外循环是列
{
k=(double)(X_end-i)/(X_end-X_start);
for (int j=0;j<dst.rows;j++)//内循环是行
{
if(i==400)
int a=2;
if(src_dealt.at<Vec3b>(j,i)==Vec3b(0,0,0))
dst.at<Vec3b>(j,i) = src_ori.at<Vec3b>(j,i);
else
dst.at<Vec3b>(j,i) = k*src_dealt.at<Vec3b>(j,i)+(1-k)*src_ori.at<Vec3b>(j,i);
}
}
return;
}
int main()
{
Mat srcImage1 = cv::imread("3.jpg");
Mat srcImage2 = cv::imread("4.jpg");
if(!srcImage1.data || !srcImage2.data)
{
printf("读取错误!\n");
}
Mat grayImage1;
Mat grayImage2;
cv::cvtColor(srcImage1,grayImage1,CV_RGB2GRAY);
cv::cvtColor(srcImage2,grayImage2,CV_RGB2GRAY);
///*------------------------------------------------------SURF特征点检测
int minHEssian = 2000;//hessian阈值,越大筛选越严格,匹配的特征点越少
//初始化SURF类和特征点向量
cv::Ptr<SURF>detector = SURF::create(minHEssian);
std::vector<KeyPoint> keyPoint1,keyPoint2;
//检测特征点,保存在vector中
detector->detect(grayImage1,keyPoint1);
detector->detect(grayImage2,keyPoint2);
//计算特征向量描述符
cv::Ptr<SURF>extractor = SURF::create();
Mat descriptors1,descriptors2;
std::vector< DMatch > matches;
extractor->compute ( grayImage1, keyPoint1, descriptors1);
extractor->compute ( grayImage2, keyPoint2, descriptors2);
//使用匹配器匹配
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());
//绘制匹配点
Mat imgMatches;
cv::drawMatches(srcImage2,keyPoint2,srcImage1,keyPoint1,matches,imgMatches);
cv::imshow("特征点匹配",imgMatches);
///*----------------------------------------------------------坐标系转换
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;
srCorners.posLU= Point2f (0,0);
srCorners.posLD= Point2f(0,srcImage1.rows);
srCorners.posRU= Point2f(srcImage1.cols,0);
srCorners.posRD= Point2f(srcImage1.cols,srcImage1.rows); //第一幅图像的四个点 x对应长
transCorners.posLU=PointTrans(srCorners.posLU,transMat);
transCorners.posLD=PointTrans(srCorners.posLD,transMat);
transCorners.posRU=PointTrans(srCorners.posRU,transMat);
transCorners.posRD=PointTrans(srCorners.posRD,transMat);
Best_point_src=keyPoint1[matches[0].trainIdx].pt;
Best_point_trs=PointTrans(Best_point_src,transMat);
Best_point_src2=keyPoint2[matches[0].queryIdx].pt;
///*图像同面
Mat imageTrans1,imageTrans2;
int mborder=MAX(transCorners.posRD.x,transCorners.posRU.x);
cv::warpPerspective(srcImage1,imageTrans1,transMat,cv::Size(2*srcImage1.cols,srcImage1.rows)) ;
// Mat adjustMat2=(cv::Mat_(3,3)<<1.0,0,srcImage1.cols*1.5,0,1.0,0,0,0,1.0);
// cv::warpPerspective(srcImage2,imageTrans2,adjustMat2,cv::Size(2.5*srcImage1.cols,srcImage1.rows)) ;
// cv::imshow("Image2AfterTrans",imageTrans2);
cv::circle(imageTrans1,transCorners.posRD,10,CV_RGB(255,0,0),-1);
cv::circle(imageTrans1,transCorners.posRU,10,CV_RGB(255,0,0),-1);
cv::circle(imageTrans1,transCorners.posLD,10,CV_RGB(255,0,0),-1);
cv::circle(imageTrans1,transCorners.posLU,10,CV_RGB(255,0,0),-1);
cv::circle(imageTrans1,Best_point_trs,10,CV_RGB(0,255,0),1);
cv::imshow("Image1AfterTrans",imageTrans1);
///* 将图二按照最佳匹配点的位置拼过去
// cv::Vec3b nmsl=srcImage2.at(0,0);
Mat ROIMat=srcImage2(cv::Rect(Point(Best_point_src2.x,0),Point(srcImage2.cols,srcImage2.rows)));
ROIMat.copyTo(imageTrans1(cv::Rect(Best_point_trs.x,0,srcImage2.cols-Best_point_src2.x +1, srcImage2.rows)));
cv::imshow("直接拼接",imageTrans1);
///* 按照比例系数过渡,从图像一变到图像二
Mat src2move =Mat::zeros(imageTrans1.rows, imageTrans1.cols, CV_8UC3);
Mat tempRoi=srcImage2(cv::Rect(Point(0,0),Point(srcImage2.cols,srcImage2.rows)));
tempRoi.copyTo(src2move(cv::Rect(Best_point_trs.x-Best_point_src2.x,0,srcImage2.cols,srcImage2.rows)));
Mat rimPotimize = imageTrans1.clone();
cv::imshow("图二平移后",src2move);
RimTransition(imageTrans1,src2move,rimPotimize);
cv::imshow("边缘过渡",rimPotimize);
cv::waitKey(0);
cvWaitKey(0);
}
一些感悟:
每日音乐:WYY现在都在推啥 Burning