基于SIFT图像特征识别的匹配方法比较与实现

news2025/3/3 4:27:19

基于SIFT图像特征识别的匹配方法比较与实现

1 匹配器选择

目前常用的匹配器有 BFMatcher and FlannBasedMatcher

1.1 BFMatcher

BFMatcher 全称是 Brute-Force Matcher(直译即为暴力匹配器)

大致原理:

对于 img1 中的每个描述符, BFMatcher 将其与 img2 中的所有描述符进行比较;它计算两个描述符之间的距离度量(例如,欧几里得距离或汉明距离,默认使用欧几里得距离)并跟踪最接近的匹配,具有最小距离的描述符对被认为是潜在的匹配

用法示例:

创建一个 Brute-Force 匹配器,并使用匹配器对两组描述子进行匹配

// 创建匹配器对象
BFMatcher matcher;
// 使用 K 最近邻匹配方法进行特征匹配
vector<vector<DMatch>> matches;
matcher.knnMatch(descriptors1, descriptors2, matches, 2);
总结结论:

BFMatcher 将尝试所有可能性,这种匹配算法非常慢,匹配所需的时间随着添加的特征数量线性增加,这导致更高的计算成本,所以尤其是对于大型数据集BFMatcher 是一种简单但不一定是最有效的匹配方法

1.2 Flann

Flann 全称是 Fast Library for Approximate Nearest Neighbors(直译即为近似近邻快速库)

大致原理:

FLANN 旨在快速查找近似最近邻,尤其是在高维空间中,它不是详尽地搜索所有数据点,而是使用各种技术来更有效地执行搜索,同时提供相当准确的结果;FLANN 会构建了一个高效的数据结构,用于搜索近似邻居,FLANN 比 BFMatcher 快得多,但它只能找到近似的最近邻,这是一个很好的匹配,但不一定是最好的,可以调整 FLANN 的参数以提高精度,但这将以减慢算法速度为代价(鱼和熊掌不可兼得)

用法示例:

创建一个 FLANN 匹配器,并使用匹配器对两组描述子进行匹配

// 创建匹配器对象
FlannBasedMatcher matcher;
// 使用 K 最近邻匹配方法进行特征匹配
vector<vector<DMatch>> matches;
matcher.knnMatch(descriptors1, descriptors2, matches, 2);
总结结论:

FLANN 在牺牲一些精度的情况下,提供了更快的搜索速度,特别是在高维空间或大型数据集中

1.3 比较总结

  1. 准确度
    • BF 匹配器:BF 匹配提供精确的最近邻搜索,这意味着它可以高精度地找到最接近的匹配项;它适用于精度至关重要的任务,例如某些图像识别或对象跟踪任务
    • FLANN:FLANN 执行近似最近邻搜索,这意味着它会找到可能不是精确最近邻的近似匹配。虽然 FLANN 旨在提供良好的结果,但与 BF 匹配相比,速度的权衡可能会导致结果的准确性稍差
  2. 速度
    • BF 匹配器:BF 匹配简单明了,但计算成本可能很高,尤其是在高维空间或处理大型数据集时;它涉及将一个描述符与所有其他描述符进行比较,导致匹配 N 个关键点的时间复杂度为 O(N^2)
    • FLANN 专为高效的最近邻搜索而设计,特别是在高维空间中。它在速度方面通常优于 BF 匹配;FLANN的算法(KD-Tree、K-Means Tree、LSH等)的选择和参数调整可以进一步优化搜索速度

测试:

SIFT+FLANN+比率测试筛选ratio=0.6SIFT+BF+比率测试筛选ratio=0.6
效果img_matches(FLANN+knn0.6)0.338931秒img_matches(BF+knn0.6)0.494063秒
匹配+筛选用时0.338931秒0.494063秒

可以看出,两张图片效果基本相同,都有着极高的质量,但是在时间上,FLANN 比较与 BF 快了很多,效率更高

在许多计算机视觉和机器学习应用中,FLANN 是首选,因为它在速度和准确性之间取得了良好的平衡,使其适用于广泛的任务

所以我们选择 FLANN 作为匹配器

2 筛选方法

对于用不同匹配器得出的结果都会出现很多错误匹配,例如下图直接使用BF匹配器进行匹配:

img_matches(BF不筛选)

可以看到有数不清的点都进行了匹配,这样的效果非常差,这个时候筛选方法就显得十分重要了,利用好的筛选方法可以帮助我们最大程度上提升图片特征匹配的质量

2.1 距离阈值筛选

距离阈值筛选的原理很简单:它假设距离较近的特征点之间更可能是正确的匹配,而距离较远的特征点之间更可能是错误的匹配

  1. 用BF匹配器找出所有的匹配对

    BFMatcher matcher;
    vector<DMatch>matches;
    matcher.match(descriptors1, descriptors2, matches);
    
  2. 先通过遍历所有的匹配对,找的匹配中的最小距离

    double minDist = 1000;
    for (int i = 0; i < descriptors1.rows; i++)
    {
        double dist = matches[i].distance;
        if (dist < minDist)
        {
            minDist = dist;
        }
    }
    
  3. 再次遍历所有的匹配对,筛选出距离在距离阈值内的匹配

    其中 Multiples 是可设置的倍数,我们用最小距离的 Multiples 倍来当作距离阈值

    vector<DMatch>good_matches;
    for (int i = 0; i < descriptors1.rows; i++)
    {
        double dist = matches[i].distance;
        const float Multiples = 2.5; // 倍数参数
        if (dist < Multiples * minDist)
        {
            good_matches.push_back(matches[i]);
        }
    }
    

测试:SIFT+BF+距离阈值筛选

倍数Multiples结果图片
Multiples = 2.5img_matches(BF+2.5minDist)1
Multiples = 2.75img_matches(BF+2.75minDist)1
Multiples = 3.0img_matches(BF+3.0minDist)
Multiples = 3.25img_matches(BF+3.25minDist)1

2.2 比率测试筛选 Ratio test

比例测试的基本思想是比较最近邻匹配次近邻匹配的距离,并根据它们之间的距离比例来决定是否保留最近邻匹配

基本原理:距离最小的匹配是最近邻匹配,距离第二小的匹配相当于随机噪音,如果最近邻匹配无法与噪音区分开来,那么最近邻匹配就应该被剔除,因为它与噪音一样,没有带来任何有价值的信息

  1. 用BF匹配器找出所有的匹配对

    vector<vector<DMatch>> matches;
    BFMatcher matcher;
    matcher.knnMatch(descriptors1, descriptors2, matches, 2);
    

    注意这里使用 knnMatch 函数,matches 也需要设置为二维数组

    其返回每个查询描述子的前 K 个最佳匹配;K 被设置为 2,即返回每个查询描述子的两个最佳匹配:其中 matches[i][0] 表示第一个最佳匹配(最近的邻居),matches[i][1] 表示第二个最佳匹配(次相似的特征点)

  2. 设置比例参数 ratio,通常设置为 0.5 左右,筛选出最佳匹配与第二最佳匹配有明显区别的,从而丢弃我们不明确的匹配并保留好的匹配

    vector<DMatch> good_matches;
    for (int i = 0; i < matches.size(); ++i)
    {
    	const float ratio = 0.5; // 比例参数
    	if (matches[i][0].distance < ratio * matches[i][1].distance)
    	{
    		good_matches.push_back(matches[i][0]);
    	}
    }
    

测试:SIFT+BF+比率测试筛选

比例ratio结果图片
ratio = 0.4img_matches(BF+knn0.4)1
ratio = 0.5img_matches(BF+knn0.5)1
ratio = 0.6
ratio = 0.7img_matches(BF+knn0.7)1

2.3 比较总结

通过两组测试进行对比,在匹配对数量近似相等的情况下,明显比率测试筛选有着明显的优势,Multiples = 3.25 下已经出现了许多的错误,而 ratio = 0.6 在匹配对数量略大的情况下,保持较高的准确率

ratio = 0.6Multiples = 3.25
img_matches(BF+3.25minDist)1

所以最终我们选择比率测试筛选的筛选方法

3 特征点匹配实现(SIFT)

步骤1:导入必要的库和头文件

首先,导入必要的OpenCV库和头文件,设置图片路径:

#include<opencv2/opencv.hpp>
#include<iostream>
 
#define IMG_PATH1 "test_img\\1\\B23.jpg"
#define IMG_PATH2 "test_img\\1\\B24.jpg"
#define SAVE_PATH "test_img\\1\\img_matches.jpg"

using namespace std;
using namespace cv;

步骤2:加载图像

加载两幅要进行特征点匹配的图像。确保图像文件存在,并将它们放在项目目录下,或者根据您的文件路径进行适当的更改。

Mat img1 = imread(IMG_PATH1, IMREAD_GRAYSCALE);
Mat img2 = imread(IMG_PATH2, IMREAD_GRAYSCALE);
if (img1.empty() || img2.empty())
{
    cout << "Can't read image" << endl;
    return -1;
}

步骤3:创建SIFT检测器

创建SIFT检测器对象,可以选择不同的参数,例如最大特征点数量、尺度等。在本例中,我们使用默认参数。

Ptr<SIFT> sift = SIFT::create();

Ptr 是 OpenCV 提供的智能指针类,用于管理动态分配的对象,帮助防止内存泄漏和减少手动内存管理的负担

Ptr 主要用于管理 OpenCV 中的各种对象,例如图像、特征检测器、匹配器等;它可以自动跟踪和管理对象的引用计数,当对象不再需要时,会自动释放对象的内存,以确保资源被正确释放

步骤4:检测关键点和计算描述子

使用SIFT检测器在两幅图像中检测关键点,并计算它们的描述子。

vector<KeyPoint> keypoints1, keypoints2;
Mat descriptors1, descriptors2;

sift->detectAndCompute(img1, noArray(), keypoints1, descriptors1);
sift->detectAndCompute(img2, noArray(), keypoints2, descriptors2);

KeyPoint 是 OpenCV 中用于表示图像中关键点的类,关键点通常用于描述图像中的特征,例如角点、边缘、斑点等;KeyPoint 包含了关键点的位置、尺度、方向和响应等信息,它是计算机视觉中常用的数据结构之一

KeyPoint 类的主要成员包括:

  1. pt:一个 Point2f 类型的成员,表示关键点在图像中的二维坐标位置(x,y)
  2. size:一个浮点数,表示关键点的尺度,尺度通常表示关键点的特征区域大小
  3. angle:一个浮点数,表示关键点的方向,通常用于指示关键点的主要方向
  4. response:一个浮点数,表示关键点的响应值,响应值通常用于评估关键点的质量
  5. octave:一个整数,表示关键点所在的金字塔层级(octave),这是与尺度空间相关的信息
  6. class_id:一个整数,可用于标识关键点所属的类别或其他信息

detectAndCompute 是 OpenCV 中常用的函数,通常与特征检测和描述子计算相关。这个函数结合了特征检测和描述子计算两个步骤,用于从图像中检测关键点并计算关键点的描述子;它的主要作用是在一次操作中完成这两个关键的计算步骤,以提高计算效率和代码简洁性

其中第二参数是 mask 掩膜,抠出指定区域,我们先不用设置为空参数noArray() 或者 Mat()

descriptors 描述子通常是一个二维矩阵(或多维矩阵),是一个 Mat 对象,其中每一行代表一个关键点的描述子,描述子的维度取决于特征检测算法和配置参数,在 SIFT 中,描述子通常是128维的向量

步骤5:特征点匹配

创建一个Brute-Force匹配器,并使用匹配器对两组描述子进行匹配。

// 创建匹配器对象
FlannBasedMatcher matcher;
// 使用 K 最近邻匹配方法进行特征匹配
vector<vector<DMatch>> matches;
matcher.knnMatch(descriptors1, descriptors2, matches, 2);

DMatch 是 OpenCV 中用于存储特征匹配信息的结构体,它包含了两个特征点之间(一对)的匹配信息,包括以下成员:

  1. queryIdx:特征描述子在查询图像中的索引,这个索引对应于查询图像中的一个特征点
  2. trainIdx:特征描述子在训练图像中的索引,这个索引对应于训练图像中的一个特征点
  3. distance:描述了两个特征描述子之间的距离或相似性度量,通常,距离越小,表示两个特征点越相似

步骤6:筛选匹配点

筛选匹配点以获取最佳匹配,可以使用阈值来过滤掉不好的匹配

// 存储较好的匹配
vector<DMatch> good_matches;
for (int i = 0; i < matches.size(); ++i) {
    const float ratio = 0.5; // 比例参数
    // 仅保留比例参数内的较好匹配
    if (matches[i][0].distance < ratio * matches[i][1].distance) {
        good_matches.push_back(matches[i][0]);
    }
}

步骤7:绘制匹配结果

绘制匹配结果,将特征点匹配可视化。

// 绘制好的匹配结果并保存
Mat img_matches;
drawMatches(img1, keypoints1, img2, keypoints2, good_matches, img_matches);
imwrite("test_img\\img_matches.jpg", img_matches);

// 显示匹配结果的窗口
namedWindow("matches", WINDOW_NORMAL);
imshow("matches", img_matches);

// 等待用户按下任意键以关闭窗口
waitKey(0);

完整测试代码:

#include<opencv2/opencv.hpp>
#include<iostream>
#include <chrono> // 用于测试耗时
#define IMG_PATH1 "test_img\\1\\B23.jpg"
#define IMG_PATH2 "test_img\\1\\B24.jpg"
#define SAVE_PATH "test_img\\1\\img_matches.jpg"

using namespace std;
using namespace cv;
using namespace chrono; // 用于测试耗时

int main()
{
	Mat img1 = imread(IMG_PATH1, IMREAD_GRAYSCALE);
	Mat img2 = imread(IMG_PATH2, IMREAD_GRAYSCALE);
	if (img1.empty() || img2.empty())
	{
		cout << "Can't read image" << endl;
		return -1;
	}

	Ptr<SIFT> sift = SIFT::create();
	vector<KeyPoint> keypoints1, keypoints2;
	Mat descriptors1, descriptors2;

	sift->detectAndCompute(img1, noArray(), keypoints1, descriptors1);
	sift->detectAndCompute(img2, noArray(), keypoints2, descriptors2);
	
	//auto start = system_clock::now();	
	
	//--------------
	// BF+knn
	//--------------
	//vector<vector<DMatch>> matches;
	//cv::BFMatcher matcher;
	//matcher.knnMatch(descriptors1, descriptors2, matches, 2);
	//vector<DMatch> good_matches;
	//for (int i = 0; i < matches.size(); ++i)
	//{
	//	const float ratio = 0.6; // 比例参数
	//	if (matches[i][0].distance < ratio * matches[i][1].distance)
	//	{
	//		good_matches.push_back(matches[i][0]);
	//	}
	//}


	//--------------
	// FLANN+knn
	//--------------
	FlannBasedMatcher matcher;
	vector<vector<DMatch>> matches;
	matcher.knnMatch(descriptors1, descriptors2, matches, 2);
	vector<cv::DMatch> good_matches;
	for (int i = 0; i < matches.size(); ++i)
	{
		const float ratio = 0.8; // 比例参数
		if (matches[i][0].distance < ratio * matches[i][1].distance)
		{
			good_matches.push_back(matches[i][0]);
		}
	}
	
	//--------------
	// BF + 距离阈值筛选
	//--------------
	//BFMatcher matcher;
	//vector<DMatch>matches;
	//matcher.match(descriptors1, descriptors2, matches);
	//
	//double minDist = 1000;
	//for (int i = 0; i < descriptors1.rows; i++)
	//{
	//	double dist = matches[i].distance;

	//	if (dist < minDist)
	//	{
	//		minDist = dist;
	//	}
	//}

	//vector<DMatch>good_matches;
	//for (int i = 0; i < descriptors1.rows; i++)
	//{
	//	double dist = matches[i].distance;
	//	const float Multiples = 2.5; // 倍数参数
	//	if (dist < Multiples * minDist)
	//	{
	//		good_matches.push_back(matches[i]);
	//	}
	//}
    
    
	//auto end = system_clock::now();
	//auto duration = duration_cast<microseconds>(end - start);
	//cout << "花费了"
	//	 << double(duration.count()) * microseconds::period::num / microseconds::period::den
	//	 << "秒" << endl;
    
	Mat img_matches;
	drawMatches(img1, keypoints1, img2, keypoints2, good_matches, img_matches);

	imwrite(SAVE_PATH, img_matches);
	namedWindow("matches", WINDOW_NORMAL);
	imshow("matches", img_matches);
	waitKey(0);
}

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

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

相关文章

LVS DR模式负载均衡群集部署

目录 1 LVS-DR 模式的特点 1.1 数据包流向分析 1.2 DR 模式的特点 2 DR模式 LVS负载均衡群集部署 2.1 配置负载调度器 2.1.1 配置虚拟 IP 地址 2.1.2 调整 proc 响应参数 2.1.3 配置负载分配策略 2.2 部署共享存储 2.3 配置节点服务器 2.3.1 配置虚拟 IP 地址 2.3.2…

初学unity开发学习笔记----第一天

以下是学习unity知识的心得&#xff0c;类似备忘录&#xff0c;肯定是存在有漏洞的地方或者专业名词使用不恰当的地方。。。 目标&#xff1a;编写小球wasd移动的效果 1.下载unity hub和unity引擎: (1).前往官网:Unity实时内容开发平台 -实时3D引擎、2D、VR&AR可视化数据…

【新版】系统架构设计师 - 软件架构设计<SOA与微服务>

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 架构 - 软件架构设计&#xff1c;SOA与微服务&#xff1e; 考点摘要 面向服务SOA&#xff08;★★★★&#xff09;微服务&#xff08;★★★★&#xff09; 基于/面向服务的&#xff08;SOA&#xff09; 在SO…

【新版】系统架构设计师 - 软件架构设计<新版>

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 文章目录 架构 - 软件架构设计&#xff1c;新版&#xff1e;考点摘要概念架构的 4 1 视图架构描述语言ADL基于架构的软件开发方法ABSDABSD的开发模型ABSDMABSD&#xff08;ABSDM模型&#xff09;的开发过程 软件架…

C++ 多线程(future篇)

引言 在前面介绍了启动线程&#xff0c;以及多线程下如何保证共享资源的竞争访问、线程同步这些。但是thread类无法访问从线程直接返回的值&#xff0c;如果要想获取线程的的执行结果&#xff0c;一般都是依靠全局或static变量&#xff0c;或是以实参传递的变量&#xff0c;然后…

C# 辗转相除法求最大公约数

辗转相除法求最大公约数 public static void CalcGCD(int largeNumber, int smallNumber, out int GCD){GCD 1;int remain -1;while (remain ! 0){remain largeNumber % smallNumber;GCD smallNumber;largeNumber smallNumber;smallNumber remain;}}

华为云云耀云服务器L实例评测|华为云耀云L搭建zerotier服务测试

0. 环境 - Win10 - 云耀云L服务器 1. 安装docker 检查yum源&#xff0c;本EulerOS的源在这里&#xff1a; cd /etc/yum.repos.d 更新源 yum makecache 安装 yum install -y docker-engine 运行测试 docker run hello-world 2. 运行docker镜像 默认配…

计算机专业毕业设计项目推荐03-Wiki系统设计与实现(JavaSpring+Vue+Mysql)

Wiki系统设计与实现&#xff08;JavaSpringVueMysql&#xff09; **介绍****系统总体开发情况-功能模块****各部分模块实现** 介绍 本系列(后期可能博主会统一为专栏)博文献给即将毕业的计算机专业同学们,因为博主自身本科和硕士也是科班出生,所以也比较了解计算机专业的毕业设…

大秒杀系统设计

参考链接&#xff1a;http://www.taodudu.cc/news/show-5770725.html?actiononClick 1. 一些数据 大家还记得2013年的小米秒杀吗&#xff1f;三款小米手机各11万台开卖&#xff0c;走的都是大秒系统&#xff0c;3分钟后成为双十一第一家也是最快破亿的旗舰店。 经过日志统计…

[超硬核] 5000字带走读DuckDB优化器之常量折叠与比较简化

DuckDB优化器之常量折叠与比较简化 本篇文章适合学习C的小伙伴&#xff0c;适合阅读开源项目的小伙伴&#xff0c;更适合学习数据库的小伙伴&#xff0c;欢迎与我一起探索优化器知识。 目录 DuckDB优化器之常量折叠与比较简化1.优化器规则2.表达式重写 2.1 重写/访问算子2.2 应…

makefile之目标文件生成

目标文件:源码经过编译还没有链接那些中间文件.linux .o文件 gcc $(CFLAGS) -c xxx.c -o xx.o include Makefile.config SRC : $(wildcard *.c wildcard ./audio_module/*.c) SRC_OBJ $(patsubst %.c,%.o,$(SRC))all:$(SRC_OBJ) $(info contents $(SRC))$(info objfiles $(SR…

Tomcat部署与调优

一、Tomcat概述&#xff1a; Tomcat服务器是一个免费的开放源代码的Web应用服务器&#xff0c;属于轻量级应用服务器&#xff0c;在中小型系统和并发访问用户不是很多的场合下被普遍使用&#xff0c;具有处理HTML页面的功能&#xff0c;然而由于其处理静态HTML的能力远不及Apac…

81 # 多语言

多语言实现方案 1、一个完整多个路径来实现多语言 2、前端来实现多语言&#xff08;先配置好两种语言&#xff0c;动态切换内容&#xff09;&#xff0c;比如 i18n&#xff0c;vue-i18n 3、服务端的 header 来实现切换多语言 accept-language: zh-CN,zh;q0.9 const fs req…

台式万用表几位的概念以及NPLC的功能作用

数字万用表测量电流和电压的基本原理是通过检测传感器的电阻&#xff0c;电容&#xff0c;或电感等特性&#xff0c;将电流或电压转化为可以测量的电信号&#xff0c;然后这个电信号被转化为数字信号进行处理和显示。具体的&#xff0c;当测量电压时&#xff0c;万用表的输入端…

【SpringMVC】Jrebel 插件实现热部署与文件上传

目录 一、JRebel 1.1 Jrebel介绍 1.2 Jrebel插件下载 1.3 Jrebel服务下载并启动 1.4 在线生成GUID 1.5 JRebel激活 1.6 相关设置 注意❗ 二、文件上传、下载 2.1 导入pom依赖 2.2 配置文件上传解析器 2.3 文件上传表单设置 2.4 文件上传实现 2.5 文件下载实现 2…

[源码系列:手写spring] IOC第十四节:容器事件和事件监听器

代码分支 https://github.com/yihuiaa/little-spring/tree/event-and-event-listenerhttps://github.com/yihuiaa/little-spring/tree/event-and-event-listener 内容介绍 事件监听器机制 Spring的容器事件和事件监听器机制允许应用程序在容器中发生特定事件时执行自定义逻辑…

Hadoop的HDFS的集群安装部署

注意&#xff1a;主机名不要有/_等特殊的字符&#xff0c;不然后面会出问题。有问题可以看看第5点&#xff08;问题&#xff09;。 1、下载 1.1、去官网&#xff0c;点下载 下载地址&#xff1a;https://hadoop.apache.org/ 1.2、选择下载的版本 1.2.1、最新版 1.2.2、其…

SQL数据库查询超时,查询数据库的哪些表被上锁的语句

1.异常提示 2.表语句 2.1 查询锁表的语句 select request_session_id spid,OBJECT_NAME(resource_associated_entity_id) tableName from sys.dm_tran_locks where resource_typeOBJECT * 若是下面没有显示内容&#xff0c;说明当前没有锁住的表 2.2若是有显示锁住的表&#…

【实践篇】Redis最强Java客户端(一)之Redisson入门介绍

Redisson入门介绍 文章目录 Redisson入门介绍1.1 Redisson简介1.1.1 起源和历史1.1.2 优势和特点1.1.3 与其他Java Redis客户端的比较 1.2 使用和配置1.2.1 依赖和SDK1.2.2 配置文件解析1.2.3 连接池配置 1.3 优雅的让Hash的某个Field过期2. 参考资料3. 源码地址4. Redis从入门…

9. xaml ComboBox控件

1.运行图像 2.运行源码 a.Xaml源码 <Grid Name="Grid1"><!--IsDropDownOpen="True" 默认就是打开的--><ComboBox x:Name="co