3、相机标定实战:棋盘格标定、张正友标定法、标定工具使用

3.1 为什么非得做标定?

说实话,我刚入行那会儿,觉得相机标定就是个「走过场」的步骤。直到有一次,我把一个没标定过的相机直接怼到感知系统里——结果测距误差直接飙到 10% 开外。嗯,从那以后,我再也不敢偷懒了。

相机标定,说白了就是给相机「配眼镜」。每颗镜头都有畸变,每个 sensor 的安装角度都有偏差。你不标定,算法看到的画面就是变形的。你想想看,一个歪歪扭扭的画面,怎么可能算得准目标的位置?

核心结论: 标定精度直接决定感知系统的测距精度和空间一致性。标定做不好,后面的一切都是空中楼阁。

3.2 棋盘格标定——最经典的「靶子」

棋盘格为什么流行?因为它角点清晰、检测稳定。我在项目中用过圆点标定板、AprilTag,最后还是觉得棋盘格最省心。

棋盘格的关键参数就两个:内角点数量和格子尺寸。

  • 内角点数量: 比如 9×6,指的是内部角点的行列数,不是格子数。我习惯用 9×6 或 11×8,太少容易欠约束,太多容易漏检。
  • 格子尺寸: 建议用 30mm 或 40mm 的方格。太小了远距离看不清,太大了近距离拍不全。
我的经验: 打印棋盘格时,一定要用激光打印机 + 硬纸板。喷墨打印的格子边缘模糊,角点检测精度会下降。我曾经吃过这个亏,后来再也不敢用普通纸了。

3.3 张正友标定法——背后的数学逻辑

张正友标定法,是 1998 年提出的。二十多年过去了,它依然是工业界的主流。为什么?因为它只需要一个平面棋盘格,不需要高精度的 3D 标定物。

它的核心思路其实很简单:

  1. 检测角点: 从图像中提取棋盘格的内角点坐标。
  2. 建立对应关系: 每个角点在图像上的位置 (u, v) 和它在世界坐标系中的位置 (X, Y, 0) 一一对应。
  3. 求解内参: 利用多张不同角度的图像,通过最小二乘法解出相机内参矩阵 K 和畸变系数。
  4. 优化: 用 Levenberg-Marquardt 算法做全局优化,最小化重投影误差。

为什么会需要多张图像?因为单张图像只能提供 8 个自由度,而内参有 5 个参数(fx, fy, cx, cy, skew),加上外参 6 个,一共 11 个未知数。一张图不够,至少需要 3 张不同角度的图。我个人习惯拍 15-20 张,覆盖视野的各个角落。

避坑指南: 我曾经遇到过标定结果反复跳动的情况。后来发现是棋盘格摆放角度太单一——全是正对着相机。记住:一定要有倾斜、旋转、远近变化,否则内参解算会病态。

3.4 标定工具使用——手把手实操

市面上标定工具很多,我主要用两种:OpenCV 自带的 cv2.calibrateCamera() 和 MATLAB 的标定工具箱。这里我重点讲 OpenCV 的用法,因为它在自动驾驶中更常用。

3.4.1 数据采集要点

  • 光照均匀: 避免反光和阴影。我一般在阴天或室内均匀光照下采集。
  • 棋盘格占画面 1/4 到 1/2: 太小了角点检测不准,太大了容易出视野。
  • 多角度覆盖: 左右、上下、倾斜、远近,每个角度拍 2-3 张。
  • 固定焦距: 标定过程中不要变焦,否则内参会变。

3.4.2 代码实战

下面是我常用的标定代码片段。注意,这里用的是 Python + OpenCV。

import cv2
import numpy as np
import glob

# 棋盘格内角点数量
CHECKERBOARD = (9, 6)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# 世界坐标系中的点
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
# 注意:这里单位是格子数,实际物理尺寸需要乘以格子边长

objpoints = []  # 3D 点
imgpoints = []  # 2D 点

images = glob.glob('calib_images/*.jpg')

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
    
    if ret:
        objpoints.append(objp)
        corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
        imgpoints.append(corners2)
        cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)

# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None
)

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
注意: objp 中的坐标单位是「格子数」,不是毫米。如果你要得到以毫米为单位的外参,需要把 objp 乘以格子实际边长。我习惯在标定前就把物理尺寸写进去,省得后面换算。

3.4.3 结果验证

标定完一定要验证。我常用的方法有两个:

  • 重投影误差: cv2.calibrateCamera() 返回的 ret 就是重投影误差。一般小于 0.5 像素就算合格。我见过有人标到 0.1 像素,那精度确实高。
  • 去畸变效果: 用 cv2.undistort() 处理一张图,看看边缘的直线是不是变直了。如果还有弯曲,说明标定不够好。
# 去畸变
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

3.5 标定流程全景图

下面这张图是我自己总结的标定流程。每次做标定前,我都会对照着走一遍,确保不遗漏步骤。

相机标定实战流程 1. 准备棋盘格 打印、测量格子尺寸 2. 采集多角度图像 15-20张,覆盖全视野 3. 角点检测 findChessboardCorners 4. 求解内参 calibrateCamera 5. 全局优化 Levenberg-Marquardt 6. 验证结果 重投影误差 < 0.5px 合格? 不合格,重新采集 7. 保存标定参数

3.6 常见问题与避坑

问题 原因 解决办法
角点检测失败 光照不均、棋盘格模糊 调整光照,确保棋盘格清晰
重投影误差过大 图像数量不足或角度单一 增加图像,覆盖更多角度
去畸变后图像边缘扭曲 畸变系数估计不准 检查是否有模糊图像混入
内参矩阵数值异常 棋盘格尺寸写错 重新测量格子实际尺寸
我曾经踩过的坑: 有一次标定结果看起来很好,重投影误差 0.3 像素。结果放到实际场景中,测距误差大得离谱。排查了半天,发现是棋盘格打印时被拉伸了——格子不是正方形。从那以后,我每次打印完都会用卡尺量一下实际尺寸。

3.7 小结

相机标定这件事,说难不难,说简单也不简单。关键就三点:

  • 数据质量第一: 多角度、多姿态、光照均匀。数据好了,算法自然给力。
  • 验证不能省: 重投影误差 + 去畸变效果,两个都过了才算完。
  • 物理尺寸要准: 棋盘格的实际尺寸一定要测量,别信打印机的标称值。

嗯,标定做扎实了,后面的感知联调才能站得住脚。你想想看,如果相机看到的世界都是歪的,那后面的检测、跟踪、测距,怎么可能准呢?