计算机视觉基础:真正理解小孔成像与 fx、fy、cx、cy

相机内参最容易出现的学习状态是:知道矩阵里有四个数,代码也能跑,但说不清某个数变大后画面会怎么变,更不知道裁剪图片后为什么定位突然偏了。

这篇笔记从一条主线展开:三维点先变成一条方向,再把这个方向换算成像素。 理解这两步,内参就不再是一组需要死记的参数。

配套阅读:dToF 到深度图:range、Z、FOV 与裁剪。那篇将这里的几何用到测距、点云和图像预处理上。

1. 先约定:我们到底在哪个坐标系里说话

本文采用常见的相机 optical 坐标系:原点是投影中心 OOXX 向右,YY 向下,ZZ 向前;像素坐标 uu 向右、vv 向下。

名字 例子 单位 代表什么
相机三维坐标 (X,Y,Z)=(0.2,0.1,2)(X,Y,Z)=(0.2,0.1,2) 点相对相机的位置
归一化像平面坐标 (xn,yn)=(0.1,0.05)(x_n,y_n)=(0.1,0.05) 无量纲 光线相对光轴的斜率
像素坐标 (u,v)=(380,270)(u,v)=(380,270) 像素 图像上落在哪里
图像尺寸 W=640,H=480W=640,H=480 像素 图像有多少列、多少行

本文用整数表示像素中心:左上像素中心为 (0,0)(0,0),右下为 (W1,H1)(W-1,H-1)。因此整幅图像的外边缘是 0.5-0.5W0.5W-0.5,而不是两个像素中心。后文算 FOV、做最近像素投影时会保持这个约定。坐标约定可参照 RealSense 的投影说明

注意:机器人本体经常使用“前、左、上”轴,不能把本体坐标直接代入相机投影式;ROS 中名字带 camera_link 也不自动意味着它就是 optical 坐标系。参见 ROS REP 103

2. 小孔成像为什么能用相似三角形

把相机暂时想成一个很小的孔:空间点发出的光穿过同一个孔,落到后面的成像平面上。

真实成像平面在孔后形成倒像,虚拟前像平面用相似三角形推导正号投影
图中 +Z 向右、+Y 向下;虚拟像平面是计算约定,不是实际传感器。手机端可横向滑动查看。

如果真实成像平面位于孔后面,即 Z=fZ=-f,相似三角形给出:

xreal=fXZ,yreal=fYZx_{\mathrm{real}}=-f\frac{X}{Z},\qquad y_{\mathrm{real}}=-f\frac{Y}{Z}

负号就是“倒像”。计算机视觉常把计算用的像平面放到孔前面 Z=fZ=f,使用一个虚拟前像平面,于是:

x=fXZ,y=fYZx=f\frac{X}{Z},\qquad y=f\frac{Y}{Z}

不是物理传感器真的搬到了前面,而是采用了更方便的投影表示。实际镜头比小孔复杂;这里的投影中心与焦距是几何模型的等效量,不要把整个标定模型理解成拿尺子量镜头外壳。

先令虚拟平面的距离为 1,得到:

xn=XZ,yn=YZx_n=\frac{X}{Z},\qquad y_n=\frac{Y}{Z}

这两个数只表示方向。例如 (0.2,0.1,2)(0.2,0.1,2)(0.4,0.2,4)(0.4,0.2,4) 的归一化坐标相同,所以它们在同一条光线上,投影也相同。

单张图像损失的,正是沿这条光线的距离。

3. 从“方向”到“像素”:四个内参各干什么

图像用像素计数,不用米计数。对无畸变、零斜切的针孔模型:

u=fxXZ+cx,v=fyYZ+cy\boxed{u=f_x\frac{X}{Z}+c_x},\qquad \boxed{v=f_y\frac{Y}{Z}+c_y}

可以把它读成一句话:先算方向斜率,乘像素尺度,最后移动到主点位置。 这与 OpenCV 相机模型 的定义一致。

3.1 fx、fy:以像素为单位的焦距

若等效焦距为 fmmf_{\mathrm{mm}},横纵向像素间距为 px,pyp_x,p_y(毫米/像素),则:

fx=fmmpx,fy=fmmpyf_x=\frac{f_{\mathrm{mm}}}{p_x},\qquad f_y=\frac{f_{\mathrm{mm}}}{p_y}

例如 fmm=3.6mmf_{\mathrm{mm}}=3.6\,\mathrm{mm}、像素间距 0.006mm/pixel0.006\,\mathrm{mm/pixel},得到 fx=600pixelf_x=600\,\mathrm{pixel}

这里不是“镜头有两个独立的物理焦距”。同一个光学投影在横、纵像素尺度下分别表示,标定、像素形状、非等比例缩放等都可能使 fxfyf_x\ne f_y

保持图像尺寸与其余内参不变,并且主点位于图像内时:

  • fxf_x 越大,同样的水平角度占越多像素,水平视场越窄。
  • fyf_y 越大,同样的竖直角度占越多像素,垂直视场越窄。
  • 单独把 fxf_x 改大,投影横向拉宽;这不是把相机向右移动。

例如 X/Z=0.1X/Z=0.1fx=600f_x=600 时距离主点 60 像素,fx=900f_x=900 时是 90 像素。它控制的是相对主点的放大倍率

3.2 cx、cy:光轴落在图像哪里

让三维点落在光轴上,即 X=Y=0X=Y=0,立刻得到:

u=cx,v=cyu=c_x,\qquad v=c_y

因此 (cx,cy)(c_x,c_y) 是主点:理想模型中,光轴与像平面的交点在像素坐标里的位置。

主点不是三维相机中心。相机中心 OO 在空间里;主点在图像平面上。主点也不保证恰好位于图像几何中心:装配、标定以及 ROI 裁剪都会影响它。

对于 640×480 图像,像素中心集合的几何中心是 (319.5,239.5)(319.5,239.5)。工程资料也常用 (320,240)(320,240) 作为近似。本文例题明确使用 cx=320,cy=240c_x=320,c_y=240,把它视为一组示例内参,不混用这两个约定。

只把 cxc_x 加 20,所有投影像素的 uu 都加 20,但点之间的像素间距不变;改变相机真实位置则通常会产生与物体深度有关的位移,两者不是同一件事。

3.3 K:把四个数放进一张矩阵

K=[fx0cx0fycy001],Z[uv1]=K[XYZ]K= \begin{bmatrix} f_x&0&c_x\\ 0&f_y&c_y\\ 0&0&1 \end{bmatrix},\qquad Z\begin{bmatrix}u\\v\\1\end{bmatrix} =K\begin{bmatrix}X\\Y\\Z\end{bmatrix}

左边的 ZZ 不能丢掉:右侧最后一个分量是 ZZ,需要除以它,才得到像素齐次坐标 [u,v,1]T[u,v,1]^T

更一般的内参矩阵可以在第一行第二列含斜切参数 ss,此时 u=fxxn+syn+cxu=f_xx_n+sy_n+c_x。本文实验和代码假定 s=0s=0,不包含鱼眼、畸变或滚动快门模型。

4. 做一遍完整数值计算

设:

K=[60003200600240001],PC=[0.20.12]mK=\begin{bmatrix}600&0&320\\0&600&240\\0&0&1\end{bmatrix},\qquad P_C=\begin{bmatrix}0.2\\0.1\\2\end{bmatrix}\mathrm{m}

先得到方向 (0.1,0.05)(0.1,0.05),再换算像素:

u=600×0.1+320=380,v=600×0.05+240=270u=600\times0.1+320=380,\qquad v=600\times0.05+240=270

现在做三个思想实验:

  1. X,YX,Y 不变,ZZ 从 2 米变 4 米:投影变成 (350,255)(350,255),向主点靠近。
  2. X,Y,ZX,Y,Z 都乘 2:投影仍是 (380,270)(380,270),因为方向没变。
  3. 点不动,fx,fyf_x,f_y 都乘 2:投影变成 (440,300)(440,300),离主点更远。

这也解释了为什么“相机后退”和“镜头变焦”不能在所有三维场景里互相代替:不同深度物体的相对大小、遮挡与透视关系可能不同。

5. 亲手拖动内参,看公式怎么变成画面

实验固定一个与像平面平行、边长 0.6 米的正方形,其中心是 P=(0.2,0.1,Z)P=(0.2,0.1,Z)。先猜结果,再拖动滑块:单独改 fxf_x、单独改 cxc_x、最后改 ZZ

(0, 0)
  • 蓝框:0.6 m 正方形
  • 红点:P 的投影
  • 金色十字:主点

P 的投影:(380.00, 270.00) px;正方形投影尺寸:180.00 × 180.00 px。

水平视场角:56.14°;垂直视场角:43.60°。

仅模拟无畸变针孔投影,不是对真实相机重新标定;修改主点不会移动真实相机。所有计算在本页本地完成,无自动播放。

拖动后应观察到:改变主点会整体平移图形,改变焦距会改变图形尺寸,增加距离会使图形缩小。由于画面边界固定,主点偏移也会改变左右覆盖角度,不应始终套用“左右各一半 FOV”。

6. 反投影:一个像素究竟能恢复什么

把公式倒过来:

q=K1[uv1]=[(ucx)/fx(vcy)/fy1]q=K^{-1}\begin{bmatrix}u\\v\\1\end{bmatrix} =\begin{bmatrix}(u-c_x)/f_x\\(v-c_y)/f_y\\1\end{bmatrix}

这是一个 zz 分量为 1 的方向代表向量,不是单位向量,也不是已经恢复好的三维点。

若额外知道光轴深度 ZZ,才有:

PC=Zq=[(ucx)Z/fx(vcy)Z/fyZ]P_C=Zq =\begin{bmatrix}(u-c_x)Z/f_x\\(v-c_y)Z/f_y\\Z\end{bmatrix}

(380,270)(380,270)Z=2Z=2,恢复 (0.2,0.1,2)(0.2,0.1,2) 米。

如果设备给的是沿光线的距离 r=PCr=\|P_C\|,必须先归一化方向:

PC=rqq,Z=rqP_C=r\frac{q}{\|q\|},\qquad Z=\frac{r}{\|q\|}

例题里 Z=2Z=2 米,但 r=0.22+0.12+222.01246r=\sqrt{0.2^2+0.1^2+2^2}\approx2.01246 米。中心附近差别小,离光轴越远差别越大。后续 dToF 笔记 会用 45° 光线说明为什么把两者混用会让平墙弯曲。

7. 内参、外参、畸变:三件不同的事

7.1 外参先把点送到相机坐标系

如果点在世界坐标系里,应先做:

PC=RCWPW+tCWP_C=R_{CW}P_W+t_{CW}

再使用相机投影。下标表示从 WW 变到 CC,不要只写一个不注明方向的 T

这里 tCWt_{CW} 是世界原点在相机坐标中的位置,不是相机在世界坐标中的位置。相机中心的世界坐标是:

CW=RCWTtCWC_W=-R_{CW}^{T}t_{CW}

只要令 PC=0P_C=0,就能推导出来。内参回答“方向如何变成像素”,外参回答“同一个空间点换个坐标系怎么表示”。求物体位姿时可参考 OpenCV solvePnP 坐标约定

7.2 畸变不是改一改 fx 就能消除

针孔投影会让空间直线成像为直线,而真实镜头可能产生径向、切向畸变。以常见模型为例,归一化坐标半径平方为 ρ2=xn2+yn2\rho^2=x_n^2+y_n^2,径向部分会含有 1+k1ρ2+k2ρ4+1+k_1\rho^2+k_2\rho^4+\cdots,变形量随位置改变。

所以一般流程是:

PWPC(xn,yn)畸变坐标(u,v)P_W\longrightarrow P_C\longrightarrow(x_n,y_n) \longrightarrow\text{畸变坐标}\longrightarrow(u,v)

不能把原始畸变图的像素直接代入本文无畸变反投影式。 应使用设备 SDK 的反投影函数,或依据正确的模型去畸变。OpenCV undistortPoints 不传 P 时通常输出归一化坐标;传入新相机矩阵时则输出相应像素坐标,不要再重复归一化。参见 OpenCV 标定与去畸变接口

原图 K + D、矫正后图像的新内参、对齐到 RGB 后的目标内参,是不同的数据契约。ROS CameraInfo 也区分了原始图像的 K、畸变 D 与矫正后的投影矩阵 P;见 消息定义

8. 如何得到可信的内参,而不是“猜一组能用的数”

从 FOV 和分辨率可以估算焦距,但这不是对真实镜头的完整标定。实践上应当:

  1. 先固定分辨率、裁剪模式、对焦/变焦状态,保存这些配置。
  2. 使用尺寸可信、足够平整的标定板,采集覆盖中心和边缘、含不同距离与倾斜角度的清晰图像,不要只在中心正对着拍。
  3. 按镜头类型选择模型并拟合内参与畸变,检查每张图的角点和重投影残差,而不只看一个平均数。
  4. 留出未参与拟合的图像检验;观察直线矫正效果,并用已知几何或距离检查最终定位结果。
  5. 图像预处理或相机工作模式变化后,确认内参是否需要相应更新或重新标定。

重投影误差是“已知三维标定点投回图像后,与检测到的角点差多少像素”。低误差是重要证据,却不自动保证所有位置的三维尺度都准确;标定板尺寸填错、数据覆盖不足、运动模糊等仍可能出问题。流程可参照 OpenCV 相机标定教程

9. 裁剪和缩放后,为什么还要改 K

假设从原图左上 (x0,y0)(x_0,y_0) 开始裁剪,原像素在新图中的坐标是:

u=ux0,v=vy0u'=u-x_0,\qquad v'=v-y_0

因此焦距不变,主点变成 (cxx0,cyy0)(c_x-x_0,c_y-y_0)。这是图像坐标原点变了,不是镜头变了。

若再缩放,则焦距也要按横、纵比例变化;主点还要考虑具体重采样的像素中心约定。深度值如果以米表示,不能跟着图像尺寸一起乘缩放倍数

完整的偏心 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))) # (380.0, 270.0)
print(camera.depth_to_point(380, 270, 2.0)) # (0.2, 0.1, 2.0)
print(camera.ray(380, 270)) # (0.1, 0.05, 1.0),不是单位向量

先不看答案,回答下面四个问题:

  1. 为什么像素 (cx,cy)(c_x,c_y) 无法告诉我们物体离相机几米?
  2. 保持三维点不动,把 cxc_x 加 10 和把 fxf_x 加 10,结果相同吗?
  3. X/Z=1,Y=0X/Z=1,Y=0 时,轴向深度为 2 米的点,径向距离是多少?
  4. 裁掉左边 100 列后仍用原 cxc_x 反投影,会错在哪里?

答案:主点只对应光轴方向;改 cxc_x 是统一平移,改 fxf_x 的位移取决于 X/ZX/Z;径向距离为 222\sqrt{2} 米;应改成 cx100c_x-100,否则归一化水平坐标少了 100/fx100/f_x,三维 XX 的误差为 100Z/fx-100Z/f_x

11. 接下来把基础接回机器人应用

最终要记住的不是一张矩阵,而是这条链:坐标变换 → 方向 → 镜头模型 → 像素;反过来则还需要一个有明确语义的深度。

3d打印 actor-critic adaptive sampling ai辅助设计 algorithm algorithms anymal apriltag ardupilot atlas attention axis-angle bang-bang belief encoder blender bode c++ cadquery calibration camera calibration camera-intrinsics chrome cmake cmakelists cnn colcon computer-vision conan control controller_manager cpp cpu d435i dagger data_struct db depth camera depth-camera design-pattern direct collocation dots dtof economics eigen elevation map executor factory-pattern fcpx fiducial marker figure finance forge fourier fov freecad gae gazebo gdb geometry git gnu gru guitar hardware humanoid ibus imu interest isaac gym isaac lab isaaclab kdl laplace latent variable latex launch learning-notes legged locomotion legged robotics legged-robot legged_gym life linux linux-kernel mac math matlab matrix memory mlp money motion imitation motion-control motor moveit mpc mujoco music-theory network neural mapping ocs2 ode openscad operator optimal algorithm optimal-control perceptive locomotion perf performance personal-finance piano pinhole-camera pinocchio pixhawk pixhawk 6c point-cloud policy distillation ppo privileged learning profiling px4 python qgroundcontrol qos quadrotor realsense reinforcement learning representation learning reward tuning rnn robot robot parkour robotics ros ros2 ros2_control rsl_rl rtb security sensor-fusion shell signal-processing sim-to-real simulation socket soft dynamics constraints spot stairs stl stm32 tcp-ip teacher policy teacher student teacher-student temporal convolution terrain reconstruction thread tools tron1 twist ubuntu uml uncertainty unitree unitree g1 urdf vae valgrind vcxsrv velocity vim web wifi wiring work workflow wsl z-transform zero-shot transfer 中文输入 交叉编译 人形机器人 依赖管理 分支管理 动力学 四旋翼 四足机器人 实验诊断 强化学习 接触动力学 数值计算 机器人 机器人控制 机器人视觉 构建系统 浮动基 深度学习 深度相机 点云 版本控制
知识共享许可协议