3、相机标定基础:标定的目的与意义、标定板的选择与制作、张正友标定法原理、标定流程与注意事项

相机标定,说白了就是给相机做「体检」。

我刚开始做双目视觉那会儿,总觉得标定是个麻烦事。后来被现实狠狠教育了一顿——不标定,你的三维重建就是空中楼阁。你想想看,两个相机拍同一个物体,如果不知道它们各自的内参、外参,怎么可能算出准确的三维坐标?

3.1 标定的目的与意义

标定的核心目的就三个:

  • 确定内参:焦距、主点坐标、畸变系数。这些是相机本身的「出厂设置」。
  • 确定外参:两个相机之间的旋转矩阵和平移向量。这是双目系统的「骨架」。
  • 矫正畸变:镜头带来的桶形畸变、枕形畸变,必须去掉。

我记得有个项目,客户说他们的双目系统测距误差很大。我过去一看,标定文件还是三个月前的。嗯,相机被碰过,螺丝松了,外参早就变了。重新标定后,误差从5%降到了0.3%。

一句话总结:标定决定了双目系统的下限。标定做不好,后面的一切算法都是白搭。

3.2 标定板的选择与制作

标定板的选择,我踩过不少坑。这里直接说结论:

类型 优点 缺点 推荐场景
棋盘格 角点检测简单、成熟 需要完整可见、易受光照影响 入门、实验室环境
圆点阵列 亚像素精度高、抗遮挡 需要椭圆拟合、算法复杂 高精度要求
ChArUco板 支持部分遮挡、可编码 制作稍复杂 大视场、多相机

我个人习惯用棋盘格,因为OpenCV支持最好。但要注意:

  • 棋盘格必须是非对称的(比如9×6,不是10×10),否则方向会搞混。
  • 打印时用哑光纸,反光会害死角点检测。
  • 贴在刚性平板上,别用软木板。我曾经用软木板,结果标定板弯了,标定出来的焦距全是错的。

我的小技巧:标定板尺寸别太小。我一般让每个格子边长占图像宽度的1/10左右。太小了角点检测不稳定,太大了又拍不全。

3.3 张正友标定法原理

张正友标定法,说白了就是「用平面标定板来解空间问题」。为什么它这么流行?因为它只需要你拍几张不同角度的标定板照片,不需要知道标定板在空间中的精确位置。

核心思路是这样的:

  1. 假设标定板在Z=0平面上,建立世界坐标系。
  2. 通过角点检测,得到图像坐标和世界坐标的对应关系。
  3. 利用单应性矩阵(Homography),建立图像到标定板的映射。
  4. 从多个角度的单应性矩阵中,解出相机内参。
  5. 再用内参反推外参和畸变系数。
  6. 最后用非线性优化(比如Levenberg-Marquardt)做全局优化。

为什么会这样?因为单应性矩阵有8个自由度,而内参有5个参数(fx, fy, cx, cy, skew),加上外参的6个参数(3旋转+3平移),理论上3张照片就能解。但实际中,我建议至少拍15-20张。

注意:张正友标定法假设标定板是平面。如果你的标定板弯了、皱了,那标定结果就是垃圾。我曾经见过有人用海报打印的标定板,结果标定出来的畸变系数大得离谱——其实是板子不平,不是镜头畸变。

3.4 标定流程与注意事项

标定流程其实不复杂,但细节决定成败。我总结了一套标准流程:

  1. 准备标定板:打印、粘贴、测量格子尺寸(精确到0.1mm)。
  2. 采集图像:左右相机同步拍摄,至少15对。覆盖视场的各个区域,包括边缘和角落。
  3. 角点检测:用OpenCV的findChessboardCorners,检查检测结果。
  4. 单目标定:分别标定左右相机的内参和畸变系数。
  5. 双目标定:利用左右相机的外参关系,标定立体校正参数。
  6. 验证结果:用重投影误差评估,一般小于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个月或者每次搬运后重新标定一次。

相机标定知识体系 相机标定 目的与意义 确定内参、外参、畸变系数 矫正镜头畸变 建立双目系统几何关系 标定板选择与制作 棋盘格 / 圆点阵列 / ChArUco 哑光纸打印、刚性平板粘贴 非对称棋盘格(如9×6) 张正友标定法 利用单应性矩阵求解 多角度照片解算内参 非线性优化(LM算法) 标定流程与注意事项 采集15-20对图像 角点检测与筛选 单目+双目标定 验证重投影误差

公众号:蓝海资料掘金营,微信deep3321