第4章:摄像头标定——针孔相机模型、畸变模型与张正友标定法
各位同学,今天我们来聊聊摄像头标定。说实话,这是整个传感器融合里最“接地气”的一步。你想想看,摄像头拍出来的图像,如果不做标定,那误差能大到让你怀疑人生。我当年刚入行时,就吃过这个亏——装了个摄像头,自以为图像挺清晰,结果一测距离,偏差十几厘米。后来才发现,是畸变没处理好。
好,咱们一步步来。先搞清楚摄像头是怎么“看”世界的。
4.1 针孔相机模型——摄像头成像的“理想国”
针孔相机模型,说白了就是小孔成像。光线穿过一个小孔,在后面的感光面上形成倒像。这个模型虽然简单,但它是所有标定算法的基础。
数学上,我们用四个坐标系来描述这个过程:
- 世界坐标系:真实物体在三维空间中的位置 (X, Y, Z)
- 相机坐标系:以摄像头光心为原点 (Xc, Yc, Zc)
- 图像坐标系:成像平面上的物理位置 (x, y)
- 像素坐标系:图像上以像素为单位的坐标 (u, v)
从世界坐标到像素坐标,本质上就是做两次变换:
- 外参:把世界坐标转到相机坐标(旋转 + 平移)
- 内参:把相机坐标投影到像素平面(焦距、主点偏移)
公式长这样(别怕,理解意思就行):
s * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T
其中 K 就是内参矩阵:
K = [fx, 0, cx;
0, fy, cy;
0, 0, 1]
fx、fy 是焦距(像素单位),cx、cy 是主点偏移。这些值,就是我们要标定出来的东西。
4.2 畸变模型——现实世界的“扭曲”
针孔模型是理想情况。现实中的镜头,尤其是广角镜头,会引入畸变。主要有两种:
4.2.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 是像素到图像中心的距离。k1、k2、k3 就是径向畸变系数。一般 k3 只在鱼眼镜头里用,普通镜头标 k1、k2 就够了。
4.2.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.3 张正友标定法——经典中的经典
张正友教授在 1998 年提出的这个方法,到现在还是工业界的主流。为什么?因为它只需要拍几张棋盘格照片,就能把内参、外参、畸变系数全算出来。
核心思路是这样的:
- 拍一张棋盘格照片,提取角点
- 假设棋盘格在 Z=0 平面上,建立单应性矩阵 H
- 利用多张照片的 H 矩阵,求解内参 K
- 用最小二乘法优化畸变系数
- 最后用最大似然估计做全局优化
嗯,这里要注意:棋盘格必须拍不同角度、不同位置。我见过有人只拍了 5 张,结果标定出来的焦距偏差 5%。我个人习惯至少拍 15-20 张,覆盖图像的各个区域。
4.4 OpenCV 标定实践——动手干吧
理论说完了,咱们直接上代码。OpenCV 的 cv::calibrateCamera() 函数封装了张正友标定法,用起来很简单。
4.4.1 准备棋盘格图像
先拍一组棋盘格照片。注意:
- 光照均匀,不要有反光
- 棋盘格在画面中占不同位置
- 倾斜角度 15°-45° 之间
- 至少 10 张,建议 15-20 张
4.4.2 提取角点
import cv2
import numpy as np
# 棋盘格内角点数量
pattern_size = (9, 6)
# 世界坐标系中的角点坐标(假设 Z=0)
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)
# 存储所有图像的世界坐标和像素坐标
objpoints = [] # 3D 点
imgpoints = [] # 2D 点
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)
# 画出来看看
cv2.drawChessboardCorners(img, pattern_size, corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
4.4.3 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)
输出结果中,mtx 就是内参矩阵,dist 是畸变系数(顺序是 k1, k2, p1, p2, k3)。ret 是重投影误差,一般小于 0.5 像素就算不错了。
cv2.undistort() 矫正一张图,看看边缘的直线是不是变直了。如果还有弯曲,说明标定不够好,需要重新拍图。
4.4.4 图像去畸变
# 方法一:直接矫正
undistorted = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 方法二:先计算映射表,再重映射(适合视频流)
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
方法二效率更高,适合实时处理。我一般在量产代码里用 remap,因为只需要算一次映射表。
4.5 知识体系总览
下面这张图,是我自己整理的摄像头标定知识结构。你可以把它当成一张“地图”,随时回来看看。
4.6 标定结果验证
标定完了,怎么知道好不好?我一般看三个指标:
| 指标 | 合格标准 | 说明 |
|---|---|---|
| 重投影误差 | < 0.5 像素 | 角点投影回图像的平均偏差 |
| 去畸变后直线度 | 边缘直线无明显弯曲 | 肉眼观察棋盘格边缘是否变直 |
| 标定结果稳定性 | 多次标定偏差 < 2% | 换一组照片再标,看内参是否一致 |
好了,摄像头标定这部分就讲到这里。记住一句话:标定是传感器融合的基石。这一步做不好,后面所有算法都是空中楼阁。下次咱们聊激光雷达标定,那个又是另一番天地了。
公众号:蓝海资料掘金营,微信deep3321