首页
/ OpenCV 特征检测与描述全景指南:从 Harris 角点到 SIFT、ORB 与特征匹配实战

OpenCV 特征检测与描述全景指南:从 Harris 角点到 SIFT、ORB 与特征匹配实战

2026-09-06 12:31:55作者:凌朦慧Richard

本篇围绕 OpenCV Python 教程中的“特征检测与描述”系列章节展开,系统讲解图像特征(尤其是角点)的数学本质、主流检测算法(Harris、Shi-Tomasi、SIFT、FAST、ORB)、描述子与匹配器(Brute-Force 与 FLANN)的选型逻辑,以及特征匹配叠加单应性变换在复杂图像中定位已知物体的完整实战链路。读完本文后,你可以独立完成从角点检测、亚像素精化、尺度不变特征提取,到描述子匹配、RANSAC 稳健估计与目标定位的全套流程,并能对照仓库源码理解各算法在 modules/features 模块中的落地实现。

1. 为什么是角点:特征检测与描述的基本概念

“特征检测与描述”系列的第一讲解决三个问题:什么是特征?为什么它们重要?为什么角点特别重要?

这个概念可以用拼图片游戏来理解:拼图片能拼好,是因为我们能在碎片中找到独特、可追踪、可比较的图案,再把同一图案在不同图片中找出来并对齐。计算机做拼接(stitching)、三维重建(从多角度照片生成 3D 模型)依赖的正是同样的机制——在一幅图中找到特征,在另一幅图中找到相同特征,然后对齐。

关键在于哪些图案适合作为特征。用一张建筑图片来看:

不同区域(平面、边缘、角点)作为特征的定位难度

  • A、B 是平坦表面:图案在很大区域内重复,难以精确定位;
  • C、D 是边缘:沿边缘方向平移时图案不变,只能给出近似位置;
  • E、F 是角点:无论向哪个方向平移一小块区域,外观都会发生明显变化,因此唯一、易定位

平面、边缘与角点的移动对比示意

在简化图中看得更清楚:蓝色块覆盖平坦区域,怎么移动看起来都一样;黑色块含一条边缘,沿梯度方向移动会变化、沿边缘方向移动则不变;红色块覆盖角点,任何方向的移动都会改变外观,因而是独特特征。由此得出两条基本结论:

  • 特征检测(Feature Detection):寻找图像中“在任意方向上平移后强度变化最大”的区域;
  • 特征描述(Feature Description):用一段“描述”(向量)刻画特征邻域,使得计算机能在其他图像中检索到同一特征,从而完成对齐、拼接等任务。

本系列的后续章节就是按这条主线展开的:Harris 角点 → Shi-Tomasi 角点 → SIFT 尺度不变特征 → FAST 高速角点 → ORB 免费替代 SIFT/SURF 的方案 → 描述子匹配 → 匹配 + 单应性定位物体。

2. Harris 角点检测:数学原理与亚像素精化

Harris 角点检测器由 Chris Harris 与 Mike Stephens 在 1988 年的论文《A Combined Corner and Edge Detector》中提出,是第一个把“角点”概念数学化的算法。

2.1 自相关函数与 Hessian 矩阵

核心思想是寻找对任意方向位移 (u,v) 都产生最大强度变化的区域。定义自相关(自相关响应)函数:

E(u,v) = Σ w(x,y) [ I(x+u, y+v) − I(x,y) ]²

其中 w(x,y) 是窗口函数(矩形窗或高斯窗)。对 E(u,v) 做泰勒展开后得到二次型近似:

E(u,v) ≈ [u  v] · M · [u; v]

其中

M = Σ w(x,y) [ Ix²   IxIy ]
            [ IxIy  Iy²  ]

IxIy 是图像在 x、y 方向的导数,可以用 cv.Sobel() 求出。

2.2 Harris 响应函数 R

接着引入一个综合打分函数:

R = det(M) − k·(trace(M))²
  • det(M) = λ1·λ2
  • trace(M) = λ1 + λ2
  • λ1λ2M 的特征值

特征值的大小关系决定了区域类型:

  • |R| 很小(λ1、λ2 都很小)→ 平坦区域
  • R < 0(一个特征值远大于另一个)→ 边缘
  • R 很大(λ1、λ2 都大且接近)→ 角点

Harris 检测器中平坦、边缘与角点区域在 λ1-λ2 空间中的分布

Harris 检测的输出是一张灰度得分图,对得分做阈值化即得到角点位置。

2.3 OpenCV 中的 cv.cornerHarris()

cv.cornerHarris() 的参数:

参数 说明
img 输入图像,必须是灰度、float32 类型
blockSize 邻域窗口大小(2 为 2×2 像素邻域)
ksize Sobel 导数所用的孔径参数(必须为奇数)
k Harris 自由参数,典型取 0.04~0.05

示例代码(检测棋盘格角点):

import numpy as np
import cv2 as cv

filename = 'chessboard.png'
img = cv.imread(filename)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

gray = np.float32(gray)
dst = cv.cornerHarris(gray, 2, 3, 0.04)

# result is dilated for marking the corners, not important
dst = cv.dilate(dst, None)

# Threshold for an optimal value, it may vary depending on the image.
img[dst > 0.01 * dst.max()] = [0, 0, 255]

cv.imshow('dst', img)
if cv.waitKey(0) & 0xff == 27:
    cv.destroyAllWindows()

注意两处实践细节:输入前必须 np.float32(gray) 转换;结果先做 cv.dilate 膨胀以便标记,再用相对阈值 0.01 * dst.max() 判定——这个系数需要随图像内容调整。

Harris 角点检测结果

2.4 cv.cornerSubPix() 亚像素精化

当需要更高精度时,可用 cv.cornerSubPix() 把整像素角点精化到亚像素精度。流程是:先用 Harris 找出角点并阈值化 → 用 cv.connectedComponentsWithStats() 求出每个角点连通域的质心 → 以质心为初值做迭代精化。

import numpy as np
import cv2 as cv

filename = 'chessboard2.jpg'
img = cv.imread(filename)
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

# find Harris corners
gray = np.float32(gray)
dst = cv.cornerHarris(gray, 2, 3, 0.04)
dst = cv.dilate(dst, None)
ret, dst = cv.threshold(dst, 0.01 * dst.max(), 255, 0)
dst = np.uint8(dst)

# find centroids
ret, labels, stats, centroids = cv.connectedComponentsWithStats(dst)

# define the criteria to stop and refine the corners
criteria = (cv.TERM_CRITERIA_EPS + cv.TERM_CRITERIA_MAX_ITER, 100, 0.001)
corners = cv.cornerSubPix(gray, np.float32(centroids), (5, 5), (-1, -1), criteria)

# Now draw them
res = np.hstack((centroids, corners))
res = np.int0(res)
img[res[:, 1], res[:, 0]] = [0, 0, 255]   # 原始 Harris 角点(红)
img[res[:, 3], res[:, 2]] = [0, 255, 0]   # 精化后角点(绿)

cv.imwrite('subpixel5.png', img)

关键参数说明:

  • winSize=(5,5):迭代搜索角点的邻域窗口;
  • zeroZone=(-1,-1):无零区;
  • criteria(终止准则, 最大迭代次数, 精度阈值),满足“达到 100 次迭代”或“精度达到 0.001”中先发生者即停止。

亚像素角点精化结果(放大窗口显示红/绿点对比)

3. Shi-Tomasi 角点检测器与 goodFeaturesToTrack

1994 年 J. Shi 与 C. Tomasi 在论文《Good Features to Track》中对 Harris 打分函数做了小幅修改:

  • Harris:R = λ1·λ2 − k(λ1+λ2)²
  • Shi-Tomasi:R = min(λ1, λ2)

Shi-Tomasi 角点判据在 λ1-λ2 空间中的区域划分

判据变为:只要两个特征值都大于某个最小值 λmin,即视为角点(图中绿色区域)。这种“取最小特征值”的准则对边缘与角点的区分更直接,实验结果优于 Harris。

OpenCV 中对应函数是 cv.goodFeaturesToTrack(),它通过 Shi-Tomasi 方法(指定参数时也可退回 Harris 判据)找出图中 N 个最强的角点。参数含义:

参数 说明
maxCorners 要找的角点数量上限(如 25)
qualityLevel 质量等级,0~1 之间,低于该值的角点直接淘汰
minDistance 角点之间的最小欧氏距离,用于非极大值抑制式去近邻

内部逻辑:先按质量淘汰弱角点 → 按质量降序排序 → 取最强角点后丢弃最小距离范围内的所有其他角点,循环直到凑满 N 个。

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

img = cv.imread('blox.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

corners = cv.goodFeaturesToTrack(gray, 25, 0.01, 10)
corners = np.int0(corners)

for i in corners:
    x, y = i.ravel()
    cv.circle(img, (x, y), 3, 255, -1)

plt.imshow(img), plt.show()

blox 图像上找到的 25 个最强 Shi-Tomasi 角点

该函数返回的是“精选的最优特征集合”,因此特别适合特征跟踪(光流法逐帧跟踪这类稀疏特征点)。从源码结构看,其实现位于 gftt.cpp,特征筛选与非极大值抑制逻辑集中在 featureselect.cpp

4. SIFT:尺度不变特征变换

Harris 等角点检测器是旋转不变的(旋转后的图像中角点仍是角点),但不是尺度不变的:一个在缩小图像中是角点的区域,放大到同一窗口内可能只剩平坦纹理。

小窗口中缩放前后的“角点”差异

2004 年 D. Lowe(不列颠哥伦比亚大学)在论文《Distinctive Image Features from Scale-Invariant Keypoints》中提出 SIFT,同时产出关键点(keypoints)描述子(descriptors)。算法分四步:

4.1 尺度空间极值检测

不同尺度需要用不同大小的窗口。为此使用尺度空间滤波:对图像在不同 σ 下求高斯差分。LoG(高斯拉普拉斯)可作为 blob 检测器,但由于计算代价较高,SIFT 用**高斯差分(DoG, Difference of Gaussians)**近似 LoG——用两个不同 σ(σ 与 kσ)的高斯模糊结果相减。整个过程在图像的高斯金字塔、不同 octave 上进行:

SIFT 高斯差分(DoG)与高斯金字塔示意

得到 DoG 图后,在空间与尺度两个维度上搜索局部极值:每个像素与同尺度 8 个邻居、上一层 9 个像素、下一层 9 个像素共 26 个比较,若为极值则为候选关键点,即 (x, y, σ) 三元组表示该点在尺度 σ 处存在潜在特征。

论文给出的经验最优参数:octave 数 = 4、每 octave 尺度层数 = 5、初始 σ = 1.6、k = √2。

候选关键点在尺度空间中的 26 邻域极值比较

4.2 关键点定位

对候选极值做泰勒级数展开亚像素精化;若极值处强度低于阈值(论文取 0.03,对应 OpenCV 的 contrastThreshold)则剔除。

DoG 对边缘响应也高,因此还需剔除边缘点:类似 Harris 的思路,用 2×2 Hessian 矩阵计算主曲率,若曲率比(大特征值与其特征值之比,即 trace²/(r·det) 类判据)超过阈值(论文取 10,对应 OpenCV 的 edgeThreshold),则丢弃该关键点。经过这两步,剩下的都是强响应且非边缘的兴趣点。

4.3 方向分配

为每个关键点分配主方向以获得旋转不变性:在关键点邻域内(按关键点的尺度确定范围)计算梯度幅值与方向,构建覆盖 360° 的 36 bin 方向直方图(按梯度幅值加权,并乘以一个 σ 为关键点尺度 1.5 倍的高斯圆形窗口)。取最高峰作为主方向;高于峰值 80% 的局部峰也生成同位置、同尺度但方向不同的关键点,这增强了匹配稳定性。

4.4 关键点描述子

取关键点周围 16×16 邻域,划分为 16 个 4×4 子块,每个子块计算 8 bin 方向直方图,共 128 个 bin 值组成 128 维描述子向量,并对光照变化、旋转等做了鲁棒性处理。

4.5 关键点匹配(Lowe 比率检验)

匹配时利用最近邻,但当次近邻与最近邻距离很近时(噪声等原因)会产生误配。做法是取最近邻距离 / 次近邻距离的比值,大于 0.8 则拒绝。据论文统计,该方法淘汰约 90% 的误匹配,仅丢弃 5% 的正确匹配。

4.6 OpenCV 中的 SIFT

注意:SIFT 曾长期只存在于 OpenCV contrib 仓库,专利于 2020 年过期后已合并进主仓库,因此当前仓库的 cv.SIFT_create() 可直接使用(实现在 sift.dispatch.cpp)。

import numpy as np
import cv2 as cv

img = cv.imread('home.jpg')
gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

sift = cv.SIFT_create()
kp = sift.detect(gray, None)

img = cv.drawKeypoints(gray, kp, img)

cv.imwrite('sift_keypoints.jpg', img)

sift.detect() 可传入 mask 只搜索图像局部。每个 keypoint 是包含 (x, y) 坐标、有意义邻域大小 size、方向 angle、响应强度 response 等属性的结构体。

cv.drawKeypoints() 默认只画小圆圈;传入标志 cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS 会画出反映关键点大小的圆并显示其方向:

img = cv.drawKeypoints(gray, kp, img, flags=cv.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
cv.imwrite('sift_keypoints.jpg', img)

SIFT 关键点绘制结果(默认小圆与带方向/尺寸的丰富绘制)

计算描述子有两种方式:

  1. 已有关键点时调用 sift.compute()kp, des = sift.compute(gray, kp)
  2. 一步完成:sift.detectAndCompute()
sift = cv.SIFT_create()
kp, des = sift.detectAndCompute(gray, None)

此时 des 是形状为 (关键点数量 × 128) 的 numpy 数组。

5. FAST:面向实时应用的极速角点检测

前面几种检测器都很“好”,但从实时应用(如计算资源受限的 SLAM 移动机器人)角度看还不够快。FAST(Features from Accelerated Segment Test)由 Edward Rosten 与 Tom Drummond 于 2006 年论文《Machine learning for high-speed corner detection》提出(2010 年修订),核心目标是“快”。

5.1 高速分割测试

  1. 选取待判定像素 p,记其强度为 Ip
  2. 选定阈值 t
  3. 考察 p 周围一圈 16 个像素组成的圆;
  4. 若这 16 个像素中存在 n 个连续像素全部亮于 Ip + t 或全部暗于 Ip − t,则 p 是角点(n 取 12);
  5. 为避免对所有像素做完整 16 点测试,先做高速测试:只检查圆上第 1、5、9、13 四个像素。若 p 是角点,这四个点中至少三个应满足条件;不满足则直接排除,满足再对候选做完整测试。

FAST 的 16 像素圆环与高速测试的 4 个采样点

高速测试的缺点:n < 12 时排除率低、采样点选择不最优(效率依赖问题顺序与角点出现分布)、高速测试结果被丢弃、相邻位置会重复检出多个特征。前三点用机器学习解决,最后一点用非极大值抑制解决。

5.2 用机器学习训练角点检测器

  1. 选取一组(尽量来自目标应用领域的)训练图像;
  2. 对每张图运行 FAST 找出特征点;
  3. 将每个特征点周围 16 个像素存成向量,得到特征向量集 P
  4. 每个像素可处于三种状态之一:

FAST 像素三态判定的符号表示

  1. 按状态把 P 划分为三个子集 PdPsPb
  2. 定义布尔变量 Kpp 是角点为真,否则为假;
  3. ID3 决策树算法,按 Kp 的信息熵选择信息量最大的像素分支,递归直到熵为零;
  4. 得到的决策树即可在其他图像上做高速判定。

5.3 非极大值抑制

对每个检出的特征点计算得分 V——Vp 与周围 16 个像素强度差的绝对值之和;对相邻两个关键点,丢弃 V 值较低者。

小结:FAST 比已有角点检测器快若干倍,但对高噪声不鲁棒、结果依赖阈值 t

5.4 OpenCV 中的 FAST

用法与其他特征检测器一致,可配置阈值、是否非极大值抑制、邻域类型。邻域有三种标志:cv.FAST_FEATURE_DETECTOR_TYPE_5_8cv.FAST_FEATURE_DETECTOR_TYPE_7_12(默认)、cv.FAST_FEATURE_DETECTOR_TYPE_9_16

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

img = cv.imread('blox.jpg', cv.IMREAD_GRAYSCALE)  # <opencv_root>/samples/data/blox.jpg

# Initiate FAST object with default values
fast = cv.FastFeatureDetector_create()

# find and draw the keypoints
kp = fast.detect(img, None)
img2 = cv.drawKeypoints(img, kp, None, color=(255, 0, 0))

# Print all default params
print("Threshold: {}".format(fast.getThreshold()))
print("nonmaxSuppression:{}".format(fast.getNonmaxSuppression()))
print("neighborhood: {}".format(fast.getType()))
print("Total Keypoints with nonmaxSuppression: {}".format(len(kp)))

cv.imwrite('fast_true.png', img2)

# Disable nonmaxSuppression
fast.setNonmaxSuppression(0)
kp = fast.detect(img, None)

print("Total Keypoints without nonmaxSuppression: {}".format(len(kp)))

img3 = cv.drawKeypoints(img, kp, None, color=(255, 0, 0))

cv.imwrite('fast_false.png', img3)

FAST 开/关非极大值抑制的关键点数量对比

对比两次运行输出的关键点数量,可以直观看到非极大值抑制对“相邻重复检测”的消除效果。该检测器的实现位于 fast.cpp,另有 fast.avx2.cpp 提供 AVX2 SIMD 加速路径,以及 fast_score.cpp 实现非极大值抑制所需的强度差打分。

参考论文:Rosten & Drummond, "Machine learning for high speed corner detection", ECCV 2006;Rosten, Porter & Drummond, "Faster and better: a machine learning approach to corner detection", IEEE TPAMI 2010。

6. ORB:SIFT/SURF 的免费高效替代

SIFT 与 SURF 都有专利,商用需付费。ORB(Oriented Rotated BRIEF)由 Ethan Rublee、Vincent Rabaud、Kurt Konolige 与 Gary R. Bradski 于 2011 年提出(论文《ORB: An efficient alternative to SIFT or SURF》),来自 OpenCV Labs,是无专利的 SIFT/SURF 替代方案,在计算代价与匹配性能上都表现良好。

6.1 算法构成

ORB 本质上是 FAST 关键点检测器 + BRIEF 描述子的融合改造:

  1. 先用 FAST 找关键点,再用 Harris corner measure 在候选中选出 top N;
  2. 利用图像金字塔产生多尺度特征;
  3. FAST 不计算方向,因此 ORB 通过灰度质心确定方向:以角点为中心,计算邻域内以强度加权的质心,从角点到质心的向量方向即为关键点方向(用圆形区域内 x、y 的一阶矩计算);
  4. BRIEF 描述子对旋转敏感,ORB 按关键点方向“转向”BRIEF:对 n 个二进制测试点的坐标矩阵 S(2×n),用朝向 θ 的旋转矩阵将其旋转为 S_θ
  5. 朝向按 2π/30(12°)离散化,为每个离散朝向预计算 BRIEF 模式查找表,只要同一特征在不同视图的朝向一致,就会使用同一组测试点;
  6. BRIEF 的关键性质是每个比特特征方差大、均值接近 0.5;但按关键点方向转向后这一性质会退化,而高方差让特征更具判别性。ORB 还要求测试间不相关,于是对所有候选二进制测试做贪心搜索,选出方差高、均值接近 0.5 且互不相关的测试集,结果即 rBRIEF
  7. 描述子匹配采用改进传统 LSH 的 multi-probe LSH

论文结论:ORB 显著快于 SURF 与 SIFT,描述子效果优于 SURF,非常适合低功耗设备上的全景拼接等场景。

6.2 OpenCV 中的 ORB

通过 cv.ORB_create() 创建(或使用特征通用接口)。常用可选参数:

参数 说明 默认值
nFeatures 保留的最大特征数 500
scoreType 用 Harris 得分还是 FAST 得分给特征排序 Harris 得分
WTA_K 生成每个 rBRIEF 描述子元素所用的点数 2(此时匹配用 NORM_HAMMING;WTA_K 为 3 或 4 时用 NORM_HAMMING2
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt

img = cv.imread('simple.jpg', cv.IMREAD_GRAYSCALE)

# Initiate ORB detector
orb = cv.ORB_create()

# find the keypoints with ORB
kp = orb.detect(img, None)

# compute the descriptors with ORB
kp, des = orb.compute(img, kp)

# draw only keypoints location, not size and orientation
img2 = cv.drawKeypoints(img, kp, None, color=(0, 255, 0), flags=0)
plt.imshow(img2), plt.show()

ORB 在 simple 图像上检测的关键点

ORB 的检测器实现在 orb.cpp。注意 FAST 只是 ORB 的候选生成器,最终保留的特征按 Harris 得分排序,这正是 scoreType 参数的作用所在。

7. 特征匹配:Brute-Force 与 FLANN 匹配器

有了检测器与描述子,下一步是把两幅图中的描述子匹配起来。OpenCV 提供两种技术:Brute-Force 匹配器与基于 FLANN 的匹配器。

7.1 Brute-Force 匹配器

BF 匹配器很直接:取第一组某个特征的描述子,与第二组所有特征逐一计算距离,返回最近者。

创建 cv.BFMatcher() 时接受两个可选参数:

  • normType:距离度量。默认 cv.NORM_L2,适合 SIFT、SURF 等浮点描述子(也有 cv.NORM_L1);对 ORB、BRIEF、BRISK 等二进制字符串描述子应使用 cv.NORM_HAMMING(汉明距离);若 ORB 使用 WTA_K == 3 or 4,则用 cv.NORM_HAMMING2
  • crossCheck:布尔值,默认 false。为 true 时,匹配器只返回满足“双向最优”的匹配对 (i, j)——A 组第 i 个描述子的最优匹配是 B 组第 j 个,反之亦然。这是 D. Lowe 比率检验的一个良好替代,结果更一致。

创建后有两个重要方法:

  • BFMatcher.match():返回最佳匹配;
  • BFMatcher.knnMatch():返回 k 个最佳匹配,便于后续做比率检验等额外处理。

绘制方面,类似 cv.drawKeypoints()cv.drawMatches() 会把两图水平拼接并从最佳匹配处连线;cv.drawMatchesKnn() 画全部 k 个匹配线(k=2 时每关键点两条线),如需选择性绘制要传入 mask。

7.2 示例一:ORB 描述子的 BF 匹配

场景:用 box.png(queryImage)在 box_in_scene.png(trainImage)中定位该物体。对应素材在 box.pngbox_in_scene.png

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)
# 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()

ORB + BFMatcher(crossCheck) 的匹配结果

7.3 DMatch 对象

matches = bf.match(des1, des2) 返回 DMatch 对象列表,每个对象有四个属性:

  • DMatch.distance:描述子间距离,越小越好;
  • DMatch.trainIdx:训练组中的描述子下标;
  • DMatch.queryIdx:查询组中的描述子下标;
  • DMatch.imgIdx:训练图像下标。

7.4 示例二:SIFT 描述子 + 比率检验

这里用 knnMatch() 取 k=2,以便应用 Lowe 的比率检验:

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 + knnMatch 比率检验后的匹配结果

7.5 FLANN 匹配器

FLANN(Fast Library for Approximate Nearest Neighbors)是面向大数据集、高维特征快速近邻搜索的算法集合,在大数据集上比 BFMatcher 更快。创建 FLANN 匹配器需要两个字典:

  • index_params(索引参数):指定所用算法及其参数。对 SIFT、SURF 类浮点描述子可传:
FLANN_INDEX_KDTREE = 1
index_params = dict(algorithm=FLANN_INDEX_KDTREE, trees=5)

对 ORB 等二进制描述子可传:

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

(注释中的推荐值来自文档,但教程作者发现部分场景下这些值效果不理想,可按需调整。)

  • search_params(搜索参数):如 search_params = dict(checks=100),指定索引树递归遍历的次数——值越大精度越高、耗时也越长。

完整示例(SIFT + FLANN + 比率检验):

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()

FLANN 匹配 + 比率检验后的匹配绘制

匹配器的实现集中在 matchers.cpp,FLANN 算法本体则位于 modules/flann

8. 特征匹配 + 单应性变换:在复杂图像中定位物体

最后一讲把特征匹配与 calib3d 模块的 cv.findHomography() 结合起来,在杂乱背景图中精确定位已知物体。

8.1 原理

上一章我们已经在 trainImage 中找到了 queryImage 部分特征的位置,这足以把物体在 trainImage 中完整框出来:

  1. 把两组匹配点传给 cv.findHomography(),求出物体的透视变换(3×3 单应矩阵)——至少需要 4 个正确点;
  2. cv.perspectiveTransform() 把 queryImage 的四个角点变换到 trainImage 中的对应位置,从而画出物体轮廓;
  3. 匹配中难免有误,因此 cv.findHomography() 内部采用 RANSACLEAST_MEDIAN(由 flags 指定)做稳健估计:提供正确估计的匹配叫内点(inliers),其余是外点(outliers),函数同时返回内点掩码。

8.2 第一步:SIFT 特征 + FLANN 匹配 + 比率检验

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

MIN_MATCH_COUNT = 10

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_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:
        good.append(m)

设定至少 10 个有效匹配(MIN_MATCH_COUNT)才继续寻找物体,否则提示匹配不足。

8.3 第二步:求单应矩阵并投影角点

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, 255, 3, cv.LINE_AA)

else:
    print("Not enough matches are found - {}/{}".format(len(good), MIN_MATCH_COUNT))
    matchesMask = None

注意 cv.findHomography(src_pts, dst_pts, cv.RANSAC, 5.0) 的最后一个参数 5.0 是 RANSAC 的重投影误差阈值(像素),超过该误差的匹配被归为外点。

8.4 第三步:绘制内点匹配结果

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()

单应性定位:物体在杂乱图像中被白线框出,绿色线只连接内点匹配

结果中,物体轮廓以白色画在杂乱背景图上,绿色连线只保留 RANSAC 判定的内点,外点匹配被自动剔除——这正是“检测 → 描述 → 匹配 → 稳健几何估计”整条链路的一次完整落地。

9. 全链路小结与源码索引

把整个系列串起来,就是一条从图像到语义几何的流水线:

  1. 检测:角点类(Harris cv.cornerHarris()、Shi-Tomasi cv.goodFeaturesToTrack()、FAST cv.FastFeatureDetector_create())负责找“哪里是特征”;尺度不变类(SIFT cv.SIFT_create()、ORB cv.ORB_create())负责在多尺度、多旋转、多光照下找“稳定特征”;
  2. 描述:SIFT 128 维浮点向量 vs rBRIEF 二进制串,决定了后续距离度量的选择(L2 vs 汉明距离);
  3. 匹配:小数据用 BFMatcher(crossCheck 或比率检验),大数据用 FLANN(KDTREE 索引浮点描述子、LSH 索引二进制描述子);
  4. 几何验证cv.findHomography() + RANSAC 从匹配点恢复物体位姿,内点掩码给出可信度。

对照当前仓库源码,各核心组件的落点为:FAST 在 fast.cppfast.avx2.cpp,ORB 在 orb.cpp,SIFT 在 sift.dispatch.cpp,goodFeaturesToTrack 在 gftt.cppfeatureselect.cpp,关键点/描述子基类在 feature2d.cppkeypoint.cpp,匹配器在 matchers.cpp,FLANN 库在 modules/flann,而各教程示例所用的测试素材(box.pngbox_in_scene.pngblox.jpg 等)位于 samples/data 目录。

工程选型上可记住几条与源码/文档一致的结论:追求实时且可接受噪声敏感选 FAST;无专利、适合低功耗设备选 ORB;追求尺度/光照鲁棒性选 SIFT(2020 年后专利过期,主仓库可直接使用);只跟踪稀疏稳定角点选 goodFeaturesToTrack。本文所有代码示例均为教程原始可运行形式,图片与素材路径均已给出,可直接在本地 OpenCV 环境中复现。

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