第四章 视觉前端:特征点提取与跟踪

视觉前端,说白了就是让机器人「看见」并「记住」周围的环境。我做了这么多年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(传统) | SuperPoint(深度学习) 特征匹配 暴力匹配 | FLANN 光流跟踪 LK光流(金字塔) 特征点对应关系 图:视觉前端核心流程——从图像到特征点对应关系

避坑指南:我曾经在光照剧烈变化的场景下,ORB特征匹配率从80%掉到20%。后来加了自适应阈值和直方图均衡化,才把匹配率拉回来。记住——预处理有时候比算法本身更重要。

好了,视觉前端的内容就聊到这儿。特征点提取、匹配和光流跟踪,这三板斧是SLAM的基石。你想想看,如果连特征点都找不准,后面的位姿估计、地图构建还有什么意义?


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