opencv实践之图像拼接

news2025/1/16 8:10:28

目录

  • 1.简介
  • 2. 步骤
    • 2.1 特征检测与提取
    • 2.2 关键点检测
    • 2.3 关键点和描述符
    • 2.4 特征匹配
    • 2.5 比率测试
    • 2.6 估计单应性
  • 3. 完整代码

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

我们为两个图像都设置了一组关键点和描述符。如果我们使用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);
}

交叉检查布尔参数表示这两个特征是否具有相互匹配才视为有效。换句话说,对于被认为有效的一对特征(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;
}

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/491341.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu在线安装及卸载MySQL

目录 1 安装 1.1 更新包管理工具apt-get 1.2 安装MySQL服务器端 1.3 安装MySQL客户端 1.4 初始化配置 1.5 检查MySQL服务状态 2 卸载 2.1 查看MySQL依赖项 2.2 卸载mysql-common 2.3 再卸载 mysql-server-8.0 2.4 再用dpkg --list|grep mysql查看&#xff0c;还剩什么…

MySQL -- 基础

目录 1. 数据库的操作 1.1 显示当前数据库 1.2 创建数据库 1.3 使用数据库 1.4 删除数据库 2. 常用的数据类型 2.1 数值类型 2.2 字符串类型 2.3 日期类型 3. 表的操作 3.1 查看表的结构 3.2 创建表 3.4 删除表 1. 数据库的操作 1.1 显示当前数据库 show databases; 1.2…

网络安全合规-Tisax(三)

一、什么是TISAX? TISAX 可信信息安全评估与交换标准是基于ISO 27001信息安全管理体系标准和VDA-ISA信息安全评价检查表而建立的汽车行业专用信息安全标准。TISAX 为汽车行业内不同服务商提供了信息安全评估结果互认的模式&#xff0c;供应商通过了该评估&#xff0c;即意味着…

如何访问chatGPT-国内上chatGPT的方法

如何在国内使用GPT 在国内使用 GPT&#xff08;Generative Pre-trained Transformer&#xff09;技术&#xff0c;可以实现多种自然语言处理和语义分析的任务&#xff0c;如机器翻译、聊天机器人、文本生成、问答系统等。但对于许多人来说&#xff0c;如何在国内使用 GPT 技术…

Java每日一练(20230505) 递增路径、编辑距离、数据流

目录 1. 矩阵中的最长递增路径 &#x1f31f;&#x1f31f;&#x1f31f; 2. 编辑距离 &#x1f31f;&#x1f31f;&#x1f31f; 3. 数据流的中位数 &#x1f31f;&#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Pyt…

解决修改es默认host时的问题

微信公众号也不定期更新&#xff0c; leetcode_algos_life 背景 es默认只能是在非root场景下进行启动&#xff0c;且默认host是本地地址&#xff0c;即localhost或者是127.0.0.1。 当线上部署restful服务时&#xff0c;此时如果其他服务与es服务不在同一台机器上&#xff0c;会…

Mysql数据库管理与高可用

目录 一、克隆/复制一个表1.1 方法一1.2 方法二 二、清空表&#xff0c;删除表内所有数据2.1 方法一2.2 方法二2.3 drop、truncate、delete对比①.drop table name②.truncate table table_name③.delete from table_name小结&#xff1a; 三、创建临时的表四、用户管理4.1 新建…

MSQL知识学习07(MySQL执行计划分析)

1、什么是执行计划&#xff1f; 执行计划 是指一条 SQL 语句在经过 MySQL 查询优化器 的优化会后&#xff0c;具体的执行方式。 执行计划通常用于 SQL 性能分析、优化等场景。通过 EXPLAIN 的结果&#xff0c;可以了解到如数据表的查询顺序、数据查询操作的操作类型、哪些索引…

【刷题之路Ⅱ】LeetCode 143. 重排链表

【刷题之路Ⅱ】LeetCode 143. 重排链表 一、题目描述二、解题1、方法1——线性表辅助1.1、思路分析1.2、代码实现 2、方法2——中间节点反转链表合并链表2.1、思路分析2.2、代码实现 一、题目描述 原题连接&#xff1a; 143. 重排链表 题目描述&#xff1a; 给定一个单链表 L …

7. Docker——Dockerfile

本章讲解知识点 DockerfileDockerfile 常用命令Dockerfile 综合示例Docker Compose当我们理解了镜像的基本原理后,我们就可以开始 Dockerfile 的学习了。 1. Dockerfile Dockerfile 是用于构建 Docker 镜像的脚本。它包含一组指令,按顺序执行以创建 Docker 镜像,从而使其可…

多通道振弦传感器无线采集仪通过短信和FTP文件修改参数

多通道振弦传感器无线采集仪通过短信和FTP文件修改参数 通过短信修改参数 向设备发送参数修改指令&#xff0c;设备在下次采发过程中若收到包含有合法指令的短信时会解析并执行短信内的指令&#xff0c;参数修改完成后会以短信形式回发应答信息。短信指令的格式如下&#xff1a…

ChatGPT实现仪表盘生成

仪表盘生成 Grafana是开源社区最流行的数据可视化软件&#xff0c;一定程度上也和 superset 一起被视为 tableau 等商业 BI 的开源替代品&#xff0c;很多IT 团队、科研团队&#xff0c;都会使用 Grafana 来做数据监控、挖掘分析。Grafana社区也有很多贡献者&#xff0c;在 gi…

设计模式-基本概念

设计模式-基本概念 基本概念奇异递归模板模式&#xff08;CRTP&#xff09;说明示例例子1&#xff1a;对象计数例子2&#xff1a;多态复制构造例子4&#xff1a;std::enable_shared_from_this例子5 树简单遍历 混合继承属性SOLID 设计原则 参考 基本概念 奇异递归模板模式&…

带你彻底理解Spark的分区

前言 我&#xff1a;什么是RDD&#xff1f; 面试者&#xff1a;RDD是被分区的&#xff0c;由一系列分区组成… … 我&#xff1a;你怎么理解分区&#xff1f; 面试者&#xff1a;… 我&#xff1a;Spark中有哪些可以实现分区的方法&#xff1f;分别使用的场景是什么&#xff1…

nodejs-前端工程化环境-安装-webpack打包工具

文章目录 1.安装nodejs1.1.新建项目1.2.安装jQuery。1.3.查看全局模块安装目录 2.安装Vue2.1.安装2.2.创建vue项目 3.安装webpack4.安装 Grunt5.安装uglify-js > js代码压缩打包工具。6.因为在此系统上禁止运行脚本……解决办法 1.安装nodejs 从官网下载长期支持版本&#…

数值分析-埃尔米特插值的概念、实现与应用

目录 一、引言 二、埃尔米特插值的基本概念 2.1 埃尔米特插值的定义 2.2 埃尔米特插值的优点 三、埃尔米特插值的实现方法 3.1 基于拉格朗日插值的埃尔米特插值 2.2 基于牛顿插值的埃尔米特插值 四、埃尔米特插值的应用 4.1 基于埃尔米特插值的函数逼近 4.2 基于埃尔…

2分钟搞懂人工智能、机器学习和深度学习

不少高校的小伙伴找我聊入门人工智能该怎么起步&#xff0c;如何快速入门&#xff0c;多长时间能成长为中高级工程师&#xff08;聊下来感觉大多数学生党就是焦虑&#xff0c;毕业即失业&#xff0c;尤其现在就业环境这么差&#xff09;&#xff0c;但聊到最后&#xff0c;很多…

java遍历集合的方法

java中&#xff0c;集合的遍历是一项基本且重要的技能。我们不仅要知道集合中元素的个数&#xff0c;还要知道元素的值&#xff0c;以及它们之间的关系。 在 Java中&#xff0c;如果需要遍历集合中的某个元素&#xff0c;可以使用以下方法&#xff1a; 1.通过 return语句将集合…

工赋开发者社区 | 装备制造企业数字化转型总体框架

导读 当前&#xff0c;面对技术、市场以及供应链等多重挑战&#xff0c;在软件定义、数据驱动、数字孪生、大数据、人工智能及元宇宙等技术加持下&#xff0c;装备制造企业不断采用新工艺、新材料&#xff0c;以新模式推动产品快速创新。企业积极关注并探索数字化转型路径&…

ThingsBoard使用docker compose集群部署

1、概述 今天我将讲解官方文档说的使用docker compose集群部署ThingsBoard,这种部署方式也是目前企业中常用的形式,希望大家能够掌握,我不是直接使用官方的镜像,我是自己拉起代码,然后自己构建镜像,在传到服务器上,使用自己的镜像来部署。而且这种部署中间有个大坑,我…