视觉SLAM相机成像几何模型:从针孔模型到OpenCV实战

发布时间:2026/8/11 8:11:53
视觉SLAM相机成像几何模型:从针孔模型到OpenCV实战 1. 项目概述从三维世界到二维像素的桥梁做视觉SLAM即时定位与地图构建相机就是我们感知世界的“眼睛”。但你是否想过这双“眼睛”看到的和我们人眼理解的其实大不相同它看到的不是立体的、有深度的场景而是一张张扁平的、由无数小点像素组成的图像。从真实的三维空间点到相机传感器上那个小小的二维像素坐标中间到底发生了什么这个过程就是相机成像几何模型要讲清楚的核心。理解它是后续所有视觉里程计、特征匹配、乃至后端优化的基石。没有这个基础后面的代码写得再漂亮也像是在沙地上盖楼随时可能因为对几何关系的误解而崩塌。这一讲的内容看似是枯燥的数学公式推导实则是打通任督二脉的关键。无论是想用OpenCV去读取一张图片还是想用特征点法做位姿估计亦或是理解深度学习里的相机投影层都绕不开这里面的几个核心概念针孔模型、畸变、以及图像在计算机里的存储方式。我刚开始学的时候也曾觉得这些坐标变换绕来绕去但后来在写代码做三角化从2D点恢复3D点时因为一个坐标系的疏忽调试了一整天才深刻体会到“基础不牢地动山摇”的含义。所以无论你是SLAM新手还是对计算机视觉感兴趣都值得花时间把这一讲的原理啃透。接下来我会结合代码和实际图像带你一步步拆解这个过程并分享一些我踩过的坑和调试技巧。2. 核心原理拆解相机如何“看见”世界2.1 从物理世界到理想图像针孔相机模型想象一个封闭的盒子在一面戳一个小孔对面内壁就会形成外界景物倒立的像。这就是最朴素的针孔相机模型。在数学上我们用一个更规整的模型来描述它。首先我们建立几个坐标系世界坐标系 (World Frame): 一个固定的参考系用来描述物体和相机在真实世界中的位置。相机坐标系 (Camera Frame): 以相机光心小孔为原点Z轴指向相机正前方光轴X轴向右Y轴向下的右手坐标系。这是分析的核心。图像坐标系 (Image Frame): 在相机成像平面上以光轴与成像平面的交点主点为原点x轴向右y轴向下的二维坐标系。像素坐标系 (Pixel Frame): 在数字图像上以左上角为原点(0,0)u轴向右v轴向下的坐标系。这是我们最终在电脑里看到的。成像过程的第一步是刚体变换。一个在世界坐标系中的点 ( P_w [X_w, Y_w, Z_w]^T )需要通过相机的外参旋转矩阵 ( R ) 和平移向量 ( t )转换到相机坐标系下 [ P_c R P_w t ] 或者用齐次坐标更简洁地表示为 [ \begin{bmatrix} P_c \ 1 \end{bmatrix} \begin{bmatrix} R t \ 0^T 1 \end{bmatrix} \begin{bmatrix} P_w \ 1 \end{bmatrix} T_{cw} \begin{bmatrix} P_w \ 1 \end{bmatrix} ] 这里 ( T_{cw} ) 就是从世界到相机的变换矩阵。这一步决定了相机“站在哪里看向何方”。第二步是透视投影。在相机坐标系下点 ( P_c [X_c, Y_c, Z_c]^T ) 被投影到归一化平面一个虚拟的、在光心前方 ( Z1 ) 处的平面上得到一个归一化坐标 ( p_n [X_c/Z_c, Y_c/Z_c, 1]^T [x, y, 1]^T )。这个过程丢掉了深度信息 ( Z_c )也是造成“近大远小”透视效果的根本原因。第三步加入内参得到像素坐标。归一化坐标 ( [x, y]^T ) 还需要经过相机内参矩阵 ( K ) 的变换才能得到最终的像素坐标 ( [u, v]^T )。 [ \begin{bmatrix} u \ v \ 1 \end{bmatrix} \begin{bmatrix} f_x 0 c_x \ 0 f_y c_y \ 0 0 1 \end{bmatrix} \begin{bmatrix} x \ y \ 1 \end{bmatrix} K p_n ] 这里( f_x, f_y ) 是以像素为单位表示的焦距通常 ( f_x f_y ) 如果像素是正方形的( c_x, c_y ) 是主点坐标即光轴在图像像素坐标系上的投影。内参矩阵 ( K ) 描述了相机自身的几何属性在出厂后通常是固定的除非变焦。注意这里有一个初学者极易混淆的点。很多资料和OpenCV的函数默认使用“先畸变校正再乘内参”的步骤。但上述推导是理想情况下的“内参变换”。实际上真实的镜头会引入畸变所以完整的流程是世界点-相机坐标系-归一化平面-添加畸变-乘内参得到像素坐标。顺序不能错。2.2 当理想照进现实透镜畸变及其校正针孔模型是理想的但真实的相机需要透镜来汇聚更多光线提高成像亮度。透镜的物理特性会引入畸变主要分为两类径向畸变 (Radial Distortion)由透镜形状缺陷引起成像点沿径向方向偏离其理想位置。它又分为桶形畸变 (Barrel Distortion)图像边缘的点向中心收缩像通过一个桶看到的画面。广角镜头常见。枕形畸变 (Pincushion Distortion)图像边缘的点向外膨胀像通过一个枕头看到的画面。长焦镜头常见。 数学模型通常用多项式来近似最常用的是布朗-康拉德模型中的前几项。对于归一化平面上的点 ( [x, y]^T )其畸变后的坐标 ( [x_{distorted}, y_{distorted}]^T ) 为 [ \begin{aligned} x_{distorted} x (1 k_1 r^2 k_2 r^4 k_3 r^6) \ y_{distorted} y (1 k_1 r^2 k_2 r^4 k_3 r^6) \end{aligned} ] 其中 ( r^2 x^2 y^2 )。( k_1, k_2, k_3 ) 是径向畸变系数通常 ( k_1 ) 起主导作用。切向畸变 (Tangential Distortion)由透镜制造和安装误差导致透镜平面与成像平面不平行。其数学模型为 [ \begin{aligned} x_{distorted} x 2 p_1 x y p_2 (r^2 2x^2) \ y_{distorted} y p_1 (r^2 2y^2) 2 p_2 x y \end{aligned} ] 其中 ( p_1, p_2 ) 是切向畸变系数。完整的畸变校正过程是对于一个归一化坐标点 ( [x, y]^T )先计算径向和切向畸变的综合偏移量得到畸变点 ( [x_d, y_d]^T )然后再乘以内参矩阵得到最终的畸变像素坐标。反过来当我们从一张畸变的图像上提取了一个像素坐标 ( [u, v]^T ) 后想得到它对应的归一化平面理想坐标就需要进行去畸变操作这通常需要通过迭代算法来求解。实操心得在OpenCV中cv::undistort()函数或者initUndistortRectifyMap结合remap函数可以方便地完成图像去畸变。但关键是要有准确的相机内参和畸变系数。这些参数通过“相机标定”获得。对于常见的消费级相机如手机、USB摄像头如果SLAM系统对精度要求不是极端苛刻有时可以忽略切向畸变( p_1, p_2 )和高阶径向畸变( k_3 )只使用 ( k_1, k_2 ) 就能获得不错的校正效果这能减少参数数量优化时更稳定。2.3 数字图像的表示与存储相机最终给我们的是数字图像在计算机里它就是一个巨大的二维矩阵。每个矩阵元素就是一个像素其值代表了该点的亮度灰度图或颜色彩色图。灰度图每个像素用一个数值如unsigned char范围0-255表示其灰度强度。0代表纯黑255代表纯白。这是最简单的形式很多视觉算法如特征提取、光流先在灰度图上进行因为计算量小。彩色图最常见的是RGB模型每个像素由红(R)、绿(G)、蓝(B)三个通道的强度值组合而成。在OpenCV中默认的彩色图像存储顺序是BGR而不是我们通常认为的RGB。这是一个经典的坑用cv::imread()读取彩色图后矩阵的第一个通道是蓝色第二个是绿色第三个是红色。如果你用cv::imshow()显示没问题因为该函数认识BGR顺序。但如果你把图像数据直接送给一个期望RGB顺序的库如某些深度学习框架或图像处理库颜色就会完全错乱。除了RGB另一种重要的颜色空间是HSV/HSL。它将颜色信息色调H、饱和度S、明度V/L分离开在某些场景下比RGB更直观比如基于颜色的物体追踪可以在HSV空间里通过阈值轻松分离出某种颜色的区域而不受光照亮度V的强烈影响。图像在内存中的存储是连续的。对于一张宽为cols高为rows的灰度图其数据按行优先存储在一个长度为rows * cols的数组中。彩色图如BGR三通道则可以看作一个三维数组维度为(rows, cols, 3)但在内存中仍然是一维连续的排列方式通常是B00, G00, R00, B01, G01, R01, ...。理解内存布局对于高效地遍历和操作像素至关重要。3. 关键工具与接口OpenCV实战指南理论需要代码来落地。OpenCV是计算机视觉的“标准库”这里我们深入几个核心操作。3.1 图像的读取、显示与保存这是最基本的操作但细节决定成败。#include opencv2/opencv.hpp #include iostream int main() { // 1. 读取图像 // cv::IMREAD_COLOR: 默认以BGR三通道格式读取忽略透明度。 // cv::IMREAD_GRAYSCALE: 以灰度图格式读取。 // cv::IMREAD_UNCHANGED: 按原样读取包括可能的Alpha通道。 cv::Mat image cv::imread(path/to/your/image.jpg, cv::IMREAD_COLOR); if (image.empty()) { std::cerr Could not open or find the image! std::endl; return -1; } // 2. 获取图像基本信息 std::cout Image width: image.cols std::endl; std::cout Image height: image.rows std::endl; std::cout Number of channels: image.channels() std::endl; // 深度图像元素的数据类型如CV_8U8位无符号整数 std::cout Image depth: image.depth() std::endl; // 3. 显示图像 cv::namedWindow(Display Window, cv::WINDOW_AUTOSIZE); // 创建一个窗口 cv::imshow(Display Window, image); // 在窗口中显示图像 cv::waitKey(0); // 等待任意按键0表示无限等待。这是必须的否则窗口会一闪而过。 // 4. 保存图像 // 第二个参数是质量对于JPEG范围是0-100默认95。 bool isSaved cv::imwrite(output_image.png, image); if (!isSaved) { std::cerr Failed to save the image! std::endl; } return 0; }注意事项cv::waitKey()是显示图像的关键它不仅等待按键还是OpenCV处理GUI事件如刷新窗口的必须调用。在循环中显示视频帧时通常用cv::waitKey(1)来产生一个短暂的延迟并处理事件。文件路径建议使用绝对路径或确保相对路径相对于可执行文件的工作目录是正确的。路径中包含中文或特殊字符有时会导致读取失败。cv::Mat是OpenCV的核心数据结构它智能地管理图像数据内存。当多个cv::Mat指向同一块数据时它们共享内存浅拷贝。只有显式调用clone()或copyTo()时才会进行深拷贝。这提高了效率但使用时需心中有数避免意外的修改。3.2 像素级的访问与操作为了理解图像的本质我们常常需要直接操作像素。方法一使用ptr指针高效推荐用于遍历// 假设 image 是一个 CV_8UC3 的彩色图 (BGR) int rows image.rows; int cols image.cols; for (int i 0; i rows; i) { // 获取第i行行首的指针类型是 unsigned char* cv::Vec3b* row_ptr image.ptrcv::Vec3b(i); for (int j 0; j cols; j) { // 访问第j个像素的B, G, R通道 cv::Vec3b pixel row_ptr[j]; unsigned char blue pixel[0]; unsigned char green pixel[1]; unsigned char red pixel[2]; // 例如将此处设置为红色 pixel[0] 0; // B pixel[1] 0; // G pixel[2] 255; // R // 或者计算灰度值常用加权平均法 // uchar gray_value 0.299 * red 0.587 * green 0.114 * blue; } }方法二使用at方法直观但效率略低于指针适合随机访问// 访问第i行第j列的像素彩色 cv::Vec3b pixel image.atcv::Vec3b(i, j); uchar blue pixel[0]; // 如果是灰度图则是单通道 cv::Mat gray_image; cv::cvtColor(image, gray_image, cv::COLOR_BGR2GRAY); uchar intensity gray_image.atuchar(i, j); // 注意是 uchar不是 Vec3b实操心得在需要遍历整张图像进行密集计算时如自定义滤波器、计算直方图务必使用ptr指针方式它比反复调用at快一个数量级。cv::Vec3b是一个长度为3的uchar数组专用于表示BGR像素。另外OpenCV提供了很多高效的矩阵运算函数如cv::add,cv::multiply以及矩阵表达式能利用SIMD指令优化比自己写循环快得多应优先使用。3.3 相机标定获取内参和畸变系数相机标定是视觉SLAM预处理中至关重要的一步。OpenCV提供了cv::calibrateCamera函数来简化这个过程。其核心思想是通过观察一个已知几何形状的物体如棋盘格利用大量的“3D-2D”点对应关系来反解出相机的内参和畸变系数。标定步骤简述准备标定板打印一张棋盘格图案例如9x6的内角点并将其贴在一个平坦的硬板上。采集数据从不同角度、不同距离拍摄标定板的照片通常需要15-20张确保标定板在图像中清晰、完整且姿态多样。提取角点使用cv::findChessboardCorners自动查找每张图像中棋盘格的内角点黑白方格的交点。亚像素优化使用cv::cornerSubPix将角点位置精确到亚像素级别。准备对象点定义棋盘格在“世界坐标系”中的3D坐标。通常将棋盘格平面设为Z0然后根据方格尺寸生成每个角点的 (X, Y, 0) 坐标。执行标定调用cv::calibrateCamera输入所有图像的对象点集合和对应的图像点集合函数会输出相机内参矩阵 ( K )、畸变系数向量 ( distCoeffs )、每张图像的旋转向量和平移向量外参以及重投影误差。代码框架示例// 假设 objectPointsList 是 std::vectorstd::vectorcv::Point3f存储所有图像的世界点 // imagePointsList 是 std::vectorstd::vectorcv::Point2f存储所有图像的像素角点 // imageSize 是图像尺寸 (cv::Size) cv::Mat cameraMatrix; // 内参矩阵 K cv::Mat distCoeffs; // 畸变系数 [k1, k2, p1, p2, k3, ...] std::vectorcv::Mat rvecs, tvecs; // 每张图的外参 double reprojError cv::calibrateCamera(objectPointsList, imagePointsList, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, cv::CALIB_FIX_K3, // 可以固定某些参数 cv::TermCriteria(cv::TermCriteria::COUNT cv::TermCriteria::EPS, 30, 1e-6)); std::cout Reprojection error: reprojError std::endl; std::cout Camera matrix K:\n cameraMatrix std::endl; std::cout Distortion coefficients: distCoeffs.t() std::endl;注意事项重投影误差这是评价标定质量的关键指标。它表示利用标定出的参数将3D点重新投影到2D图像上与检测到的实际2D点之间的平均像素距离。一般应小于0.5像素越小越好。标定板姿态多样性照片要覆盖图像的各个区域中心、四角、边缘并且棋盘格要有明显的倾斜和旋转这样标定结果才更鲁棒能更好地估计畸变。畸变系数顺序OpenCV默认的畸变系数向量是[k1, k2, p1, p2, k3, k4, k5, k6]。cv::calibrateCamera默认输出5个系数 (k1, k2, p1, p2, k3)。在使用cv::undistort或cv::initUndistortRectifyMap时需要确保传入的系数向量顺序和长度匹配。4. 实践演练手写图像去畸变与点云投影理解了原理和API我们通过两个小实验来加深印象。4.1 实验一根据模型手写图像去畸变函数我们不用cv::undistort而是根据畸变公式自己实现一个去畸变函数这能让你对公式的理解刻骨铭心。思路对于目标去畸变后图像上的每一个像素坐标(u, v)我们想找到它在原始畸变图像上对应的位置(u_distorted, v_distorted)然后通过插值如双线性插值从原始图像获取颜色填到目标图像上。这是一个“反向映射”的过程。像素坐标转归一化坐标(u, v) - (x, y)。 [ \begin{aligned} x (u - c_x) / f_x \ y (v - c_y) / f_y \end{aligned} ]对归一化坐标施加畸变正向过程利用畸变系数 ( k_1, k_2, p_1, p_2 ) 计算畸变后的坐标(x_d, y_d)。 [ \begin{aligned} r^2 x^2 y^2 \ x_{distorted} x (1 k_1 r^2 k_2 r^4) 2 p_1 x y p_2 (r^2 2 x^2) \ y_{distorted} y (1 k_1 r^2 k_2 r^4) p_1 (r^2 2 y^2) 2 p_2 x y \end{aligned} ]畸变归一化坐标转畸变像素坐标(x_d, y_d) - (u_d, v_d)。 [ \begin{aligned} u_d f_x * x_d c_x \ v_d f_y * y_d c_y \end{aligned} ]采样与插值(u_d, v_d)很可能不是整数坐标。我们需要从原始畸变图像的该位置通过双线性插值计算出像素值然后赋给目标图像的位置(u, v)。void myUndistortImage(const cv::Mat distorted, cv::Mat undistorted, const cv::Mat cameraMatrix, const cv::Mat distCoeffs) { // 从参数中提取内参和畸变系数 double fx cameraMatrix.atdouble(0, 0); double fy cameraMatrix.atdouble(1, 1); double cx cameraMatrix.atdouble(0, 2); double cy cameraMatrix.atdouble(1, 2); double k1 distCoeffs.atdouble(0); double k2 distCoeffs.atdouble(1); double p1 distCoeffs.atdouble(2); double p2 distCoeffs.atdouble(3); // 假设我们只用前四个系数 (k1, k2, p1, p2) undistorted.create(distorted.size(), distorted.type()); // 创建目标图像 int rows undistorted.rows; int cols undistorted.cols; for (int v 0; v rows; v) { // v 是像素坐标的行索引 for (int u 0; u cols; u) { // u 是像素坐标的列索引 // 步骤1: 像素坐标 - 归一化坐标 double x (u - cx) / fx; double y (v - cy) / fy; // 步骤2: 应用畸变模型正向 double r2 x * x y * y; double r4 r2 * r2; double x_distorted x * (1 k1 * r2 k2 * r4) 2 * p1 * x * y p2 * (r2 2 * x * x); double y_distorted y * (1 k1 * r2 k2 * r4) p1 * (r2 2 * y * y) 2 * p2 * x * y; // 步骤3: 畸变归一化坐标 - 畸变像素坐标 double u_distorted fx * x_distorted cx; double v_distorted fy * y_distorted cy; // 步骤4: 双线性插值 if (u_distorted 0 u_distorted cols - 1 v_distorted 0 v_distorted rows - 1) { int u0 static_castint(u_distorted); int v0 static_castint(v_distorted); int u1 std::min(u0 1, cols - 1); // 防止越界 int v1 std::min(v0 1, rows - 1); double s u_distorted - u0; double t v_distorted - v0; // 获取四个邻域点的像素值以灰度图为例 uchar I00 distorted.atuchar(v0, u0); uchar I01 distorted.atuchar(v0, u1); uchar I10 distorted.atuchar(v1, u0); uchar I11 distorted.atuchar(v1, u1); uchar pixel_value static_castuchar( (1 - s) * (1 - t) * I00 s * (1 - t) * I01 (1 - s) * t * I10 s * t * I11 ); undistorted.atuchar(v, u) pixel_value; } else { // 映射到图像外的点设为黑色 undistorted.atuchar(v, u) 0; } } } }这个实验能让你彻底明白cv::initUndistortRectifyMap生成的映射图mapx, mapy到底是什么——它们就是为每个目标像素(u,v)预先计算好的(u_d, v_d)坐标查找表。4.2 实验二将图像特征点反投影到虚拟三维空间在单目SLAM中我们只能得到像素坐标丢失了深度。但我们可以做一个假设来可视化特征点的“射线”这有助于理解极几何。假设我们有一张图像用ORB特征提取器检测到了一些特征点keypoints并且我们已经有了相机的内参矩阵 ( K )。我们可以将每个特征点的像素坐标转换到相机坐标系下的归一化平面并假设一个任意的深度比如1米来生成一个三维点。这些点构成了一条从光心出发的射线。// 假设 keypoints 是 std::vectorcv::KeyPoint // cameraMatrix 是内参矩阵 cv::Mat std::vectorcv::Point3f rays; // 存储生成的三维点在相机坐标系下 double fake_depth 1.0; // 假设的深度单位米 for (const auto kp : keypoints) { // 1. 像素坐标 (u, v) double u kp.pt.x; double v kp.pt.y; // 2. 像素坐标 - 归一化平面坐标 (x, y, 1) // 使用内参矩阵的逆 cv::Mat pt_pixel (cv::Mat_double(3,1) u, v, 1); cv::Mat pt_norm cameraMatrix.inv() * pt_pixel; // 得到 (x, y, 1) // 3. 赋予假设深度得到相机坐标系下的三维点 double x pt_norm.atdouble(0) * fake_depth; double y pt_norm.atdouble(1) * fake_depth; double z fake_depth; // 注意归一化坐标的z是1所以乘以深度后就是深度值本身。 rays.push_back(cv::Point3f(x, y, z)); } // 现在 rays 中的点都在相机前方1米处的归一化射线方向上。 // 你可以使用Pangolin或Open3D等库将这些点与相机光心(0,0,0)连线画出来 // 就能直观地看到所有特征点对应的“视线”了。这个简单的演示揭示了单目视觉的尺度不确定性我们不知道特征点的真实深度fake_depth是随意设的只知道它们的方向。在后续的多帧三角化或PnP中正是通过几何约束极线约束、运动视差来求解这些未知的深度从而恢复出场景的三维结构。5. 工程经验与深度思考5.1 相机模型的选择针孔与鱼眼我们前面讨论的都是针孔模型加畸变模型。但对于视角超过180度的鱼眼相机这种模型就不够用了因为畸变太大多项式模型拟合效果很差。鱼眼相机通常使用不同的投影模型如等距投影、立体投影等。OpenCV提供了fisheye命名空间下的标定和去畸变函数如cv::fisheye::calibrate,cv::fisheye::undistortImage。如何选择普通镜头/广角镜头使用普通的cv::calibrateCamera和针孔畸变模型即可。视角通常在120度以内。鱼眼镜头/全景镜头必须使用鱼眼模型。OpenCV的鱼眼模型可以处理视角达到180度甚至更大的情况。在SLAM系统中选择正确的相机模型对于前端特征匹配的准确性至关重要。错误的模型会导致去畸变后的图像仍然存在扭曲使得“直线不直”进而破坏特征匹配的几何约束。5.2 图像预处理对SLAM的影响从相机拿到原始图像后直接用于SLAM算法往往不是最优的。适当的预处理可以显著提升鲁棒性。直方图均衡化特别是对于光照变化剧烈的场景如从室内走到室外均衡化可以增强图像对比度使特征点如角点、边缘更突出。但要注意它也可能放大噪声。高斯滤波/中值滤波轻微的平滑滤波可以抑制图像传感器噪声避免产生过多的、不稳定的特征点。中值滤波对“椒盐噪声”特别有效。但滤波会模糊图像过度滤波会导致特征点定位不准需要权衡。图像金字塔这是多尺度特征提取如ORB-SLAM中的核心。通过构建图像金字塔可以在不同尺度上提取和匹配特征使算法对尺度变化具有不变性。同时在金字塔上层小尺度图像上进行光流跟踪或特征匹配速度更快搜索范围更大。一个实用的预处理流水线可能是原始图像 - 可选颜色空间转换BGR2GRAY- 轻度高斯滤波去噪 - 构建图像金字塔 - 在金字塔各层进行特征提取/跟踪。5.3 时间同步与传感器融合的考量在实际的机器人或VR/AR设备上视觉SLAM rarely works alone。它通常与IMU惯性测量单元融合。这就引出了一个关键问题时间同步。相机采集一帧图像需要时间曝光图像数据从传感器传到处理器也需要时间。IMU的数据输出频率通常几百Hz远高于相机通常30-60Hz。如果直接将某一时刻的IMU数据与图像时间戳简单对应会引入误差在高速运动时尤为明显。常见的解决方案硬件同步使用同步信号线让相机在曝光的中间时刻给IMU发送一个触发脉冲IMU记录下精确的时刻。这是最精确但需要硬件支持的方式。软件插值在软件层面记录图像和IMU数据各自的时间戳尽可能使用高精度的时钟。在处理时对于某一帧图像的时间戳 ( t_{img} )找到前后两个IMU测量值通过插值如线性插值、球面线性插值对于旋转来估计 ( t_{img} ) 时刻的IMU状态。在代码中务必为每一帧图像和每一个IMU数据包打上可靠的时间戳这是进行后续多传感器融合的基础。忽略同步问题VIO视觉惯性里程计的精度会大打折扣甚至导致估计发散。理解相机与图像不仅仅是理解几个公式和API调用。它是连接物理世界与数字算法的桥梁决定了我们感知数据的质量和形式。从坐标系的转换到畸变的校正再到图像数据的处理每一步的疏忽都可能在后端被放大。花时间亲手标定一次自己的摄像头写代码实现一遍去畸变比单纯看书收获大得多。当你看到扭曲的线条被拉直当你看到特征点沿着正确的射线方向投射出去时你对视觉SLAM的理解就真正上了一个台阶。在后续的视觉里程计中我们将会频繁地与像素坐标、归一化坐标、相机坐标系和世界坐标系打交道现在的扎实基础会让未来的路走得更加顺畅。