第三章 图像特征提取与匹配
各位同学好,我是你们的讲师。今天我们来聊聊视觉SLAM里最基础也最关键的一环——图像特征提取与匹配。说实话,我刚开始做SLAM的时候,觉得这部分就是调调OpenCV的API,后来踩了不少坑才明白,特征选得好不好,直接决定了整个系统的上限。
咱们先理清一个概念:为什么要提取特征?说白了,就是让机器人能认出「同一个点」。相机拍到的是一堆像素,但像素本身没有语义。我们需要找到那些独特的、稳定的点——比如墙角、桌角、纹理丰富的地方。这些点就是特征点。
3.1 Harris角点检测
Harris角点是最经典的方法之一。它的核心思想很简单:窗口在各个方向移动时,灰度变化都很大,那就是角点。
我记得刚入行时,导师让我用Harris做特征提取。我调了个阈值,结果要么全是角点,要么一个都没有。后来才明白,阈值这东西得根据图像动态调整。你想想看,一张白墙和一张布满纹理的木桌,同样的阈值效果能一样吗?
Harris的数学本质是计算自相关矩阵的特征值。两个特征值都大,就是角点;一个大一个小,是边缘;两个都小,是平坦区域。嗯,这里要注意,Harris对尺度变化很敏感——说白了,你离得近看到的角点,离远了可能就变成边缘了。
// Harris角点检测示例
cv::Mat src = cv::imread("scene.jpg");
cv::Mat gray;
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
cv::Mat harris;
cv::cornerHarris(gray, harris, 2, 3, 0.04);
// 阈值处理
cv::Mat harris_norm;
cv::normalize(harris, harris_norm, 0, 255, cv::NORM_MINMAX);
cv::Mat harris_scaled;
convertScaleAbs(harris_norm, harris_scaled);
// 提取角点坐标
std::vector<cv::Point> corners;
for(int i = 0; i < harris_norm.rows; i++) {
for(int j = 0; j < harris_norm.cols; j++) {
if((int)harris_norm.at<float>(i,j) > 200) {
corners.push_back(cv::Point(j,i));
}
}
}
3.2 SIFT特征
SIFT(尺度不变特征变换)是特征提取界的「老大哥」。它解决了Harris最头疼的问题——尺度变化。为什么SIFT能做到?因为它构建了高斯差分金字塔,在不同尺度下寻找极值点。
我在做室外大场景SLAM时,特别喜欢用SIFT。车辆从远处开到近处,特征点尺度变化很大,但SIFT依然能稳定匹配。代价是什么?慢。非常慢。一个800×600的图像,提取SIFT特征可能要几百毫秒,这在实时SLAM里是致命的。
SIFT的每个特征点有一个128维的描述子。为什么是128维?因为SIFT把特征点周围的16×16区域分成4×4的子块,每个子块统计8个方向的梯度直方图,4×4×8=128。这个设计很巧妙,保证了旋转不变性——你想想看,图像旋转了,梯度方向也跟着转,但相对关系不变。
3.3 SURF特征
SURF是SIFT的加速版。它用Hessian矩阵的行列式来检测特征点,用Haar小波响应来构建描述子。说白了,就是把SIFT里那些复杂的计算简化了,用积分图像加速。
我个人习惯是:如果硬件性能允许,优先用SIFT;如果追求实时性,用SURF。但说实话,现在ORB出来之后,SURF的位置有点尴尬——比SIFT快但不如SIFT准,比ORB准但不如ORB快。
| 特征 | 检测速度 | 匹配精度 | 尺度不变 | 旋转不变 | 专利 |
|---|---|---|---|---|---|
| Harris | 快 | 低 | 否 | 是 | 无 |
| SIFT | 慢 | 高 | 是 | 是 | 有 |
| SURF | 中 | 中高 | 是 | 是 | 有 |
| ORB | 极快 | 中 | 否 | 是 | 无 |
3.4 ORB特征
ORB(Oriented FAST and Rotated BRIEF)是目前视觉SLAM里最常用的特征。为什么?因为它快,而且免费。ORB = FAST角点检测 + BRIEF描述子,再加上方向和尺度信息。
FAST角点检测的思路很暴力:一个像素周围有16个像素,如果其中有连续N个像素比中心像素亮或暗,那就是角点。N通常取9或12。这个方法快得离谱,但缺点是没有方向信息,也不抗尺度变化。
ORB的改进在于:用灰度质心法给FAST角点加方向,用rBRIEF(旋转后的BRIEF)做描述子。这样就有了旋转不变性。至于尺度不变性,ORB通过构建图像金字塔来实现——虽然不如SIFT精细,但够用了。
// ORB特征提取示例
cv::Ptr<cv::ORB> orb = cv::ORB::create(500); // 最多提取500个特征点
std::vector<cv::KeyPoint> keypoints;
cv::Mat descriptors;
orb->detectAndCompute(image, cv::Mat(), keypoints, descriptors);
// 绘制特征点
cv::Mat img_with_keypoints;
cv::drawKeypoints(image, keypoints, img_with_keypoints, cv::Scalar::all(-1));
3.5 特征匹配
特征提取完了,接下来就是匹配——把两帧图像里的特征点对应起来。常用的方法有两种:暴力匹配和FLANN匹配。
3.5.1 暴力匹配
暴力匹配(Brute-Force Matching)就是最朴素的思路:对于第一张图的每个特征点,计算它与第二张图所有特征点的距离,取最近的那个。说白了,就是穷举搜索。
暴力匹配的优点是简单、准确。缺点是慢——如果两张图各有1000个特征点,就要计算100万次距离。在实时SLAM里,这通常不可接受。
// 暴力匹配示例
cv::BFMatcher matcher(cv::NORM_HAMMING); // ORB用汉明距离
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);
// 按距离排序,取前50个好的匹配
std::sort(matches.begin(), matches.end());
matches.resize(50);
3.5.2 FLANN匹配
FLANN(Fast Library for Approximate Nearest Neighbors)是近似最近邻搜索。它不保证找到绝对最近的点,但保证找到「足够近」的点,而且速度快得多。
FLANN的核心是构建索引结构,比如KD-Tree或LSH(局部敏感哈希)。对于高维描述子(比如SIFT的128维),KD-Tree的效果不错;对于二值描述子(比如ORB的256位二进制),LSH更合适。
// FLANN匹配示例(适用于SIFT/SURF)
cv::FlannBasedMatcher flann_matcher;
std::vector<cv::DMatch> matches;
flann_matcher.match(descriptors1, descriptors2, matches);
// 或者用knnMatch找最近的两个匹配
std::vector<std::vector<cv::DMatch>> knn_matches;
flann_matcher.knnMatch(descriptors1, descriptors2, knn_matches, 2);
3.6 RANSAC去除误匹配
匹配完了,你会发现有很多错误匹配。为什么?因为图像里可能有重复纹理、光照变化、遮挡。这时候就需要RANSAC(随机采样一致性)来剔除误匹配。
RANSAC的思路很巧妙:随机选几对匹配点,计算一个变换模型(比如单应矩阵或基础矩阵),然后看有多少其他匹配点符合这个模型。符合的越多,说明模型越靠谱。重复这个过程,找到最优模型。
我曾经在做一个室内定位项目时,匹配率只有40%左右。用了RANSAC之后,虽然匹配数量减少了,但剩下的都是正确的,位姿估计精度直接翻倍。嗯,这里要提醒大家:RANSAC不是万能的。如果误匹配率超过50%,RANSAC也可能失效。
// RANSAC去除误匹配示例
std::vector<cv::Point2f> pts1, pts2;
for(const auto& match : matches) {
pts1.push_back(keypoints1[match.queryIdx].pt);
pts2.push_back(keypoints2[match.trainIdx].pt);
}
cv::Mat mask;
cv::Mat H = cv::findHomography(pts1, pts2, cv::RANSAC, 3.0, mask);
// 筛选内点
std::vector<cv::DMatch> inlier_matches;
for(int i = 0; i < mask.rows; i++) {
if(mask.at<uchar>(i)) {
inlier_matches.push_back(matches[i]);
}
}
最后总结一下我的个人经验:
- 实时性要求高(比如无人机、手持设备):用ORB + FLANN + RANSAC
- 精度要求高(比如建图、三维重建):用SIFT + 暴力匹配 + RANSAC
- 纹理丰富:Harris或ORB都行
- 纹理稀疏:SIFT或SURF,因为它们的描述子更鲁棒
特征提取与匹配是视觉SLAM的基石。这一步做不好,后面的位姿估计、回环检测都是空中楼阁。希望大家在实际项目中多试几种组合,找到最适合自己场景的方案。下一章我们会讲光流法,那又是另一种思路了。