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 坐标系转换:从世界到像素的完整链路
整个转换过程,可以概括为三步:
- 世界 → 相机: 刚体变换(旋转 + 平移)
- 相机 → 图像: 透视投影(针孔模型)
- 图像 → 像素: 仿射变换(缩放 + 平移)
下面这张图,是我自己画的一个流程,帮你理清逻辑:
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 向上。混用必出问题。
- 齐次坐标: 一定要用齐次坐标来做矩阵运算。我见过有人手动拼接旋转和平移,结果维度搞错,调试了一整天。
- 标定板精度: 内参标定时,棋盘格角点的检测精度直接影响后续所有重建结果。我建议用亚像素级别的角点检测,不要偷懒用整数坐标。
嗯,这一节就到这里。记住:相机模型不是理论玩具,它是你手里最锋利的工具。把它用熟了,后面的深度估计和三维重建,你会觉得豁然开朗。