☰
视觉SLAM四坐标系详解:从三维世界到像素坐标的完整投影链路
2026/10/15 5:28:57 网站建设 项目流程

如果你正在啃《视觉SLAM十四讲》或者任何一套视觉SLAM教程,大概率会在前几章就被“世界坐标系、相机坐标系、图像物理坐标系、像素坐标系”这四兄弟绕晕。我当年第一次看到那个投影公式时,真的在草稿纸上画了半天,还是没想明白为什么一个三维点经过一堆矩阵乘来乘去,最后就变成了图像上那两个整数坐标。

这篇总结就是把这条“从真实三维世界到二维像素”的完整链路扒开揉碎,讲清楚每个坐标系存在的意义、坐标系之间是怎么变换的,以及那些特别容易踩的坑。不管你是刚入门想弄懂基础概念,还是已经写过代码但偶尔被坐标变换折腾,这篇内容都值得你花十分钟读一遍。

1. 为什么坐标变换是SLAM的第一道坎

1.1 从三维世界到二维像素,信息到底丢了什么

先想一个问题:相机拍照,本质上在干什么?

一个三维空间中的点,经过透镜投影到成像传感器上,传感器再把光信号转换成像素灰度,最后你在屏幕上看到一张二维图像。在这个过程中,三维点变成了二维点,最直观的损失是深度信息——拍照的那一瞬间,深度被“压扁”了。

视觉SLAM里所有事情都建立在这条链路上:相机观测到像素点,像素点要能对应回三维空间里的某个点,否则就没法定位也没法建图。而像素和三维点之间的桥梁,就是坐标变换。

所以视觉SLAM前端里那些看起来高大上的名词,比如特征点匹配、三角化、PNP求解位姿,剥开外衣之后,底层全是在不同坐标系之间做转换。你把这个转换搞明白了,后面学本质矩阵、单应矩阵、光束法平差都会顺很多。

1.2 四个坐标系的分工定位

从三维世界到最终像素,聪明的做法不是一步到位,而是拆成几步。每拆一步,就引入一个新的“观察视角”,这就是四个坐标系产生的原因。

坐标系原点位置单位维度一句话理解
世界坐标系人为选定米三维SLAM地图和轨迹所在的“真实世界基准”
相机坐标系相机光心米三维以相机自己为中心看出去的三维空间
图像物理坐标系焦点在主点毫米/米二维投影到成像平面上的物理位置
像素坐标系图像左上角像素二维最终图像上离散的格子坐标

这套链路就像用不同“身份”描述同一个点:同一个物理点,你在世界坐标系里叫它“某点”,换到相机坐标系里它就成了“位于相机前方多少米、左侧多少米的一个点”,再到图像上就是“第几行第几列哪个颜色的点”。

每个坐标系都有自己存在的理由,我刚学的时候总想省掉中间某一步,后来发现省掉任何一步,数学表达都会变得别扭,代码也会越写越乱。老老实实按四步走,反而是最优解。

2. 四个坐标系逐个拆解:定义与物理意义

2.1 世界坐标系:SLAM地图的“绝对锚点”

世界坐标系是一个人为选定的三维参考系。原点放在哪里、坐标轴指向哪里,完全由你决定。比如你可以把原点设在室内某个墙角,X轴指向东,Y轴指向北,Z轴向上;也可以把原点设在机器人起始位置。

SLAM建图的结果——地图点、相机轨迹——都是在这个坐标系下表达的。换句话说,你最后拿到的那张稀疏点云地图,里面每个点的三维坐标就是世界坐标系的坐标。

有人可能会问:既然相机在运动,那直接用相机坐标系作为全局基准不行吗?不行。因为相机坐标系的原点跟着相机移动,如果以它作为全局基准,那建出来的地图就会一直“抖动”,局部不变,全局全乱。真实世界里一个静止的墙面,它的坐标应该保持不变,所以必须有一个固定的世界坐标系来锚定一切。

在十四讲里,习惯用 (P_w) 或者 (p_w) 表示世界坐标系下的三维点,下标里的 (w) 就是 world 的意思。

2.2 相机坐标系:以光心为原点的观察者视角

相机坐标系和世界坐标系一样,也是一个三维直角坐标系,但原点从“世界某处”搬到了相机的光心。

这个坐标系怎么定义?按视觉SLAM里常用的约定:X轴指向相机右侧,Y轴指向相机下方,Z轴指向相机前方,也就是主光轴方向。这是一个右手坐标系,朝Z轴正方向看时,X轴到Y轴是逆时针关系。

相机坐标系的核心价值是描述“相机眼中”的三维结构。世界坐标系下一个点距离原点可能有几十米,但它在相机坐标系里可能只是前方两米、偏右零点三米的一个点。这样一来,透镜投影的数学关系就变得非常简单。

有一点要特别注意:相机坐标系下的点仍然是三维的,它的Z值就是物体到相机光心沿光轴方向的深度。这个Z值后面会在投影公式里出现无数次,千万别把深度理解成“物体到相机的直线距离”的欧氏距离,那是不一样的概念。

2.3 图像物理坐标系与归一化平面:三维到二维的变身

从三维到二维的第一次“降维”,就发生在图像物理坐标系这里。

相机坐标系下有个三维点 (P_c=(X_c, Y_c, Z_c)),它通过透镜中心投影到成像平面上,形成一个像点。如果我们用一个假想的、位于Z=1处的归一化平面来描述这个投影,那么:

[ x' = X_c / Z_c ] [ y' = Y_c / Z_c ]

意思是:三维点投影到距离光心1米处的平面上,得到两个二维比例值。这个二维平面就是归一化平面,坐标 ((x', y')) 称为归一化坐标。

为什么要引入归一化平面?因为它是把“三维点转换到像素点”这个非线性过程拆得足够干净的一个中间产物。归一化坐标本身没有单位、没有尺度,它是纯几何比例关系。等后面加上内参,才变成有单位的像素坐标。

很多教材里还会提到“图像物理坐标系”,原点在图像中心(主点),单位是毫米,物理意义是成像传感器上实际像点的位置。它是归一化平面的放缩版,两者本质上描述的是同一件事,只是尺度不同。在十四讲里提到“图像物理坐标系”时,你直接把它理解成投影几何关系下的二维坐标即可。

2.4 像素坐标系:最终你在图像里看到的数字

像素坐标系就是我们每天处理图像时接触的那个坐标系了。它的原点通常在图像的左上角,U轴向右,V轴向下,单位是像素。

注意,像素坐标是离散的整数格子,虽然像素坐标形式上可以写浮点数,但它代表的永远是传感器上的“第几个像素”。所以到了像素坐标系里,已经没有“米”的概念了,只有“格子”的概念。

从归一化平面到像素坐标系,其实只需要两步:先按焦距相关的比例缩放,再做一次原点平移。

[ u = f_x \cdot x' + c_x ] [ v = f_y \cdot y' + c_y ]

这里的 (f_x)、(f_y) 是焦距在像素单位下的度量,(c_x)、(c_y) 是主点在像素坐标系中的位置,也就是图像中心通常对应的那个像素。这四个参数共同组成了相机内参矩阵K。

我在 2.3 和 2.4 之间来回看过很多次,最后记住一句话就够了:归一化坐标是纯几何比例,像素坐标是带单位和原点的最终刻度。

3. 变换链路与数学原理:外参内参怎么串起来

3.1 外参:世界系到相机系的刚体变换

把世界坐标系和相机坐标系对齐,需要一次刚体变换。刚体变换的意思是不改变物体形状,只做旋转和平移。

[ P_c = R \cdot P_w + t ]

其中 R 是 3x3 旋转矩阵,t 是 3x1 平移向量。这组参数描述了相机在世界坐标系里的姿态和位置,所以叫外参。

外参总共6个自由度:旋转3个、平移3个。旋转矩阵虽然看起来有9个数,但由于它必须是正交矩阵且行列式为1,实际只有3个独立变量,可以用旋转向量、欧拉角或四元数表示。

这里要强调命名习惯的问题。很多代码库里 (T_{cw}) 表示“把世界坐标系下的点变换到相机坐标系”的矩阵,也就是 (P_c = T_{cw} \cdot P_w)。但也有库用完全相反的命名,写代码前先看清楚注释,不然一步错步步错。

一个直观类比:你在房间里站着(世界坐标系),相机拍向你。相机看到的你,可以用“相机需要旋转多少度、平移多少米”才能和世界坐标系重合来描述。R 和 t 就是这个“相机观察者视角”的数学化表达。

3.2 内参:相机系到像素系的一步化映射

有了相机坐标系下的三维点,还要经过投影才能变成像素坐标。完整过程分成两步思考:

第一步,把相机坐标投影到归一化平面:

[ x' = X_c / Z_c, \quad y' = Y_c / Z_c ]

第二步,用内参矩阵把归一化坐标变成像素坐标:

[ u = f_x \cdot x' + c_x ] [ v = f_y \cdot y' + c_y ]

两行合起来,内参矩阵 K 就出现了:

[ K = \begin{bmatrix} f_x & 0 & c_x \ 0 & f_y & c_y \ 0 & 0 & 1 \end{bmatrix} ]

为什么 (f_x) 和 (f_y) 会不一样?因为相机传感器的像素不是正方形的话,同一个物理焦距在横向和纵向对应的像素个数就不同。很多手机或工业相机像素是正方形的,此时 (f_x=f_y),但代码里千万不要默认它们相等,标定出来是多少就用多少。

内参矩阵是相机本身的属性,在相机出厂并通过标定后基本不变。所以内参只需要标定一次,外参会随着相机运动不断变化。

3.3 齐次坐标与完整的投影方程

直接把旋转和平移两个东西分开写在公式里,表达起来还行,但如果变换链路再长一点就麻烦了。于是数学上引入齐次坐标:一个三维点 (P=(X,Y,Z)) 写成 ((X,Y,Z,1)),旋转矩阵 R 和平移向量 t 扩成4x4变换矩阵 T:

[ T = \begin{bmatrix} R & t \ 0 & 1 \end{bmatrix} ]

这样一来,一次刚体变换就是一个矩阵乘法。

完整的投影方程写成齐次形式就是:

[ \lambda \cdot p = K \cdot T_{cw} \cdot P_w ]

这里 (\lambda) 不是随便一个比例常数,它就是相机坐标系下的深度 (Z_c)。这个方程的意思是:世界点先乘外参到相机系,再乘内参到像素系,最后因为投影关系,结果齐次坐标的第一、第二分量和像素坐标差了一个深度缩放。

很多新手把 (\lambda) 丢掉,直接 (p = K \cdot T \cdot P_w),算出来的像素坐标和真实差了几倍甚至为负。先记住:只要是从三维点算像素,别忘了最后一步除以深度或者用齐次坐标归一化。

3.4 逆变换:从像素回到三维世界

SLAM里不只正向投影,反向也很常用:已知像素坐标和相机位姿,想恢复三维坐标。

反向的路径是:像素坐标 ( (u,v)) 先减去主点,再除以焦距,得到归一化坐标:

[ x' = (u - c_x) / f_x ] [ y' = (v - c_y) / f_y ]

但是到这里你只能得到一条射线上的方向,也就是 ((x', y', 1)),并不知道点离相机有多远。想恢复成相机坐标系下的三维点,必须知道深度 (Z_c):

[ X_c = x' \cdot Z_c ] [ Y_c = y' \cdot Z_c ] [ Z_c = Z_c ]

然后再用外参的逆变换回到世界坐标系:

[ P_w = R^T \cdot (P_c - t) ]

这就是深度相机和双目相机能够直接“测距”的原因:它们给了每个像素一个深度 Z,有了深度,像素坐标就能还原成三维坐标;而单目相机没有深度,只能靠多帧之间的三角化来估计深度。

4. 实操记录:能跑通的投影与反投影代码

4.1 正向投影:世界坐标点投影到像素坐标

很多SLAM工程里都需要把地图点投影到当前帧图像上,用于特征匹配验证或者可视化。这里给出一段可直接运行的思路,用C++和Eigen实现。

#include <Eigen/Core> #include <Eigen/Geometry> #include <iostream> int main() { // 内参(以某数据集的相机为例,单位是像素) double fx = 458.654, fy = 457.296; double cx = 367.215, cy = 248.375; // 外参:世界坐标系 -> 相机坐标系 Eigen::Matrix3d R_cw; Eigen::Vector3d t_cw; R_cw = Eigen::AngleAxisd(0.2, Eigen::Vector3d::UnitZ()).toRotationMatrix(); t_cw << 0.1, 0.0, -0.1; // 世界坐标系下的一个三维点 Eigen::Vector3d pw(2.0, 3.0, 5.0); // 1. 世界系 -> 相机系 Eigen::Vector3d pc = R_cw * pw + t_cw; // 2. 如果点在相机后面,投影出来没有意义 if (pc.z() <= 0.0) { std::cerr << "point is behind camera" << std::endl; return -1; } // 3. 相机系 -> 像素系(先除深度) double u = fx * pc.x() / pc.z() + cx; double v = fy * pc.y() / pc.z() + cy; std::cout << "u = " << u << ", v = " << v << std::endl; return 0; }

代码里先判断pc.z() <= 0这一步是我实际项目里踩过坑之后养成的习惯。相机后面的点不该被投影出来,如果不加这个判断,算出来的像素坐标会出现在奇怪的位置,debug的时候特别迷惑。

4.2 反向投影:像素坐标加深度恢复世界坐标

反过来,已知像素坐标和深度,想恢复它在世界坐标系下的坐标,代码如下:

#include <Eigen/Core> #include <Eigen/Geometry> #include <iostream> int main() { double fx = 458.654, fy = 457.296; double cx = 367.215, cy = 248.375; Eigen::Matrix3d R_cw; Eigen::Vector3d t_cw; R_cw = Eigen::AngleAxisd(0.2, Eigen::Vector3d::UnitZ()).toRotationMatrix(); t_cw << 0.1, 0.0, -0.1; // 已知:像素坐标 + 深度(单位:米) double u = 400.0, v = 300.0; double zc = 5.0; // 1. 像素系 -> 归一化平面 double nx = (u - cx) / fx; double ny = (v - cy) / fy; // 2. 归一化平面 + 深度 -> 相机系 Eigen::Vector3d pc(nx * zc, ny * zc, zc); // 3. 相机系 -> 世界系 Eigen::Vector3d pw = R_cw.transpose() * (pc - t_cw); std::cout << "world point: " << pw.transpose() << std::endl; return 0; }

这段代码的第三步,我见过太多人写成R_cw.transpose() * pc + t_cw,这算出来完全不对。理由在 3.4 已经推导过:外参是 (P_c = R \cdot P_w + t),解出 (P_w) 时必须先减 (t) 再乘 (R^T)。

4.3 归一化平面在三角化和PNP中的角色

写代码久了你会发现,很多SLAM算法模块并不直接操作像素坐标,而是先用内参把像素坐标转成归一化坐标,再做后续计算。

比如两个相邻关键帧之间估计本质矩阵 (E) 时,OpenCV的函数需要输入的是归一化坐标,不是像素坐标。因为归一化坐标已经去掉了内参的影响,几何关系更纯粹。

再比如单目三角化,已知两帧相机位姿和两个匹配的归一化坐标,问题就变成了“两条空间射线求交点”。如果直接拿像素坐标去算,还得时刻惦记着内参,公式推导会被拖得很长。

所以我的习惯是:在预处理阶段一次性把像素点去畸变并转成归一化坐标,后续所有多视图几何计算都用归一化坐标进行。这样代码结构清晰,也方便换相机时只改内参。

5. 常见问题与避坑实录

5.1 深度Zc到底该除以哪一个

这个问题我被人问过好多次:投影的时候为什么是除以 (z),而不是除以 (x) 或 (y)?

因为相机坐标系定义时,Z轴沿光轴指向场景前方。中心投影的几何关系是:像点坐标与三维点的 (x/z)、(y/z) 成正比,这个比例天然对应透视关系。除以 Z 之后,才有归一化平面那一说。

如果你把除以 (x) 或者除以别的分量,得到的就不是正确的投影关系。判断自己有没有除错,可以做个简单验证:点离相机越远(Z越大),像素坐标的变化应该越平缓;点离相机很近时,稍微动一点,像素位置会剧烈变化。镜头前放个挥手的人,背景几乎不动,就是这个道理。

5.2 坐标系轴向差异:为什么u轴和x轴不是一回事

相机坐标系按OpenCV习惯是X向右、Y向下、Z向前;像素坐标系是U向右、V向下。看起来方向一致,但单位不同:相机系单位是米,像素系单位是像素。

图像物理坐标系里,原点在图像中心,单位是毫米;如果直接用图像物理坐标去做像素变换,得先知道每个像素物理尺寸多少毫米。而归一化坐标更方便,它直接把单位消掉了,与内参结合时天然匹配。

有教材把相机坐标系的Y轴定义为向上,这在视觉里不常见,但在机械臂标定里很常见。如果你用了别的库,一定要先确认坐标系轴方向定义,否则求出来的位姿可能绕X轴转了180°却完全看不出来。

5.3 畸变校正要在什么时候做

相机镜头不是完美的针孔模型,所以像素点存在畸变。畸变模型通常有径向畸变和切向畸变两部分,畸变发生在真实投影的成像平面上,而不是在像素坐标上直接加减一个值。

正确的校正顺序是:像素坐标反投影到归一化平面,在归一化坐标上根据畸变模型校正,再投射回像素坐标。千万不要直接在像素坐标上做畸变校正公式的代入,那样算出来是错的。

畸变公式大致长这样:

[ x_{dist} = x \cdot (1 + k_1 r^2 + k_2 r^4 + k_3 r^6) + 2 p_1 x y + p_2 (r^2 + 2x^2) ] [ y_{dist} = y \cdot (1 + k_1 r^2 + k_2 r^4 + k_3 r^6) + p_1 (r^2 + 2y^2) + 2 p_2 x y ]

这里的 (x,y) 是归一化坐标,(r^2 = x^2 + y^2)。SLAM前端处理图像时,通常会提前用标定文件去畸变整张图,或者对特征点坐标做去畸变,保证后续几何约束建立在理想针孔模型上。

5.4 逆变换的符号陷阱:R^T与t的顺序

逆变换公式 (P_w = R^T(P_c - t)) 和 (P_w = R^T P_c - R^T t) 是等价的,但很多人写着写着就变成 (R^T P_c + R^T t)。

怎么验证自己有没有写错?取一个极简外参,比如 R 是单位矩阵,t 是 (1,0,0)。这时候相机原点在世界坐标系的 (1,0,0) 处。相机坐标系下原点自身坐标是 (0,0,0),那它对应世界坐标应该是 (1,0,0)。代入 (R^T(0 - t) = -t = (-1,0,0)),显然错了;应该是 (P_w = R^T(P_c - t) = R^T( - t) = (-1,0,0))?等等,这里我算一下。

等等,如果 R=I, t=(1,0,0):外参公式 (P_c = P_w + t)。相机原点在世界系中:(P_c = 0),求 (P_w)。(P_w = P_c - t = (-1,0,0))。所以世界系相机原点在 (-1,0,0),不是(1,0,0)。注意外参的定义:“世界系点变换到相机系”。相机坐标系原点在世界系是什么?设世界系原点 (P_w=0),相机系里为 (P_c = t=(1,0,0))。这说明世界系原点在相机前方1米处。那相机光心在世界系中就是 (-t=(-1,0,0))。我的验证例子设错了。好,用文字说明时要注意这个逻辑。

验证技巧:直接代入边界情况,把R设成单位阵,把 (P_w=0) 代入外参公式,得到 (P_c=t),再把这个情况代入逆变换公式,看能否恢复 (P_w=0): [ P_w = R^T(P_c - t) = I \cdot (t - t) = 0 ] 如果写成 (R^T P_c + R^T t),结果变成 (2t),就错了。这个代入法每次都能快速自检。

5.5 坐标变换在SLAM前后端中的扩展应用

坐标变换不是只出现在投影公式里。前端特征匹配后估计相对运动,本质是在求两个相机坐标系之间的变换;后端图优化里的误差项,也经常会把地图点重投影到图像平面上,计算重投影误差;回环检测里做相似变换 Sim3,则比刚体变换多一个尺度因子。

把这些知识点串起来后你会发现,所谓SLAM,本质上就是不断估计和优化坐标系之间变换关系的过程。不同模块只是换了不同场景下的变换组合而已。

我自己后来每写一个和相机相关的模块,不管多简单,都会先在心里把那四个坐标系过一遍:我手里数据在哪个坐标系,我要算的目标在哪个坐标系,中间要经过几次刚体变换、几次投影变换。想清楚了再动手写代码,基本一次就能写对,比起写完再调试半天划算得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询