ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于KITTI数据集与C++的特征点法视觉里程计实现指南

基于KITTI数据集与C++的特征点法视觉里程计实现指南 简介本资源是一套基于KITTI数据集的视觉里程计C实现工程面向计算机视觉方向的学习者、自动驾驶算法初学者及C图像处理实践者旨在解决单目视觉定位中相机位姿估计的核心问题。压缩包共31个文件包含3个核心cpp源码含main.cpp与practice.cpp、1个Visual Studio解决方案.sln及配套项目配置.vcxproj、.filters、编译生成物.exe、.pdb、.obj和开发缓存文件.ipch、.tlog等整体大小47.12MB结构完整可直接加载VS2019及以上版本编译运行。已有528人学习下载资源提供从图像预处理、ORB特征提取与匹配、RANSAC几何验证到PnP位姿求解的全流程代码实现覆盖KITTI数据读取接口、轨迹评估模块及关键参数调优注释便于读者理解VO系统各模块耦合逻辑并开展二次开发与性能对比实验。1. 项目概述从KITTI数据集到视觉里程计如果你正在研究自动驾驶、机器人定位或者三维视觉那么“视觉里程计”这个词对你来说一定不陌生。简单来说它就像一辆车的“眼睛”和“大脑”通过分析连续图像估算出自身在三维空间中的运动轨迹和姿态而不依赖GPS等外部信号。这对于在隧道、室内或城市峡谷等GPS信号弱或失效的场景下实现自主导航至关重要。而KITTI数据集无疑是这个领域最经典、最权威的“考场”和“训练场”。它由德国卡尔斯鲁厄理工学院和丰田美国技术研究院联合创办采集自真实的城市、乡村和高速公路环境包含了立体图像、激光雷达点云、GPS/IMU数据等多模态信息且提供了精确的基准真值。因此用KITTI数据集来开发、测试和验证视觉里程计算法是业界公认的标准流程。这个项目“cvNew_KITTI_KITTI数据集_视觉里程计C实现_”的核心就是使用C语言从零开始实现一个能够处理KITTI数据集的视觉里程计系统。它不是一个简单的调用现有库如OpenCV的calcOpticalFlowPyrLK的教程而是深入到特征提取、匹配、运动估计、优化乃至局部地图管理的底层逻辑。通过这个项目你不仅能理解VOVisual Odometry的完整流水线更能掌握如何用高效的C代码处理大规模图像数据、进行矩阵运算和优化这是理论学习无法替代的实战经验。无论你是计算机视觉的入门者希望夯实基础还是有一定经验的开发者想挑战更纯粹的算法实现这个项目都能提供一条清晰的路径。2. 核心思路与方案选型为何是“特征点法”与C实现视觉里程计有多种技术路线比如直接法如LSD-SLAM, DSO和特征点法如ORB-SLAM, VINS-Mono。对于从KITTI入门而言特征点法无疑是更合适的选择。直接法虽然理论上更优雅能利用所有像素信息但其对光照变化、相机增益调整更为敏感且实现复杂度高调试困难。KITTI数据集虽然场景丰富但图像序列相对规整相机运动平缓这恰恰是特征点法发挥稳定性的舞台。特征点法的核心思想可以概括为“跟踪可重复的显著点”。我们并不关心图像中每一个像素的变化而是先提取出一些鲁棒的特征点如角点、斑点然后在相邻帧间匹配这些特征点最后通过这些匹配点对来计算相机运动。这个过程直观、可解释性强且每一步都有成熟的算法和大量的调试工具可视化匹配结果等非常适合学习和实践。在编程语言上选择C几乎是必然的。视觉里程计是一个对计算效率要求极高的任务。我们需要在每秒10帧左右的图像流中实时完成特征检测、描述子计算、匹配、运动估计等一系列操作。C的零成本抽象、直接内存操作能力和强大的编译优化使得它成为实现高性能计算内核的不二之选。虽然Python在原型验证和快速实验上优势明显但其运行效率尤其是循环和数值计算和内存管理在实时系统中可能成为瓶颈。使用C我们能更深入地控制算法细节例如自定义特征匹配的搜索策略、优化雅可比矩阵的计算等这对于理解算法本质和后续优化至关重要。因此本项目的技术栈非常明确以C为核心采用经典的特征点法流水线基于KITTI数据集进行实现和评估。我们将构建一个包含以下核心模块的系统图像读取与预处理解析KITTI的raw data格式进行去畸变、灰度化等操作。特征提取与描述选取如ORB、SIFT等特征并计算其描述子。特征匹配在两帧图像间建立特征点的对应关系。运动估计根据匹配点对估计两帧间的相机旋转和平移即本征运动。局部优化与轨迹生成累积运动并可能引入局部Bundle Adjustment来优化轨迹。注意虽然深度学习在视觉里程计中取得了很大进展但基于几何的传统方法仍然是理解问题本质的基石。掌握这套流程将为学习更先进的包括基于学习的方法打下坚实的基础。3. 环境搭建与KITTI数据准备工欲善其事必先利其器。一个清爽高效的开发环境能极大提升编码和调试体验。这里我推荐使用VSCode CMake GCC/Clang的组合它轻量、跨平台且插件生态丰富。3.1 开发环境配置首先确保你的系统已安装必要的编译工具链。在Ubuntu上可以一键安装sudo apt update sudo apt install build-essential cmake git对于Windows用户建议安装MSYS2或直接使用Visual Studio的CMake项目功能但考虑到后续可能涉及一些Linux特有的库如eigen3在Windows下配置会稍复杂。本文以Linux环境为主要示例。接下来是核心的第三方库OpenCV计算机视觉的“瑞士军刀”用于图像IO、预处理、特征提取、可视化等。安装版本建议在4.x以上。sudo apt install libopencv-devEigen一个高性能的C模板库用于线性代数、矩阵和向量运算。它是许多SLAM项目的数学运算基石。通常只需下载头文件即可。sudo apt install libeigen3-devPangolin或SDL2用于轨迹和点云的可视化。Pangolin在SLAM社区更流行轻量且功能专一。你可以从GitHub克隆并编译安装。g2o或Ceres Solver用于非线性优化例如Bundle Adjustment。在项目初期我们可以先用简单的线性方法但后续引入优化器能显著提升精度。可以选择性安装。在VSCode中配置CMake Tools插件和C/C插件。在项目根目录创建CMakeLists.txt文件正确链接上述库。一个最小化的CMakeLists.txt示例如下cmake_minimum_required(VERSION 3.10) project(VisualOdometryKITTI) set(CMAKE_CXX_STANDARD 14) # 寻找OpenCV find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS}) # 寻找Eigen (只需头文件) find_package(Eigen3 REQUIRED) include_directories(${EIGEN3_INCLUDE_DIR}) # 添加可执行文件 add_executable(vo_main src/main.cpp src/visual_odometry.cpp) target_link_libraries(vo_main ${OpenCV_LIBS})3.2 KITTI数据集下载与解析KITTI数据集官网提供了多个任务的数据。对于视觉里程计我们需要的是“KITTI Odometry Benchmark”数据。它包含22个序列00-10带有真值11-21用于测试每个序列包含左目灰度图像、右目灰度图像、激光雷达点云和标定参数。下载后数据集目录结构通常如下kitti_data_odometry/ ├── dataset/ │ └── sequences/ │ ├── 00/ │ │ ├── image_0/ # 左目图像 (000000.png, 000001.png, ...) │ │ ├── image_1/ # 右目图像 │ │ ├── calib.txt # 相机标定参数 │ │ └── times.txt # 图像时间戳 │ ├── 01/ │ └── ... └── poses/ ├── 00.txt # 序列00的真值轨迹 (每行12个数字3x4变换矩阵) └── ...我们需要编写一个Dataset类来封装数据读取逻辑。关键点在于解析calib.txt文件它提供了相机内参矩阵P0、P1、P2、P3分别对应不同相机以及外参矩阵将点从相机坐标系转换到激光雷达坐标系。对于单目视觉里程计我们通常使用P2左目彩色相机但灰度图也用它的内参。P2是一个3x4的投影矩阵其前3x3部分就是内参矩阵K。class Dataset { public: Dataset(const std::string path); bool init(); Frame::Ptr nextFrame(); // 返回下一帧数据 // 获取相机内参 cv::Mat getK() const { return K_; } cv::Mat getDistCoef() const { return dist_coef_; } // 畸变系数KITTI通常已去畸变 private: std::string dataset_path_; int current_index_ 0; cv::Mat K_; // 内参矩阵 cv::Mat dist_coef_; std::vectordouble timestamps_; // ... 其他成员如图像路径列表 };在初始化时从calib.txt中解析出P2并分解出内参矩阵K。KITTI的图像已经过畸变校正所以畸变系数通常设为零。实操心得KITTI的图像文件名是6位数字填充如000000.png用std::setfill(0)和std::setw(6)可以方便地生成路径。另外真值轨迹文件poses/XX.txt中的位姿是相对于第一帧的格式是每一行一个3x4的变换矩阵按行展开这个矩阵是[R | t]即世界坐标系第一帧相机坐标系到当前帧相机坐标系的变换。我们在可视化时需要将其取逆或进行适当转换才能得到相机在世界中的运动轨迹。4. 视觉里程计核心模块实现有了数据和环境我们开始构建视觉里程计的核心。我们将系统分解为几个关键的类Frame帧、Feature特征点、MapPoint地图点、VisualOdometry视觉里程计主类。4.1 帧与特征管理Frame类代表一帧图像它包含图像数据、特征点、以及该帧的位姿旋转矩阵R和平移向量t。struct Frame { typedef std::shared_ptrFrame Ptr; unsigned long id_ 0; // 帧ID double time_stamp_; // 时间戳 cv::Mat color_, gray_; // 彩色和灰度图像 SE3 pose_; // 位姿使用Sophus库的SE3d类型表示更方便 std::vectorstd::shared_ptrFeature features_; // 本帧提取的特征点 Frame() {} Frame(long id, double time_stamp, const SE3 pose, const cv::Mat color); };Feature类关联一个Frame和一个MapPoint存储特征点在图像上的像素坐标、描述子等信息。struct Feature { typedef std::shared_ptrFeature Ptr; std::weak_ptrFrame frame_; // 观测到该特征的帧 cv::KeyPoint position_; // 像素坐标 cv::Mat descriptor_; // 描述子 std::weak_ptrMapPoint map_point_; // 关联的地图点可能为空 Feature() {} Feature(std::shared_ptrFrame frame, const cv::KeyPoint kp) : frame_(frame), position_(kp) {} };MapPoint类代表三维空间中的一个点它被多个Feature观测到。struct MapPoint { typedef std::shared_ptrMapPoint Ptr; unsigned long id_ 0; Vec3 pos_ Vec3::Zero(); // 世界坐标系下的3D位置 std::liststd::weak_ptrFeature observations_; // 所有观测到该点的特征 // ... 其他信息如描述子用于回环检测、被观测次数等 };这种Frame-Feature-MapPoint的关联结构是许多现代SLAM系统如ORB-SLAM的基础它清晰地分离了观测2D和地标3D。4.2 特征提取与匹配策略特征提取是流水线的第一步。OpenCV提供了多种特征检测器和描述子提取器。ORBOriented FAST and Rotated BRIEF因其速度快和旋转不变性是实时系统的热门选择。SIFT/SURF精度更高但速度慢A-KAZE是另一种不错的选择。// 在VisualOdometry类中 void VisualOdometry::extractKeypointsAndDescriptors() { cv::Ptrcv::Feature2D detector cv::ORB::create(num_features_); std::vectorcv::KeyPoint keypoints; cv::Mat descriptors; detector-detectAndCompute(current_frame_-gray_, cv::noArray(), keypoints, descriptors); // 将keypoints和descriptors转换为自定义的Feature对象存入current_frame_ for (size_t i 0; i keypoints.size(); i) { auto feat std::make_sharedFeature(current_frame_, keypoints[i]); feat-descriptor_ descriptors.row(i).clone(); current_frame_-features_.push_back(feat); } }匹配是为当前帧的特征在前一帧或参考帧中寻找对应点。对于相邻帧由于运动较小我们可以使用描述子匹配如暴力匹配BFMatcher或快速近似最近邻FLANN结合交叉验证和比率测试来剔除误匹配。void VisualOdometry::featureMatching() { std::vectorcv::DMatch matches; cv::BFMatcher matcher(cv::NORM_HAMMING); // ORB用汉明距离 matcher.match(prev_frame_-descriptors, current_frame_-descriptors, matches); // 比率测试保留距离比值小于阈值的匹配 std::sort(matches.begin(), matches.end()); const float ratio_thresh 0.7f; std::vectorcv::DMatch good_matches; for (size_t i 0; i matches.size() - 1; i) { if (matches[i].distance ratio_thresh * matches[i 1].distance) { good_matches.push_back(matches[i]); } } // 将good_matches转换为current_frame_和prev_frame_中Feature对象的关联 setRef3DPoints(good_matches); // 同时为当前帧的特征点设置对应的3D地图点来自上一帧的重建 }这里有一个关键操作setRef3DPoints在匹配成功后我们需要知道这些匹配点对应的三维空间点是什么。对于新帧其特征点还没有关联的MapPoint。因此我们利用上一帧已经三角化好的MapPoint通过特征匹配关系将其“传递”给当前帧的特征点。这为后续的运动估计提供了3D-2D的对应关系。注意事项单纯的描述子匹配在快速旋转或光照变化下容易失效。因此成熟的系统会结合运动模型进行预测在特征点附近的一个小窗口内进行搜索光流法或者使用词袋模型进行更全局的匹配。在项目初期我们可以先用描述子匹配但要知道这是精度和鲁棒性的一个潜在瓶颈。4.3 运动估计从2D-2D到3D-2D得到匹配点对后就可以估计相机运动了。这里通常分两步走第一步使用对极几何估计初始位姿2D-2D对于刚初始化的系统或者还没有足够多三角化好的3D点时我们只有两帧图像上的2D点对。这时可以使用对极几何。通过匹配点对计算基础矩阵Fundamental Matrix或本质矩阵Essential Matrix然后从中分解出旋转矩阵R和平移向量t带尺度模糊性。void VisualOdometry::poseEstimation2D2D() { // 收集匹配点对的像素坐标 std::vectorcv::Point2f pts1, pts2; for (auto match : feature_matches_) { pts1.push_back(prev_frame_-features_[match.queryIdx]-position_.pt); pts2.push_back(current_frame_-features_[match.trainIdx]-position_.pt); } // 计算基础矩阵使用RANSAC剔除外点 cv::Mat fundamental_matrix cv::findFundamentalMat(pts1, pts2, cv::FM_RANSAC, 3.0, 0.99); // 从基础矩阵恢复本质矩阵 E K^T * F * K cv::Mat essential_matrix K_.t() * fundamental_matrix * K_; // 从本质矩阵恢复R, t (四个解) cv::recoverPose(essential_matrix, pts1, pts2, K_, R, t, mask); // 通过三角化检查点在两个相机前的深度为正来选择正确的解 }这种方法恢复的平移向量t只有方向没有尺度即我们不知道移动了1米还是10米。这就是单目视觉里程计的尺度不确定性问题。第二步使用PnP优化位姿3D-2D一旦我们通过三角化得到了一些3D地图点MapPoint并且当前帧的特征点通过匹配关联到了这些3D点我们就有了3D-2D的对应关系。这时可以使用Perspective-n-Point (PnP)方法来求解相机位姿。PnP问题是有尺度的因为它利用了已知的3D点结构。OpenCV提供了cv::solvePnP函数可以使用迭代法如EPnP求解。更优的做法是将其构建为一个非线性最小二乘问题使用高斯-牛顿法或列文伯格-马夸尔特法LM进行优化这能更好地处理噪声和误匹配。void VisualOdometry::poseEstimation3D2D() { // 准备3D点世界坐标和2D点当前帧像素坐标 std::vectorcv::Point3f pts3d; std::vectorcv::Point2f pts2d; for (auto feat : current_frame_-features_) { auto mp feat-map_point_.lock(); if (mp) { pts3d.push_back(cv::Point3f(mp-pos_.x(), mp-pos_.y(), mp-pos_.z())); pts2d.push_back(feat-position_.pt); } } if (pts3d.size() 4) { // PnP至少需要4个点 LOG(WARNING) 3D-2D correspondences less than 4, use 2D-2D method.; poseEstimation2D2D(); return; } cv::Mat rvec, tvec, inliers; // 使用RANSAC版本的PnP鲁棒性更强 cv::solvePnPRansac(pts3d, pts2d, K_, cv::Mat(), rvec, tvec, false, 100, 4.0, 0.99, inliers); cv::Rodrigues(rvec, R); // 旋转向量转旋转矩阵 // 将R, t转换为SE3格式更新current_frame_-pose_ current_frame_-pose_ SE3(SO3(R), Vec3(tvec.atdouble(0), tvec.atdouble(1), tvec.atdouble(2))); }在实际系统中我们通常会维护一个局部地图里面包含许多三角化好的MapPoint。对于每一帧新图像我们首先通过运动模型或恒速模型预测一个初始位姿然后在当前帧投影这些局部地图点在其投影点附近搜索匹配投影匹配得到大量3D-2D匹配对最后用PnP通常结合非线性优化来优化位姿。这个过程比单纯的帧间匹配更稳定因为利用了更多历史信息。4.4 三角化从2D到3D当估计出两帧之间的相对位姿后我们就可以将匹配的特征点三角化生成新的3D地图点MapPoint。三角化的原理是交汇测量两条来自不同相机光心的射线理论上应该交汇于空间中的一点。设第一帧的相机位姿为T1 [I | 0]作为世界坐标系第二帧的位姿为T2 [R | t]。特征点在第一帧的归一化平面坐标为x1在第二帧为x2。它们满足depth2 * x2 R * (depth1 * x1) t我们可以构建一个线性方程组来求解depth1和depth2。OpenCV提供了cv::triangulatePoints函数。void VisualOdometry::triangulateNewPoints() { // 获取两帧的位姿和匹配点对 SE3 T1 prev_frame_-pose_; SE3 T2 current_frame_-pose_; std::vectorcv::Point2f pts1, pts2; std::vectorstd::shared_ptrFeature feats1, feats2; // 对应的特征对象 // 收集尚未关联地图点的匹配对 for (auto match : feature_matches_) { auto f1 prev_frame_-features_[match.queryIdx]; auto f2 current_frame_-features_[match.trainIdx]; if (!f1-map_point_.lock() !f2-map_point_.lock()) { pts1.push_back(f1-position_.pt); pts2.push_back(f2-position_.pt); feats1.push_back(f1); feats2.push_back(f2); } } if (pts1.empty()) return; // 三角化 cv::Mat pts_4d; cv::triangulatePoints(P1, P2, pts1, pts2, pts_4d); // P1, P2是3x4的投影矩阵 // 将齐次坐标转换为3D坐标并检查深度为正在相机前方 for (int i 0; i pts_4d.cols; i) { cv::Mat x pts_4d.col(i); x / x.atfloat(3, 0); // 归一化 Vec3 point_world(x.atfloat(0, 0), x.atfloat(1, 0), x.atfloat(2, 0)); // 检查重投影误差和深度 if (isGoodTriangulation(point_world, T1, T2, feats1[i], feats2[i])) { auto mp std::make_sharedMapPoint(); mp-id_ next_map_point_id_; mp-pos_ point_world; // 关联特征点和地图点 feats1[i]-map_point_ mp; feats2[i]-map_point_ mp; mp-observations_.push_back(feats1[i]); mp-observations_.push_back(fats2[i]); // 将地图点加入局部地图 map_-insertMapPoint(mp); } } }三角化生成的点需要经过严格的筛选深度必须为正、重投影误差要小于阈值、视差角不能太小否则深度估计极不稳定。只有通过检查的点才能被加入地图。4.5 局部地图与优化一个简单的帧间VO会随着时间累积误差导致轨迹漂移。引入一个局部地图可以缓解这个问题。局部地图维护最近若干关键帧以及它们观测到的地图点。新帧到来时不仅与上一帧匹配还与局部地图中的地图点进行匹配和优化。更进一步的优化是局部Bundle Adjustment (BA)。BA是一种同时优化多个相机位姿和三维点位置的技术。它最小化重投影误差即观测到的像素位置与地图点投影位置之间的差异的平方和。对于局部窗口内的关键帧和它们观测到的地图点我们可以构建一个BA问题并用g2o或Ceres求解。// 伪代码示意 void LocalBundleAdjustment(std::vectorFrame::Ptr keyframes, std::vectorMapPoint::Ptr local_mappoints) { // 构建图优化问题 g2o::SparseOptimizer optimizer; // 设置求解器如LM // 添加顶点关键帧位姿SE3和地图点位置3D for (auto kf : keyframes) { addVertex(kf-pose_); } for (auto mp : local_mappoints) { addVertex(mp-pos_); } // 添加边重投影误差边连接位姿顶点和地图点顶点 for (每个观测关系) { addEdge(pose_vertex_id, point_vertex_id, measurement_pixel, information_matrix); } // 优化 optimizer.initializeOptimization(); optimizer.optimize(10); // 迭代10次 // 更新优化后的位姿和地图点 }局部BA能显著提高局部轨迹和地图的精度是提升VO性能的关键步骤。在资源允许的情况下应该定期例如每加入一个关键帧执行。5. 系统集成、可视化与评估将上述模块串联起来就构成了视觉里程计的主循环。在VisualOdometry类中我们需要一个run()函数bool VisualOdometry::run() { while (dataset_-hasNext()) { current_frame_ dataset_-nextFrame(); LOG(INFO) Processing frame current_frame_-id_; // 步骤1: 特征提取 extractKeypointsAndDescriptors(); // 步骤2: 特征匹配与上一帧或局部地图 if (status_ VOStatus::INITIALIZING) { // 初始化阶段需要足够的视差来三角化 featureMatching(); if (checkInitialization()) { initializeMap(); status_ VOStatus::TRACKING_GOOD; } } else if (status_ VOStatus::TRACKING_GOOD || status_ VOStatus::TRACKING_BAD) { // 跟踪阶段 featureMatchingWithLocalMap(); // 步骤3: 运动估计 poseEstimation3D2D(); // 优先使用3D-2D PnP // 步骤4: 检查跟踪质量内点数量、重投影误差 if (checkTrackQuality()) { status_ VOStatus::TRACKING_GOOD; // 步骤5: 三角化新点 triangulateNewPoints(); // 步骤6: 管理关键帧和局部地图 if (needNewKeyFrame()) { addKeyFrame(); // 步骤7: 可选局部BA localBundleAdjustment(); } // 步骤8: 剔除冗余地图点 pruneMap(); } else { status_ VOStatus::TRACKING_BAD; // 跟踪丢失尝试重定位 } } else if (status_ VOStatus::LOST) { // 重定位逻辑 relocalization(); } // 更新上一帧 prev_frame_ current_frame_; // 可视化 visualize(); } return true; }可视化是调试和理解的利器。我们可以用Pangolin实时绘制出相机轨迹将估计的位姿逐帧连接起来并与KITTI提供的真值轨迹对比。局部地图点云显示三角化出的三维点。当前帧图像与特征点用不同颜色标注跟踪成功的内点、新提取的特征等。评估VO性能的黄金标准是绝对轨迹误差ATE和相对位姿误差RPE。我们可以使用像evo这样的工具将估计的轨迹文件每行保存时间戳和位姿与真值轨迹文件进行比对生成误差曲线和统计指标如均方根误差RMSE。这能客观地衡量我们算法的精度。6. 常见问题、调试技巧与优化方向即使按照流程实现了所有模块你的第一个VO系统很可能无法正常工作或者精度很差。以下是我在实现过程中踩过的坑和总结的技巧问题1特征匹配大量错误导致运动估计完全失效。排查首先可视化匹配结果。在图像上画出匹配线如果很多线交叉混乱说明匹配质量差。解决加强筛选除了比率测试增加交叉验证从A匹配到B再从B匹配回A要求一致。使用光流跟踪对于连续帧用LK光流法跟踪特征点比描述子匹配更稳定、更快。然后用跟踪到的点进行运动估计。运动模型约束假设匀速运动预测特征点在当前帧的大致位置在预测位置附近小范围内进行匹配或光流跟踪。检查特征点分布确保特征点不是只集中在纹理丰富的区域如天空、地面要尽量均匀分布可以用网格划分图像在每个网格里保留响应最强的几个点。问题2尺度漂移或尺度不确定。现象单目VO估计的轨迹形状可能正确但大小缩放比例不对且会随时间变化。解决引入关键帧和BA这是治本的方法。局部BA可以约束尺度。融合IMU如果你有KITTI的IMU数据可以尝试紧耦合的VIOIMU能直接提供尺度信息。假设已知高度对于地面机器人或汽车可以假设地面是平的通过特征点估计的地面高度来恢复尺度需要分割地面点。初始化时固定基线在初始化三角化时将平移向量t归一化为单位长度后续通过三角化点的平均深度来恢复一个初始尺度。但这个尺度还是会漂。问题3旋转估计不准特别是绕垂直轴yaw的旋转。原因对于前向运动的汽车图像中特征点的视差主要来自横向移动绕yaw旋转引起的像素运动较小容易被噪声淹没。解决使用更稳定的旋转估计方法在PnP中使用SOLVEPNP_ITERATIVE或SOLVEPNP_EPNP并设置合理的迭代次数和重投影误差阈值。增加鲁棒核函数在非线性优化中使用Huber或Cauchy核函数降低外点的影响。融合其他传感器这是最有效的办法用IMU或轮速计来辅助估计旋转。问题4系统在转弯或快速运动时跟踪丢失。原因特征点可能因运动模糊而无法提取或匹配或者视场中特征点数量骤减。解决自适应特征提取在图像模糊或特征点少时增加提取的特征数量或降低提取阈值。预测与搜索利用IMU或运动模型更准确地预测特征位置扩大搜索窗口。重定位机制维护一个全局地图或关键帧数据库当跟踪丢失时提取当前帧的全局描述子如词袋向量与数据库匹配找回位姿。使用直接法或半直接法在纹理缺失区域直接法可能比特征点法更鲁棒。性能优化方向多线程将特征提取、匹配、地图点管理等耗时操作放入独立线程。特征点网格管理将图像分成网格管理每个网格内的特征点加速投影匹配时的搜索。描述子匹配加速使用快速近似最近邻搜索FLANN或词袋模型进行快速粗匹配。选择性地三角化只对视差足够大的匹配点进行三角化避免生成大量质量差的地图点。地图点生命周期管理定期剔除那些很久未被观测到、或者重投影误差大的地图点保持地图紧凑。实现一个鲁棒、高精度的视觉里程计是一个不断迭代和调试的过程。从最简单的帧间匹配PnP开始逐步加入关键帧、局部地图、BA优化再到尝试融合其他传感器每一步都会带来新的挑战和收获。这个基于KITTI和C的项目为你深入理解SLAM/VO的核心原理和工程实现提供了一个绝佳的起点。当你看到自己实现的程序在KITTI序列上跑出一条与真值基本重合的轨迹时那种成就感是无与伦比的。本文还有配套的精品资源点击获取
返回列表