• opencv实践项目-图像拼接


    1.简介

    图像拼接是计算机视觉中最成功的应用之一。如今,很难找到不包含此功能的手机或图像处理API。在本
    文中,我们将讨论如何使用OpenCV进行图像拼接。也就是,给定两张共享某些公共区域的图
    像,目标是“缝合”它们并创建一个全景图像场景。当然也可以是给定多张图像,但是总会转换成两张共享
    某些公共区域图像拼接的问题。

    2. 步骤

    拼接的两幅图
    在这里插入图片描述
    在这里插入图片描述

    2.1 特征检测与提取

    给定上述一对图像,我们希望将它们缝合以创建全景场景。重要的是要注意,两个图像都需要
    有一些公共区域。当然,我们上面给出的两张图像时比较理想的,有时候两个图像虽然具有公
    共区域,但是同样还可能存在缩放、旋转、来自不同相机等因素的影响。但是无论哪种情况,
    我们都需要检测图像中的特征点。

    2.2 关键点检测

    最初的并且可能是幼稚的方法是使用诸如Harris Corners之类的算法来提取关键点。然后,我
    们可以尝试基于某种相似性度量(例如欧几里得距离)来匹配相应的关键点。众所周知,角点
    具有一个不错的特性:角点不变。这意味着,一旦检测到角点,即使旋转图像,该角点仍将存
    在。
    但是,如果我们旋转然后缩放图像怎么办?在这种情况下,我们会很困难,因为角点的大小不
    变。也就是说,如果我们放大图像,先前检测到的角可能会变成一条线!
    总而言之,我们需要旋转和缩放不变的特征。那就是更强大的方法(如SIFT,SURF和
    ORB)。

    2.3 关键点和描述符

    诸如SIFT和SURF之类的方法试图解决角点检测算法的局限性。通常,角点检测器算法使用固
    定大小的内核来检测图像上的感兴趣区域(角)。不难看出,当我们缩放图像时,该内核可能
    变得太小或太大。为了解决此限制,诸如SIFT之类的方法使用高斯差分(DoD)。想法是将
    DoD应用于同一图像的不同缩放版本。它还使用相邻像素信息来查找和完善关键点和相应的描
    述符。
    首先,我们需要加载2个图像,一个查询图像和一个训练图像。最初,我们首先从两者中提取
    关键点和描述符。通过使用OpenCV detectAndCompute()函数,我们可以一步完成它。请注
    意,为了使用detectAndCompute(),我们需要一个关键点检测器和描述符对象的实例。它可
    以是ORB,SIFT或SURF等。此外,在将图像输入给detectAndCompute()之前,我们将其转
    换为灰度。
    代码:

    void detectAndDescribe(const cv::Mat &image, Extract_Features_Method method, std::vector<KeyPoint> &keypoints, cv::Mat &descriptor)
    {
    	switch (method)
    	{
    	case Extract_Features_Method::METHOD_SIFT:
    	{
    		Ptr<cv::SIFT> detector = cv::SIFT::create(800);
    		detector->detectAndCompute(image, cv::Mat(), keypoints, descriptor);
    		break;
    	}
    	case Extract_Features_Method::METHOD_SURF:
    	{
    		int minHessian = 400;
    		Ptr<cv::xfeatures2d::SURF> detector = cv::xfeatures2d::SURF::create(minHessian);
    		detector->detectAndCompute(image, cv::Mat(), keypoints, descriptor);
    		break;
    	}
    	case Extract_Features_Method::METHOD_BRISK:
    	{
    		int minHessian = 400;
    		
    		Ptr<BRISK> detector = BRISK::create(minHessian);
    		detector->detectAndCompute(image, cv::Mat(), keypoints, descriptor);
    		break;
    	}
    	case Extract_Features_Method::METHOD_ORB:
    	{
    		int minHessian = 400;
    
    		Ptr<ORB> detector = ORB::create(minHessian);
    		detector->detectAndCompute(image, cv::Mat(), keypoints, descriptor);
    		break;
    	}
    	default:
    		break;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    我们为两个图像都设置了一组关键点和描述符。如果我们使用SIFT作为特征提取器,它将为
    每个关键点返回一个128维特征向量。如果选择SURF,我们将获得64维特征向量。

    2.4 特征匹配

    现在,我们想比较两组特征,并尽可能显示更多相似性的特征点
    对。使 用 OpenCV , 特 征 点 匹 配 需 要 Matcher 对 象 。 在 这 里 , 我 们 探 索 两 种 方 式 : 暴 力 匹 配 器(BruteForce)和KNN(k最近邻)。
    BruteForce(BF)Matcher的作用恰如其名。给定2组特征(来自图像A和图像B),将A组的每个特征与B组的所有特征进行比较。默认情况下,BF Matcher计算两点之间的欧式距离。因此,对于集合A中的每个特征,它都会返回集合B中最接近的特征。对于SIFT和SURF,OpenCV建议使用欧几里得距离。对于ORB和BRISK等其他特征提取器,建议使用汉明距离。我们要使用OpenCV创建BruteForce Matcher,一般情况下,我们只需要指定2个参数即可。第一个是距离度量。第二个是是否进行交叉检测的布尔参数。
    具体代码如下:

    auto createMatcher(Extract_Features_Method method, bool crossCheck)
    {
    
    	if (method == Extract_Features_Method::METHOD_SIFT || method == Extract_Features_Method::METHOD_SURF)
    	{
    		return cv::BFMatcher(cv::NORM_L2, crossCheck);
    	}
    
    	return cv::BFMatcher(cv::NORM_HAMMING, crossCheck);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    交叉检查布尔参数表示这两个特征是否具有相互匹配才视为有效。换句话说,对于被认为有效的一对特征(f1,f2),f1需要匹配f2,f2也必须匹配f1作为最接近的匹配。此过程可确保提供更强大的匹配功能集,这在原始SIFT论文中进行了描述。
    但是,对于要考虑多个候选匹配的情况,可以使用基于KNN的匹配过程。KNN不会返回给定特征的单个
    最佳匹配,而是返回k个最佳匹配。需要注意的是,k的值必须由用户预先定义。如我们所料,KNN提供
    了更多的候选功能。但是,在进一步操作之前,我们需要确保所有这些匹配对都具有鲁棒性。

    2.5 比率测试

    为了确保KNN返回的特征具有很好的可比性,SIFT论文的作者提出了一种称为比率测试的技术。一般情
    况下,我们遍历KNN得到匹配对,之后再执行距离测试。对于每对特征(f1,f2),如果f1和f2之间的距
    离在一定比例之内,则将其保留,否则将其丢弃。同样,必须手动选择比率值。
    本质上,比率测试与BruteForce Matcher的交叉检查选项具有相同的作用。两者都确保一对检测到的特征确实足够接近以至于被认为是相似的。下面2个图显示了BF和KNN Matcher在SIFT特征上的匹配结果。我们选择仅显示100个匹配点以清晰显示。
    在这里插入图片描述

    需要注意的是,即使做了多种筛选来保证匹配的正确性,也无法完全保证特征点完全正确匹配。尽管如
    此,Matcher算法仍将为我们提供两幅图像中最佳(更相似)的特征集。接下来,我们利用这些点来计算
    将两个图像的匹配点拼接在一起的变换矩阵。
    这种变换称为单应矩阵。简而言之,单应性是一个3x3矩阵,可用于许多应用中,例如相机姿态估计,透
    视校正和图像拼接。它将点从一个平面(图像)映射到另一平面。

    2.6 估计单应性

    随机采样一致性(RANSAC)是用于拟合线性模型的迭代算法。与其他线性回归器不同,RANSAC被设计为对异常值具有鲁棒性。
    像线性回归这样的模型使用最小二乘估计将最佳模型拟合到数据。但是,普通最小二乘法对异常值非常敏感。如果异常值数量很大,则可能会失败。RANSAC通过仅使用数据中的一组数据估计参数来解决此问题。下图显示了线性回归和RANSAC之间的比较。需要注意数据集包含相当多的离群值。
    在这里插入图片描述
    我们可以看到线性回归模型很容易受到异常值的影响。那是因为它试图减少平均误差。因此,它倾向于支持使所有数据点到模型本身的总距离最小的模型。包括异常值。相反,RANSAC仅将模型拟合为被识别为点的点的子集。这个特性对我们的用例非常重要。在这里,我们将使用RANSAC来估计单应矩阵。事实证明,单应矩阵对我们传递给它的数据质量非常敏感。因此,重要的是要有一种算法(RANSAC),该算法可以从不属于数据分布的点中筛选出明显属于数据分布的点。
    估计了单应矩阵后,我们需要将其中一张图像变换到一个公共平面上。在这里,我们将对其中一张图像应用透视变换。透视变换可以组合一个或多个操作,例如旋转,缩放,平移或剪切。我们可以使用
    OpenCV warpPerspective()函数。它以图像和单应矩阵作为输入。

    3. 完整代码

    typedef enum
    {
    	METHOD_SIFT,
    	METHOD_SURF,
    	METHOD_BRISK,
    	METHOD_ORB
    }Extract_Features_Method;
    
    void detectAndDescribe(const cv::Mat &image, Extract_Features_Method method, std::vector<KeyPoint> &keypoints, cv::Mat &descriptor)
    {
    	switch (method)
    	{
    	case Extract_Features_Method::METHOD_SIFT:
    	{
    		Ptr<cv::SIFT> detector = cv::SIFT::create(800);
    		detector->detectAndCompute(image, cv::Mat(), keypoints, descriptor);
    		break;
    	}
    	case Extract_Features_Method::METHOD_SURF:
    	{
    		int minHessian = 400;
    		Ptr<cv::xfeatures2d::SURF> detector = cv::xfeatures2d::SURF::create(minHessian);
    		detector->detectAndCompute(image, cv::Mat(), keypoints, descriptor);
    		break;
    	}
    	case Extract_Features_Method::METHOD_BRISK:
    	{
    		int minHessian = 400;
    		
    		Ptr<BRISK> detector = BRISK::create(minHessian);
    		detector->detectAndCompute(image, cv::Mat(), keypoints, descriptor);
    		break;
    	}
    	case Extract_Features_Method::METHOD_ORB:
    	{
    		int minHessian = 400;
    
    		Ptr<ORB> detector = ORB::create(minHessian);
    		detector->detectAndCompute(image, cv::Mat(), keypoints, descriptor);
    		break;
    	}
    	default:
    		break;
    	}
    }
    
    auto createMatcher(Extract_Features_Method method, bool crossCheck)
    {
    
    	if (method == Extract_Features_Method::METHOD_SIFT || method == Extract_Features_Method::METHOD_SURF)
    	{
    		return cv::BFMatcher(cv::NORM_L2, crossCheck);
    	}
    
    	return cv::BFMatcher(cv::NORM_HAMMING, crossCheck);
    }
    
    int main()//stich_demo()
    {
    	string imgPath1 = "E:\\code\\Yolov5_Tensorrt_Win10-master\\pictures\\stich1.jpg";
    	string imgPath2 = "E:\\code\\Yolov5_Tensorrt_Win10-master\\pictures\\stich2.jpg";
    	Mat img1 = imread(imgPath1, IMREAD_GRAYSCALE);
    	Mat img2 = imread(imgPath2, IMREAD_GRAYSCALE);
    
    	std::vector<cv::KeyPoint> keypoint1;
    	cv::Mat describe1;
    	detectAndDescribe(img1, Extract_Features_Method::METHOD_SIFT, keypoint1, describe1);
    
    	std::vector<cv::KeyPoint> keypoint2;
    	cv::Mat describe2;
    	detectAndDescribe(img2, Extract_Features_Method::METHOD_SIFT, keypoint2, describe2);
    
    	auto matcher = createMatcher(Extract_Features_Method::METHOD_SIFT, false);
    
    	vector<DMatch> firstMatches;
    	matcher.match(describe1, describe2, firstMatches);
    	
    	vector<cv::Point2f> points1, points2;
    	for (vector<DMatch>::const_iterator it = firstMatches.begin(); it != firstMatches.end(); ++it)
    	{
    		points1.push_back(keypoint1.at(it->queryIdx).pt);
    		points2.push_back(keypoint2.at(it->trainIdx).pt);
    	}
    
    	auto inliers = vector<uchar>(keypoint1.size(), 0);
    	cv::Mat h12 = cv::findHomography(points1, points2, inliers, RANSAC, 1.0);
    	Mat h21;
    	invert(h12, h21, DECOMP_LU);
    	
    	Mat canvas;
    	Mat img1_color = imread(imgPath1);
    	Mat img2_color = imread(imgPath2);
    
    	warpPerspective(img2_color, canvas, h21, Size(img1.cols * 2, img1.rows));
    	imshow("warp", canvas);
    	img1_color.copyTo(canvas(Range::all(), Range(0, img1.cols)));
    
    	imshow("canvas", canvas);
    
    	waitKey(0);
    
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104

    生成的全景图像如下所示。如我们所见,结果中包含了两个图像中的内容。另外,我们可以看
    到一些与照明条件和图像边界边缘效应有关的问题。理想情况下,我们可以执行一些处理技术
    来标准化亮度,例如直方图匹配,这会使结果看起来更真实和自然一些。在这里插入图片描述

    本文参考Python视觉实战项目71讲,将其中的python代码移植到c++

  • 相关阅读:
    bert Bert文本分类教程 数据+完整代码 可直接运行
    ECharts数据可视化项目【3】
    【python学习】-列表运算(列表元素均加减乘除某个数、两个列表间的运算、遍历列表等)
    WPF —— 动画
    64 ---- 平面与直线的位置关系
    Java多线程
    IMS异常场景介绍
    php-文件存取
    lc[链表]---阶段总结
    ssm+java+vue基于微信小程序的电影院票务系统(可选座评论等功能)#毕业设计
  • 原文地址:https://blog.csdn.net/wyw0000/article/details/130498696