2020-12.24 用SURF实现简易的两张图像拼接

第九天

今日写了一天代码,故打算细写一篇记录。

为什么写拼接

   尽管OPENCV已经已经提供了方便强大且全面的stiching类,但是作为初学者~~(尤其是opencv正儿八经的工程都没写过一个的)~~ ,有必要体验一下整个缝合的过程。加之之前书看了一堆,到底怎么用,还是得自己动手才有感觉。
   环境:opencv3.4.5+vscode2017+ubuntu18.04,注意opencv要有contrib库,别跟我个憨憨一样再回头装。如果你真的没有。

原理

  整体流程有四+1步(主要参考别人博客)

  1. 从两幅图片中提取特征点,可以用SURF、SIFT、ORB、FLANN等花里胡哨的,我用的是SURF。
  2. 匹配两幅图像中的特征点,这一步是核心,匹配效果的好坏将严重影响效果。
  3. 根据配对的特征点求出旋转矩阵,这里只用了仿射变换,但opencv的实现里好像还有相机参数估计和透视变换。
  4. 将其中一张图片旋转到另一个的坐标系下,并将重合的部分拼接到一起
  5. 边缘优化。

这里贴出两个主要参考的博客:
原始大哥的博客
超级大哥的博客,参考了上面,但内容更全

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优化,排序,然后取最相近的前几个点,虽然有用,但效果有限,主要是为了去除明显错误的匹配。效果如下:

2020-12.24 用SURF实现简易的两张图像拼接_第1张图片网上有很短匹配的优化算法,这里我只是DD尝试版,就不列出了。

3.求出变换矩阵

基础知识:
2020-12.24 用SURF实现简易的两张图像拼接_第2张图片

   首先说一下基本思想,就是为什么我们知道对应特征点之后就能把图像匹配过去了
首先我们得明确匹配在一起的特征点的物理意义是什么:它们是我们在不同的图片中发现的“相同物体”——至少算法认为它们是相同的。也就是说,相当于现实里物体的一点,你站了两个不同的角度去拍摄它,尽管在两幅图上的位置和样子不同,但它是同一个点!而仿射变换处理的是什么呢?是一个二维图形的旋转、平移、缩放、翻转(严格的来说仿射只是二维的,但我们可以认为拍摄两个照片的位置非常接近,相机只进行了上述运动)。
   但是!!!运动是相对的,对一个图像进行向左的平移,不就等于你的摄像头向右平移吗?因此,既然两幅图是在不同的位置(位姿不同)对统一个东西(匹配的特征点)投影生成的二维图像,那么这两个相机的位置就一定对应了一个仿射变换矩阵,而我们只需要有足够的数据,即特征点(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;

我中间加了个平移矩阵,是因为,有的点仿射变换后会跑到外边去,这样我能把他移回来(主要是横向)。
2020-12.24 用SURF实现简易的两张图像拼接_第3张图片

仿射变换:
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

变换后(有平移,绿色是最佳匹配点):
2020-12.24 用SURF实现简易的两张图像拼接_第4张图片

4.图像配准与拼接

    我这里是按照大哥的方法找到最佳匹配点然后将两幅图像在此纵向对齐,你也可以粗暴的直接覆盖到右边,当然不管怎么样都得先扩大一下图像面积,然后将两张图精准的放进去,剩下的就主要数字游戏了。

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);


拼出来挺丑的,主要是特征点匹配没优化。
2020-12.24 用SURF实现简易的两张图像拼接_第5张图片
这部分由于OPencv用的不熟被自己恶心了好久,以后还是要多练。

5.边缘优化

   上图的边缘处过于"泾渭分明"了,主要是光照和糟糕的匹配点造成的,根据大哥和超级大哥的做法,进行了图片过渡处理,就是在重叠处从左至右富裕两个图片不同的权重比,越向左图一的权重越大,反之(就好比调两个图的透明度一样)。也就是说,中间处二者的图像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);

效果好了很多,但是有重影
2020-12.24 用SURF实现简易的两张图像拼接_第6张图片   如果要取得好的效果,需要调调第一步的阈值,和特征点匹配对数,我这里测试的结果是越严格越好,而且很明显即便调高阈值,排名靠前的仍会有一些错误匹配,因此多一个点少一个点匹配效果天壤之别,这就是不优化的下场…
   最后,总的代码:

#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);
}

调了两下参,好了一点:
2020-12.24 用SURF实现简易的两张图像拼接_第7张图片

一些感悟:

  • 学习opencv还是要多写,不然总是被基础语法恶心,例如:Mat初始化赋值方法、图像一共有rows+1行和cols+1列、opencv的数组越界这种内存错误只会报一堆莫名其妙的错误…
  • 基础知识很重要,有的数学过程可以不求甚解,但是得知道是个什么东西
  • 虽然匹配特征点可以用一个简单的函数解决,但是效果往往不好,不管用什么办法基本都需要优化
  • 拼接也一样,怎么拼效果才能更好。
  • 图像重合部分不多效果会很差,也不要对动态目标使用(不能一幅图里有的动有的不动,不然矩阵必然不准)。
  • 多个图像拼接会复杂的多。

每日音乐:WYY现在都在推啥 Burning

你可能感兴趣的:(学习日志)