坐标系基础:世界坐标系、相机坐标系、图像坐标系、像素坐标系

做传感器标定这么多年,我见过太多人在坐标系转换上栽跟头。说实话,这玩意儿看着简单,但真要动手写代码的时候,坐标轴方向搞反、单位换算出错的情况比比皆是。今天咱们就把这四个坐标系彻底捋清楚。

为什么需要四个坐标系?

你想想看,一个三维空间里的点,要变成屏幕上显示的像素,中间得经过多少道工序?我刚开始接触这个领域时也觉得麻烦,后来在项目中调试一个双目视觉系统,才发现每个坐标系都有它存在的必要性。

简单来说:

  • 世界坐标系:描述物体在真实世界中的位置,单位是米或毫米
  • 相机坐标系:以相机光心为原点,描述物体相对于相机的位置
  • 图像坐标系:在成像平面上,用物理单位(毫米)表示像素位置
  • 像素坐标系:最终屏幕上看到的,以像素为单位的坐标

嗯,说白了就是从三维到二维的降维打击过程。

世界坐标系 → 相机坐标系

这是第一步,也是最直观的一步。世界坐标系里的点,通过旋转和平移,就能变换到相机坐标系下。

我记得有一次做车载摄像头标定,世界坐标系的原点选在了车辆后轴中心。结果发现相机安装位置有偏差,旋转矩阵算出来总是不对。后来才意识到,坐标系定义必须统一,不然所有计算都是白搭。

核心公式:

P_cam = R * P_world + T

其中 R 是 3x3 旋转矩阵,T 是 3x1 平移向量。

这里有个细节要注意:旋转矩阵是正交矩阵,它的逆等于它的转置。我在做手眼标定时,经常利用这个性质来简化计算。

相机坐标系 → 图像坐标系

这一步用到了小孔成像模型。相机坐标系下的点 (X, Y, Z),投影到成像平面上,得到图像坐标 (x, y)。

公式很简单:

x = f * X / Z
y = f * Y / Z

f 是焦距,单位是毫米。这里 Z 是深度信息,一旦投影到平面上,深度就丢失了。这也是为什么单目相机无法直接恢复深度——你想想看,一个近处的小物体和一个远处的大物体,在图像上可能一模一样。

我曾经踩过的坑: 在做鱼眼相机标定时,直接用这个针孔模型去套,结果畸变校正后图像边缘全是扭曲的。后来才明白,鱼眼相机用的是等距投影模型,不是针孔模型。不同相机模型,投影公式完全不同。

图像坐标系 → 像素坐标系

图像坐标系的单位是毫米,但最终显示在屏幕上的是像素。这一步就是做单位换算和原点平移。

公式:

u = x / dx + u0
v = y / dy + v0

其中 dx、dy 是每个像素的物理尺寸(毫米/像素),(u0, v0) 是图像中心在像素坐标系下的坐标。

我个人习惯把这一步写成矩阵形式,方便后续做批量计算:

[u]   [1/dx  0    u0] [x]
[v] = [0     1/dy v0] [y]
[1]   [0     0    1 ] [1]

完整的转换链路

把上面三步串起来,就得到了从世界坐标到像素坐标的完整变换:

Z * [u]   [fx  0   cx] [R | T] [X_w]
    [v] = [0   fy  cy] *       [Y_w]
    [1]   [0   0   1 ]         [Z_w]
                               [1  ]

这个 3x4 的矩阵,就是传说中的相机投影矩阵 P。fx、fy 是焦距的像素表示,cx、cy 是主点坐标。

实战建议: 在写代码时,我习惯用齐次坐标来处理所有变换。这样旋转和平移可以统一成一个矩阵乘法,代码简洁很多。OpenCV 里的 cv2.projectPoints 就是基于这个原理实现的。

坐标系转换的SVG结构图

下面这张图展示了四个坐标系之间的转换关系,以及每一步用到的参数:

世界坐标系 (Xw, Yw, Zw) 相机坐标系 (Xc, Yc, Zc) 图像坐标系 (x, y) 像素坐标系 (u, v) R, T 旋转+平移 f 小孔成像 dx, dy, u0, v0 离散化 关键参数总结 • 外参:R(旋转矩阵)、T(平移向量)—— 描述世界与相机的关系 • 内参:f(焦距)、dx/dy(像素尺寸)、u0/v0(主点)—— 描述相机内部结构 • 畸变参数:k1,k2,k3(径向畸变)、p1,p2(切向畸变)—— 修正镜头变形

常见问题与避坑指南

在实际项目中,坐标系转换最容易出问题的几个地方:

  1. 坐标轴方向搞反:OpenCV 的图像坐标系 y 轴向下,而数学里通常 y 轴向上。我刚开始做项目时,就因为没注意这个,投影出来的点全在图像外面。
  2. 单位不统一:世界坐标用米,焦距用毫米,像素尺寸用微米。三个单位混在一起,算出来的结果肯定不对。我的习惯是全部统一成毫米。
  3. 齐次坐标忘记归一化:从齐次坐标转回笛卡尔坐标时,要除以最后一个分量。这个步骤虽然简单,但写代码时很容易漏掉。

我曾经犯过的错: 在做多相机联合标定时,把左相机的坐标系当成了世界坐标系。结果右相机的位姿算出来全是错的。后来才意识到,世界坐标系应该选一个固定的参考系,比如标定板上的某个角点。

代码实现示例

下面是一个用 Python + NumPy 实现的完整转换函数:

import numpy as np

def world_to_pixel(P_world, R, T, K):
    """
    世界坐标 → 像素坐标
    P_world: 世界坐标系下的3D点 (N, 3)
    R: 旋转矩阵 (3, 3)
    T: 平移向量 (3, 1)
    K: 内参矩阵 (3, 3)
    """
    # 世界 → 相机
    P_cam = (R @ P_world.T + T).T  # (N, 3)
    
    # 相机 → 图像(归一化平面)
    x = P_cam[:, 0] / P_cam[:, 2]
    y = P_cam[:, 1] / P_cam[:, 2]
    
    # 图像 → 像素
    uv_hom = K @ np.vstack([x, y, np.ones_like(x)])
    u = uv_hom[0, :] / uv_hom[2, :]
    v = uv_hom[1, :] / uv_hom[2, :]
    
    return np.vstack([u, v]).T

这段代码我用了好几年,基本没出过问题。唯一要注意的是输入的点要保证在相机前方,也就是 Z_cam > 0,否则投影到图像上的点会是反的。

小技巧: 调试时可以用棋盘格标定板来验证转换是否正确。先检测出角点的像素坐标,再用已知的世界坐标反投影回去,看看误差有多大。我一般要求重投影误差小于 0.5 像素才算通过。

好了,坐标系基础就讲到这里。这四个坐标系之间的转换,是外参标定的基本功。你只要把每一步的物理意义搞清楚了,后面学手眼标定、多传感器融合都会轻松很多。

可可精品资料库