基于sift算法的图像配准、Homograph Matrix、RANSAC

写在前面

1、文中所有资源、参考已给出来源链接,如有侵权请联系删除
2、为了尽量描述每个问题,文章内涉及分支较多,建议先整体阅读,想对某一块知识深究原理的再点击相应链接继续学习
3、本文实验环境:win10+vs2019+opencv440(vs2019配置opencv+contrib-440 + PCL1.10.0 + 源码单步调试https://blog.csdn.net/qq_41102371/article/details/108727224)
4、码字不易,转载本文请注明出处,本文链接:https://blog.csdn.net/qq_41102371/article/details/116031738

配准实验结果

原图来自opencv(\opencv-4.4.0\samples\data目录下)https://github.com/opencv/opencv/tree/master/samples/data
graf3.png(左图)graf3.png(右图)
特征匹配
在这里插入图片描述
求单应性矩阵并RANSAC剔除误匹配点后
在这里插入图片描述
使用单应性变换矩阵将右图配准至左图效果
在这里插入图片描述
完整项目文件免费下载地址:
[share_noel/Opencv&Image Processing/202105image registration]https://blog.csdn.net/qq_41102371/article/details/125646840
愿意用c币支持的朋友也可在此下载:
202105image_registration.zip https://download.csdn.net/download/qq_41102371/18717986
(上述下载链接中csdn与网盘的文件完全相同,只不过网盘免费下载)

图像配准

图像配准(Image registration)就是将不同时间、不同传感器(成像设备)或不同条件下(天候、照度、摄像位置和角度等)获取的两幅或多幅图像进行匹配、叠加的过程。
图像配准方法分为三个主要类别:基于灰度信息法、变换域法和基于特征法。
本文使用基于特征的方法,流程为:
首先对两幅图像进行特征提取得到特征点;
通过进行相似性度量找到匹配的特征点对;
然后通过匹配的特征点对得到图像A但图像B空间坐标变换参数(单应性变换矩阵);
使用单应性矩阵将图A进行变换,将图B复制到变换后图A的特定位置;
对重叠边界进行处理使配准图像更自然。
百度百科:图像配准 https://baike.baidu.com/item/%E5%9B%BE%E5%83%8F%E9%85%8D%E5%87%86/9020482?fr=aladdin#5
OpenCV探索之路(二十四)图像拼接和图像融合技术 https://www.cnblogs.com/skyfsm/p/7411961.html

特征点提取及匹配

特征检测

特征点的检测方法有很多,比较常用的有SIFT,ORB,SURF,Harris等;(OpenCV探索之路(二十三):特征检测和特征匹配方法汇总 https://www.cnblogs.com/skyfsm/p/7401523.html)在追求速度的场景中,用的较多的是ORB,比如视觉SLAM,但ORB不具备尺度不变性;追求精度的场景更多用SIFT、SURF,比如图像配准、基于SFM的影像三维重建等。
本文使用opencv440中的SIFT特征检测算子,关于SIFT算法讲解的一些文章、博客,总结得特别全:RobHess的SIFT源码分析:综述 https://blog.csdn.net/masibuaa/article/details/9191309
在opencv440版本中的SIFT算法使用方法如下,创建一个SIFT对象siftdetector,使用detectAndCompute()函数同时检测特征点及生成对应的特征描述子

    Ptr<Feature2D> siftdetector = cv::SIFT::create(0, 3, 0.04, 10);// SIFT提取特征点参数0, 3, 0.04, 10
    vector<KeyPoint> keyPoint1, keyPoint2;//特征点
    cv::Mat imageDesc1, imageDesc2;//描述子
    //特征点检测与描述,为下边的特征点匹配做准备 
    siftdetector->detectAndCompute(image1, noArray(), keyPoint1, imageDesc1);
    siftdetector->detectAndCompute(image2, noArray(), keyPoint2, imageDesc2);

特征匹配

在特征检测阶段得到的特征描述子就是用来特征匹配用的,原理就是判定两个特征点的欧氏距离;特征匹配使用k近邻搜索,设置k=2,返回图A的特征点 F A F_A FA在图B的2个近邻点:最近邻特征 F B 1 F_{B1} FB1以及次近邻特征点 F B 2 F_{B2} FB2;使用Lowe的比率阈值剔除错误匹配:对于图A中的特征点 F A F_A FA,其在B图与最近邻特征点 F B 1 F_{B1} FB1的为距离 d 1 d_1 d1,与次近邻特征点 F B 2 F_{B2} FB2的距离为 d 2 d_2 d2,若 d 1 < α ∗ d 2 d_1<\alpha*d_2 d1<αd2,则认为 F B 1 F_{B1} FB1 F A F_A FA的特征匹配点。其中 α \alpha α是一个常数,Lowe原文给出的是0.8([share_noel/papers/SIFT-Lowe2004-Distinctive Image Features from Scale-Invariant Keypoints.pdf]https://blog.csdn.net/qq_41102371/article/details/125646840
在这里插入图片描述
但作者对大量任意存在尺度、旋转和亮度变化的两幅图片进行匹配,结果表明ratio取值在0. 4~0. 6 之间最佳,小于0. 4的很少有匹配点,大于0. 6的则存在大量错误匹配点,所以建议ratio的取值原则如下:
ratio=0. 4:对于准确度要求高的匹配;
ratio=0. 6:对于匹配点数目要求比较多的匹配;
ratio=0. 5:一般情况下
(OpenCV探索之路(二十三):特征检测和特征匹配方法汇总 https://www.cnblogs.com/skyfsm/p/7401523.html)
特征点匹配方法(SIFT匹配)的一点见解 https://blog.csdn.net/holybin/article/details/28597349
特征匹配代码:

    FlannBasedMatcher matcher;//创建一个特征匹配的对象
    //匹配点对容器的容器,每个容器里面装最近邻和次近邻的匹配点对
    vector<vector<DMatch> > matchePoints; 
    //匹配点对容器,装匹配点对
    vector<DMatch> GoodMatchePoints;

    //向matcher传入特征描述子
    vector<Mat> train_desc(1, imageDesc1);
    matcher.add(train_desc);
    matcher.train();
    //使用k近邻(knn)查找imageDesc2的每个特征点在imageDesc1中的最近邻和次近邻点,所以最后一个参数设置为2
    matcher.knnMatch(imageDesc2, matchePoints, 2);
    cout << "total match points: " << matchePoints.size() << endl;

    // Lowe's algorithm,获取优秀匹配点
    for (int i = 0; i < matchePoints.size(); i++)
    {
        //判定最近邻与次近邻的比值是不是小于alpha,是就保留该配对的最近点对
        if (matchePoints[i][0].distance < 0.4 * matchePoints[i][1].distance)
        {
            //由上可知[i][0]是最近邻,[i][1]是次近邻,满足比值的情况下保留最近邻
            GoodMatchePoints.push_back(matchePoints[i][0]);
        }
    }

对称检测

在得到对应特征点对后,还可以使用对称检测来剔除误匹配点对,主要思想为:如果图A中特征点 F A F_A FA在图B中的对应特征点是 F B F_B FB,那么同样使用k近邻的方法检测 F B F_B FB的对应特征点,如果还是 F A F_A FA,那么就保留,如果不是,那么剔除这对特征点对;本文实验并未使用此方法,因为为了方便观察和调试,本文特意设置 α = 0.4 \alpha=0.4 α=0.4以得到较少的特征点,这些点还是比较可靠的。此方法的使用可参考此文的代码实现:SfM多视图三维点云重建–【VS2015+OpenCV3.4+PCL1.8】 https://blog.csdn.net/YunLaowang/article/details/88388130

// 消除误匹配:对称性检测
void symmetryTest(const vector<DMatch>& matches1, const vector<DMatch>& matches2, vector<DMatch>& symMatches)
{
	symMatches.clear();
	for (vector<DMatch>::const_iterator matchIterator1 = matches1.begin(); matchIterator1 != matches1.end(); ++matchIterator1)
		for (vector<DMatch>::const_iterator matchIterator2 = matches2.begin(); matchIterator2 != matches2.end(); ++matchIterator2)
			if ((*matchIterator1).queryIdx == (*matchIterator2).trainIdx && (*matchIterator1).trainIdx == (*matchIterator2).queryIdx)
				symMatches.push_back(*matchIterator1);
}

特征匹配及第一次剔除误匹配后的点对
在这里插入图片描述

单应性矩阵Homography matrix

理解

单应性矩阵 H H H就是描述平面与平面之间变换关系的 3 × 3 3\times3 3×3矩阵,可以想象从不同视角去观察一个平面得到的结果是不一样的,比如对一面涂鸦墙 A A A进行两个 C B C_{B} CB C C C_C CC两个角度的拍摄,得到图像 B B B C C C,实际上 B B B C C C也是平面,只不过是图像平面,当我们想知道 B B B的内容在 C C C_C CC的视角下是什么样时,我们就可以用单应性矩阵 H H H来表示平面与平面之间的两两转换关系。比如 B B B C C C之间的单应性变换就可以用 H B C H_{BC} HBC来表示:
B C = H B C ∗ B B_C=H_{BC}*B BC=HBCB
此时 C C C B C B_C BC是同一视角下的图像,拼接在一起就有了更多的信息,于是就有了图像配准这样的应用;
并且单应性矩阵是可逆的,我们可以使用 H C B = H B C − 1 H_{CB}=H_{BC}^{-1} HCB=HBC1来表示视角 C C C_C CC变换到 C B C_B CB的单应性变换。(上述单应性矩阵与图像矩阵相乘的表达是不严谨的,仅为了方便描述,实际上使用时是对图像的齐次坐标做单应性变换)
以上描述的单应性变换关系由简单的旋转、平移、缩放、透视组成,具体剖析以及代码示例见此文:
图像旋转平移、仿射变换、透视变换 https://blog.csdn.net/qq_41102371/article/details/116245483
那么单应性矩阵怎么求呢?由上文的工作,我们检测到了两图像之间的特征点以及对应点对,既然是两平面之间存在单应性变换,那么平面上的对应点肯定也是遵循同样的单应性变换,只要我们有比较好的对应点对,就能够反求解出单应性矩阵。

opencv函数接口

findHomography()函数在\opencv-4.4.0\modules\calib3d\src\fundam.cpp line:350

Mat cv::findHomography	(
InputArray 	srcPoints,
InputArray 	dstPoints,
int 	method = 0,
double 	ransacReprojThreshold = 3,
OutputArray 	mask = noArray(),
const int 	maxIters = 2000,
const double 	confidence = 0.995 
)	

本文使用opencv提供的单应性矩阵求解函数,opencv的官方文档中有对函数接口的解释以及单应性变换的描述
opencv官方文档findHomography–https://docs.opencv.org/4.4.0/d9/d0c/group__calib3d.html#ga4abc2ece9fab9398f2e560d53c8c9780

关于单应性矩阵的更详细公式、推导、代码讲解放在了这篇文章里面:待完成…
关于RANSAC算法及其在单应性矩阵求解中的应用、代码讲解放在了这篇文章:待完成…

现在使用得到的特征点对进行homography matrix的求解,直接使用opencv的函数
输入参数:imagePoints特征点对,优化方法使用RANSAC,重投影误差为3个像素,空的掩膜矩阵
输出:函数返回单应性矩阵,同时更新了掩膜矩阵

    //获取图像1到图像2的投影映射矩阵 尺寸为3*3  
    Mat homo = findHomography(imagePoints1, imagePoints2, cv::RANSAC, 3.0, mask);
    cout << "1变换矩阵为:\n" << homo << endl << endl; //输出映射矩阵
    /*使用掩膜矩阵剔除错误的匹配点对
    本程序为了方便后面的调试分析,   将误匹配的特征点对以及相应的特征点都删除了
    留下的正确匹配对按顺序一一对应
    */
    maskout_points(imagePoints1, mask);
    maskout_points(imagePoints2, mask);

    maskout_keypoints(keyPoint1, mask);
    maskout_keypoints(keyPoint2, mask);
    maskout_matches(GoodMatchePoints, mask);
    //将RANSAC后的特征匹配可视化
    cv::Mat second_match;
    drawMatches(image02, keyPoint2, image01, keyPoint1, GoodMatchePoints, second_match);
    cv::namedWindow("second_match", WINDOW_NORMAL);
    imshow("second_match", second_match);
    imwrite("second_match.jpg", second_match);
    cv::waitKey(0);
    

   /*计算配准图的四个顶点坐标
   * 图像经过单应性变换后,边界可能会超出之前的范围
   * 因此只要算出四个顶点变换的像素坐标就能知道变换后图像的范围
   */
    CalcCorners(homo, image01);
    cout << "left_top:" << corners.left_top << endl;
    cout << "left_bottom:" << corners.left_bottom << endl;
    cout << "right_top:" << corners.right_top << endl;
    cout << "right_bottom:" << corners.right_bottom << endl;

RANSAC剔除误匹配后的特征匹配
在这里插入图片描述

图像配准

单应性变换

使用得到的单应性矩阵对图像1进行单应性变换,变换后的图像为imageTransform1,变换后的尺寸与之前求出顶点的范围有关,图像的高度不变,使用image02.rows,超出去的部分会被忽略,图像的宽度为右上顶点与右下顶点中x坐标最大的那个

    //图像配准  
    Mat imageTransform1, imageTransform2;
    warpPerspective(image01, imageTransform1, homo, Size(MAX(corners.right_top.x, corners.right_bottom.x), image02.rows));
    //warpPerspective(image01, imageTransform1, homo, Size(3200, 1280));
    //warpPerspective(image01, imageTransform2, adjustMat*homo, Size(image02.cols*1.3, image02.rows*1.8));
    imshow("直接经过透视矩阵变换", imageTransform1);
    imwrite("trans1.jpg", imageTransform1);

拼接

现在图1已经转到图2的视角,将两图放在同一块画布上即可实现图像的拼接,但是由于误差,接缝处会效果不是太好,使用权重插值的方法,对边界处进行优化处理

    //创建拼接后的图,需提前计算图的大小
    int dst_width = imageTransform1.cols;  //取最右点的长度为拼接图的长度
    int dst_height = image02.rows;

    Mat dst(dst_height, dst_width, CV_8UC3);
    dst.setTo(0);//图像初始化为每个像素是0
    //将两幅图像复制进同一目标图像
    imageTransform1.copyTo(dst(Rect(0, 0, imageTransform1.cols, imageTransform1.rows)));
    imshow("b_dst0", dst);
    image02.copyTo(dst(Rect(0, 0, image02.cols, image02.rows)));
    cv::namedWindow("b_dst", WINDOW_NORMAL);
    imshow("b_dst", dst);
    imwrite("dst0.jpg", dst);
    //接缝优化
    OptimizeSeam(image02, imageTransform1, dst);
    imshow("dst", dst);
    imwrite("dst.jpg", dst);
    waitKey();

接缝优化前
在这里插入图片描述

优化函数

函数通过对左右图像的像素分配不同的权重,使图像衔接更加完美,加权公式以及详细叙述在此文:基于SIFT特征的全景图像拼接https://blog.csdn.net/masibuaa/article/details/9246493

//优化两图的连接处,使得拼接自然
void OptimizeSeam(Mat& img1, Mat& trans, Mat& dst)
{
    int start = MIN(corners.left_top.x, corners.left_bottom.x);//开始位置,即重叠区域的左边界  
    //忽略小于0的部分
    if (start < 0)
    {
        start = 0;
    }
    double processWidth = img1.cols - start;//重叠区域的宽度  
    int rows = dst.rows;
    int cols = img1.cols; //注意,是列数*通道数
    double alpha = 1;//img1中像素的权重  
    for (int i = 0; i < rows; i++)
    {
        uchar* p = img1.ptr<uchar>(i);  //获取第i行的首地址
        uchar* t = trans.ptr<uchar>(i);
        uchar* d = dst.ptr<uchar>(i);
        for (int j = start; j < cols; j++)
        {
            //如果遇到图像trans中无像素的黑点,则完全拷贝img1中的数据
            if (t[j * 3] == 0 && t[j * 3 + 1] == 0 && t[j * 3 + 2] == 0)
            {
                alpha = 1;
            }
            else
            {
                //img1中像素的权重,与当前处理点距重叠区域左边界的距离成正比,实验证明,这种方法确实好  
                alpha = (processWidth - (j - start)) / processWidth;
            }

            d[j * 3] = p[j * 3] * alpha + t[j * 3] * (1 - alpha);
            d[j * 3 + 1] = p[j * 3 + 1] * alpha + t[j * 3 + 1] * (1 - alpha);
            d[j * 3 + 2] = p[j * 3 + 2] * alpha + t[j * 3 + 2] * (1 - alpha);

        }
    }
}

优化后
在这里插入图片描述

参考

vs2019配置opencv+contrib-440 + PCL1.10.0 + 源码单步调试
https://github.com/opencv/opencv/tree/master/samples/data
百度百科:图像配准
OpenCV探索之路(二十四)图像拼接和图像融合技术 https://www.cnblogs.com/skyfsm/p/7411961.html
OpenCV探索之路(二十三):特征检测和特征匹配方法汇总
Lowe2004-Distinctive Image Features from Scale-Invariant Keypoints
RobHess的SIFT源码分析:综述
特征点匹配方法(SIFT匹配)的一点见解 https://blog.csdn.net/holybin/article/details/28597349
SfM多视图三维点云重建–【VS2015+OpenCV3.4+PCL1.8】 https://blog.csdn.net/YunLaowang/article/details/88388130
图像旋转平移、仿射变换、透视变换 https://blog.csdn.net/qq_41102371/article/details/116245483
opencv官方文档findHomography–https://docs.opencv.org/4.4.0/d9/d0c/group__calib3d.html#ga4abc2ece9fab9398f2e560d53c8c9780
基于SIFT特征的全景图像拼接https://blog.csdn.net/masibuaa/article/details/9246493
Opencv FlOAT64类型Mat访问错误 https://blog.csdn.net/weixin_41108706/article/details/88566069
单应性矩阵的理解及求解https://blog.csdn.net/liubing8609/article/details/85340015

如有错漏,敬请指正
--------------------------------------------------------------------------------------------诺有缸的高飞鸟202105

  • 16
    点赞
  • 81
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

诺有缸的高飞鸟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值