首页
/ OpenCV 特征匹配实战:BFMatcher 暴力匹配、SIFT Ratio Test 与 FLANN 加速匹配

OpenCV 特征匹配实战:BFMatcher 暴力匹配、SIFT Ratio Test 与 FLANN 加速匹配

2026-09-06 12:16:33作者:滕妙奇

本篇技术指南基于 OpenCV 官方 Python 教程《Feature Matching》(教程原文),讲解如何在两张图像之间进行特征匹配:先创建 query/train 图像对的描述子,再用 Brute-Force Matcher(cv.BFMatcher)与 FLANN Matcher(cv.FlannBasedMatcher)完成匹配,并用 cv.drawMatches / cv.drawMatchesKnn 可视化结果。读完后,你将掌握 normType 与 crossCheck 两大核心参数的选型规则、Lowe's Ratio Test 的代码落地方式,以及 FLANN 索引参数(IndexParams/SearchParams)的调优方法,并能直接复制运行教程中的三段完整示例。

特征匹配的目标:把 queryImage 在 trainImage 中找出来

OpenCV 的特征匹配流程遵循统一的三步范式:

  1. 在两张图像上分别检测关键点并计算描述子(detectAndCompute);
  2. 用匹配器在两组描述子之间搜索最近邻(match / knnMatch);
  3. 用绘制函数把匹配对画出来(drawMatches / drawMatchesKnn)。

教程使用的示例图像为仓库自带的 box.png(queryImage,前景盒子)与 box_in_scene.png(trainImage,盒子置于场景中)。下面所有示例都围绕“在 trainImage 中定位 queryImage”展开。

Brute-Force Matcher 原理与两个关键参数

Brute-Force matcher 的逻辑非常直接:取出第一组中某个特征的描述子,用某种距离度量与第二组中所有特征逐一比较,返回距离最近的一个。

从 C++ 源码看,匹配器类定义在 features.hpp 中,构造函数签名为:

BFMatcher( int normType=NORM_L2, bool crossCheck=false );

这对应 Python 中 cv.BFMatcher(normType, crossCheck) 的两个可选参数:

  • normType(距离度量):默认 cv.NORM_L2,适合 SIFT、SURF 这类浮点描述子;cv.NORM_L1 同样适用。对 ORB、BRIEF、BRISK 等二进制串描述子,应使用 cv.NORM_HAMMING(海明距离);若 ORB 的 WTA_K == 3 或 4,则应改用 cv.NORM_HAMMING2。源码注释(features.hpp#L1290-L1293)明确给出了这一选型建议。
  • crossCheck(交叉校验):默认 false。设为 true 时,匹配器只返回满足双向最近邻条件的匹配对 (i, j):A 组第 i 个描述子的最佳匹配是 B 组第 j 个,且 B 组第 j 个的最佳匹配也必须是 A 组第 i 个。源码注释指出,这种技术“通常能以最少的离群点得到最好的结果,且是 D. Lowe 在 SIFT 论文中提出的 ratio test 的一种替代方案”(features.hpp#L1294-L1299)。

对象创建后,两个核心方法:

  • BFMatcher.match():返回每个 query 描述子的最佳匹配;
  • BFMatcher.knnMatch():返回每个 query 描述子的 k 个最佳匹配(k 由用户指定),便于在其上做额外筛选(如 ratio test)。

示例一:ORB 描述子 + BFMatcher(NORM_HAMMING + crossCheck)

第一部分是加载图像、用 ORB 检测关键点与描述子:

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt

img1 = cv.imread('box.png', cv.IMREAD_GRAYSCALE)          # queryImage
img2 = cv.imread('box_in_scene.png', cv.IMREAD_GRAYSCALE) # 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)

接下来创建 BFMatcher:因为用的是 ORB 二进制描述子,距离度量选 cv.NORM_HAMMING,并开启 crossCheck=True 以获得更稳定的结果。然后用 match() 得到最佳匹配,按距离升序排序(距离越小越好),只画前 10 对(仅为可读性,可自行增加):

# 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], None,
                      flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)

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

运行结果(左侧为 box.png,右侧为 box_in_scene.png,绿线为前 10 对匹配):

ORB + BFMatcher(NORM_HAMMING) 的匹配结果

DMatch 对象:match() 的返回结构

matches = bf.match(des1, des2) 的结果是一个 DMatch 对象列表。每个 DMatch 含 4 个属性:

  • DMatch.distance:两个描述子之间的距离,越小越好;
  • DMatch.trainIdx:该匹配在 train 描述子集合中的索引;
  • DMatch.queryIdx:该匹配在 query 描述子集合中的索引;
  • DMatch.imgIdx:train 图像在多图像匹配场景中的图像索引。

这与 C++ 侧的定义完全一致。DMatch 类在 core/types.hpp#L844-L866 中声明:

class CV_EXPORTS_W_SIMPLE DMatch
{
public:
    CV_PROP_RW int queryIdx; //!< query descriptor index
    CV_PROP_RW int trainIdx;  //!< train descriptor index
    CV_PROP_RW int imgIdx;    //!< train image index
    CV_PROP_RW float distance;
    bool operator<(const DMatch &m) const;  // less is better
};

注意 operator< 的注释“less is better”——这正是教程中对 matchesx.distance 升序排序的底层依据。

示例二:SIFT 描述子 + BFMatcher.knnMatch + Ratio Test

第二个示例改用 knnMatch() 取 k=2,从而可以执行 D. Lowe 在 SIFT 论文中提出的 ratio test:只有当最近邻距离显著小于次近邻距离(教程取 0.75 倍)时,该匹配才被认为是可靠的——因为如果最佳匹配与次佳匹配距离过于接近,说明该 query 描述子的归属存在歧义。

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt

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

# Initiate SIFT detector
sift = cv.SIFT_create()

# 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, None,
                        flags=cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)

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

SIFT 是浮点描述子,因此这里使用 cv.BFMatcher() 默认参数(即 NORM_L2)。结果如下,可见 ratio test 滤除了大量不稳定的匹配:

SIFT + BFMatcher knnMatch(k=2) + ratio test 的匹配结果

这里还体现了两个匹配器的 API 细节:bf.match() 返回 DMatch 的扁平列表,而 bf.knnMatch() 返回每个 query 对应一组 DMatch 的嵌套结构,因此 drawMatchesKnn 要求传入 list of lists,教程中 good.append([m]) 的单层包裹即为此而设。

示例三:FLANN Matcher(大数据集的近邻加速)

FLANN 全称 Fast Library for Approximate Nearest Neighbors,是专为大规模数据集和高维特征设计的快速最近邻搜索算法库。当 train 描述子集合很大时,暴力匹配的 O(n×m) 开销难以接受,FLANN 通过建立索引结构换取速度。

从源码结构看,FlannBasedMatcher 的实现在 features.hpp#L1316-L1359:它内部用 cv::flann::Index 对 train 描述子集合训练索引(持有 indexParamssearchParamsflannIndex 三个成员),匹配时调用索引的 nearest search 方法;类注释同时说明它不支持通过 mask 限制允许的匹配对,因为 flann::Index 本身不支持。

创建 FLANN matcher 需要传入两个字典:

1. IndexParams(索引参数)——指定建树算法及相关参数。对 SIFT、SURF 等浮点描述子,可用 KD-Tree 索引:

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

对 ORB 这类二进制描述子,则用 LSH(局部敏感哈希)索引。下面注释中的取值是 FLANN 文档的推荐值,但教程作者实测在某些场景下效果不佳,换用其他取值反而更好:

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

2. SearchParams(搜索参数)——指定遍历索引树的递归次数(checks)。值越大精度越高,但耗时也越长。若要修改,传 search_params = dict(checks=100) 即可。

完整示例(SIFT + KD-Tree + ratio test 0.7):

import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt

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

# Initiate SIFT detector
sift = cv.SIFT_create()

# 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 range(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 = cv.DrawMatchesFlags_DEFAULT)

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

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

注意与前一个示例的差别:这里不是构造过滤后的 good 列表,而是构造 matchesMask(通过/未通过 ratio test 的标志掩码),配合 drawMatchesKnnmatchesMask 参数选择性绘制——这也是文档强调“k=2 时每个关键点会画两条匹配线,因此若要选择性绘制必须传 mask”的原因。draw_params 中的 matchColor=(0,255,0) 把匹配线染成绿色、singlePointColor=(255,0,0) 把关键点染成红色,结果中只有通过 ratio test 的匹配被高亮:

FLANN matcher + ratio test mask 的匹配结果

源码层面的补充:匹配器实现与绘制函数

  • 三个示例对应的 C++ 实现(BFMatcher::knnMatchImplFlannBasedMatcher::knnMatchImpl 等)位于 matchers.cpp;算法级正确性测试见 test_matchers_algorithmic.cpp,性能基准见 perf_matcher.cpp
  • BFMatcher::isMaskSupported() 返回 !crossCheckfeatures.hpp#L1287)——从源码结构看,开启 crossCheck 后匹配器只保留自洽的双向最近邻对,此时外部 mask 便失去意义,这与文档“crossCheck 是 ratio test 的替代方案”的说法相互印证。
  • drawMatches / drawMatchesKnn 会将两幅图像水平拼接,并从第一幅图的关键点向第二幅图画连线;cv.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS 标志可跳过绘制孤立的单点关键点,突出匹配关系。

小结:三种匹配方案的选型

场景 推荐方案 关键参数
小数据集、ORB/BRIEF/BRISK 二进制描述子 cv.BFMatcher(cv.NORM_HAMMING, crossCheck=True) + match() normType=NORM_HAMMING(WTA_K=3/4 用 NORM_HAMMING2)
小数据集、SIFT/SURF 浮点描述子 cv.BFMatcher() + knnMatch(k=2) + ratio test(0.75) normType=NORM_L2(默认)
大规模 train 数据集 cv.FlannBasedMatcher(index_params, search_params) + knnMatch(k=2) + ratio test(0.7)+ mask 绘制 KD-Tree(浮点)/ LSH(二进制);checks 控制精度-速度权衡

两个距离度量之外的核心取舍——crossCheck 与 ratio test——本质上是同一思想(双向/歧义性一致性校验)的两种实现:前者在匹配器内部完成、无需额外代码;后者保留了 k 近邻的完整信息,可通过调节比值阈值灵活控制过滤强度。

(参考文件:py_matcher.markdownfeatures.hppcore/types.hppmatchers.cppbox.pngbox_in_scene.png

登录后查看全文
热门项目推荐
相关项目推荐