4. 相机成像模型:针孔相机模型、内参矩阵、畸变模型
聊到手眼标定,绕不开的第一个硬核知识点就是相机成像模型。说白了,就是搞明白「三维世界里的一个点,是怎么跑到二维图像上那个像素位置的」。这个映射关系搞不清楚,后面标定出来的结果全是空中楼阁。
我个人习惯把相机模型拆成三块来讲:针孔模型(理想情况)、内参矩阵(数学表达)、畸变模型(现实修正)。这三块吃透了,相机在你眼里就不再是一个黑盒子。
4.1 针孔相机模型——最朴素的成像原理
先回到最原始的针孔相机。一个小孔,一堵暗箱,光线穿过小孔在底片上倒立成像。这就是所有相机模型的鼻祖。
为什么用针孔模型?因为它把复杂的透镜系统简化成了一个光心和一个成像平面。三维点 P(X, Y, Z) 经过光心投影到成像平面上的点 p(x, y),满足一个简单的相似三角形关系:
x = f * (X / Z)
y = f * (Y / Z)
这里的 f 是焦距,单位是物理长度(毫米)。
我在项目中遇到过一位同事,死活标定不准,最后发现是他把成像平面的物理坐标直接当像素坐标用了。嗯,这个坑其实挺常见的。
4.2 内参矩阵——从物理坐标到像素坐标
针孔模型给出的是物理坐标 (x, y),但相机最终输出的是像素坐标 (u, v)。怎么转?靠内参矩阵。
内参矩阵 K 长这样:
K = [ fx 0 cx
0 fy cy
0 0 1 ]
其中:
- fx, fy:焦距在 x、y 方向上的像素量纲。fx = f / dx,dx 是每个像素的物理宽度。
- cx, cy:主点坐标,也就是光轴与成像平面的交点。理想情况下在图像中心,但实际会有偏移。
完整的投影公式就是:
Z * [u, v, 1]^T = K * [X, Y, Z]^T
你想想看,这个公式把三维世界和二维像素联系起来了。手眼标定里,我们反复在用这个公式做正向投影和反向求解。
| 参数 | 含义 | 常见范围 |
|---|---|---|
| fx | x 方向焦距(像素) | 500 ~ 2000 |
| fy | y 方向焦距(像素) | 500 ~ 2000 |
| cx | 主点 x 坐标 | 图像宽度的一半 ± 几十像素 |
| cy | 主点 y 坐标 | 图像高度的一半 ± 几十像素 |
4.3 畸变模型——现实世界的修正
针孔模型是理想情况。现实中的镜头有透镜,透镜会引入畸变。我见过最夸张的一个案例,标定板在图像边缘弯成了弧形——那就是畸变没校正。
畸变主要分两类:径向畸变和切向畸变。
4.3.1 径向畸变
径向畸变是光线经过透镜边缘时弯曲程度不同导致的。越靠近图像边缘,畸变越明显。
- 桶形畸变:图像向外膨胀,像桶一样。常见于广角镜头。
- 枕形畸变:图像向内收缩,像枕头一样。常见于长焦镜头。
数学模型用泰勒级数展开来拟合:
x_corrected = x * (1 + k1*r^2 + k2*r^4 + k3*r^6)
y_corrected = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)
其中 r^2 = x^2 + y^2,k1, k2, k3 是径向畸变系数。一般用 k1, k2 就够了,鱼眼镜头才需要 k3。
4.3.2 切向畸变
切向畸变是透镜和成像平面不平行导致的。说白了就是镜头装歪了。
x_corrected = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_corrected = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]
p1, p2 是切向畸变系数。
4.4 完整成像流程——一张图看懂
把上面所有环节串起来,完整的成像流程是这样的:
- 世界坐标系下的三维点 P_w
- 通过外参(旋转+平移)变换到相机坐标系 P_c
- 通过针孔投影得到归一化平面上的物理坐标 (x, y)
- 应用畸变模型得到校正后的物理坐标 (x', y')
- 通过内参矩阵映射到像素坐标系 (u, v)
下面这张图把这个流程画清楚了:
4.5 畸变校正实战——OpenCV 代码示例
理论讲完了,来点实际的。用 OpenCV 做畸变校正,核心就三步:
import cv2
import numpy as np
# 假设你已经标定得到了内参和畸变系数
K = np.array([[fx, 0, cx],
[0, fy, cy],
[0, 0, 1]], dtype=np.float32)
dist = np.array([k1, k2, p1, p2, k3], dtype=np.float32)
# 读取一张畸变图像
img = cv2.imread('distorted.jpg')
h, w = img.shape[:2]
# 计算校正映射
new_K, roi = cv2.getOptimalNewCameraMatrix(K, dist, (w, h), 1, (w, h))
mapx, mapy = cv2.initUndistortRectifyMap(K, dist, None, new_K, (w, h), cv2.CV_32FC1)
# 应用映射
undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
# 裁剪掉黑色边框
x, y, w_roi, h_roi = roi
undistorted = undistorted[y:y+h_roi, x:x+w_roi]
cv2.imwrite('undistorted.jpg', undistorted)
getOptimalNewCameraMatrix 的 alpha 参数控制输出图像大小。alpha=1 保留所有像素(有黑边),alpha=0 裁剪到最大有效区域。我一般用 alpha=0,省得后面还要处理黑边。
4.6 本章小结
相机成像模型是手眼标定的地基。你想想看,如果连相机怎么把三维点变成二维像素都搞不清楚,后面标定出来的手眼矩阵怎么可能准?
记住三个核心:
- 针孔模型是理想基础,理解相似三角形投影
- 内参矩阵是物理到像素的桥梁,fx、fy、cx、cy 四个参数
- 畸变模型是现实修正,径向畸变(k1, k2, k3)和切向畸变(p1, p2)都要考虑