一、ORB特征点:为什么它成了视觉SLAM的标配?

做SLAM这些年,我试过不少特征点。SIFT太慢,SURF有专利问题,直到ORB出现,才真正解决了实时性与鲁棒性的矛盾。说白了,ORB就是FAST角点加上BRIEF描述子,再做一些改进。你想想看,一个能在手机上跑30帧的特征提取方法,谁不爱?

我个人习惯把ORB拆成三块来理解:

  • 特征点提取:用FAST找角点
  • 方向信息:用灰度质心法算主方向
  • 描述子:用改进的BRIEF生成二进制描述

嗯,这里要注意,ORB的全称是Oriented FAST and Rotated BRIEF,重点就在"Oriented"和"Rotated"这两个词上。没有方向信息的FAST,旋转后就匹配不上了。

二、FAST角点:快是真的快,但问题也不少

2.1 FAST的核心思想

FAST的全称是Features from Accelerated Segment Test。它的思路很简单:如果一个像素和它周围一圈的像素差别很大,那它很可能是个角点。

具体怎么做?我画个图你就明白了:

p 1 2 3 4 5 6 7 8 9 10 11 12 图1:FAST-16角点检测模板,p为候选像素

判断规则很简单:如果圆周上有连续N个像素的亮度超过或低于中心像素一定阈值,就认为p是角点。N通常取12,这就是FAST-12。我最早用OpenCV跑FAST时,一张640×480的图只要几毫秒,比Harris快了近一个数量级。

2.2 FAST的改进:多尺度与方向

但FAST有个致命问题——它没有尺度不变性。图像一缩放,角点就找不到了。解决办法是构建图像金字塔,每层都提取FAST角点。

还有一个问题:FAST没有方向信息。ORB的解决办法是用灰度质心法:

// 计算图像块的矩
double m00 = 0, m10 = 0, m01 = 0;
for (int y = -r; y <= r; y++) {
    for (int x = -r; x <= r; x++) {
        double val = image.at<uchar>(cy+y, cx+x);
        m00 += val;
        m10 += x * val;
        m01 += y * val;
    }
}
// 质心坐标
double cx = m10 / m00;
double cy = m01 / m00;
// 方向角度
double angle = atan2(cy, cx);
我的经验: 灰度质心法算出来的方向在纹理丰富的区域很稳定,但在弱纹理区域容易飘。我曾经在室内白墙场景吃过亏,后来加了自适应阈值才解决。

三、BRIEF描述子:二进制才是王道

3.1 BRIEF的原理

BRIEF描述子说白了就是一组二进制位。它在特征点周围随机选128对或256对像素,比较每对的亮度大小,亮的记1,暗的记0。

举个例子,假设我们选了3对像素:

  • 对1:p1(10,20) 和 p2(15,25),p1更亮 → 1
  • 对2:p3(30,40) 和 p4(35,45),p4更亮 → 0
  • 对3:p5(50,60) 和 p6(55,65),p5更亮 → 1

最终描述子就是101...(共256位)。两个描述子的相似度用汉明距离衡量——数一下有多少位不同就行。这个计算用CPU的POPCNT指令,一次能算64位,快得飞起。

3.2 旋转不变性:steered BRIEF

原始BRIEF没有旋转不变性。ORB的改进是:根据之前算出的方向角,把像素对旋转到对应方向再比较。

我记得第一次实现这个时踩了个坑——旋转后的像素坐标是浮点数,需要插值。直接用最近邻插值会导致描述子对旋转敏感,后来改用双线性插值才稳定。

四、特征匹配:暴力匹配 vs FLANN

4.1 暴力匹配

暴力匹配就是拿每个查询描述子,跟所有训练描述子算一遍距离,取最近的那个。代码很简单:

// OpenCV暴力匹配示例
cv::BFMatcher matcher(cv::NORM_HAMMING);
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);

暴力匹配的复杂度是O(N×M),N和M分别是两幅图的特征点数。如果每幅图有2000个点,就要算400万次汉明距离。在CPU上大概要几十毫秒,勉强能接受。

4.2 FLANN匹配

FLANN(Fast Library for Approximate Nearest Neighbors)用KD-Tree或LSH来加速。对于二进制描述子,OpenCV的FLANN默认用LSH(局部敏感哈希)。

// FLANN匹配示例
cv::FlannBasedMatcher matcher(
    new cv::flann::LSHParams(20, 10, 2)
);
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);
什么时候用哪个?
  • 特征点少于500:暴力匹配更快,因为建索引也有开销
  • 特征点超过1000:FLANN优势明显,能快3-5倍
  • 精度要求极高:暴力匹配更可靠,FLANN是近似解

五、特征点均匀化策略:别让特征扎堆

做SLAM时最怕什么?特征点全挤在纹理丰富的区域,其他地方一个都没有。这样位姿估计会退化,甚至直接崩掉。

我常用的均匀化策略有几种:

5.1 网格化提取

把图像分成M×N的网格,每个网格最多提取K个特征点。这样能保证特征点分布均匀。

// 网格化提取伪代码
int grid_rows = 4, grid_cols = 4;
int max_per_cell = 50;
for each grid cell:
    extract FAST corners in this cell
    keep top max_per_cell by Harris response
    add to final keypoints

5.2 四叉树均匀化

ORB-SLAM用的就是这个方法。它把图像空间递归四等分,直到每个节点包含的特征点数量满足要求。我特别喜欢这个方法,因为它能自适应地分配特征点——纹理丰富的地方节点多,纹理少的地方节点少。

避坑指南: 我曾经在ORB-SLAM里把四叉树的最大深度设成了10,结果在低纹理场景下,每个叶子节点只有一个特征点,导致特征点数量严重不足。后来我改成根据图像大小动态调整深度,才解决问题。

5.3 非极大值抑制

FAST角点经常扎堆出现。非极大值抑制(NMS)能保留响应最强的那个,去掉周围的弱角点。我一般用Harris响应值作为排序依据,效果比直接用FAST得分好。

六、实战建议:ORB参数调优

最后分享一些我在项目中积累的参数经验:

参数 推荐值 说明
nfeatures 1000-2000 特征点数量,室内1000够用,室外建议2000
scaleFactor 1.2 金字塔缩放因子,越大尺度覆盖越广
nlevels 8 金字塔层数,8层能覆盖约2.5倍的尺度变化
fastThreshold 20 FAST阈值,越大角点越少但越稳定
patchSize 31 描述子计算窗口,31像素是经验值

嗯,说到参数调优,我记得有一次在无人机上跑SLAM,图像分辨率是1920×1080,我设了3000个特征点,结果每帧处理时间飙到了80ms。后来降到1500个点,配合四叉树均匀化,稳定在30ms以内。所以我的建议是:特征点数量不是越多越好,够用就行

ORB这套组合拳,从FAST的快速检测,到BRIEF的紧凑描述,再到均匀化策略保证分布质量,每一步都有它的道理。你在实际项目中遇到问题时,不妨从这几个环节逐一排查,往往能找到症结所在。


公众号:蓝海资料掘金营,微信deep3321