4. 相机成像模型
聊到手眼标定,绕不开一个基础问题——相机到底是怎么“看”世界的?
你想想看,一个三维空间里的点,怎么就能映射到一张二维图片上?这背后就是相机成像模型在起作用。我个人习惯把相机模型拆成三块来理解:针孔模型(理想情况)、透镜畸变(现实误差)、内参矩阵(数学表达)。
嗯,咱们一个一个说。
4.1 针孔相机模型:理想化的“小孔成像”
针孔模型是最经典的成像模型。说白了,就是初中物理课上的小孔成像实验——光线穿过一个小孔,在后面的平面上形成倒立的像。
但在机器视觉里,我们通常把成像平面“虚拟”地放到相机前方,这样图像就是正立的,计算起来也方便。这个虚拟平面,就是归一化成像平面。
从三维世界到二维图像,经历了四次坐标变换:
- 世界坐标系 → 相机坐标系(刚体变换,旋转+平移)
- 相机坐标系 → 归一化成像平面(透视投影)
- 归一化成像平面 → 像素坐标系(内参缩放+平移)
用数学公式表达就是:
λ * [u, v, 1]^T = K * [R | t] * [Xw, Yw, Zw, 1]^T
其中:
[u, v]是像素坐标[Xw, Yw, Zw]是世界坐标K是内参矩阵[R | t]是外参矩阵λ是尺度因子
关键理解:针孔模型是线性的,但现实中的镜头不是针孔,所以我们需要引入畸变模型来修正。
4.2 透镜畸变:现实世界的“不完美”
我在项目中遇到过最头疼的问题,就是标定板拍出来边缘是弯的。你想想看,一个正方形的标定板,在图像边缘变成了“枕形”或“桶形”,这还怎么标定?
畸变主要分两类:
| 畸变类型 | 产生原因 | 视觉效果 | 数学模型 |
|---|---|---|---|
| 径向畸变 | 透镜形状不完美(球面误差) | 直线变弯曲,越靠近边缘越明显 | δr = k1*r^3 + k2*r^5 + k3*r^7 |
| 切向畸变 | 透镜与成像平面不平行(装配误差) | 图像倾斜、梯形失真 | δx = 2*p1*x*y + p2*(r^2 + 2*x^2) |
径向畸变又分两种:
- 桶形畸变:图像中心向外膨胀(广角镜头常见)
- 枕形畸变:图像边缘向内收缩(长焦镜头常见)
切向畸变嘛,说白了就是镜头装歪了。我见过一个案例,某品牌的工业相机,出厂时镜头座就有0.1mm的偏斜,导致整个视场的切向畸变都偏大。嗯,这种问题只能通过标定来补偿。
避坑指南:我曾经在标定一个120度广角镜头时,只用了2个径向畸变参数(k1, k2),结果标定残差一直下不去。后来加了k3参数,残差立刻降了一个数量级。所以,对于大畸变镜头,一定要用3个径向畸变参数。
4.3 内参矩阵:相机的“身份证”
内参矩阵 K 描述了相机内部的几何特性。它把归一化成像平面上的点,映射到像素坐标系上。
标准形式:
K = [fx, 0, cx;
0, fy, cy;
0, 0, 1]
参数含义:
fx, fy:焦距在x、y方向上的像素当量(单位:像素)cx, cy:主点坐标(光轴与成像平面的交点,单位:像素)
你可能会问:为什么焦距会有两个值?
因为像素不一定是正方形的。工业相机里,有些传感器的像元尺寸是5.3μm × 5.3μm,那就是正方形;但有些是5.3μm × 5.0μm,那就不是正方形。所以 fx 和 fy 会略有差异。
个人经验:我习惯在标定完成后,先看一眼 cx 和 cy 是否接近图像中心。如果偏差超过10个像素,我会检查标定板是否拍偏了,或者镜头是否松动。这个“直觉检查”帮我发现过好几次标定数据异常。
4.4 完整成像模型:从世界坐标到像素坐标
把上面三块拼起来,就是完整的成像模型:
- 世界坐标
Pw通过外参[R|t]转到相机坐标Pc - 相机坐标
Pc通过透视投影转到归一化平面Pn - 归一化坐标
Pn加上畸变修正,得到Pd - 畸变后的坐标
Pd通过内参K转到像素坐标Puv
这个流程,我画了一张图帮你理解:
4.5 HALCON中的相机模型与标定
在HALCON里,相机模型用 cam_par 这个数据结构来表示。你可以通过 create_calib_data 创建标定数据,然后调用 calibrate_cameras 来求解内参和畸变参数。
常用的标定算子:
| 算子 | 功能 | 输出 |
|---|---|---|
create_calib_data |
创建标定数据对象 | CalibDataID |
set_calib_data_cam_param |
设置相机初始参数 | — |
find_calib_object |
在图像中查找标定板 | 标定点坐标 |
calibrate_cameras |
执行标定计算 | 内参、外参 |
get_calib_data |
获取标定结果 | 相机参数 |
一个典型的标定流程:
// 创建标定数据
create_calib_data ('calibration_object', 'calibration_plate', [], [], CalibDataID)
// 设置相机初始参数(先给个大概值)
set_calib_data_cam_param (CalibDataID, 0, 'area_scan_division', [0.008, 0, 0.008, 0, 640, 480, 640, 480])
// 对每张标定图像查找标定板
for i := 1 to 15 by 1
read_image (Image, 'calib_' + i$'02d')
find_calib_object (Image, CalibDataID, 0, 0, i, [], [])
endfor
// 执行标定
calibrate_cameras (CalibDataID, Errors)
// 获取标定结果
get_calib_data (CalibDataID, 'camera', 0, 'params', CamParam)
小技巧:标定完成后,我习惯用 disp_caltab 把标定板重投影到图像上,看看重投影误差。如果误差超过0.3像素,我会检查标定板是否弯曲,或者图像是否模糊。这个习惯帮我避免过很多次“标定成功但定位不准”的尴尬。
4.6 本章小结
相机成像模型是手眼标定的数学基础。你只有理解了:
- 针孔模型如何把三维点投影到二维平面
- 畸变模型如何修正镜头的物理误差
- 内参矩阵如何描述相机的内部几何
才能在后续的手眼标定中,准确地把相机坐标系和机器人坐标系关联起来。