ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Harris与ORB特征提取实战:从原理到OpenCV实现

Harris与ORB特征提取实战:从原理到OpenCV实现 1. 从“机器怎么看图”说起特征提取到底在解决什么问题做图像处理这几年我越来越觉得特征提取是整个视觉任务的“地基”。你后面不管是做全景拼接、目标跟踪、三维重建还是简单的图像配准第一步几乎都是同一件事从图像里找到那些“稳定、独特、可重复”的点。这就是特征提取而这些点就是特征点业内习惯叫关键点角点是其中最常见的一类。为什么要强调“稳定、独特、可重复”你换个角度想两张照片拍同一个物体角度偏了几度、光线暗了一档、缩放变了一下如果算法每次找到的点都不一样后面的匹配、拼接、重建就全乱套了。所以特征提取算法比拼的本质上就是谁能在各种扰动下仍然稳定地找到同一批点。正因为这个诉求2010年前后那一波特征提取算法的爆发成了计算机视觉历史上的一个高光时刻Harris角点给了一个数学上特别漂亮的定义SIFT把尺度不变性做到了极致ORB则在实时性上杀出了一条血路。这三者的演进脉络稍微捋一下基本就能把现代特征提取的思路看清一大半。这篇文章我会围绕Harris和ORB展开从数学原理一路讲到OpenCV代码实现再给出同一场景下的实测对比和选型建议。读者如果是刚接触OpenCV图像处理这里面每个概念的来龙去脉我都会交代清楚如果已经写过一段时间OpenCV代码那后半部分的参数调优和坑点排查大概率能帮你省点时间。2. 核心思路拆解为什么是角点为什么绕不开尺度和方向2.1 角点的数学直觉为什么“角落”这么重要先问个问题图像里那么多像素凭什么某些点适合做特征我们来看三种情况。第一种图像里一大片纯色区域比如白墙。你在墙上随便点一个点挪到旁边一点周围像素长得几乎一模一样这种点完全不具备区分度没法用来匹配。第二种边缘上的点比如桌子边缘。沿着边缘方向滑动灰度变化很小你很难精确定位“到底是边缘上的哪个点”。第三种角点比如桌角、窗口拐角。往任何方向挪一点点灰度都会剧烈变化这个点就像坐标系里的原点一样位置信息极其精确。所以角点之所以特殊是因为它在二维方向上同时拥有高梯度变化。这个直观认知恰恰就是Harris角点检测整个数学推导的出发点。如果你要在两张图中做匹配你当然希望找到这种“唯一性”强的点。这就是整个特征提取的第一性原理找那些在局部邻域内信息熵最高、位置最不容易含糊的点。2.2 特征提取算法的三条路线检测、描述、匹配完整理解特征提取要把流程拆成两段看关键点检测和关键点描述。检测负责找到“在哪里”描述负责给每个点算一个“身份证号”也就是描述子。匹配阶段再用描述子的相似度判断两张图里哪些点是同一物理位置。很多人初学时把Harris当成了特征提取的全部其实Harris只做了检测这一步它没有配套的描述子。也就是说Harris角点给了你一堆点的坐标但你还得自己想办法描述它们。SIFT和ORB则是“检测描述”一体的完整方案。ORB中负责检测的是FAST负责描述的是BRIEF的改进版rBRIEF。明白这个分层后面看代码就不会晕。2.3 Harris、SIFT、ORB的定位差异一个表格说清楚算法核心贡献旋转不变尺度不变光照鲁棒实时性典型场景Harris角点响应数学定义否否中等高标定、基础配准SIFT尺度空间梯度直方图是是强低三维重建、遥感ORBFAST金字塔rBRIEF是有限中等极高SLAM、实时跟踪这个表格值得展开说几句。Harris虽然古老但并没有被淘汰它计算量小、数学干净在很多棋盘格角点检测场景里仍然是首选。SIFT精度最高但因为专利和速度问题移动端和实时系统里很少用。ORB是Ethan Rublee等人2011年在ICCV上提出的设计目标非常明确在接近SIFT的匹配能力的同时速度提升一到两个数量级。它后来成为ORB-SLAM系列的基础也基本成了实时视觉领域的默认特征方案。3. Harris角点检测从数学推导到OpenCV代码3.1 自相关函数和窗口移动一步步推过去Harris的核心思想用一句话概括窗口移动后窗口内灰度变化越大中心点越可能是角点。设一个图像窗口在某个点((x,y))处分别沿(x)方向和(y)方向平移((u,v))窗口内灰度变化量可以用下面这个式子近似[ E(u,v) \sum_{(x,y)} w(x,y) [I(xu, yv) - I(x,y)]^2 ]其中(w(x,y))是窗口权重函数常见做法是取高斯权重离中心越远权重越小。这个加权处理很有必要它能降低远处像素的干扰同时起到一定的平滑降噪作用。对(I(xu, yv))做一阶泰勒展开[ I(xu, yv) \approx I(x,y) I_x u I_y v ]代入后化简E可以写成矩阵形式[ E(u,v) \approx \begin{bmatrix} u v \end{bmatrix} M \begin{bmatrix} u \ v \end{bmatrix} ]其中M是结构张量结构性矩阵[ M \sum_{(x,y)} w(x,y) \begin{bmatrix} I_x^2 I_x I_y \ I_x I_y I_y^2 \end{bmatrix} ]这里的(I_x)和(I_y)分别是图像在x、y方向的梯度。注意M是实对称矩阵可以特征分解。两个特征值(\lambda_1)和(\lambda_2)就包含了判断角点的全部信息两个特征值都很小窗口内灰度近似平坦属于平坦区域一个特征值大、另一个小沿某个方向灰度变化剧烈属于边缘两个特征值都大沿任意方向灰度变化都剧烈属于角点但逐个算特征值代价偏高Harris聪明的地方在于它用行列式和迹的组合来近似判断定义了角点响应函数[ R \det(M) - k \cdot \operatorname{trace}(M)^2 ]也就是[ R \lambda_1 \lambda_2 - k(\lambda_1 \lambda_2)^2 ](k)是经验常数OpenCV里默认取0.04。R大于某个阈值时判定为角点。后来Shi-Tomasi对这个做了改进直接把min(λ1, λ2)当成响应值效果往往更好OpenCV里对应的函数是goodFeaturesToTrack原理和Harris一脉相承。3.2 OpenCV实现cornerHarris函数参数与完整示例先交代一下环境。我用的是Python的OpenCVPython 3.9到3.12版本我都跑过直接pip安装就行pip install opencv-python opencv-contrib-python如果你在用Anaconda也可以conda install -c conda-forge opencvcornerHarris的接口非常简洁cv2.cornerHarris(src, blockSize, ksize, k)参数含义如下src输入图像必须是单通道8位或浮点型灰度图blockSize计算结构张量时考虑的邻域大小常见取2到6ksizeSobel算子求梯度时的卷积核大小必须为奇数k角点响应函数中的自由参数经验值0.04到0.06给出一个完整且可直接运行的示例我加进了可视化环节import cv2 import numpy as np import matplotlib.pyplot as plt img cv2.imread(chessboard.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray np.float32(gray) dst cv2.cornerHarris(gray, blockSize2, ksize3, k0.04) # 膨胀是让角点位置更明显便于观察不是检测的必要步骤 dst cv2.dilate(dst, None) img[dst 0.01 * dst.max()] [0, 0, 255] plt.figure(figsize(10, 8)) plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) plt.title(Harris Corner Detection) plt.axis(off) plt.show()这段代码里最关键的是那一行阈值判断。dst里面存的是每个像素的R值R值越大越像角点。0.01 * dst.max()这个比例我在实际项目里一般会调到0.02到0.05因为真正常见的做法是先把角点坐标提取出来而不是直接改原图像素。下面这段更适合工程使用# 提取角点坐标 corners np.where(dst 0.02 * dst.max()) corners list(zip(corners[1], corners[0])) # (x, y) 坐标 for x, y in corners: cv2.circle(img, (x, y), 3, (0, 255, 0), -1)关于Harris的参数有一个我踩过很多次的坑blockSize取太小比如1结构张量对噪声极度敏感图像上会出现密密麻麻的假角点取太大比如8以上角点定位精度下降边缘附近会误检出一堆点。从我的经验看室内场景2到4比较合适纹理密集的场景可以适当调大。3.3 Harris的两个先天短板尺度和旋转Harris角点本身不具备尺度不变性。一张图里的桌角在近距离拍摄时角点响应很强远距离拍摄时它在图像上可能只占一两个像素窗口内的梯度结构完全变了检测结果自然对不上。同样地图像旋转后边缘梯度的方向改变了虽然Harris的响应函数对旋转有一定对称性但窗口是方的实际效果还是会打折扣。这两个短板决定了Harris适合用在标定板检测、图像配准等相对受控的场景。真要到自然场景的匹配、识别就得靠SIFT或者ORB。4. ORB检测与描述一体化的实时方案ORB全称Oriented FAST and Rotated BRIEF从名字就能看出它由三部分拼装而成FAST关键点检测、灰度质心法定向、rBRIEF描述子。下面逐一拆开。4.1 FAST速度优先的关键点检测FAST的思路极其暴力。取一个像素p以它为中心画一个半径3的圆圆上有16个像素。如果这16个像素里有连续N个像素的灰度明显比p亮或者明显比p暗就认为p是角点。N一般取9或12对应FAST-9、FAST-12。这个检测逻辑里没有乘法、没有矩阵运算只有像素灰度比较所以FAST的速度快到离谱这也是ORB能实时运行的根本原因。但FAST有一个明显问题它检测出来的点经常扎堆一个角点附近可能出现好几个响应。所以OpenCV在FAST检测后默认会做非极大值抑制只保留局部响应最大的那个。FAST还有一个特点值得注意它对边缘特别敏感沿着物体轮廓会检出一排密密麻麻的点。这些点不是真正的角点但对ORB来说影响不大因为后面还有描述子匹配阶段来筛选。4.2 尺度金字塔与灰度质心法ORB如何“曲线救国”ORB的尺度不变性靠的是图像金字塔。它把原图逐层降采样得到不同分辨率的图像层然后在每一层上用FAST检测关键点。这样近处的角点在高分辨率层被检测到远处的角点在低分辨率层被检测到匹配时跨层找对应关系就实现了有限的尺度不变性。方向不变性则靠灰度质心法这个思路很巧妙。对每个关键点取它周围的一个图像块算出这个图像块的灰度质心[ m_{pq} \sum_{x,y} x^p y^q I(x,y) ]质心坐标为[ C \left( \frac{m_{10}}{m_{00}}, \frac{m_{01}}{m_{00}} \right) ]从关键点坐标指向质心的向量方向就定义为该关键点的主方向[ \theta \operatorname{atan2}(m_{01}, m_{10}) ]有了这个主方向后续的BRIEF描述子可以按这个方向旋转后再采样从而获得旋转不变性。这里有个细节灰度质心法要求图像块是圆形的这样旋转不会导致采样范围变化ORB实际实现时取了以关键点为中心半径为r的圆形区域来计算矩。4.3 BRIEF与rBRIEF把图像块压缩成二进制指纹BRIEF描述子的思路是在关键点周围随机选取若干对像素点比较每对点的灰度大小关系结果大于就记1、小于就记0把这些结果拼成一个二进制串。比如选256对点就得到一个256位的二进制描述子。描述子之间用汉明距离比较也就是二进制串中有多少位不同。这个距离可以通过位异或操作一条指令算出比SIFT那样算128维浮点向量的欧氏距离快得多。但原始的BRIEF对旋转非常敏感。图像一转那些随机点对的相对位置关系变化了描述子就会面目全非。ORB的改进在于先用主方向角θ把所有点对坐标旋转到统一坐标系再计算BRIEF描述子。改完之后这个描述子被命名为steered BRIEF。不过steered BRIEF有一个问题它的方差降低、区分度变差因为旋转操作让点对之间的相关性上升了。于是ORB作者又做了一步关键改进从所有可能的点对中通过贪婪搜索挑选出一批方差高、相关性低的点对这批精选的点对就是rBRIEF。rBRIEF是ORB匹配效果的重要保障它保证了二进制描述子在保持高速比较的同时仍然有足够的区分度。4.4 OpenCV实现ORB_create参数与完整匹配流程ORB在OpenCV里的接口从Python到C几乎完全一样。一个完整的特征提取加匹配示例import cv2 import numpy as np img1 cv2.imread(left.png, cv2.IMREAD_GRAYSCALE) img2 cv2.imread(right.png, cv2.IMREAD_GRAYSCALE) orb cv2.ORB_create( nfeatures1000, scaleFactor1.2, nlevels8, edgeThreshold31, firstLevel0, WTA_K2, scoreTypecv2.ORB_HARRIS_SCORE, patchSize31, fastThreshold20 ) kp1, des1 orb.detectAndCompute(img1, None) kp2, des2 orb.detectAndCompute(img2, None) # 暴力匹配器汉明距离 bf cv2.BFMatcher(cv2.NORM_HAMMING, crossCheckTrue) matches bf.match(des1, des2) # 按距离排序取前50个 matches sorted(matches, keylambda x: x.distance)[:50] result cv2.drawMatches(img1, kp1, img2, kp2, matches, None, flagscv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS) cv2.imshow(ORB Matching, result) cv2.waitKey(0) cv2.destroyAllWindows()ORB_create这些参数里面nfeatures是最大特征点数scaleFactor是金字塔缩放因子越小金字塔层数越密但计算量越大。scoreType有两种ORB_HARRIS_SCORE和ORB_FAST_SCORE前者用Harris响应打分、角点质量更好但慢一些后者用FAST打分、更快但点比较集中。我的经验是一般场景用默认的HARRIS_SCORE就行追求极致速度才切到FAST_SCORE。还有两个参数容易被忽略但影响很大patchSize和fastThreshold。patchSize是计算描述子时的图像块大小如果做旋转不变这个值不能太小31是比较合理的默认值。fastThreshold是FAST检测的灰度差阈值默认20偏保守如果图像纹理很弱导致特征点太少可以把阈值降到10左右试试。5. 实测对比同一场景下Harris、ORB的差异化表现5.1 旋转与光照变换下的表现我拿了一套自己办公室场景的照片做实验。原图是一张桌面俯视图有键盘、鼠标、纸张然后把手机旋转约45度再拍一张。用ORB提取并匹配特征点结果匹配上了200多对其中绝大多数是正确的。再看Harris旋转后的角点坐标和原图坐标直接对不上需要额外做模板匹配才能勉强对齐。光照变化测试我做了关灯开灯两组。ORB在这种光照变化下匹配对数会明显下降但剩下的匹配对大多是可靠的。Harris这边光照变化对角点检测本身影响相对小因为角点结构在明暗变化下梯度方向基本保留但如果你只用Harris检测点而没有可靠的描述子方案后续匹配依然无从谈起。5.2 尺度变化下的表现把纸张从镜头前30厘米移到60厘米焦距不变物距拉大一倍。ORB因为有多层金字塔近处检测到的特征点和远处检测到的特征点还能对上一部分匹配对数大概是同尺度下的三分之一这个表现够用了。Harris在这种场景下基本是全军覆没的状态尺度一变窗口内的灰度梯度结构全变了。5.3 速度对比实测同一张1280x720图像单次特征提取时间Harris大约12到15毫秒ORB的默认配置是20到25毫秒。这个结果可能会让一些人意外ORB这么复杂怎么反而比Harris慢原因有两个ORB要建立8层金字塔每层都要跑FAST检测这本身就有固定开销另外rBRIEF描述子的计算虽然比SIFT快得多但比Harris那种纯响应计算还是要贵一些。ORB的真正优势体现在“提取匹配”全流程上加上汉明距离的暴力匹配ORB全流程大约40毫秒如果用FLANN匹配还能更快而Harris全程算下来反而不知道怎么描述特征点陷入了巧妇难为无米之炊的境地。5.4 实际选型建议什么场景选什么算法直接给结论棋盘格角点检测、相机标定优先用Harris或Shi-Tomasi检测稳定、定位准配合OpenCV的findChessboardCorners使用实时视觉SLAM、AR跟踪ORB是目前工程上的默认选择离线高精度三维重建SIFT仍然是效果天花板ORB可以作为加速初选手机端或嵌入式设备上跑特征匹配ORB几乎是唯一靠谱的选择选型的关键是你最在意什么定位精度、匹配稳定性、计算速度。通常三者不能兼得得做取舍。6. 实战中出现频率极高的几个问题与排查思路6.1 环境安装阶段的问题网上关于OpenCV安装的提问非常多。如果你的pip安装遇到超时或下载慢可以用国内镜像源pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simpleWindows上用Visual Studio配C OpenCV的同学记住一个关键点库的版本要和编译器位数一致。VS项目配置里把opencv的build目录下x64/vc16/lib加入附加依赖目录然后记得把bin目录下的opencv_world470.dll复制到exe旁边否则运行时会报找不到dll。C版和Python版接口高度一致但C里Mat类型和Python里numpy数组之间的差异是新手最常懵的地方。6.2 no module named cv2 的排查链条一个非常常见的报错modulenotfounderror: no module named opencv。注意正确的导入包名是cv2不是opencv。Python里装的是opencv-python导入的是cv2这个命名是历史遗留官方故意保留的。排查顺序如下确认当前用的是哪个Python解释器在Pycharm里经常出现终端用的是anaconda的python而项目解释器是虚拟环境里的两边包不互通pip list看看opencv-python在不在列表里如果装的是opencv-contrib-python不用重复装opencv-python两个包同时存在容易冲突如果依然报错卸载重装pip uninstall opencv-python opencv-contrib-python然后装其中一个6.3 ORB匹配结果杂乱无章时怎么筛ORB的原始匹配往往夹杂不少误匹配尤其当图片里有重复纹理或者遮挡时。最经典的处理手段是比例测试# 使用 knnMatch 取每个点的前两个最近邻匹配 bf cv2.BFMatcher(cv2.NORM_HAMMING) matches bf.knnMatch(des1, des2, k2) good [] for m, n in matches: if m.distance 0.75 * n.distance: good.append(m)这个0.75的灵感来自SIFT领域的经典论文意思是如果第一个匹配点距离和第二个匹配点距离太接近说明这个匹配点的唯一性不高属于模糊匹配直接丢弃。处理完之后再用RANSAC求单应矩阵或基础矩阵还能进一步剔除离群点。6.4 FLANN匹配ORB描述子时的配置要点用FLANN做ORB匹配时需要明确告诉匹配器用LSH局部敏感哈希索引因为ORB的二进制描述子不能直接用KDTree。一个能跑通的配置FLANN_INDEX_LSH 6 index_params dict( algorithmFLANN_INDEX_LSH, table_number6, key_size12, multi_probe_level1 ) search_params dict(checks50) flann cv2.FlannBasedMatcher(index_params, search_params)我自己因为默认用了KDTree索引FLANN直接抛出类型不匹配错误的次数两只手数不过来。记住一个原则SIFT、SURF这类浮点描述子用KDTreeORB、BRISK这类二进制描述子用LSH。7. 个人经验谈做特征提取项目时的几个通盘建议先从最朴素的建议说起。正式跑模型之前先在你的真实样本上做一次快速可视化检查把检测到的关键点画出来看看位置是否合理、数量是否合适。如果关键点全挤在图像边缘多半是edgeThreshold太小如果整幅图就零星几个点先检查图像是不是太暗或者太模糊。代码层面我习惯把特征提取封装成一个独立的类输入灰度图输出关键点和描述子。这样后面切换算法时只改一行配置。配合Python的dataclass、C里构造函数传入参数都能实现这个效果。OpenCV不同版本之间ORB_create的默认参数略有差异如果升级了opencv-python后发现结果变了先查版本日志。后续要扩展的话建议关注两个方向一是局部特征描述子的后续升级比如BEBLID匹配精度比ORB的rBRIEF更高而且速度几乎不损失二是深度学习的特征点方案像SuperPoint、LoFTR在小纹理区域和极端视角变化下的表现远超传统特征缺点是推理需要GPU、过高的算力要求限制了实时场景的使用。理解了Harris到ORB这一路的设计取舍再去看这些新方法就会轻松很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进