3. 相机标定原理:针孔模型、内参、外参、畸变参数,张正友标定法
相机标定,说白了就是搞清楚相机到底是怎么“看”世界的。你想想看,一个三维空间里的点,怎么就能映射到一张二维照片上?这中间经历了什么变换?又引入了哪些误差?
我个人习惯把标定分成两步走:先理解理想模型(针孔模型),再修正现实偏差(畸变参数)。张正友标定法,就是连接这两步最实用的桥梁。
3.1 针孔模型:理想世界的映射
针孔模型是最基础的成像模型。它假设光线从物体表面反射,穿过一个极小的孔(光心),在后面的成像平面上形成倒立的像。虽然简单,但它是所有标定理论的起点。
这个模型涉及四个坐标系的转换:
- 世界坐标系:你放物体的大空间,单位是米或毫米。
- 相机坐标系:以相机光心为原点,Z轴指向镜头前方。
- 图像坐标系:在成像平面上,以物理尺寸(毫米)为单位。
- 像素坐标系:最终图像,以像素为单位,原点通常在左上角。
整个映射过程可以概括为:
世界点 (Xw, Yw, Zw) → 相机点 (Xc, Yc, Zc) → 图像点 (x, y) → 像素点 (u, v)
核心公式(齐次坐标形式):
s * [u, v, 1]^T = K * [R | t] * [Xw, Yw, Zw, 1]^T
其中,K 是内参矩阵,[R | t] 是外参矩阵。
3.2 内参:相机自己的“基因”
内参是相机本身的固有属性,一旦出厂,理论上就不变了。它描述了从相机坐标系到像素坐标系的映射关系。
| 参数 | 符号 | 含义 |
|---|---|---|
| 焦距 | fx, fy | 以像素为单位的焦距,通常 fx ≈ fy |
| 主点 | cx, cy | 光轴与成像平面的交点,理想情况在图像中心 |
| 倾斜因子 | s | 像素坐标系x轴与y轴的不垂直度,现代相机几乎为0 |
内参矩阵 K 长这样:
K = | fx s cx |
| 0 fy cy |
| 0 0 1 |
我的经验: 我在项目中遇到过一台工业相机,标定出来的 cx 和 cy 明显偏离图像中心。一开始以为是标定板没放正,后来查了 datasheet 才发现,这款相机的 sensor 在封装时就有 2-3 个像素的偏移。所以,永远不要假设主点就在图像中心,标定出来的值才是真实的。
3.3 外参:相机在空间中的“位置”
外参描述的是相机相对于世界坐标系的位姿。说白了,就是相机放在哪、朝哪看。
- 旋转矩阵 R:3x3 的正交矩阵,表示相机的朝向。
- 平移向量 t:3x1 的向量,表示相机在世界坐标系中的位置。
外参将世界坐标系下的点变换到相机坐标系下:
Xc = R * Xw + t
这里有个容易混淆的地方:R 和 t 是相机坐标系相对于世界坐标系的变换。如果你把相机固定在一个机械臂末端,那么外参就是相机与机械臂末端之间的位姿关系——这就是手眼标定的核心。
避坑指南: 我曾经在做一个移动机器人项目时,把外参的平移向量 t 理解反了。结果机器人明明应该往前走,相机却显示它在后退。后来检查发现,我把世界坐标系和相机坐标系的转换方向搞反了。记住:外参是把世界点“拉”到相机坐标系下,不是把相机“推”到世界坐标系下。
3.4 畸变参数:现实世界的“偏差”
针孔模型是理想情况,但真实镜头有畸变。主要分两类:
- 径向畸变:光线通过镜头边缘时弯曲程度不同,导致直线变弯。分为桶形畸变和枕形畸变。
- 切向畸变:镜头与成像平面不平行,导致图像倾斜。
畸变模型通常用多项式来描述:
径向畸变:
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)
切向畸变:
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]
其中,k1, k2, k3 是径向畸变系数,p1, p2 是切向畸变系数。r 是像素点到主点的距离。
| 畸变类型 | 参数 | 典型表现 |
|---|---|---|
| 径向畸变 | k1, k2, k3 | 直线变弯,越靠近边缘越明显 |
| 切向畸变 | p1, p2 | 图像倾斜,像被“拧”了一下 |
我的习惯: 标定时我一般只估计 k1, k2, p1, p2 四个参数。k3 通常只在鱼眼镜头或畸变特别大的镜头下才需要。参数太多反而容易过拟合,标定结果不稳定。
3.5 张正友标定法:从棋盘格到参数
张正友标定法是目前最主流的标定方法。它只需要拍摄不同角度的棋盘格图像,就能同时解出内参、外参和畸变参数。
核心思路分三步:
- 检测角点:在每张棋盘格图像中提取亚像素精度的角点坐标。
- 求解单应性矩阵:每张图像都可以计算一个单应性矩阵 H,它连接了棋盘格平面和图像平面。
- 分解参数:利用多个 H 矩阵的约束,通过最小二乘法解出内参,再反推外参和畸变参数。
关键约束: 张正友标定法利用了棋盘格是平面这一事实。棋盘格上的所有角点都在同一个平面上(Zw=0),这大大简化了计算。你想想看,如果标定板是三维的,那问题就复杂多了。
下面是我用 OpenCV 做标定的标准流程:
import cv2
import numpy as np
# 1. 准备棋盘格角点的世界坐标
pattern_size = (9, 6) # 内角点数量
objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
# 2. 存储所有图像的世界坐标和像素坐标
objpoints = [] # 世界坐标
imgpoints = [] # 像素坐标
# 3. 遍历图像,检测角点
for fname in image_files:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
objpoints.append(objp)
# 亚像素精化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 4. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
避坑指南: 我曾经只拍了 5 张棋盘格图像就做标定,结果内参波动很大。后来发现,图像数量至少要在 10-15 张以上,而且角度要覆盖:正对、左倾、右倾、上仰、下俯、旋转。棋盘格要占图像面积的 1/4 到 1/2,太小了角点检测不准,太大了容易超出视野。
3.6 标定流程与知识体系
下面这张图总结了相机标定的完整知识体系:
3.7 标定质量评估
标定完怎么知道结果好不好?我一般看三个指标:
- 重投影误差:将世界坐标点通过标定结果投影到图像上,计算与检测到的像素坐标的误差。通常要求小于 0.5 像素。
- 内参合理性:fx 和 fy 应该接近,cx 和 cy 应该在图像中心附近。
- 畸变系数大小:k1 通常为负(桶形畸变),值一般在 0.1 到 0.5 之间。如果 k1 的值大于 1,说明镜头畸变很大,或者标定有问题。
我的经验: 有一次标定结果重投影误差只有 0.3 像素,但实际用起来就是不准。后来发现是棋盘格打印精度不够,角点世界坐标本身就有误差。所以,标定板的精度直接影响标定结果。建议用玻璃基板或陶瓷基板的高精度棋盘格,别用普通打印纸贴硬纸板。
相机标定是 3D 视觉的基石。内参、外参、畸变参数,这三个概念贯穿了整个 3D 视觉定位系统。后面的手眼标定、深度估计、点云配准,全都依赖这里标出来的参数。嗯,这一章的内容就到这里,把基础打牢,后面才能走稳。