第四章 视觉前端:特征点提取与跟踪
视觉前端,说白了就是让机器人「看见」并「记住」周围的环境。我做了这么多年SLAM,最深的体会是:前端做不好,后端再牛的优化算法也白搭。今天咱们就聊聊视觉前端的三个核心模块——特征点提取、特征匹配和光流跟踪。
4.1 特征点提取:ORB vs SuperPoint
特征点提取,就是让算法找到图像中那些「与众不同」的像素点。这些点得具备可重复性——换个角度、换个光照,还能被找到。
4.1.1 ORB特征:经典中的经典
ORB(Oriented FAST and Rotated BRIEF)是我个人最常用的传统特征。它把FAST角点检测和BRIEF描述子结合,还加了旋转不变性。
为什么选ORB?三个理由:
- 快——比SIFT快两个数量级,嵌入式设备也能跑
- 够用——旋转不变性、尺度不变性(图像金字塔)都有
- 开源——OpenCV直接调用,不用折腾
我在项目中遇到过一个问题:ORB在纹理稀疏的场景下,特征点数量会骤降。比如白墙、光滑地面,FAST角点检测基本失效。这时候就得靠其他传感器补位,或者降低特征点阈值。
核心参数调优:
- nFeatures:一般设1000-2000,太少容易跟丢,太多拖慢速度
- scaleFactor:1.2,金字塔缩放因子,太大尺度变化剧烈,太小计算量大
- nlevels:8层,足够覆盖常见的尺度变化
- edgeThreshold:31,边缘阈值,避免在图像边界提取不稳定特征
// ORB特征提取示例代码
cv::Ptr<cv::ORB> orb = cv::ORB::create(
1000, // nFeatures
1.2f, // scaleFactor
8, // nlevels
31, // edgeThreshold
0, // firstLevel
2, // WTA_K
cv::ORB::HARRIS_SCORE, // scoreType
31, // patchSize
20 // fastThreshold
);
std::vector<cv::KeyPoint> keypoints;
cv::Mat descriptors;
orb->detectAndCompute(image, cv::noArray(), keypoints, descriptors);
4.1.2 SuperPoint:深度学习的新选择
SuperPoint是2018年提出的基于学习的特征提取网络。说实话,我第一次用的时候被惊艳到了——在纹理稀疏场景下,它提取的特征点比ORB稳定得多。
SuperPoint的核心思路:
- 用自监督学习训练,不需要人工标注
- 同时输出特征点位置和描述子
- 对光照、视角变化鲁棒性更强
但SuperPoint也有短板:
- 需要GPU加速,嵌入式设备跑不动
- 模型体积大,部署麻烦
- 实时性不如ORB(即使有GPU)
我的建议:如果你做的是PC端SLAM,或者有GPU的机器人平台,SuperPoint是更好的选择。如果是嵌入式、无人机这类资源受限的场景,老老实实用ORB。
4.2 特征匹配:暴力匹配 vs FLANN
特征点提取完了,接下来就是匹配——找到两帧图像中对应的特征点。匹配质量直接决定位姿估计的精度。
4.2.1 暴力匹配(Brute-Force)
暴力匹配,顾名思义,就是把每个特征点跟另一帧的所有特征点算一遍距离。简单粗暴,但有效。
什么时候用暴力匹配?
- 特征点数量少(几百个级别)
- 对精度要求高,不在乎那点计算时间
- 调试阶段,方便排查问题
// 暴力匹配示例
cv::BFMatcher matcher(cv::NORM_HAMMING);
std::vector<cv::DMatch> matches;
matcher.match(descriptors1, descriptors2, matches);
// 筛选好的匹配(距离阈值法)
double min_dist = 100, max_dist = 0;
for (int i = 0; i < descriptors1.rows; i++) {
double dist = matches[i].distance;
if (dist < min_dist) min_dist = dist;
if (dist > max_dist) max_dist = dist;
}
std::vector<cv::DMatch> good_matches;
for (int i = 0; i < descriptors1.rows; i++) {
if (matches[i].distance <= std::max(2 * min_dist, 30.0)) {
good_matches.push_back(matches[i]);
}
}
4.2.2 FLANN匹配:大规模场景的救星
当特征点数量上万时,暴力匹配就扛不住了。FLANN(Fast Library for Approximate Nearest Neighbors)用KD-Tree或K-Means树做近似搜索,速度能快一个数量级。
我曾经在做一个大场景重建项目,每帧提取了5000个特征点,暴力匹配一帧要200ms。换成FLANN后,降到30ms,实时性一下就上来了。
注意:FLANN是近似匹配,偶尔会漏掉真正的匹配对。如果你做的是高精度视觉定位,建议用暴力匹配+交叉验证。
// FLANN匹配示例
cv::FlannBasedMatcher flann_matcher(new cv::flann::KDTreeIndexParams(5));
std::vector<std::vector<cv::DMatch>> knn_matches;
flann_matcher.knnMatch(descriptors1, descriptors2, knn_matches, 2);
// 比率测试(Lowe's ratio test)
const float ratio_thresh = 0.7f;
std::vector<cv::DMatch> good_matches;
for (size_t i = 0; i < knn_matches.size(); i++) {
if (knn_matches[i][0].distance < ratio_thresh * knn_matches[i][1].distance) {
good_matches.push_back(knn_matches[i][0]);
}
}
4.3 光流法跟踪:LK光流
特征匹配虽然可靠,但计算量大。光流法提供了一种更轻量的选择——假设图像亮度恒定,通过像素梯度来跟踪特征点的运动。
4.3.1 LK光流原理
LK(Lucas-Kanade)光流基于三个假设:
- 亮度恒定——同一个点在相邻帧亮度不变
- 小运动——帧间位移不能太大
- 空间一致性——相邻像素运动相似
说白了,就是在一个小窗口内解一个超定方程组,求像素的位移向量。
实际应用中的坑:
我曾经在快速旋转的场景下用LK光流,结果跟踪点全飞了。为什么?因为「小运动」假设被打破了。解决办法是用图像金字塔,从粗到细逐层跟踪。
// 金字塔LK光流示例
std::vector<cv::Point2f> prev_pts, next_pts;
std::vector<uchar> status;
std::vector<float> err;
cv::calcOpticalFlowPyrLK(
prev_img, curr_img,
prev_pts, next_pts,
status, err,
cv::Size(21, 21), // 窗口大小
3, // 金字塔层数
cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 30, 0.01),
cv::OPTFLOW_USE_INITIAL_FLOW
);
// 筛选跟踪成功的点
std::vector<cv::Point2f> good_prev, good_next;
for (size_t i = 0; i < status.size(); i++) {
if (status[i]) {
good_prev.push_back(prev_pts[i]);
good_next.push_back(next_pts[i]);
}
}
4.3.2 光流 vs 特征匹配:怎么选?
| 对比维度 | LK光流 | 特征匹配 |
|---|---|---|
| 计算速度 | 快(只跟踪已知点) | 慢(需要提取+匹配) |
| 鲁棒性 | 差(依赖亮度恒定假设) | 好(描述子有光照不变性) |
| 适用场景 | 帧间运动小、光照稳定 | 大运动、光照变化大 |
| 特征点管理 | 需要定期补充新点 | 每帧重新提取 |
我个人习惯的做法是:在帧间运动较小时用LK光流跟踪,每隔10-20帧做一次特征匹配来「校正」跟踪结果。这样既保证了实时性,又不会累积漂移。
4.4 本章知识体系
下面这张图展示了视觉前端的完整流程和各个模块的关系:
避坑指南:我曾经在光照剧烈变化的场景下,ORB特征匹配率从80%掉到20%。后来加了自适应阈值和直方图均衡化,才把匹配率拉回来。记住——预处理有时候比算法本身更重要。
好了,视觉前端的内容就聊到这儿。特征点提取、匹配和光流跟踪,这三板斧是SLAM的基石。你想想看,如果连特征点都找不准,后面的位姿估计、地图构建还有什么意义?
公众号:蓝海资料掘金营,微信deep3321