3. 图像特征提取与匹配:角点检测、特征描述与匹配
图像特征提取与匹配,是视觉SLAM里最基础、也最核心的一环。说白了,就是让机器人「看懂」画面里哪些点是关键点,然后让它在不同帧之间认出同一个点。
我刚开始做SLAM时,觉得特征提取不就是找几个角点嘛,随便用个算法就行。后来在真实场景里跑了一次,才发现光照一变、视角一转,特征点全丢了,定位直接崩掉。嗯,从那以后我再也不敢轻视这一步了。
核心逻辑:特征提取 → 特征描述 → 特征匹配 → 误匹配剔除。每一步都决定了整个SLAM系统的鲁棒性。
3.1 角点检测:Harris 与 Shi-Tomasi
角点,就是图像里那些「拐弯抹角」的地方。你想想看,一面白墙上的像素点,你往左挪一格、往右挪一格,灰度值几乎不变——这种点没法用来做匹配。但角点不一样,稍微动一下,灰度变化就很大,所以特别适合做特征点。
Harris角点检测的原理,我简单说一下:它用一个窗口在图像上滑动,计算窗口内灰度变化的大小。如果往哪个方向滑变化都大,那就是角点。
数学上,它构造了一个自相关矩阵 M:
M = ∑ [ Ix² IxIy ]
[ IxIy Iy² ]
响应值 R = det(M) - k * (trace(M))²
其中 k 通常取 0.04 ~ 0.06
我在项目中遇到过一个问题:Harris的k值对结果挺敏感的。k太大,角点检测得少;k太小,又容易把边缘也当成角点。后来我习惯用Shi-Tomasi,它直接取M矩阵两个特征值中较小的那个作为响应值,更直观,也更稳定。
我的经验:Shi-Tomasi在纹理丰富的场景下表现很好,但如果你在拍白墙或者光滑地面,角点检测基本就废了。这时候得靠别的特征,比如边缘或者线特征。
3.2 尺度不变特征:SIFT 与 SURF
Harris和Shi-Tomasi有个硬伤——它们对尺度变化很敏感。你想想,一个角点在近处看是清晰的,退后几步可能就模糊了,甚至变成一条边。这时候就需要尺度不变特征。
SIFT(Scale-Invariant Feature Transform)是经典中的经典。它通过构建高斯差分金字塔(DoG),在不同尺度空间里找极值点,然后给每个点分配主方向,最后生成128维的描述子。
我记得第一次用SIFT做特征匹配时,效果确实惊艳——旋转、缩放、光照变化,它都能扛住。但代价也很明显:慢。一张640×480的图,提取几百个特征点,可能要花几百毫秒。这在实时SLAM里基本没法用。
SURF(Speeded-Up Robust Features)是SIFT的加速版。它用Hessian矩阵代替DoG,用积分图加速计算,描述子也降到了64维。速度比SIFT快3-5倍,但鲁棒性稍微差一点。
| 特征类型 | 速度 | 鲁棒性 | 适用场景 |
|---|---|---|---|
| SIFT | 慢(~300ms/帧) | 极高 | 离线建图、高精度重建 |
| SURF | 中等(~100ms/帧) | 高 | 离线或准实时 |
| ORB | 快(~15ms/帧) | 中等 | 实时SLAM(ORB-SLAM) |
注意:SIFT和SURF都是有专利保护的算法。商业项目里用它们要小心授权问题。ORB是开源的,没有这个烦恼。
3.3 ORB特征:实时SLAM的首选
ORB(Oriented FAST and Rotated BRIEF)是现在视觉SLAM里用得最多的特征。为什么?因为它又快又够用。
ORB由两部分组成:
- FAST角点检测:比较一个像素和它周围16个像素的亮度差,如果连续N个点都比中心点亮或暗,就认为是角点。速度极快。
- BRIEF描述子:在特征点周围随机选128对或256对像素,比较它们的亮度大小,生成二进制串。匹配时用汉明距离,计算速度飞快。
ORB在FAST的基础上加了方向信息(通过灰度质心法),所以具备旋转不变性。虽然它没有SIFT那样的尺度不变性,但在SLAM里,我们通常用图像金字塔来模拟多尺度,效果已经够用了。
我曾经在一个移动机器人项目里,用ORB-SLAM2跑室内环境,CPU占用率只有30%左右,帧率稳定在30fps。换成SIFT的话,直接掉到5fps,根本没法实时。
// OpenCV中ORB特征提取示例
cv::Ptr<cv::ORB> orb = cv::ORB::create(500); // 最多500个特征点
std::vector<cv::KeyPoint> keypoints;
cv::Mat descriptors;
orb->detectAndCompute(image, cv::noArray(), keypoints, descriptors);
3.4 特征匹配与误匹配剔除
特征提取完了,接下来就是匹配。说白了,就是找两帧图像里哪些特征点是同一个。
暴力匹配(Brute-Force)是最直接的方法:对第一帧的每个特征点,计算它和第二帧所有特征点的距离,取最近的那个。对于二进制描述子(如ORB),用汉明距离;对于浮点描述子(如SIFT),用欧氏距离。
FLANN(Fast Library for Approximate Nearest Neighbors)是加速版,适合大规模匹配。它用KD-Tree或LSH索引,速度比暴力匹配快很多,但会牺牲一点点精度。
匹配完以后,你会发现一个问题——误匹配太多了。尤其是纹理重复的场景,比如地板、砖墙,经常出现「张冠李戴」的情况。
误匹配剔除的常用方法:
- 距离阈值法:只保留距离小于某个阈值的匹配对。简单粗暴,但阈值不好调。
- 最近邻与次近邻比值法(Lowe's ratio test):如果最近距离 / 次近距离 < 0.7~0.8,才认为是好匹配。这是最常用的方法。
- RANSAC(随机采样一致性):随机选几对匹配点,估计一个变换模型(如单应矩阵或基础矩阵),然后看哪些匹配符合这个模型。不符合的就是误匹配。
我曾经在一个项目里,用暴力匹配后匹配对了300对,但误匹配有150对,几乎一半是错的。用了Lowe's ratio test(阈值0.75)后,剩下180对,误匹配只有10对左右。效果立竿见影。
// 使用Lowe's ratio test剔除误匹配
std::vector<cv::DMatch> good_matches;
for (size_t i = 0; i < matches.size(); i++) {
if (matches[i].distance < 0.75 * matches_ratio[i].distance) {
good_matches.push_back(matches[i]);
}
}
避坑指南:我曾经把ratio test的阈值设成0.5,结果匹配对太少,位姿估计直接失败。后来改成0.75,效果好了很多。这个值不是固定的,建议你在自己的数据集上调一下。
嗯,特征提取与匹配这部分,说白了就是「找点、描述点、认点、去噪」四个步骤。每一步都有坑,但踩过之后,你会发现SLAM系统的鲁棒性就是这么一点一点堆起来的。
公众号:蓝海资料掘金营,微信deep3321