相机模型与成像原理:从三维世界到像素坐标

做机器视觉这么多年,我始终觉得一个道理没变过——不懂相机模型,标定就是瞎蒙。你想想看,如果连图像上的一个点是怎么从真实世界投影过来的都不清楚,那你怎么去矫正畸变?怎么去算三维坐标?

这一章,我们就来把相机成像这件事彻底讲透。我会从最经典的针孔模型开始,然后聊镜头畸变这个"捣蛋鬼",最后把内参和外参矩阵掰开揉碎。嗯,这些都是标定脚本里绕不开的基础。

相机模型与成像原理 · 知识体系 相机成像模型 针孔相机模型 镜头畸变 矩阵参数 小孔成像原理 相似三角形 倒立实像 径向畸变 切向畸变 畸变系数 k1 k2 p1 p2 内参矩阵 K 外参矩阵 [R|t] 世界→相机→图像→像素 理解这三块,标定脚本的数学基础就稳了

针孔相机模型:最朴素的成像逻辑

针孔相机模型,说白了就是初中物理课上那个小孔成像实验。光线穿过一个小孔,在后面的感光平面上形成一个倒立的实像。虽然现在的镜头复杂得多,但数学上我们依然用这个模型来近似。

核心公式其实就一个相似三角形关系:

Z / f = X / x = Y / y

其中:
Z  —— 物体到相机光心的距离(深度)
f  —— 焦距(光心到成像平面的距离)
X, Y —— 物体在三维空间中的坐标
x, y —— 物体在成像平面上的坐标

这个公式看着简单,但它是整个视觉几何的基石。我在做第一个3D测量项目时,就是靠这个公式反推物体尺寸的。当时没做标定,直接用标称焦距算,结果差了5毫米——嗯,后来才明白,焦距不是标称值,得标定出来。

关键点:针孔模型假设光线是直线传播的,没有畸变。但现实中的镜头都有畸变,所以我们需要先做畸变矫正,再用针孔模型去算。

镜头畸变:理想模型的"捣蛋鬼"

为什么实际镜头会有畸变?因为镜头是透镜组,不是小孔。光线穿过透镜时会发生折射,尤其是边缘区域。这就导致了两种主要的畸变:

径向畸变

径向畸变是沿着镜头半径方向发生的变形。离光心越远,变形越明显。它又分两种:

  • 桶形畸变:图像向外凸出,像桶一样。广角镜头常见。
  • 枕形畸变:图像向内凹陷,像枕头。长焦镜头常见。

数学上用泰勒级数来建模:

x_corrected = x * (1 + k1 * r² + k2 * r⁴ + k3 * r⁶)
y_corrected = y * (1 + k1 * r² + k2 * r⁴ + k3 * r⁶)

其中:
r —— 像素点到光心的距离
k1, k2, k3 —— 径向畸变系数

一般标定只算k1和k2就够了,k3用在鱼眼镜头那种大畸变场景。我遇到过一台老相机,k1大得离谱,标定完一看畸变图,边缘都快扭成麻花了。

切向畸变

切向畸变是因为镜头和成像平面不平行造成的。说白了就是镜头装歪了,或者传感器贴歪了。数学上:

x_corrected = x + [2 * p1 * x * y + p2 * (r² + 2 * x²)]
y_corrected = y + [p1 * (r² + 2 * y²) + 2 * p2 * x * y]

其中:
p1, p2 —— 切向畸变系数

我的经验:在HALCON中,畸变系数通常用5个参数表示:(k1, k2, p1, p2, k3)。但大多数工业镜头只需要前4个。如果k3标出来接近0,就别用了,省得引入噪声。

内参矩阵:相机自己的"身份证"

内参矩阵描述的是相机内部的光学特性。它把相机坐标系中的点映射到像素坐标系。矩阵形式如下:

     [ fx   0   cx ]
K =  [  0  fy   cy ]
     [  0   0    1  ]

其中:
fx, fy —— 焦距在x和y方向上的像素单位(通常fx ≈ fy)
cx, cy —— 主点坐标(光心在图像上的像素位置)

这个矩阵的作用,就是把三维相机坐标 (Xc, Yc, Zc) 投影到像素坐标 (u, v):

[ u ]   [ fx   0   cx ]   [ Xc ]
[ v ] = [  0  fy   cy ] * [ Yc ]
[ 1 ]   [  0   0    1 ]   [ Zc ]

注意这里有个隐含的除法——实际像素坐标是 (u/Zc, v/Zc)。这就是为什么远处的物体看起来小。

常见坑:很多初学者以为cx, cy就是图像中心。其实不是!由于镜头安装和传感器切割的误差,主点往往偏离中心几个像素甚至几十个像素。我见过一台相机,主点偏了15个像素,用中心代替的话,测量误差直接翻倍。

外参矩阵:相机在空间中的"位置和姿态"

外参矩阵描述的是相机坐标系和世界坐标系之间的变换关系。它由两部分组成:

  • 旋转矩阵 R:3×3矩阵,描述相机的朝向
  • 平移向量 t:3×1向量,描述相机在世界坐标系中的位置

合起来就是:

[ Xc ]   [ R11  R12  R13  tx ]   [ Xw ]
[ Yc ] = [ R21  R22  R23  ty ] * [ Yw ]
[ Zc ]   [ R31  R32  R33  tz ]   [ Zw ]
[ 1  ]   [   0    0    0   1  ]   [ 1  ]

这个矩阵把世界坐标 (Xw, Yw, Zw) 转换到相机坐标 (Xc, Yc, Zc)。然后经过内参矩阵,最终得到像素坐标。

整个成像链路的完整流程就是:

  1. 世界坐标 → 外参矩阵 → 相机坐标
  2. 相机坐标 → 内参矩阵 → 图像坐标
  3. 图像坐标 → 畸变模型 → 实际像素坐标

一句话总结:外参告诉你相机在哪、朝哪看;内参告诉你相机怎么把光线变成像素;畸变模型告诉你实际像素和理想像素的偏差。三者缺一不可。

HALCON中的参数表示

在HALCON里,标定结果通常用以下方式存储:

参数 HALCON变量名 说明
内参矩阵 CamParam 包含 fx, fy, cx, cy, 畸变系数等
外参矩阵 Pose 包含旋转和平移,通常用7个值表示
畸变系数 内嵌在CamParam中 k1, k2, p1, p2, k3

举个例子,读取标定文件后的典型代码:

* 读取相机内参
read_cam_par('calib.cal', CamParam)

* 读取某一幅图像的外参
read_pose('pose_01.dat', Pose)

* 查看内参
get_cam_par_data(CamParam, 'focus', Focus)
get_cam_par_data(CamParam, 'sx', PixelSizeX)
get_cam_par_data(CamParam, 'sy', PixelSizeY)
get_cam_par_data(CamParam, 'cx', CenterX)
get_cam_par_data(CamParam, 'cy', CenterY)
get_cam_par_data(CamParam, 'k1', K1)
get_cam_par_data(CamParam, 'k2', K2)
get_cam_par_data(CamParam, 'p1', P1)
get_cam_par_data(CamParam, 'p2', P2)

个人习惯:我每次标定完都会把内参打印出来看一眼。如果fx和fy相差超过1%,或者cx, cy偏离图像中心超过图像宽高的5%,我就会检查一下标定板图像质量,或者重新拍一组。别偷懒,这一步能省掉后面很多调试时间。