针孔相机模型
机器人最丰富的传感器是相机,而相机首先是一台把三维世界压成二维图像的机器:它丢掉了每个点的深度,只保留方向。这一节从一个小孔出发,用相似三角形推出 x = fX/Z,再用交互图弄清焦距、视场角、消失点、滑动变焦,以及为什么单张图像无法恢复深度。后面的标定、立体视觉、SLAM、NeRF 全都从这一个映射出发。
1为什么机器人需要它
相机便宜、轻、信息量巨大:一帧图像就有几十万到几百万个像素,而且它几乎是机器人唯一能"看见"颜色、纹理、文字和语义的传感器。抓取要知道杯子在哪里,导航要知道门在哪里,视觉伺服要把手爪对准螺丝孔,模仿学习的策略网络直接以图像为输入。但图像里没有一个数字写着"这个点离你 2.3 米"。要把像素变回三维世界,第一件事就是弄清楚:相机到底丢掉了什么,保留了什么。
这是 Part III(3D 视觉与几何)的第一节,所以先说一下这一部分在讲什么。它只有一条主线:三维点 → 像素这个映射,以及怎样求它的逆。第 10 章(相机与投影)研究这个映射本身:针孔模型(本节)、齐次坐标(10.2)、内参外参与畸变(10.3)、标定(10.4)、平面到平面的单应(10.5)。第 11 章(多视图几何)问:有了两张以上的图像,怎样恢复深度和相机运动?对极几何、立体视觉、三角化、PnP、特征匹配与光束法平差。第 12 章(三维表示与配准)问:恢复出来的三维结构怎样存储、对齐、渲染?点云与 ICP、体素/网格/SDF、图形学管线、NeRF 与 3D Gaussian Splatting。每一章都是同一个映射的变奏,而它的第一个版本就是这一页。
它和前面的内容有天然联系:像素坐标是一个向量(1.1 节),投影是一个"几乎线性"的映射(1.3 节),它的局部线性化就是雅可比(3.3 节),相机本身是一个坐标系(5.1 节)。装在机械臂末端的相机,位姿就是正运动学(7.3 节)再乘一个固定的手眼变换,没有任何新东西。
2直觉:光线穿过一个小孔
最早的"相机"是一间暗室:在墙上开一个小孔,对面的墙上就出现一幅倒立的室外景象。原理只有一条:光沿直线传播。场景中每个点向四面八方发光,其中只有恰好穿过小孔的那一根射线能到达像平面,所以像平面上的每个点只"听到"场景里一个方向的光。
下图是侧视图:针孔在原点,光轴指向右,物体是右边那根琥珀色的箭头(高 \(H\),距离针孔 \(Z\)),像平面在针孔后面 \(f\) 处(\(f\) 叫焦距,这里指"针孔到像平面的距离")。过针孔的那条射线把图分成两个相似三角形,一个以物体为边,一个以像为边,所以
负号表示像是倒立的。计算机视觉里一般把像平面"搬"到针孔前面同样的距离 \(f\) 处(图中的虚线竖线,叫虚拟像平面):同一条射线与它相交在同一高度的镜像点,数值相同,像却是正的。以后我们永远用这个正立的约定。拖动箭头尖端,调焦距和孔径,注意三件事:像的大小 \(\propto f/Z\);孔变大时,一个场景点在像平面上被抹成一个光斑,直径 \(b = a(1+f/Z)\);而进光量 \(\propto a^2\)。孔也不能太小:太小则太暗,而且光的衍射会让光斑重新变大(最清晰的孔径大致与 \(\sqrt{f\lambda}\) 同量级,\(\lambda\) 是波长)。
- 让像和物体一样高(\(|h'| = H\),误差 4% 以内)。\(f\) 和 \(Z\) 之间是什么关系?
- 把孔径 \(a\) 开大,直到模糊斑比整个像还大。针孔为什么必须很小?可是太小又会怎样?
- 切换到"虚拟像平面",看像变成正立的。
用透镜升级一下。小孔太暗,所以真实相机用透镜把穿过一个大孔径的整束光重新汇聚到一点。薄透镜公式 \(\tfrac1Z+\tfrac1{z_i}=\tfrac1F\)(\(F\) 是透镜焦距,\(z_i\) 是清晰成像的像距)说明:只有一个深度的平面能恰好聚焦,其他深度会模糊成直径 \(c\approx a\,|s-z_i|/z_i\) 的弥散圆(\(s\) 是传感器到透镜的距离),可接受的模糊范围就是景深;光圈数 \(N=F/a\) 越大(孔径越小)景深越深,越接近针孔。关键是:对"一个点落在哪个像素"这个几何问题,对焦清晰时透镜相机和针孔相机给出同一个答案,唯一的区别是 \(f\) 现在是"透镜到传感器的距离"(对远处物体就是 \(F\) 附近)。所以本课以及整个 Part III 都用针孔模型。
针孔相机保留方向,丢掉距离。每个像素对应空间里过针孔的一整条射线,射线上的所有点(近的小东西、远的大东西)成同一个像。从图像恢复三维,本质上就是在每条射线上把丢掉的那个数 \(Z\) 找回来。
3动手玩:相机在世界里
现在把相机放进三维世界。左图是世界:地面网格(红线沿 \(x\),绿线沿 \(y\))、一个 1 m 的方盒(琥珀色)、三根竖直的杆(蓝色)和一台相机的视锥;相机自带的坐标轴是 OpenCV 光学坐标系:\(x_c\) 向右(红),\(y_c\) 向下(绿),\(z_c\) 向前(蓝)。右图是这台相机拍到的画面,像素坐标 \((u,v)\):\(u\) 向右,\(v\) 向下,原点在左上角。画面外的淡色线是"如果传感器更大会看到什么"。
用滑块把相机绕场景转、拉远拉近、抬高降低,再调焦距。看三件事:平行线怎样汇聚到消失点(彩色圆圈,与世界轴同色;用同样方式画出的虚线是地平线);同一个方盒怎样随位置变大变小;以及改变焦距与移动相机有什么不同。
- 让地面上的一族平行线汇聚到图像内部的一点(某个消失点落进画面)。提示:试试"沿着路看"。
- 让相机几乎垂直向下看:三根竖直的杆也开始汇聚(蓝色消失点进入画面)。为什么平视时它们是平行的?
- 让方盒占满画面(像高超过画面高度的 85%)。有几种办法?
- 让方盒缩成一个小点(像高不到 40 px,仍然完整在相机前方)。
- 把视场角(FOV)调到 100° 以上:画面边缘的物体发生了什么?再用"远、长焦"预设对比。
相机有两类旋钮:位置/朝向(外参,6 个自由度)决定"从哪里看",焦距(内参)决定"取多大一块视野放大到传感器上"。平行线在三维中永不相交,在图像中却相交于消失点:消失点只取决于方向与相机朝向,是一个在无穷远处的点的像,这正是下一节(10.2 射影几何)要处理的无穷远点。
4第二、第三视角:深度去哪了,透视由什么决定
深度丢失:一个像素就是一条射线
反过来问:已知一个像素,三维点在哪里?答案是一条射线:沿射线的每一点都投影到这个像素。下图(侧视图,相机在左边,像平面归一化到 \(f=1\),实际换算用 \(f=800\) px)里,琥珀色手柄在像平面上选像素,拖动射线上的点手柄改变深度,像始终不变。一个近处的小东西,一个远处的大东西,一个更远的巨人,它们的像完全相同,这就是单目的尺度歧义:玩具车模型和真车拍出来分不出来。
怎么解决?再来一台相机。第二台相机的射线与第一条相交于一点,交点就是三维点(深度恢复,11.3 节)。打开"两台相机",第二台相机里同一个点的像素位置随深度变化,差值就是视差 \(d = f\,b/Z\)(\(b\) 是基线,这里为了画在侧视图里把基线画成竖直的;真实的立体相机是水平的)。
- 沿射线把点的深度改变 2.6 倍以上,而像素一动不动。物体的真实高度 \(S\) 变化了多少?
- 打开"两台相机",调节深度使视差正好是 100 px(±4)。反过来由视差算深度:\(Z = f b/d\)。
透视强弱由相机位置决定:滑动变焦
很多人以为"广角镜头让透视夸张,长焦镜头把背景压扁"。更准确的说法是:透视的强弱由相机的位置决定,焦距只决定取景范围。把相机从主体前退后的同时拉长焦距,让主体在画面里保持同样大(\(f/Z\) 不变),背景却会"涨"起来:这就是希区柯克《迷魂记》里的"滑动变焦"(dolly zoom,Vertigo effect)。道理很简单:主体深度 \(Z\)、背景深度 \(Z+D\),同样大小的东西在画面里的比是
这个比值只与 \(Z\) 和 \(D\) 有关,与 \(f\) 无关。\(Z\to\infty\) 时比值趋于 1(背景与主体一样大,画面"平",即长焦的压缩感);\(Z\) 小时背景被甩得很小(广角的"纵深感")。下图的左半是俯视图(相机在下,光轴向上,青色扇形是视场),右半是相机画面(主体是琥珀色的人,后面是 3 m 和 8 m 处的两排建筑)。选"锁定主体大小"后,拖动距离,焦距会自动跟着变。
- 打开"锁定主体大小",把相机距离改变 3 倍以上。主体不动,背景在"呼吸",同时视场角怎么变?
- 让主体仍有 120 px 以上高,同时近处那排建筑的比值 \(Z/(Z+D)\) 达到 0.8:这就是长焦的"压扁"。
- 反过来:主体仍有 120 px 以上高,视场角超过 110°:背景被甩得很远,边缘被拉伸。
很远时:弱透视,与投影的雅可比
当物体的深度起伏 \(\Delta Z\) 远小于它到相机的平均距离 \(Z_0\),投影公式里的除数 \(Z\) 几乎是常数 \(Z_0\),投影就近似成一个线性映射:\(u\approx f X/Z_0 + c_x\),即弱透视(缩放的正交投影):把三维点丢掉 \(Z\),整体缩放 \(f/Z_0\)。下图把一个立方体放在深度 \(Z_0\),琥珀色是真实的透视投影,紫色虚线是弱透视(两者都用 \(Z_0/f\) 换回米,所以弱透视的立方体永远是同一个大小)。右图是最大偏差(占立方体边长的百分比)随 \(Z_0\) 的变化。
弱透视的另一个说法是投影雅可比的第三列趋于 0。完整的像素 \(\mathbf u = (u,v)\) 对相机系三维点 \(\mathbf X=(X,Y,Z)\) 的雅可比是 \(2\times3\) 矩阵(推导见第 5 节):\(J = \tfrac1Z\begin{bmatrix}f_x&0&-f_xX/Z\\0&f_y&-f_yY/Z\end{bmatrix}\)。前两列是"横向移动 1 m,像素动 \(f/Z\) px",第三列 \(-f X/Z^2\) 是"靠近或远离时像素沿径向外扩",\(Z\) 大时可以忽略。它是 \(2\times3\),必有一维零空间,这一维就是视线方向 \(\mathbf X\) 本身(读数里验证 \(J\mathbf X=\mathbf 0\)):沿射线移动三维点,像素纹丝不动,与 FIG 3 完全吻合。这个雅可比在视觉伺服里叫"图像雅可比"(interaction matrix 的平移部分),在光束法平差(11.6 节)里则是每个重投影误差对点坐标的导数。
- 把立方体推到远处,让透视与弱透视的偏差小于 3%。此时雅可比第三列相对第一列有多大?
- 把立方体拉近,让偏差超过 25%:此时"缩放的正交投影"完全不可用了。
几何:像素 = 过针孔的一条射线(FIG 1、3),深度是射线上的位置。代数:\(u = f_xX/Z + c_x\),一个除以深度的非线性映射,下一节把它写成线性的齐次坐标形式。微分:投影的雅可比是 \(2\times3\),零空间是视线(FIG 5),对三维点位置的微小改变,像素只对垂直于视线的分量有反应。三种看法是同一件事:相机只保留方向。
5再看数学
提示:把鼠标悬停在公式中带底色的符号上,FIG 1 中对应的长度会加粗。
投影方程
取相机坐标系:原点在针孔,\(x\) 向右,\(y\) 向下,\(z\) 沿光轴向前(OpenCV 光学坐标系,Part III 一律使用)。点 \(\mathbf X=(X,Y,Z)\),\(Z>0\)。虚拟像平面 \(z=f\) 上的交点由相似三角形给出:
把度量单位的像平面坐标 \((x,y)\) 换成像素:像素是边长为 \(p\)(像元尺寸)的小方块,原点在左上角,主点(光轴与图像的交点)在 \((c_x,c_y)\)。令 \(f_x=f_y=f/p\),即以像素为单位的焦距:
例如 2.8 mm 的镜头、3 µm 的像元:\(f_x = 2.8/0.003\approx 933\) px;若图像宽 1280 px,水平视场角是 \(2\arctan\tfrac{1280/2}{933}\approx 68.9^\circ\)。一般地,宽 \(w\) 像素的传感器对应的水平视场角(FOV)是
注意是半角的正切再乘 2,而不是 \(w/f\) 的反正切。也要注意一个像素对应的视角约为 \(1/f_x\) rad,在距离 \(Z\) 处一个像素覆盖约 \(Z/f_x\) 米:933 px 的镜头在 5 m 处,每个像素约 5.4 mm。
它不是线性的:齐次坐标的预告
把 \(u,v\) 写成矩阵形式,会发现差一点就是线性的:
右边对 \(\mathbf X_c\) 是线性的,左边的 \(\tilde{\mathbf u}=(u,v,1)^\top\) 乘上了一个未知的标量 \(\lambda=Z\):这个"相差一个比例"的等价关系就是齐次坐标,10.2 节会把它做成一个完整的工具;10.3 节把外参 \({}^{c}T_{w}\) 乘进来得到 \(\lambda\tilde{\mathbf u}=K[R\,|\,\mathbf t]\tilde{\mathbf X}\)。注意外参是相机位姿 \({}^{w}T_{c}\) 的逆。
反投影:像素 → 射线
求逆只能把 \(\lambda\) 留作自由量:
深度 \(Z\) 一旦知道(激光、深度相机、立体视觉、已知尺寸),三维点就确定了。\(\mathbf d\) 是射线方向(未归一化,\(z\) 分量为 1),\({}^{w}\mathbf t_{c}\) 是相机中心,即射线的起点。
投影的雅可比及其零空间
对 \(u=f_xX/Z+c_x\)、\(v=f_yY/Z+c_y\) 求偏导(3.3 节):
\(J\) 的秩是 2,零空间(1.6 节)由射线方向 \((X,Y,Z)\) 张成。更一般地,投影只依赖于 \(X/Z\) 与 \(Y/Z\),两者在 \(\mathbf X\to s\mathbf X\) 下不变。这也解释了尺度歧义:单目相机观测到的一切都只能确定到整体缩放(本课程之后的单目 SLAM 与运动恢复结构都要面对这一点)。
已知尺寸 ⇒ 深度,及其误差
若一个垂直于光轴的物体真实长度为 \(S\),像长 \(s\) 像素,则 \(s=f_xS/Z\),即
深度误差随距离的平方增长(这与 4.5 节的误差传播同出一辙)。例如 \(S=0.16\) m、\(f_x=600\) px、\(Z=3\) m:\(s=32\) px,像长误差 0.5 px 就带来 \(3^2\cdot0.5/(600\cdot0.16)\approx 4.7\) cm 的深度误差。
6机器人中的它:由已知尺寸估计距离
机器人常常贴一个尺寸已知的方形标记(AprilTag、ArUco),用它既能认出"这是哪个物体",又能直接估计距离。下面的实验是这个思路的蒙特卡洛版:对 0.5 到 10 m 之间等间隔的 250 个距离,用 \(\tilde s = fS/Z + \) 噪声 测得像长,再用 \(\hat Z = fS/\tilde s\) 估计距离。测量噪声取 \(\sigma_s = \sqrt2\,\sigma\)(一条边的长度由两个角点各带噪声 \(\sigma\) 的位置之差得到)。绿色带是线性化预测的 \(\pm1\sigma\) 范围 \(Z^2\sigma_s/(fS)\):近处几乎看不出误差,远处散点张成喇叭口。
这个模型是简化的:真实的标记检测器用角点的亚像素位置,并用四个角点通过 PnP(11.4 节)求完整的 6 自由度位姿,不要求标记正对相机;但误差随 \(Z^2\) 增长、与 \(fS\) 成反比的规律是一样的。所以要想看得远,办法只有三条:标记做大、焦距拉长(视场收窄)、噪声压低。
- 选择参数让 \(Z_{5\%}\) 超过 12 m。你调了哪个旋钮?每个旋钮在真实系统里的代价是什么(视场、成本、体积)?
选镜头:视场角与分辨率是一对矛盾,\(f_x\) 固定时 FOV 越宽,每个像素覆盖的角度越大。机器人上常见的做法是头部/固定相机用中等视场看全局,腕部相机用广角看到手爪周围;ALOHA 一类的双臂遥操作系统在两条手臂的腕部各装一台相机,再加固定视角;UMI 的手持夹爪用带鱼眼镜头的 GoPro。广角镜头会有明显畸变(10.3 节),所以这两类系统都要把畸变建模。工程接口:ROS 的 sensor_msgs/CameraInfo 就是把 \(K\)(内参)、畸变系数 \(D\)、矫正和投影矩阵 \(R, P\) 放在一条消息里;OpenCV 的 projectPoints、undistort 实现本课的映射。已知尺寸标记:AprilTag、ArUco 等基准标记,OpenCV 的 aruco 模块可用。快门:卷帘快门(rolling shutter)逐行曝光,相机或物体快速运动时图像会扭曲;全局快门(global shutter)整幅同时曝光,高速运动、视觉里程计中更受欢迎。事件相机:每个像素异步地只报告亮度变化,延迟低、动态范围大,但仍然遵循同样的针孔几何。深度相机:Intel RealSense、Azure Kinect、ZED 等把"再多一个传感器"集成在机身里,来解决本课的深度丢失,原理是下面几节的立体视觉、结构光或飞行时间。
7常见误区
镜头标注的 4 mm 是毫米,而 \(K\) 里的 \(f_x\) 是像素:\(f_x=f_{\text{mm}}/p\)。把 4 代进 \(K\) 会得到一个"视场角 \(2\arctan(320/4)\approx178^\circ\)"的荒谬相机。同一个镜头换成不同分辨率或不同像元的传感器,\(f_x\) 会变,而视场角取决于焦距与传感器物理尺寸之比。图像被降采样或裁剪后,\(f_x, c_x\) 也要按同样的比例变。另外,镜头的焦距 \(F\) 和针孔模型里的 \(f\)(到像平面的距离)只在对焦于无穷远时相同,近距离对焦会"呼吸":\(f\) 略有变化。
"广角拍人像鼻子大"不是镜头的错,而是你离得太近。FIG 4 说明了:站在原地换镜头只改变取景范围(等价于裁剪并放大,信息完全相同);只有移动相机才改变主体与背景的相对大小。所以估计"相机的透视"看的是相机中心的位置,而不是 \(f\)。反过来,在固定位置用更长焦距相当于裁剪;这也是为什么同一位置拍的长焦图像在几何上就是广角图像的局部裁剪(除了景深与分辨率)。
三个"上下"互相打架:世界 \(z\) 向上,相机系 \(y_c\) 向下,像素 \(v\) 向下(原点左上);针孔的真实像还是倒立的。画图时 Canvas 的 \(y\) 向下,数学图向上,很容易再翻一次。本课所有公式用 \(v\) 向下,FIG 1 为了直观用"高度向上",已经在图注里提示过。看到一张图像上下颠倒或左右镜像,第一反应应该是检查约定,而不是怀疑标定。
两个像素相距 100 px 对应多少米?取决于深度(\(Z/f_x\) m/px,FIG 3),也取决于物体是否正对相机。不能在单张图像里量长度,除非已知深度、已知尺寸、或者已知物体位于某个已知平面上(10.5 节的单应就是这种情形:平面把"射线上的哪一点"这个歧义消掉了)。深度学习的单目深度估计是用先验"猜"出深度,其尺度也可能是错的。
公式 \(u=f_xX/Z+c_x\) 在 \(Z<0\) 时照样给出一个有限的数。点 \((1,0,-2)\) 与点 \((-1,0,2)\) 投影到同一个像素(\(f_x=500\) 时 \(u=c_x-250\)),因为它们在过针孔的同一条直线上(FIG 3 中红色虚线)。所以任何投影代码在除以 \(Z\) 之前必须检查 \(Z>0\)(本课程的 Vision 辅助函数对 \(Z\le0\) 返回 NaN),三角化和光束法平差里也要检查"点在相机前方"(cheirality)。靠近 \(Z=0\) 的点会投影到无穷远,这在绘制长线段时尤其要裁剪(FIG 2 里每条线都先对近平面裁剪)。
8小练习
9延伸阅读
- R. Hartley & A. Zisserman, Multiple View Geometry in Computer Vision, 2nd ed., Cambridge University Press, 2004, Ch. 6 — 相机模型:有限相机、投影矩阵 \(P=K[R|t]\)、主点与焦距、弱透视等仿射相机
- R. Szeliski, Computer Vision: Algorithms and Applications, 2nd ed., Springer, 2022, Ch. 2 — 图像形成:几何基元与变换、三维到二维投影、镜头与光学、传感器
- D. Forsyth & J. Ponce, Computer Vision: A Modern Approach, 2nd ed., Pearson, 2011, Ch. 1 — 几何相机模型:针孔相机、透视投影、弱透视与正交投影
- Y. Ma, S. Soatto, J. Košecká & S. S. Sastry, An Invitation to 3-D Vision, Springer, 2004, Ch. 3 — 图像形成:针孔模型、内参矩阵、射影坐标,并有大量与运动恢复结构的联系