3、相机标定基础:标定的目的与意义、标定板的选择与制作、张正友标定法原理、标定流程与注意事项
相机标定,说白了就是给相机做「体检」。
我刚开始做双目视觉那会儿,总觉得标定是个麻烦事。后来被现实狠狠教育了一顿——不标定,你的三维重建就是空中楼阁。你想想看,两个相机拍同一个物体,如果不知道它们各自的内参、外参,怎么可能算出准确的三维坐标?
3.1 标定的目的与意义
标定的核心目的就三个:
- 确定内参:焦距、主点坐标、畸变系数。这些是相机本身的「出厂设置」。
- 确定外参:两个相机之间的旋转矩阵和平移向量。这是双目系统的「骨架」。
- 矫正畸变:镜头带来的桶形畸变、枕形畸变,必须去掉。
我记得有个项目,客户说他们的双目系统测距误差很大。我过去一看,标定文件还是三个月前的。嗯,相机被碰过,螺丝松了,外参早就变了。重新标定后,误差从5%降到了0.3%。
一句话总结:标定决定了双目系统的下限。标定做不好,后面的一切算法都是白搭。
3.2 标定板的选择与制作
标定板的选择,我踩过不少坑。这里直接说结论:
| 类型 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 棋盘格 | 角点检测简单、成熟 | 需要完整可见、易受光照影响 | 入门、实验室环境 |
| 圆点阵列 | 亚像素精度高、抗遮挡 | 需要椭圆拟合、算法复杂 | 高精度要求 |
| ChArUco板 | 支持部分遮挡、可编码 | 制作稍复杂 | 大视场、多相机 |
我个人习惯用棋盘格,因为OpenCV支持最好。但要注意:
- 棋盘格必须是非对称的(比如9×6,不是10×10),否则方向会搞混。
- 打印时用哑光纸,反光会害死角点检测。
- 贴在刚性平板上,别用软木板。我曾经用软木板,结果标定板弯了,标定出来的焦距全是错的。
我的小技巧:标定板尺寸别太小。我一般让每个格子边长占图像宽度的1/10左右。太小了角点检测不稳定,太大了又拍不全。
3.3 张正友标定法原理
张正友标定法,说白了就是「用平面标定板来解空间问题」。为什么它这么流行?因为它只需要你拍几张不同角度的标定板照片,不需要知道标定板在空间中的精确位置。
核心思路是这样的:
- 假设标定板在Z=0平面上,建立世界坐标系。
- 通过角点检测,得到图像坐标和世界坐标的对应关系。
- 利用单应性矩阵(Homography),建立图像到标定板的映射。
- 从多个角度的单应性矩阵中,解出相机内参。
- 再用内参反推外参和畸变系数。
- 最后用非线性优化(比如Levenberg-Marquardt)做全局优化。
为什么会这样?因为单应性矩阵有8个自由度,而内参有5个参数(fx, fy, cx, cy, skew),加上外参的6个参数(3旋转+3平移),理论上3张照片就能解。但实际中,我建议至少拍15-20张。
注意:张正友标定法假设标定板是平面。如果你的标定板弯了、皱了,那标定结果就是垃圾。我曾经见过有人用海报打印的标定板,结果标定出来的畸变系数大得离谱——其实是板子不平,不是镜头畸变。
3.4 标定流程与注意事项
标定流程其实不复杂,但细节决定成败。我总结了一套标准流程:
- 准备标定板:打印、粘贴、测量格子尺寸(精确到0.1mm)。
- 采集图像:左右相机同步拍摄,至少15对。覆盖视场的各个区域,包括边缘和角落。
- 角点检测:用OpenCV的
findChessboardCorners,检查检测结果。 - 单目标定:分别标定左右相机的内参和畸变系数。
- 双目标定:利用左右相机的外参关系,标定立体校正参数。
- 验证结果:用重投影误差评估,一般小于0.5像素就算合格。
这里有几个坑,我一个个说:
- 图像数量:别少于15对。我见过有人用5对就敢标定,结果重投影误差0.1像素,但实际测距误差大得吓人——过拟合了。
- 角度变化:标定板要倾斜、旋转、平移,别只在一个平面内移动。我习惯让标定板与相机光轴成30°-60°角。
- 光照均匀:避免强光直射标定板,否则角点检测会飘。我一般在漫射光环境下拍。
- 固定相机:标定过程中相机不能动。动了?重新拍。
避坑指南:我曾经在一个项目中,标定结果一直不稳定。折腾了两天,最后发现是相机镜头没锁紧,轻微晃动导致外参一直在变。嗯,从那以后我每次标定前都会检查所有螺丝。
最后,贴一段我常用的标定代码框架(基于OpenCV):
import cv2
import numpy as np
# 棋盘格参数
pattern_size = (9, 6) # 内角点数量
square_size = 30.0 # 格子边长(mm)
# 准备世界坐标
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_l = [] # 左图角点
imgpoints_r = [] # 右图角点
# 对每对图像
for img_l, img_r in zip(left_images, right_images):
ret_l, corners_l = cv2.findChessboardCorners(img_l, pattern_size, None)
ret_r, corners_r = cv2.findChessboardCorners(img_r, pattern_size, None)
if ret_l and ret_r:
objpoints.append(objp)
imgpoints_l.append(corners_l)
imgpoints_r.append(corners_r)
# 单目标定
ret_l, mtx_l, dist_l, rvecs_l, tvecs_l = cv2.calibrateCamera(
objpoints, imgpoints_l, img_l.shape[::-1], None, None)
ret_r, mtx_r, dist_r, rvecs_r, tvecs_r = cv2.calibrateCamera(
objpoints, imgpoints_r, img_r.shape[::-1], None, None)
# 双目标定
ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate(
objpoints, imgpoints_l, imgpoints_r,
mtx_l, dist_l, mtx_r, dist_r,
img_l.shape[::-1], criteria=(cv2.TERM_CRITERIA_EPS+cv2.TERM_CRITERIA_MAX_ITER, 30, 1e-6))
print(f"重投影误差: {ret:.4f} 像素")
这段代码虽然简单,但够用。实际项目中,我还会加一些异常检测——比如检查角点是否在图像边缘、重投影误差是否突然变大等。
最后提醒一句:标定不是一次性工作。相机用久了、温度变了、螺丝松了,都需要重新标定。我一般建议每3个月或者每次搬运后重新标定一次。
公众号:蓝海资料掘金营,微信deep3321