第三章:视觉特征提取与匹配

各位同学,今天我们来聊聊视觉SLAM里最基础、也最核心的一环——特征提取与匹配。说实话,我刚入行那会儿,觉得这步就是调个OpenCV函数,没什么技术含量。直到我在一个低纹理的仓库里做AGV定位,特征点疯狂丢失,我才意识到:特征选不好,后面全白搞

3.1 FAST角点:快,但不够聪明

FAST角点,全称是Features from Accelerated Segment Test。名字里就带个"加速",它的核心思想很简单:如果一个像素和它周围一圈的像素差别很大,那它很可能是个角点

具体怎么判断?我习惯用16个像素的圆环来做测试。如果连续N个像素(通常N=9或12)都比中心点亮或暗一个阈值,那就判定为角点。

FAST角点的核心逻辑:

  • 速度快,适合实时系统
  • 没有方向信息,旋转后匹配会出问题
  • 对尺度变化敏感,图像拉近拉远就找不到了

我在项目中遇到过一个问题:用FAST在室内走廊提取特征,结果墙上的纹理和踢脚线边缘全被当成角点。嗯,这时候就需要加一个非极大值抑制,不然特征点扎堆,匹配时全是歧义。

避坑指南:我曾经在低光照环境下用FAST,阈值设得太小,结果全是噪声点。后来我改成自适应阈值,根据图像局部亮度动态调整,效果好了很多。

3.2 ORB特征:FAST的升级版

ORB,全称Oriented FAST and Rotated BRIEF。说白了,它就是在FAST的基础上加了两个东西:方向描述子

方向怎么加?用灰度质心法。你想想看,一个角点周围像素的灰度分布,其实隐含了方向信息。我算一下质心,从角点到质心的向量,就是主方向。

描述子用的是BRIEF的改进版。BRIEF本质上是比较一对像素的灰度大小,生成一个二进制串。ORB加了个旋转补偿,让描述子具备旋转不变性。

我的经验:ORB在嵌入式设备上特别好用。我在一个基于树莓派的视觉里程计项目里,用ORB提取300个特征点,每帧处理时间控制在15ms以内。相比SIFT,ORB的速度优势是数量级的。

ORB的尺度不变性怎么解决?其实它没有真正解决,而是靠图像金字塔。我一般建4层金字塔,每层缩放0.8倍,这样能应对一定范围的尺度变化。

3.3 SIFT特征简述:精度之王,但慢

SIFT,Scale-Invariant Feature Transform。这玩意儿是特征提取界的"老大哥",精度高、鲁棒性强,但计算量也大得吓人。

SIFT的核心思想是:在尺度空间中找极值点。它用高斯差分金字塔来模拟尺度变化,然后在不同尺度下检测关键点。每个关键点还会分配一个主方向,生成128维的描述子。

特征 旋转不变性 尺度不变性 光照鲁棒性 速度
FAST 一般 极快
ORB 有限(金字塔) 较好
SIFT 优秀

我记得有一次做户外大场景重建,用ORB匹配出来的点对,在远处建筑物上全是错的。换成SIFT后,虽然每帧处理时间从20ms变成了200ms,但匹配质量明显提升。所以,选特征就是选取舍

注意:SIFT是有专利的,商用项目要小心。不过OpenCV里已经把它放到nonfree模块了,学术研究随便用。

3.4 特征匹配:暴力匹配 vs FLANN

特征提取完了,接下来就是匹配。说白了,就是找两帧图像里哪些特征点是同一个物理点。

3.4.1 暴力匹配

暴力匹配,Brute-Force Matcher。思路简单粗暴:对每个特征点,计算它和另一帧所有特征点的距离,取最近的那个

距离怎么算?二进制描述子(ORB)用汉明距离,浮点描述子(SIFT)用欧氏距离。

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

暴力匹配的优点是简单、准确。缺点是慢——特征点一多,复杂度是O(n²)。我试过在5000个特征点的情况下做暴力匹配,一帧花了300ms,这显然不现实。

3.4.2 FLANN匹配

FLANN,Fast Library for Approximate Nearest Neighbors。它用KD-Tree或LSH等数据结构,做近似最近邻搜索。速度比暴力匹配快一个数量级。

// FLANN匹配示例
cv::FlannBasedMatcher flannMatcher;
std::vector<cv::DMatch> matches;
flannMatcher.match(descriptors1, descriptors2, matches);

我个人的习惯是:特征点少于500个用暴力匹配,多于500个用FLANN。FLANN虽然快,但偶尔会漏掉正确的匹配,不过对于SLAM系统来说,后面还有RANSAC兜底,问题不大。

小技巧:FLANN的参数需要调。我一般设置KD-Tree的树数量为4,搜索时的检查次数为32。这个配置在大多数场景下表现不错。

3.5 RANSAC剔除误匹配

匹配完的特征点对里,肯定有错的。为什么?因为图像里可能有重复纹理、光照变化、遮挡。这时候就需要RANSAC(Random Sample Consensus)来剔除误匹配。

RANSAC的核心思想是:随机选一小部分点对,拟合一个模型,然后看其他点对是否符合这个模型。符合的越多,模型越可信。

在视觉里程计里,我们通常用RANSAC来求解基础矩阵或单应矩阵。具体步骤:

  1. 随机选4对匹配点(基础矩阵需要8对,但可以用归一化方法简化)
  2. 计算模型参数
  3. 计算所有匹配点到模型的投影误差
  4. 误差小于阈值的点算内点,否则算外点
  5. 重复步骤1-4,找到内点最多的模型
// OpenCV中RANSAC求解基础矩阵
cv::Mat mask;
cv::Mat F = cv::findFundamentalMat(
    points1, points2, 
    cv::FM_RANSAC, 
    3.0,  // 重投影误差阈值
    0.99, // 置信度
    mask   // 输出内点掩码
);

关键参数:

  • 阈值:一般设1-3个像素。设太小会丢掉好点,设太大则误匹配混进来
  • 迭代次数:理论上越多越好,但实际设500-2000次就够了
  • 置信度:0.99表示99%的概率至少有一次采样全是内点

我曾经在一个动态场景里做RANSAC,结果发现内点率只有30%。为什么?因为场景里有行人走动,这些运动点对基础矩阵来说是"外点",但RANSAC把它们全剔除了。后来我加了运动一致性检查,效果才好转。

避坑指南:RANSAC不是万能的。如果误匹配率超过50%,RANSAC基本就失效了。这时候你需要回头检查特征提取和匹配的环节,而不是死磕RANSAC的参数。

3.6 总结与实战建议

好了,这一章的内容就这些。我最后给几个实战建议:

  • 选特征:嵌入式用ORB,PC端追求精度用SIFT,实时性要求极高用FAST
  • 匹配策略:先粗匹配(FLANN),再精匹配(RANSAC),最后用双向匹配再过滤一遍
  • 阈值调优:不要用固定阈值。我习惯根据图像亮度分布动态调整FAST阈值
  • 调试技巧:把匹配结果可视化出来,一眼就能看出问题在哪

下一章我们会讲光流法,那是一种完全不同的特征跟踪思路。到时候你会看到,有些场景下光流比特征匹配更靠谱。

嗯,今天就到这里。有问题随时问我。