2. 相机成像模型:针孔相机模型、坐标系转换与内外参矩阵

聊到仿生眼的三维重建,绕不开的第一个坎儿就是——相机到底是怎么「看」到世界的?

我刚开始接触这个领域时,总觉得相机成像很简单,不就是镜头一照,传感器一记录嘛。直到第一次做双目立体匹配,发现怎么算都不对,才意识到:如果不把相机模型吃透,后面的深度估计全是空中楼阁

这一节,我们就来把针孔相机模型、四个坐标系的转换、以及内参外参矩阵彻底讲明白。我会结合自己踩过的坑,帮你少走弯路。

2.1 针孔相机模型:最朴素的成像原理

说白了,针孔相机模型就是用一个「小孔」来代替镜头。光线从物体表面反射,穿过这个小孔,投射到后面的成像平面上。

你想想看,这其实就是一个相似三角形的关系。物体在三维空间中的位置,和它在成像平面上的投影,存在一个简单的比例缩放。

核心公式(理想针孔模型):

X_image = f * (X_world / Z_world)
Y_image = f * (Y_world / Z_world)

其中 f 是焦距(小孔到成像平面的距离),(X_world, Y_world, Z_world) 是物体在相机坐标系下的三维坐标。

嗯,这里要注意:实际相机用的都是透镜,不是针孔。但针孔模型是理解所有相机几何的基础。我当年在实验室调一个工业相机时,发现图像边缘有畸变,就是先用针孔模型算出理想坐标,再套畸变模型去校正的。

2.2 四个坐标系:世界、相机、图像、像素

做三维重建,你脑子里必须时刻装着这四个坐标系。它们之间的转换,就是整个视觉几何的骨架。

我个人习惯把它们分成两组来理解:

  • 三维组:世界坐标系、相机坐标系
  • 二维组:图像坐标系、像素坐标系

为什么要分这么细?因为每个坐标系都有自己的「原点」和「单位」。搞混了,你的重建结果就会歪到天上去。

2.2.1 世界坐标系 (World Coordinate System)

这是你定义场景的「绝对坐标系」。你可以把原点放在任何地方——比如房间的角落、机器人的底座、或者棋盘格标定板的角点。单位通常是米或毫米。

2.2.2 相机坐标系 (Camera Coordinate System)

原点在相机的光心(也就是针孔的位置)。Z轴指向相机的前方(沿着光轴),X轴向右,Y轴向下(符合图像的行列习惯)。

我的经验: 在写代码时,我习惯把世界坐标系到相机坐标系的转换单独封装成一个函数。这样调试时,只要看旋转矩阵和平移向量对不对,就能快速定位问题。

2.2.3 图像坐标系 (Image Coordinate System)

这是一个二维坐标系,原点在成像平面的中心(光轴与成像平面的交点)。单位是物理长度(毫米)。它描述的是物体在成像平面上的实际位置。

2.2.4 像素坐标系 (Pixel Coordinate System)

这也是二维坐标系,但原点在图像的左上角。单位是像素。我们最终拿到的图像,就是像素坐标系下的数据。

为什么会多出这一步?因为相机的传感器是由一个个感光单元(像素)组成的,每个像素有固定的物理尺寸。从图像坐标系的毫米,到像素坐标系的像素,需要一个缩放和偏移。

2.3 坐标系转换:从世界到像素的完整链路

整个转换过程,可以概括为三步:

  1. 世界 → 相机: 刚体变换(旋转 + 平移)
  2. 相机 → 图像: 透视投影(针孔模型)
  3. 图像 → 像素: 仿射变换(缩放 + 平移)

下面这张图,是我自己画的一个流程,帮你理清逻辑:

世界坐标系 (Xw, Yw, Zw) 相机坐标系 (Xc, Yc, Zc) 图像坐标系 (x, y) 毫米 像素坐标系 (u, v) 像素 外参 R, t 投影 针孔模型 内参 fx, fy, cx, cy 完整转换公式(齐次坐标形式) Zc · [u, v, 1]^T = K · [R | t] · [Xw, Yw, Zw, 1]^T 其中 K 为内参矩阵,[R | t] 为外参矩阵

2.4 内参矩阵:相机的「出厂设置」

内参矩阵 K 描述了相机内部的几何特性。它把相机坐标系下的三维点,映射到像素坐标系下的二维点。

标准形式如下:

K = [ fx   0   cx
      0   fy   cy
      0    0    1 ]
  • fx, fy: 分别是 x 和 y 方向上的焦距(以像素为单位)。fx = f / dx,其中 f 是物理焦距,dx 是每个像素的物理宽度。
  • cx, cy: 主点坐标(光轴与成像平面的交点,在像素坐标系下的位置)。

我曾经踩过的坑: 有一次我用一个工业相机,标定出来的 cx 和 cy 并不是图像中心。我一开始以为是标定错了,后来查手册才发现,这个相机的传感器封装有偏移。如果你默认主点就在图像正中心,重建出来的物体位置会整体偏移。

2.5 外参矩阵:相机在空间中的「位姿」

外参矩阵描述了相机在世界坐标系下的位置和朝向。它是一个 3x4 的矩阵,由旋转矩阵 R 和平移向量 t 组成:

[R | t] = [ r11  r12  r13  t1
            r21  r22  r23  t2
            r31  r32  r33  t3 ]

R 是 3x3 的正交旋转矩阵,t 是 3x1 的平移向量。它们共同决定了如何把世界坐标系下的点,变换到相机坐标系下。

我个人习惯用欧拉角(尤其是 Yaw-Pitch-Roll 顺序)来理解旋转,但在代码里,我强烈建议用四元数或者旋转向量来存储和计算。为什么?因为欧拉角有万向锁问题,我在做仿生眼云台控制时就吃过这个亏——某个角度下,两个旋转轴突然重合,控制直接失灵。

2.6 实战:用 OpenCV 构建投影矩阵

理论说完了,我们来看代码。下面这个例子,演示了如何用 OpenCV 构建内参和外参,并完成一个三维点到像素坐标的投影。

import numpy as np
import cv2

# 1. 定义内参矩阵(假设相机分辨率 640x480,焦距 500 像素)
K = np.array([[500,   0, 320],
              [  0, 500, 240],
              [  0,   0,   1]], dtype=np.float64)

# 2. 定义外参(相机在世界坐标系下的位姿)
# 假设相机位于 (0, 0, 2) 米处,看向原点
R = np.eye(3)  # 单位矩阵,表示无旋转
t = np.array([[0], [0], [2]])  # 平移向量

# 构建外参矩阵 [R | t]
extrinsic = np.hstack([R, t])

# 3. 定义一个世界坐标系下的三维点(单位:米)
point_world = np.array([[1], [1], [0], [1]])  # 齐次坐标

# 4. 投影到像素坐标系
# 先转到相机坐标系
point_cam = extrinsic @ point_world  # 3x1

# 再投影到像素
point_pixel_hom = K @ point_cam  # 3x1
u = point_pixel_hom[0, 0] / point_pixel_hom[2, 0]
v = point_pixel_hom[1, 0] / point_pixel_hom[2, 0]

print(f"三维点 (1, 1, 0) 投影到像素坐标: ({u:.1f}, {v:.1f})")
# 输出应该是 (820, 620) 左右

一个小技巧: 在实际项目中,我通常会用 cv2.projectPoints() 这个函数,它一次性处理多个点,而且支持畸变参数。但理解底层的手动计算过程,能帮你更灵活地调试问题。

2.7 总结与避坑指南

这一节的内容,是仿生眼三维重建的「地基」。我建议你花时间把四个坐标系的转换关系画在纸上,直到闭着眼睛也能说出来。

最后,分享几个我亲身经历的教训:

  • 坐标系方向: 不同相机(比如 OpenCV 和 OpenGL)的坐标系定义不同。OpenCV 的相机坐标系是 Z 向前,Y 向下;而 OpenGL 是 Z 向后,Y 向上。混用必出问题。
  • 齐次坐标: 一定要用齐次坐标来做矩阵运算。我见过有人手动拼接旋转和平移,结果维度搞错,调试了一整天。
  • 标定板精度: 内参标定时,棋盘格角点的检测精度直接影响后续所有重建结果。我建议用亚像素级别的角点检测,不要偷懒用整数坐标。

嗯,这一节就到这里。记住:相机模型不是理论玩具,它是你手里最锋利的工具。把它用熟了,后面的深度估计和三维重建,你会觉得豁然开朗。


专注资料整理