惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
Security Latest
Security Latest
NISL@THU
NISL@THU
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
Cybersecurity and Infrastructure Security Agency CISA
Cloudbric
Cloudbric
T
Threat Research - Cisco Blogs
大猫的无限游戏
大猫的无限游戏
C
CXSECURITY Database RSS Feed - CXSecurity.com
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
雷峰网
雷峰网
C
Cisco Blogs
V
Vulnerabilities – Threatpost
S
Security Archives - TechRepublic
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
J
Java Code Geeks
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Know Your Adversary
Know Your Adversary
博客园 - 叶小钗
腾讯CDC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
P
Privacy International News Feed
P
Palo Alto Networks Blog
博客园_首页
V
V2EX
WordPress大学
WordPress大学
Schneier on Security
Schneier on Security
月光博客
月光博客
博客园 - 司徒正美
Google DeepMind News
Google DeepMind News
TaoSecurity Blog
TaoSecurity Blog
博客园 - 聂微东
酷 壳 – CoolShell
酷 壳 – CoolShell
人人都是产品经理
人人都是产品经理
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
The Cloudflare Blog
罗磊的独立博客
美团技术团队
N
News | PayPal Newsroom
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Last Week in AI
Last Week in AI
K
Kaspersky official blog
Google Online Security Blog
Google Online Security Blog
S
SegmentFault 最新的问题
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tailwind CSS Blog

又见苍岚

COLMAP PatchMatch Stereo 算法详解 事件驱动的状态机框架:从理论到工程实践 Git 在国内网络环境下无法 Push 的排查与修复 —— 配置 Clash 代理 分段五次多项式插值原理详解 路径插值方法深度对比研究 Claude Code 使用指南 OpenClaw 记忆管理与技能创建指南 CBS(Conflict-Based Search)算法详解 A* 算法及其变种详解 OpenClaw 配置多 Agents Windows Powershell 无法加载文件,因为在此系统上禁止运行脚本问题的解决方案 MaxClaw 安装流程 大模型 AI 名词介绍 AList 网盘聚合工具简介 Protobuf 简介与测试 Claude Code 简介以及 GLM 4.7 模型接入 Github 歌词下载工具 163MusicLyrics Python __getattr__ 懒加载 Python TypedDict 机器人仿真平台 Gazebo 安装记录 机器人仿真平台 Gazebo 简介 多机器人路径规划问题(Multi-Agent Path Finding, MAPF)简介 Python exifread 读取修改过的 jpeg 信息错误问题修复 3D 坐标系变换的理解 3D 旋转矩阵基本概念 MongoDB Compass 介绍 Python 环境管理工具 uv Flutter 开发指南 Snipaste 安装下载与黑屏问题解决方案 全局路径规划算法记录 2025 Python 版本性能测试 Flutter Hello World Flutter 安装环境配置 Ubuntu VMware 硬盘扩容后 SMBus Host controller not enabled 报错问题解决 Python NetworkX 教程 Docker GPU 报错 - Failed to initialize NVML Unknown Error 解决方案 Python matplotlib 图表绘制 cuda-toolkit 安装替代 Cuda 与 Cudnn Jinja2 Python 利用 docxtpl 和 Jinja2 生成基于模板的 Word 文档 Docker 实现 CPU 核心隔离 LoFTR 基于 Transformer 的特征提取匹配算法 OmniGlue 特征匹配 SuperGlue 使用图神经网络学习特征匹配 Ubuntu 下将 xlsx 文件按照 sheet 转换为 图片 Python 使用 SQLAlchemy Python FastAPI 教程 openwrt 软路由配置安装 Nav2 地图文件(PGM/YAML)规范标准 3D OBJ 模型转换为 glb 瓦片格式 Python 源码 Redis 数据库介绍 Ubuntu 22.04 内核自动升级导致 MongoDB 7.0.12 错误记录 ubuntu 20.04 安装 ROS Noetic ubuntu 18.04 安装 ROS Melodic VMware Workstation Pro 个人免费版下载、安装、使用指南 Hybrid A-star 路径规划 Reeds-Shepp 曲线 Dubins 曲线 Linux kvm 虚拟机网络不通的问题解决方法 Ubuntu 自动内存清理 BiliBili 缓存视频转 mp4 Python 求解线性规划 3D Gaussian Splatting 官方源码实践记录 ImageMagick 教程 Ubuntu 22.04 安装 Colmap 对数几率 odds Ubuntu nmcli 网络管理工具使用指南 SuperPoint 自监督深度学习特征点提取 SyncTV Music Tag Web 在线音乐信息整理工具 ncm 格式转 mp3 MusicBrainz 音乐元数据百科数据库 Ubuntu 网络流量监控工具 私人云音乐平台 Navidrome 入门 手眼标定 四元数(Quaternions) OHTTPS 实现免费自动 https 证书申请、更新、部署 ubuntu 22.04 安装 CloudCompare 单机 KVM 虚拟机冷迁移 Ubuntu 22.04 使用 mdadm 实现软 raid 小鱼 一键安装 ROS-humble Fluid -46- 基于 Simpletex API 构建公式识别页面 公式识别 API 简介 -- Simpletex 使用 Python web 部署库 waitress 3D Gaussian Splatting for Real-Time Radiance Field Rendering Ubuntu Swap 简介与空间扩展 Ubuntu 24.04 安装 forticlient Clash Verge 使用 MongoDB 7.0.17 集群 Docker 构建源码 Error code - 2013. Lost connection to MySQL server during query 问题解决 Python 日志记录库 loguru 使用指北 Python 实现 Web 日志查看服务 MySQL LOAD DATA LOCAL INFILE 极速数据加载 Image size exceeds limit of 89478485 pixels 解决方案 Docker 使用 NVIDIA GPU 驱动错误解决 阿里云 docker 镜像仓库 Ubuntu中没有wired connected的解决方案 MinIO 简介 subconverter 代理订阅格式转换 修复 node –openssl-legacy-provider is not allowed in NODE_OPTIONS 错误
BF、FLANN 特征匹配原理与 OpenCV 实现
Yiwei Zhang · 2024-07-09 · via 又见苍岚

在使用特征提取算法提取了描述子后,需要对不同图像之间的特征进行匹配,本文记录 FLANN 以及相关内容。

简介

一般我们用 SIFT、SURF或者 ORB 方法提取到图像关键点的特征描述子之后,需要进行匹配(不然提取了干嘛的呢)。

匹配的方法最直观来看是使用欧式距离,两两相互匹配,对距离排序,也就是 蛮力(Brute-Force)匹配。

Brute-Force 匹配

蛮力匹配器很简单。首先在第一幅图像中选取一个关键点然后依次与第二幅图像的每个关键点进行(描述符)距离测试,最后返回距离最近的关键点。

OpenCV 实现

1
cv.BFMatcher([normType, crossCheck])
  • normType

    对于BF匹配器,首先我们必须使用 cv.BFMatcher() 创建一个BFMatcher对象。它需要两个可选的参数,第一个是 normType,它指定要使用的距离测量,默认情况下,它是 cv.NORM_L2,它适用于 SIFT,SURF 等( cv.NORM_L1也在那里)。对于基于二进制字符串的描述符,如 ORB,BRIEF,BRISK 等,应使用cv.NORM_HAMMING,它使用汉明距离作为度量。如果 ORB 使用 WTA_K==3或4,则应使用 cv.NORM_HAMMING2

  • crossCheck

    第二个参数是布尔变量crossCheck,默认为 false。如果为真,则 Matcher 仅返回具有值 $(i,j)$ 的那些匹配,使得集合 A 中的第 $i$ 个描述符具有集合 B 中的第$j$ 个描述符作为最佳匹配,反之亦然。也就是说,两组中的两个特征应该相互匹配。它提供了一致的结果,是 D.Lowe 在 SIFT 论文中提出的比率测试的一个很好的替代方案。

一旦创建,两个重要的方法是 BFMatcher.match()BFMatcher.knnMatch()。第一个返回最佳匹配。第二种方法返回k个最佳匹配,其中k由用户指定。当我们需要做更多的工作时,它可能是有用的。

就像我们使用 cv.drawKeypoints() 来绘制关键点一样,cv.drawMatches() 帮助我们绘制匹配项。它水平堆叠两个图像,并从第一个图像到第二个图像绘制线条,显示最佳匹配。还有 cv.drawMatchesKnn,它绘制了所有k个最佳匹配。如果 $k = 2$,它将为每个关键点绘制两条匹配线。因此,如果我们想要有选择地绘制它,我们必须传递一个 mask。

DMatch 对象

BFMatcher.match() 方法返回的是 DMatch 对象的列表,此DMatch对象具有以下属性:

  • DMatch.distance - 描述符之间的距离。越低越好。
  • DMatch.trainIdx - 训练描述符中描述符的索引
  • DMatch.queryIdx - 查询描述符中描述符的索引
  • DMatch.imgIdx - 训练图像的索引。

ORB 特征 BF 匹配示例

在这里,我们将看到一个关于如何匹配两个图像之间的特征的简单示例。在这种情况下,我有一个查询图像和一个目标图像。我们将尝试使用特征匹配在目标图像中查找查询图像。(图片为OpenCV 示例图片中的 /samples/c/box.png/samples/c/box_in_scene.png

我们使用ORB描述符来匹配功能。所以让我们从加载图像,查找描述符等开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt

img1 = cv.imread('box.png',0) # queryImage
img2 = cv.imread('box_in_scene.png',0) # trainImage

# Initiate ORB detector
orb = cv.ORB_create()

# find the keypoints and descriptors with ORB
kp1, des1 = orb.detectAndCompute(img1,None)
kp2, des2 = orb.detectAndCompute(img2,None)

接下来,我们使用距离测量cv.NORM_HAMMING创建一个BFMatcher对象(因为我们使用的是ORB),并且启用了crossCheck以获得更好的结果。然后我们使用Matcher.match()方法在两个图像中获得最佳匹配。我们按照距离的升序对它们进行排序,以便最佳匹配(低距离)出现在前面。然后我们只绘制前10场比赛(太多了看不清,如果愿意的话你可以多画几条)

1
2
3
4
5
6
7
8
9
10
11
12
13
# create BFMatcher object
bf = cv.BFMatcher(cv.NORM_HAMMING, crossCheck=True)

# Match descriptors.
matches = bf.match(des1,des2)

# Sort them in the order of their distance.
matches = sorted(matches, key = lambda x:x.distance)

# Draw first 10 matches.
img3 = cv.drawMatches(img1,kp1,img2,kp2,matches[:10], flags=2)

plt.imshow(img3),plt.show()

结果如下图所示:

SIFT 特征 BF 匹配示例

这一次,我们将使用BFMatcher.knnMatch()来获得最佳匹配。在这个例子中,我们将采用k = 2,以便我们可以在他的论文中应用D.Lowe解释的比率测试。

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
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

img1 = cv.imread('box.png',0) # queryImage
img2 = cv.imread('box_in_scene.png',0) # trainImage

# Initiate SIFT detector
sift = cv.SIFT()

# find the keypoints and descriptors with SIFT
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)

# BFMatcher with default params
bf = cv.BFMatcher()
matches = bf.knnMatch(des1,des2, k=2)

# Apply ratio test
good = []
for m,n in matches:
if m.distance < 0.75*n.distance:
good.append([m])

# cv.drawMatchesKnn expects list of lists as matches.
img3 = cv.drawMatchesKnn(img1,kp1,img2,kp2,good,flags=2)

plt.imshow(img3),plt.show()

结果如下图所示:

FLANN匹配

FLANN 是快速最近邻搜索包(Fast_Library_for_Approximate_Nearest_Neighbors)的简称。它是一个对大数据集和高维特征进行最近邻搜索的算法的集合,而且这些算法都已经被优化过了。在面对大数据集时它的效果要好于 BFMatcher。

算法的选择

随机k-d树算法 (The Randomized k-d TreeAlgorithm)
  • Classick-d tree

    找出数据集中方差最高的维度,利用这个维度的数值将数据划分为两个部分,对每个子集重复相同的过程。

  • Randomizedk-d tree

    建立多棵随机k-d树,从具有最高方差的N_d维中随机选取若干维度,用来做划分。在对随机k-d森林进行搜索时候,所有的随机k-d树将共享一个优先队列。

    增加树的数量能加快搜索速度,但由于内存负载的问题,树的数量只能控制在一定范围内,比如20,如果超过一定范围,那么搜索速度不会增加甚至会减慢

优先搜索k-means树算法(The Priority Search K-MeansTree Algorithm)

随机k-d森林在许多情形下都很有效,但是对于需要高精度的情形,优先搜索k-means树更加有效。 K-means tree 利用了数据固有的结构信息,它根据数据的所有维度进行聚类,而随机k-d tree一次只利用了一个维度进行划分。

算法描述

步骤1 建立优先搜索k-means tree:

  1. 建立一个层次化的 k-means 树;
  2. 每个层次的聚类中心,作为树的节点;
  3. 当某个cluster内的点数量小于K时,那么这些数据节点将做为叶子节点。

步骤2 在优先搜索k-means tree中进行搜索:

(1) 从根节点N开始检索;

(2) 如果是N叶子节点则将同层次的叶子节点都加入到搜索结果中,count += |N|;

(3) 如果N不是叶子节点,则将它的子节点与query Q比较,找出最近的那个节点 Cq,同层次的其他节点加入到优先队列中;

(4) 对Cq节点进行递归搜索;

(5) 如果优先队列不为空且 count<L,那么从取优先队列的第一个元素赋值给N,然后重复步骤(1)。

聚类的个数K,也称为branching factor 是个非常主要的参数。

建树的时间复杂度 = $O( ndKI ( log(n)/log(K) )) $

  • n为数据点的总个数

  • I 为K-means的迭代次数。

搜索的时间复杂度 = $O( L/K * Kd * ( log(n)/(log(K) ) ) = O(Ld ( log(n)/(log(K) ) )$。

层次聚类树 (The Hierarchical ClusteringTree)

层次聚类树采用 k-medoids 的聚类方法,而不是 k-means。即它的聚类中心总是输入数据的某个点,但是在本算法中,并没有像 k-medoids 聚类算法那样去最小化方差求聚类中心,而是直接从输入数据中随机选取聚类中心点,这样的方法在建立树时更加简单有效,同时又保持多棵树之间的独立性。

同时建立多棵树,在搜索阶段并行地搜索它们能大大提高搜索性能(归功于随机地选择聚类中心,而不需要多次迭代去获得更好的聚类中心)。建立多棵随机树的方法对k-d tree也十分有效,但对于k-means tree却不适用。

遍历次数

它用来指定递归遍历的次数。值越高结果越准确,但是消耗的时间也越多。如果想修改这个值,可以传入参数。

OpenCV Flann

用例一

对于基于FLANN的匹配器,我们需要传递两个字典,指定要使用的算法和其他相关参数等。首先是IndexParams。对于各种算法,要传递的信息在FLANN文档中进行了解。总而言之,对于像SIFT,SURF等算法,你可以传递以下内容:

1
2
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)

但使用 ORB 时,我们要传入的参数如下。注释掉的值是文献中推荐使用的,但是它们并不适合所有情况,其他值的效果可能会更好。

1
2
3
4
5
FLANN_INDEX_LSH = 6
index_params= dict(algorithm = FLANN_INDEX_LSH,
table_number = 6, # 12
key_size = 12, # 20
multi_probe_level = 1) #2

第二个字典是SearchParams。它指定应递归遍历索引中的树的次数。值越高,精度越高,但也需要更多时间。如果要更改该值,请传递search_params = dict(checks = 100)。

有了这些信息,我们就可以开始工作了。

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
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

img1 = cv.imread('box.png',0) # queryImage
img2 = cv.imread('box_in_scene.png',0) # trainImage

# Initiate SIFT detector
sift = cv.SIFT()

# find the keypoints and descriptors with SIFT
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)

# FLANN parameters
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks=50) # or pass empty dictionary

flann = cv.FlannBasedMatcher(index_params,search_params)

matches = flann.knnMatch(des1,des2,k=2)

# Need to draw only good matches, so create a mask
matchesMask = [[0,0] for i in xrange(len(matches))]

# ratio test as per Lowe's paper
for i,(m,n) in enumerate(matches):
if m.distance < 0.7*n.distance:
matchesMask[i]=[1,0]

draw_params = dict(matchColor = (0,255,0),
singlePointColor = (255,0,0),
matchesMask = matchesMask,
flags = 0)

img3 = cv.drawMatchesKnn(img1,kp1,img2,kp2,matches,None,**draw_params)

plt.imshow(img3,),plt.show()

结果如下图所示:

用例二

图像1(原始图像)

图像2(缩放、旋转、错切)

尝试在图像中找到SIFT特征并应用比率测试来找到最佳匹配。

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
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

MIN_MATCH_COUNT = 10

img1 = cv.imread('test.jpg',0) # queryImage
img2 = cv.imread('test2.jpg',0) # trainImage

# Initiate SIFT detector
sift = cv.xfeatures2d.SIFT_create()

# find the keypoints and descriptors with SIFT
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)

FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm = FLANN_INDEX_KDTREE, trees = 5)
search_params = dict(checks = 50)

flann = cv.FlannBasedMatcher(index_params, search_params)
matches = flann.knnMatch(des1,des2,k=2)

# store all the good matches as per Lowe's ratio test.
good = []
for m,n in matches:
if m.distance < 0.7*n.distance and m.distance < 60:
good.append(m)

if len(good)>MIN_MATCH_COUNT:
src_pts = np.float32([ kp1[m.queryIdx].pt for m in good ]).reshape(-1,1,2)
dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good ]).reshape(-1,1,2)

M, mask = cv.findHomography(src_pts, dst_pts, cv.RANSAC,5.0)
matchesMask = mask.ravel().tolist()

h,w = img1.shape
pts = np.float32([ [0,0],[0,h-1],[w-1,h-1],[w-1,0] ]).reshape(-1,1,2)
dst = cv.perspectiveTransform(pts,M)

img2 = cv.polylines(img2,[np.int32(dst)],True,0,3, cv.LINE_AA)
else:
print( "Not enough matches are found - {}/{}".format(len(good), MIN_MATCH_COUNT) )
matchesMask = None

draw_params = dict(matchColor = (0,255,0), # draw matches in green color
singlePointColor = None,
matchesMask = matchesMask, # draw only inliers
flags = 2)

img3 = cv.drawMatches(img1,kp1,img2,kp2,good,None,**draw_params)

plt.imshow(img3, 'gray'),plt.show()

右图的黑色边界为左图变换后的结果

参考资料

文章链接:
https://www.zywvvd.com/notes/study/image-processing/feature-match/flann-match/