相机内参最容易出现的学习状态是:知道矩阵里有四个数,代码也能跑,但说不清某个数变大后画面会怎么变,更不知道裁剪图片后为什么定位突然偏了。
这篇笔记从一条主线展开:三维点先变成一条方向,再把这个方向换算成像素。 理解这两步,内参就不再是一组需要死记的参数。
配套阅读:dToF 到深度图:range、Z、FOV 与裁剪。那篇将这里的几何用到测距、点云和图像预处理上。
1. 先约定:我们到底在哪个坐标系里说话
本文采用常见的相机 optical 坐标系:原点是投影中心 O,X 向右,Y 向下,Z 向前;像素坐标 u 向右、v 向下。
| 名字 |
例子 |
单位 |
代表什么 |
| 相机三维坐标 |
(X,Y,Z)=(0.2,0.1,2) |
米 |
点相对相机的位置 |
| 归一化像平面坐标 |
(xn,yn)=(0.1,0.05) |
无量纲 |
光线相对光轴的斜率 |
| 像素坐标 |
(u,v)=(380,270) |
像素 |
图像上落在哪里 |
| 图像尺寸 |
W=640,H=480 |
像素 |
图像有多少列、多少行 |
本文用整数表示像素中心:左上像素中心为 (0,0),右下为 (W−1,H−1)。因此整幅图像的外边缘是 −0.5 和 W−0.5,而不是两个像素中心。后文算 FOV、做最近像素投影时会保持这个约定。坐标约定可参照 RealSense 的投影说明。
注意:机器人本体经常使用“前、左、上”轴,不能把本体坐标直接代入相机投影式;ROS 中名字带 camera_link 也不自动意味着它就是 optical 坐标系。参见 ROS REP 103。
2. 小孔成像为什么能用相似三角形
把相机暂时想成一个很小的孔:空间点发出的光穿过同一个孔,落到后面的成像平面上。
图中 +Z 向右、+Y 向下;虚拟像平面是计算约定,不是实际传感器。手机端可横向滑动查看。
如果真实成像平面位于孔后面,即 Z=−f,相似三角形给出:
xreal=−fZX,yreal=−fZY
负号就是“倒像”。计算机视觉常把计算用的像平面放到孔前面 Z=f,使用一个虚拟前像平面,于是:
x=fZX,y=fZY
不是物理传感器真的搬到了前面,而是采用了更方便的投影表示。实际镜头比小孔复杂;这里的投影中心与焦距是几何模型的等效量,不要把整个标定模型理解成拿尺子量镜头外壳。
先令虚拟平面的距离为 1,得到:
xn=ZX,yn=ZY
这两个数只表示方向。例如 (0.2,0.1,2) 和 (0.4,0.2,4) 的归一化坐标相同,所以它们在同一条光线上,投影也相同。
单张图像损失的,正是沿这条光线的距离。
3. 从“方向”到“像素”:四个内参各干什么
上一节的 x=fX/Z 还没有回答一个工程问题:这个 x 究竟是毫米,还是图像中的第几列?如果 f 使用毫米,那么 x 也是像平面上的毫米距离;但程序处理的是像素数组,不能把“0.36 mm”直接当成“第 0.36 个像素”。
因此完整链条其实有四层:
| 层次 |
水平方向的量 |
单位 |
含义 |
| 空间方向 |
xn=X/Z |
无量纲 |
光线相对光轴的斜率 |
| 物理像平面 |
xmm=fmmxn |
mm |
成像点距离光轴多少毫米 |
| 像素网格 |
Δu=xmm/px |
pixel |
成像点距离主点多少个横向像素间距 |
| 图像坐标 |
u=Δu+cx |
pixel |
加上像素坐标原点后,落在图像的哪一列 |
竖直方向完全相同,只需把 X,xn,xmm,px,u,cx 换成 Y,yn,ymm,py,v,cy。把中间两步合并,才得到代码里常见的无畸变、零斜切针孔公式:
u=fxZX+cx,v=fyZY+cy
可以把它读成一句话:先算方向斜率,用 fx,fy 把方向换成像素偏移,最后移动到主点位置。 这与 OpenCV 相机模型 的定义一致。
3.1 fx、fy:把物理焦距从毫米换成像素
先只看水平截面。若等效物理焦距为 fmm,三维点在物理像平面上的横向位置是:
xmm=fmmZX
这里的 X/Z 没有单位,因此结果跟 fmm 一样是毫米。它回答的是“投影点距离光轴多少毫米”,还不是图像坐标。
设传感器横向相邻像素中心的物理间距(pixel pitch)为 px,单位是 mm/pixel。用毫米距离除以每个像素间距,才得到相对主点的像素偏移:
Δu=pxxmm=pxfmmZX
因此把括号中的换算系数定义为:
fx=pxfmm,fy=pyfmm
单位也会自然约掉:
mm/pixelmm=pixel
所以 fx 可以直观理解成:一个物理焦距长度里,横向能排下多少个像素间距。 它不是说镜头“长 600 个像素”,而是把同一个焦距改用像素网格来计量。
例如:
fmm=3.6mm,px=0.006mm/pixel
那么 fx=3.6/0.006=600pixel。再取 X=0.2m,Z=2m,完整计算是:
ZX=0.1⟹xmm=3.6×0.1=0.36mm
Δu=0.0060.36=60pixel⟹u=60+cx
若 cx=320,最终就是 u=380。这一步清楚地区分了两个问题:xmm=0.36mm 是传感器上的物理位置,u=380pixel 才是程序访问图像时使用的列坐标。若跳过像素尺寸,直接把毫米与 cx 相加,单位本身就不成立。
从角度看同一件事,令水平光线夹角为 θx:
tanθx=ZX,Δu=fxtanθx
因此 fx 表示“给定视线角度会展开成多少像素”。在光轴附近,移动 1 pixel 对应的角度近似为 1/fx 弧度;当 fx=600 时,光轴旁一个像素约为 arctan(1/600)≈0.0955∘。但整幅图上的每像素角度并非严格恒定,因为准确关系是 θx=arctan((u−cx)/fx)。
这里不是“镜头有两个独立的物理焦距”。同一个光学投影在横、纵像素尺度下分别表示,标定、像素形状、非等比例缩放等都可能使 fx=fy。
为什么一定要引入 fx,fy? 因为 fmmX/Z 的输出是物理像平面上的毫米位置,而视觉算法最终必须得到图像数组中的像素位置;fx,fy 正是连接这两个世界的换算尺度。只保存毫米焦距还不够:同一个 fmm 搭配不同 pixel pitch,会得到不同的像素投影;图像经过缩放后,镜头的毫米焦距没有变化,fx,fy 却必须跟着像素坐标尺度变化。实际标定给出的 fx,fy 是当前分辨率与成像管线下的有效参数,不应总是假定它们能由镜头铭牌和传感器规格精确相除得到。
保持图像尺寸与其余内参不变,并且主点位于图像内时:
- fx 越大,同样的水平角度占越多像素,水平视场越窄。
- fy 越大,同样的竖直角度占越多像素,垂直视场越窄。
- 单独把 fx 改大,同一角度会占据更多横向像素,投影横向拉宽;这不是把相机向右移动。
例如 X/Z=0.1,fx=600 时距离主点 60 像素,fx=900 时是 90 像素。它控制的是相对主点的放大倍率。
3.2 cx、cy:光轴落在图像哪里
让三维点落在光轴上,即 X=Y=0,立刻得到:
u=cx,v=cy
因此 (cx,cy) 是主点:理想模型中,光轴与像平面的交点在像素坐标里的位置。
主点不是三维相机中心。相机中心 O 在空间里;主点在图像平面上。主点也不保证恰好位于图像几何中心:装配、标定以及 ROI 裁剪都会影响它。
对于 640×480 图像,像素中心集合的几何中心是 (319.5,239.5)。工程资料也常用 (320,240) 作为近似。本文例题明确使用 cx=320,cy=240,把它视为一组示例内参,不混用这两个约定。
只把 cx 加 20,所有投影像素的 u 都加 20,但点之间的像素间距不变;改变相机真实位置则通常会产生与物体深度有关的位移,两者不是同一件事。
3.3 K:把四个数放进一张矩阵
K=fx000fy0cxcy1,Zuv1=KXYZ
左边的 Z 不能丢掉:右侧最后一个分量是 Z,需要除以它,才得到像素齐次坐标 [u,v,1]T。
更一般的内参矩阵可以在第一行第二列含斜切参数 s,此时 u=fxxn+syn+cx。本文实验和代码假定 s=0,不包含鱼眼、畸变或滚动快门模型。
4. 做一遍完整数值计算
设:
K=60000060003202401,PC=0.20.12m
先得到方向 (0.1,0.05),再换算像素:
u=600×0.1+320=380,v=600×0.05+240=270
现在做三个思想实验:
- X,Y 不变,Z 从 2 米变 4 米:投影变成 (350,255),向主点靠近。
- X,Y,Z 都乘 2:投影仍是 (380,270),因为方向没变。
- 点不动,fx,fy 都乘 2:投影变成 (440,300),离主点更远。
这也解释了为什么“相机后退”和“镜头变焦”不能在所有三维场景里互相代替:不同深度物体的相对大小、遮挡与透视关系可能不同。
5. 亲手拖动内参,看公式怎么变成画面
实验固定一个与像平面平行、边长 0.6 米的正方形,其中心是 P=(0.2,0.1,Z)。先猜结果,再拖动滑块:单独改 fx、单独改 cx、最后改 Z。
- 蓝框:0.6 m 正方形
- 红点:P 的投影
- 金色十字:主点
P 的投影:(380.00, 270.00) px;正方形投影尺寸:180.00 × 180.00 px。
水平视场角:56.14°;垂直视场角:43.60°。
仅模拟无畸变针孔投影,不是对真实相机重新标定;修改主点不会移动真实相机。所有计算在本页本地完成,无自动播放。
拖动后应观察到:改变主点会整体平移图形,改变焦距会改变图形尺寸,增加距离会使图形缩小。由于画面边界固定,主点偏移也会改变左右覆盖角度,不应始终套用“左右各一半 FOV”。
6. 反投影:一个像素究竟能恢复什么
把公式倒过来:
q=K−1uv1=(u−cx)/fx(v−cy)/fy1
这是一个 z 分量为 1 的方向代表向量,不是单位向量,也不是已经恢复好的三维点。
若额外知道光轴深度 Z,才有:
PC=Zq=(u−cx)Z/fx(v−cy)Z/fyZ
对 (380,270) 和 Z=2,恢复 (0.2,0.1,2) 米。
如果设备给的是沿光线的距离 r=∥PC∥,必须先归一化方向:
PC=r∥q∥q,Z=∥q∥r
例题里 Z=2 米,但 r=0.22+0.12+22≈2.01246 米。中心附近差别小,离光轴越远差别越大。后续 dToF 笔记 会用 45° 光线说明为什么把两者混用会让平墙弯曲。
7. 内参、外参、畸变:三件不同的事
7.1 外参先把点送到相机坐标系
如果点在世界坐标系里,应先做:
PC=RCWPW+tCW
再使用相机投影。下标表示从 W 变到 C,不要只写一个不注明方向的 T。
这里 tCW 是世界原点在相机坐标中的位置,不是相机在世界坐标中的位置。相机中心的世界坐标是:
CW=−RCWTtCW
只要令 PC=0,就能推导出来。内参回答“方向如何变成像素”,外参回答“同一个空间点换个坐标系怎么表示”。求物体位姿时可参考 OpenCV solvePnP 坐标约定。
7.2 畸变不是改一改 fx 就能消除
针孔投影会让空间直线成像为直线,而真实镜头可能产生径向、切向畸变。以常见模型为例,归一化坐标半径平方为 ρ2=xn2+yn2,径向部分会含有 1+k1ρ2+k2ρ4+⋯,变形量随位置改变。
所以一般流程是:
PW⟶PC⟶(xn,yn)⟶畸变坐标⟶(u,v)
不能把原始畸变图的像素直接代入本文无畸变反投影式。 应使用设备 SDK 的反投影函数,或依据正确的模型去畸变。OpenCV undistortPoints 不传 P 时通常输出归一化坐标;传入新相机矩阵时则输出相应像素坐标,不要再重复归一化。参见 OpenCV 标定与去畸变接口。
原图 K + D、矫正后图像的新内参、对齐到 RGB 后的目标内参,是不同的数据契约。ROS CameraInfo 也区分了原始图像的 K、畸变 D 与矫正后的投影矩阵 P;见 消息定义。
8. 如何得到可信的内参,而不是“猜一组能用的数”
从 FOV 和分辨率可以估算焦距,但这不是对真实镜头的完整标定。实践上应当:
- 先固定分辨率、裁剪模式、对焦/变焦状态,保存这些配置。
- 使用尺寸可信、足够平整的标定板,采集覆盖中心和边缘、含不同距离与倾斜角度的清晰图像,不要只在中心正对着拍。
- 按镜头类型选择模型并拟合内参与畸变,检查每张图的角点和重投影残差,而不只看一个平均数。
- 留出未参与拟合的图像检验;观察直线矫正效果,并用已知几何或距离检查最终定位结果。
- 图像预处理或相机工作模式变化后,确认内参是否需要相应更新或重新标定。
重投影误差是“已知三维标定点投回图像后,与检测到的角点差多少像素”。低误差是重要证据,却不自动保证所有位置的三维尺度都准确;标定板尺寸填错、数据覆盖不足、运动模糊等仍可能出问题。流程可参照 OpenCV 相机标定教程。
9. 裁剪和缩放后,为什么还要改 K
假设从原图左上 (x0,y0) 开始裁剪,原像素在新图中的坐标是:
u′=u−x0,v′=v−y0
因此焦距不变,主点变成 (cx−x0,cy−y0)。这是图像坐标原点变了,不是镜头变了。
若再缩放,则焦距也要按横、纵比例变化;主点还要考虑具体重采样的像素中心约定。深度值如果以米表示,不能跟着图像尺寸一起乘缩放倍数。
完整的偏心 FOV、ROI、resize 半像素修正、letterbox 公式与算例,放在 dToF 到深度图的第 5—7 节,避免在两篇笔记里分别维护一套不同口径。
10. 可运行例题与自测
下载 camera_geometry_demo.py,在下载目录运行。只需要 Python 3.9+ 标准库,无需相机硬件,也不用安装 NumPy。
python3 camera_geometry_demo.py --self-test
|
也可以在同目录中导入:
from camera_geometry_demo import CameraModel
camera = CameraModel(640, 480, 600, 600, 320, 240) print(camera.project((0.2, 0.1, 2.0))) print(camera.depth_to_point(380, 270, 2.0)) print(camera.ray(380, 270))
|
先不看答案,回答下面四个问题:
- 为什么像素 (cx,cy) 无法告诉我们物体离相机几米?
- 保持三维点不动,把 cx 加 10 和把 fx 加 10,结果相同吗?
- 当 X/Z=1,Y=0 时,轴向深度为 2 米的点,径向距离是多少?
- 裁掉左边 100 列后仍用原 cx 反投影,会错在哪里?
答案:主点只对应光轴方向;改 cx 是统一平移,改 fx 的位移取决于 X/Z;径向距离为 22 米;应改成 cx−100,否则归一化水平坐标少了 100/fx,三维 X 的误差为 −100Z/fx。
11. 接下来把基础接回机器人应用
最终要记住的不是一张矩阵,而是这条链:坐标变换 → 方向 → 镜头模型 → 像素;反过来则还需要一个有明确语义的深度。