3、摄像头内参标定:张正友标定法原理、棋盘格制作要求、标定流程与畸变校正
摄像头内参标定,说白了就是搞清楚镜头到底是怎么「看」世界的。我入行那会儿,有个老工程师跟我说过一句话,我一直记着:「你标定做不好,后面所有算法都是空中楼阁。」这话一点不夸张。
今天咱们就聊聊内参标定里最经典的方法——张正友标定法。这个方法我用了快十年,从实验室到量产项目,几乎每天都在跟它打交道。
3.1 张正友标定法原理
张正友标定法为什么经典?因为它聪明。它不需要你知道相机在空间中的精确位置,只需要你拍几张不同角度的棋盘格照片就行。
核心思想其实很简单:
- 我们知道棋盘格上每个角点的物理坐标(比如角点间距30mm)
- 我们也知道这些角点在图像上的像素坐标
- 通过这两组坐标的对应关系,反推出相机的内参矩阵和畸变系数
数学上,它建立了一个从世界坐标系到像素坐标系的映射关系:
s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T
其中:
- K 是内参矩阵(包含焦距 fx, fy 和主点 cx, cy)
- [R|t] 是外参(旋转和平移)
- s 是尺度因子
- (u, v) 是像素坐标
- (X, Y, Z) 是世界坐标
张正友的巧妙之处在于:他假设棋盘格平面是 Z=0,这样就把问题简化成了平面到平面的单应性变换。然后通过多张不同角度的照片,把内参和外参解耦出来。
关键点:张正友标定法至少需要3张不同角度的棋盘格照片,但我个人建议拍15-20张。为什么?因为照片越多,噪声平均效果越好,标定结果越稳定。
3.2 棋盘格制作要求
棋盘格看着简单,但制作起来有不少门道。我在项目里踩过不少坑,今天一并告诉你。
3.2.1 尺寸与精度
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 方格尺寸 | 20-40mm | 根据视场角调整,太大太小都不好 |
| 内角点数量 | 9×6 或 11×8 | 奇数×偶数,方便识别方向 |
| 打印精度 | ±0.1mm | 用高精度打印机,别用普通喷墨 |
| 基板材质 | 亚克力或铝板 | 要平整,不能弯曲 |
嗯,这里要注意:方格尺寸不是随便选的。我见过有人用5mm的小棋盘格标定广角摄像头,结果角点检测全糊了。你想想看,如果棋盘格太小,远距离拍摄时角点根本看不清。
避坑指南:我曾经用普通A4纸打印棋盘格,贴在纸板上就开标。结果标出来的焦距偏差了3%以上。后来发现纸板受潮变形了。从那以后,我坚持用亚克力板或铝板做基材,平整度有保障。
3.2.2 棋盘格制作步骤
- 用专业软件生成棋盘格图案(推荐OpenCV的
cv2.drawChessboardCorners) - 高精度打印,测量实际方格尺寸
- 粘贴到平整基板上,确保无气泡
- 测量并记录实际尺寸,精确到0.1mm
3.3 标定流程
标定流程我总结成五步,每一步都有讲究。
3.3.1 图像采集
采集图像时,有几个要点:
- 棋盘格要占画面1/3到2/3
- 角度变化要覆盖:左右倾斜、上下俯仰、旋转
- 距离要变化:近、中、远各拍一些
- 光照要均匀,避免反光
我习惯在自然光下采集,室内灯光容易产生频闪。有一次在车间里标定,日光灯频闪导致图像明暗变化,角点检测老是失败。后来换了LED补光才搞定。
3.3.2 角点检测
OpenCV提供了现成的函数:
import cv2
import numpy as np
# 棋盘格内角点数量
pattern_size = (9, 6) # 内角点列数×行数
# 读取图像
img = cv2.imread('calib_01.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 检测角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
# 亚像素精确化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
# 绘制角点
cv2.drawChessboardCorners(img, pattern_size, corners, ret)
小技巧:亚像素精确化这步别省。我对比过,做了亚像素优化后,重投影误差能从0.3像素降到0.1像素以下。
3.3.3 标定计算
用检测到的角点进行标定:
# 准备世界坐标系中的点
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)
objp *= square_size # 乘以实际方格尺寸
# 收集所有图像的数据
objpoints = [] # 世界坐标系中的点
imgpoints = [] # 图像坐标系中的点
# 对每张图像执行角点检测并添加数据
# ...(循环处理所有图像)
# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
标定完成后,mtx 就是内参矩阵,dist 是畸变系数。
3.3.4 结果评估
标定完了一定要看重投影误差:
total_error = 0
for i in range(len(objpoints)):
imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist)
error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
total_error += error
print(f"平均重投影误差: {total_error/len(objpoints):.4f} 像素")
重投影误差小于0.2像素算合格,小于0.1像素算优秀。如果超过0.5像素,说明标定有问题,得重新来。
3.4 畸变校正
畸变校正说白了就是把变形的图像拉直。主要有两种畸变:
- 径向畸变:图像边缘的桶形或枕形变形
- 切向畸变:镜头与成像平面不平行导致的变形
校正代码很简单:
# 获取校正后的映射表
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
# 校正图像
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 裁剪图像(去掉黑边)
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
注意:getOptimalNewCameraMatrix 的 alpha 参数很关键。alpha=0 会裁剪掉所有黑边,但可能损失边缘信息;alpha=1 保留所有像素,但会有黑边。我一般设成0.5,折中处理。
畸变校正后,图像边缘的直线会变直。你可以用一张有直线的场景图来验证效果。我习惯在标定完成后,拍一张有建筑或道路标线的照片,看看校正后直线是否真的变直了。
好了,摄像头内参标定这块就聊到这儿。说白了,张正友标定法就是一套「拍棋盘→找角点→算参数→校畸变」的流程。每一步都有细节,但掌握了这些,你就能做出可靠的标定结果。
下一章咱们聊聊摄像头外参标定,也就是怎么把多个摄像头对齐到同一个坐标系里。那个更有意思。