相机内参最容易出现的学习状态是:知道矩阵里有四个数,代码也能跑,但说不清某个数变大后画面会怎么变,更不知道裁剪图片后为什么定位突然偏了。
这篇笔记从一条主线展开:三维点先变成一条方向,再把这个方向换算成像素。 理解这两步,内参就不再是一组需要死记的参数。
配套阅读:dToF 到深度图:range、Z、FOV 与裁剪。那篇将这里的几何用到测距、点云和图像预处理上。
1. 先约定:我们到底在哪个坐标系里说话
本文采用常见的相机 optical 坐标系:原点是投影中心 , 向右, 向下, 向前;像素坐标 向右、 向下。
| 名字 | 例子 | 单位 | 代表什么 |
|---|---|---|---|
| 相机三维坐标 | 米 | 点相对相机的位置 | |
| 归一化像平面坐标 | 无量纲 | 光线相对光轴的斜率 | |
| 像素坐标 | 像素 | 图像上落在哪里 | |
| 图像尺寸 | 像素 | 图像有多少列、多少行 |
本文用整数表示像素中心:左上像素中心为 ,右下为 。因此整幅图像的外边缘是 和 ,而不是两个像素中心。后文算 FOV、做最近像素投影时会保持这个约定。坐标约定可参照 RealSense 的投影说明。
注意:机器人本体经常使用“前、左、上”轴,不能把本体坐标直接代入相机投影式;ROS 中名字带 camera_link 也不自动意味着它就是 optical 坐标系。参见 ROS REP 103。
2. 小孔成像为什么能用相似三角形
把相机暂时想成一个很小的孔:空间点发出的光穿过同一个孔,落到后面的成像平面上。
如果真实成像平面位于孔后面,即 ,相似三角形给出:
负号就是“倒像”。计算机视觉常把计算用的像平面放到孔前面 ,使用一个虚拟前像平面,于是:
不是物理传感器真的搬到了前面,而是采用了更方便的投影表示。实际镜头比小孔复杂;这里的投影中心与焦距是几何模型的等效量,不要把整个标定模型理解成拿尺子量镜头外壳。
先令虚拟平面的距离为 1,得到:
这两个数只表示方向。例如 和 的归一化坐标相同,所以它们在同一条光线上,投影也相同。
单张图像损失的,正是沿这条光线的距离。
3. 从“方向”到“像素”:四个内参各干什么
图像用像素计数,不用米计数。对无畸变、零斜切的针孔模型:
可以把它读成一句话:先算方向斜率,乘像素尺度,最后移动到主点位置。 这与 OpenCV 相机模型 的定义一致。
3.1 fx、fy:以像素为单位的焦距
若等效焦距为 ,横纵向像素间距为 (毫米/像素),则:
例如 、像素间距 ,得到 。
这里不是“镜头有两个独立的物理焦距”。同一个光学投影在横、纵像素尺度下分别表示,标定、像素形状、非等比例缩放等都可能使 。
保持图像尺寸与其余内参不变,并且主点位于图像内时:
- 越大,同样的水平角度占越多像素,水平视场越窄。
- 越大,同样的竖直角度占越多像素,垂直视场越窄。
- 单独把 改大,投影横向拉宽;这不是把相机向右移动。
例如 , 时距离主点 60 像素, 时是 90 像素。它控制的是相对主点的放大倍率。
3.2 cx、cy:光轴落在图像哪里
让三维点落在光轴上,即 ,立刻得到:
因此 是主点:理想模型中,光轴与像平面的交点在像素坐标里的位置。
主点不是三维相机中心。相机中心 在空间里;主点在图像平面上。主点也不保证恰好位于图像几何中心:装配、标定以及 ROI 裁剪都会影响它。
对于 640×480 图像,像素中心集合的几何中心是 。工程资料也常用 作为近似。本文例题明确使用 ,把它视为一组示例内参,不混用这两个约定。
只把 加 20,所有投影像素的 都加 20,但点之间的像素间距不变;改变相机真实位置则通常会产生与物体深度有关的位移,两者不是同一件事。
3.3 K:把四个数放进一张矩阵
左边的 不能丢掉:右侧最后一个分量是 ,需要除以它,才得到像素齐次坐标 。
更一般的内参矩阵可以在第一行第二列含斜切参数 ,此时 。本文实验和代码假定 ,不包含鱼眼、畸变或滚动快门模型。
4. 做一遍完整数值计算
设:
先得到方向 ,再换算像素:
现在做三个思想实验:
- 不变, 从 2 米变 4 米:投影变成 ,向主点靠近。
- 都乘 2:投影仍是 ,因为方向没变。
- 点不动, 都乘 2:投影变成 ,离主点更远。
这也解释了为什么“相机后退”和“镜头变焦”不能在所有三维场景里互相代替:不同深度物体的相对大小、遮挡与透视关系可能不同。
5. 亲手拖动内参,看公式怎么变成画面
实验固定一个与像平面平行、边长 0.6 米的正方形,其中心是 。先猜结果,再拖动滑块:单独改 、单独改 、最后改 。
- 蓝框:0.6 m 正方形
- 红点:P 的投影
- 金色十字:主点
P 的投影:(380.00, 270.00) px;正方形投影尺寸:180.00 × 180.00 px。
水平视场角:56.14°;垂直视场角:43.60°。
仅模拟无畸变针孔投影,不是对真实相机重新标定;修改主点不会移动真实相机。所有计算在本页本地完成,无自动播放。
拖动后应观察到:改变主点会整体平移图形,改变焦距会改变图形尺寸,增加距离会使图形缩小。由于画面边界固定,主点偏移也会改变左右覆盖角度,不应始终套用“左右各一半 FOV”。
6. 反投影:一个像素究竟能恢复什么
把公式倒过来:
这是一个 分量为 1 的方向代表向量,不是单位向量,也不是已经恢复好的三维点。
若额外知道光轴深度 ,才有:
对 和 ,恢复 米。
如果设备给的是沿光线的距离 ,必须先归一化方向:
例题里 米,但 米。中心附近差别小,离光轴越远差别越大。后续 dToF 笔记 会用 45° 光线说明为什么把两者混用会让平墙弯曲。
7. 内参、外参、畸变:三件不同的事
7.1 外参先把点送到相机坐标系
如果点在世界坐标系里,应先做:
再使用相机投影。下标表示从 变到 ,不要只写一个不注明方向的 T。
这里 是世界原点在相机坐标中的位置,不是相机在世界坐标中的位置。相机中心的世界坐标是:
只要令 ,就能推导出来。内参回答“方向如何变成像素”,外参回答“同一个空间点换个坐标系怎么表示”。求物体位姿时可参考 OpenCV solvePnP 坐标约定。
7.2 畸变不是改一改 fx 就能消除
针孔投影会让空间直线成像为直线,而真实镜头可能产生径向、切向畸变。以常见模型为例,归一化坐标半径平方为 ,径向部分会含有 ,变形量随位置改变。
所以一般流程是:
不能把原始畸变图的像素直接代入本文无畸变反投影式。 应使用设备 SDK 的反投影函数,或依据正确的模型去畸变。OpenCV undistortPoints 不传 P 时通常输出归一化坐标;传入新相机矩阵时则输出相应像素坐标,不要再重复归一化。参见 OpenCV 标定与去畸变接口。
原图 K + D、矫正后图像的新内参、对齐到 RGB 后的目标内参,是不同的数据契约。ROS CameraInfo 也区分了原始图像的 K、畸变 D 与矫正后的投影矩阵 P;见 消息定义。
8. 如何得到可信的内参,而不是“猜一组能用的数”
从 FOV 和分辨率可以估算焦距,但这不是对真实镜头的完整标定。实践上应当:
- 先固定分辨率、裁剪模式、对焦/变焦状态,保存这些配置。
- 使用尺寸可信、足够平整的标定板,采集覆盖中心和边缘、含不同距离与倾斜角度的清晰图像,不要只在中心正对着拍。
- 按镜头类型选择模型并拟合内参与畸变,检查每张图的角点和重投影残差,而不只看一个平均数。
- 留出未参与拟合的图像检验;观察直线矫正效果,并用已知几何或距离检查最终定位结果。
- 图像预处理或相机工作模式变化后,确认内参是否需要相应更新或重新标定。
重投影误差是“已知三维标定点投回图像后,与检测到的角点差多少像素”。低误差是重要证据,却不自动保证所有位置的三维尺度都准确;标定板尺寸填错、数据覆盖不足、运动模糊等仍可能出问题。流程可参照 OpenCV 相机标定教程。
9. 裁剪和缩放后,为什么还要改 K
假设从原图左上 开始裁剪,原像素在新图中的坐标是:
因此焦距不变,主点变成 。这是图像坐标原点变了,不是镜头变了。
若再缩放,则焦距也要按横、纵比例变化;主点还要考虑具体重采样的像素中心约定。深度值如果以米表示,不能跟着图像尺寸一起乘缩放倍数。
完整的偏心 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 |
先不看答案,回答下面四个问题:
- 为什么像素 无法告诉我们物体离相机几米?
- 保持三维点不动,把 加 10 和把 加 10,结果相同吗?
- 当 时,轴向深度为 2 米的点,径向距离是多少?
- 裁掉左边 100 列后仍用原 反投影,会错在哪里?
答案:主点只对应光轴方向;改 是统一平移,改 的位移取决于 ;径向距离为 米;应改成 ,否则归一化水平坐标少了 ,三维 的误差为 。
11. 接下来把基础接回机器人应用
- dToF 到深度图:range、Z、FOV 与裁剪:从测距量恢复三维,再投影到目标图像。
- RealSense D435i 入门:主动双目深度的工程使用,注意它不是 dToF。
- AprilTag 与机器人自主回充:已知标签尺寸、角点和内参,如何进入位姿估计。
- 手眼标定:相机系与机械臂坐标系之间的变换。
最终要记住的不是一张矩阵,而是这条链:坐标变换 → 方向 → 镜头模型 → 像素;反过来则还需要一个有明确语义的深度。