OpenCV 特征检测与描述全景指南:从 Harris 角点到 SIFT、ORB 与特征匹配实战
本篇围绕 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² ]
Ix、Iy 是图像在 x、y 方向的导数,可以用 cv.Sobel() 求出。
2.2 Harris 响应函数 R
接着引入一个综合打分函数:
R = det(M) − k·(trace(M))²
det(M) = λ1·λ2trace(M) = λ1 + λ2λ1、λ2是M的特征值
特征值的大小关系决定了区域类型:
|R|很小(λ1、λ2 都很小)→ 平坦区域;R < 0(一个特征值远大于另一个)→ 边缘;R很大(λ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() 判定——这个系数需要随图像内容调整。
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)
判据变为:只要两个特征值都大于某个最小值 λ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()
该函数返回的是“精选的最优特征集合”,因此特别适合特征跟踪(光流法逐帧跟踪这类稀疏特征点)。从源码结构看,其实现位于 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 上进行:
得到 DoG 图后,在空间与尺度两个维度上搜索局部极值:每个像素与同尺度 8 个邻居、上一层 9 个像素、下一层 9 个像素共 26 个比较,若为极值则为候选关键点,即 (x, y, σ) 三元组表示该点在尺度 σ 处存在潜在特征。
论文给出的经验最优参数:octave 数 = 4、每 octave 尺度层数 = 5、初始 σ = 1.6、k = √2。
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.compute():kp, des = sift.compute(gray, kp); - 一步完成:
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 高速分割测试
- 选取待判定像素
p,记其强度为Ip; - 选定阈值
t; - 考察
p周围一圈 16 个像素组成的圆; - 若这 16 个像素中存在 n 个连续像素全部亮于
Ip + t或全部暗于Ip − t,则p是角点(n 取 12); - 为避免对所有像素做完整 16 点测试,先做高速测试:只检查圆上第 1、5、9、13 四个像素。若
p是角点,这四个点中至少三个应满足条件;不满足则直接排除,满足再对候选做完整测试。
高速测试的缺点:n < 12 时排除率低、采样点选择不最优(效率依赖问题顺序与角点出现分布)、高速测试结果被丢弃、相邻位置会重复检出多个特征。前三点用机器学习解决,最后一点用非极大值抑制解决。
5.2 用机器学习训练角点检测器
- 选取一组(尽量来自目标应用领域的)训练图像;
- 对每张图运行 FAST 找出特征点;
- 将每个特征点周围 16 个像素存成向量,得到特征向量集
P; - 每个像素可处于三种状态之一:
- 按状态把
P划分为三个子集Pd、Ps、Pb; - 定义布尔变量
Kp:p是角点为真,否则为假; - 用 ID3 决策树算法,按
Kp的信息熵选择信息量最大的像素分支,递归直到熵为零; - 得到的决策树即可在其他图像上做高速判定。
5.3 非极大值抑制
对每个检出的特征点计算得分 V——V 为 p 与周围 16 个像素强度差的绝对值之和;对相邻两个关键点,丢弃 V 值较低者。
小结:FAST 比已有角点检测器快若干倍,但对高噪声不鲁棒、结果依赖阈值 t。
5.4 OpenCV 中的 FAST
用法与其他特征检测器一致,可配置阈值、是否非极大值抑制、邻域类型。邻域有三种标志:cv.FAST_FEATURE_DETECTOR_TYPE_5_8、cv.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.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 描述子的融合改造:
- 先用 FAST 找关键点,再用 Harris corner measure 在候选中选出 top N;
- 利用图像金字塔产生多尺度特征;
- FAST 不计算方向,因此 ORB 通过灰度质心确定方向:以角点为中心,计算邻域内以强度加权的质心,从角点到质心的向量方向即为关键点方向(用圆形区域内 x、y 的一阶矩计算);
- BRIEF 描述子对旋转敏感,ORB 按关键点方向“转向”BRIEF:对 n 个二进制测试点的坐标矩阵
S(2×n),用朝向θ的旋转矩阵将其旋转为S_θ; - 朝向按
2π/30(12°)离散化,为每个离散朝向预计算 BRIEF 模式查找表,只要同一特征在不同视图的朝向一致,就会使用同一组测试点; - BRIEF 的关键性质是每个比特特征方差大、均值接近 0.5;但按关键点方向转向后这一性质会退化,而高方差让特征更具判别性。ORB 还要求测试间不相关,于是对所有候选二进制测试做贪心搜索,选出方差高、均值接近 0.5 且互不相关的测试集,结果即 rBRIEF;
- 描述子匹配采用改进传统 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 的检测器实现在 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.png 与 box_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()
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()
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()
匹配器的实现集中在 matchers.cpp,FLANN 算法本体则位于 modules/flann。
8. 特征匹配 + 单应性变换:在复杂图像中定位物体
最后一讲把特征匹配与 calib3d 模块的 cv.findHomography() 结合起来,在杂乱背景图中精确定位已知物体。
8.1 原理
上一章我们已经在 trainImage 中找到了 queryImage 部分特征的位置,这足以把物体在 trainImage 中完整框出来:
- 把两组匹配点传给
cv.findHomography(),求出物体的透视变换(3×3 单应矩阵)——至少需要 4 个正确点; - 用
cv.perspectiveTransform()把 queryImage 的四个角点变换到 trainImage 中的对应位置,从而画出物体轮廓; - 匹配中难免有误,因此
cv.findHomography()内部采用 RANSAC 或 LEAST_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. 全链路小结与源码索引
把整个系列串起来,就是一条从图像到语义几何的流水线:
- 检测:角点类(Harris
cv.cornerHarris()、Shi-Tomasicv.goodFeaturesToTrack()、FASTcv.FastFeatureDetector_create())负责找“哪里是特征”;尺度不变类(SIFTcv.SIFT_create()、ORBcv.ORB_create())负责在多尺度、多旋转、多光照下找“稳定特征”; - 描述:SIFT 128 维浮点向量 vs rBRIEF 二进制串,决定了后续距离度量的选择(L2 vs 汉明距离);
- 匹配:小数据用 BFMatcher(crossCheck 或比率检验),大数据用 FLANN(KDTREE 索引浮点描述子、LSH 索引二进制描述子);
- 几何验证:
cv.findHomography()+ RANSAC 从匹配点恢复物体位姿,内点掩码给出可信度。
对照当前仓库源码,各核心组件的落点为:FAST 在 fast.cpp、fast.avx2.cpp,ORB 在 orb.cpp,SIFT 在 sift.dispatch.cpp,goodFeaturesToTrack 在 gftt.cpp 与 featureselect.cpp,关键点/描述子基类在 feature2d.cpp 与 keypoint.cpp,匹配器在 matchers.cpp,FLANN 库在 modules/flann,而各教程示例所用的测试素材(box.png、box_in_scene.png、blox.jpg 等)位于 samples/data 目录。
工程选型上可记住几条与源码/文档一致的结论:追求实时且可接受噪声敏感选 FAST;无专利、适合低功耗设备选 ORB;追求尺度/光照鲁棒性选 SIFT(2020 年后专利过期,主仓库可直接使用);只跟踪稀疏稳定角点选 goodFeaturesToTrack。本文所有代码示例均为教程原始可运行形式,图片与素材路径均已给出,可直接在本地 OpenCV 环境中复现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

















