3. 相机标定实战

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

你想想看,摄像头拍出来的图像,边缘是不是有点弯?远处的直线是不是有点歪?这就是镜头畸变。我刚开始做感知时,总觉得标定是件小事,直到有一次车道线检测在弯道处频频出错——排查了三天,最后发现是相机内参没标准。嗯,从那以后,我再也不敢跳过这一步了。

3.1 张正友标定法:为什么它成了行业标准?

张正友标定法,核心思路其实很简单:用一张棋盘格,从不同角度拍几张照片,就能算出相机的内参和外参。

为什么大家都用这个方法?因为它不需要精密的3D标定物,只需要一个打印出来的棋盘格。我在项目中用过好几次,说实话,只要打印平整、粘贴在硬板上,精度完全够用。

核心原理一句话:

棋盘格上的角点坐标是已知的(世界坐标系),图像上的角点坐标是检测到的(像素坐标系)。通过这两组对应关系,求解相机投影矩阵,再分解出内参、外参和畸变系数。

数学上,它用到了单应性矩阵。每张棋盘格照片,都能建立一个从世界平面到图像平面的单应性矩阵H。多张照片的H矩阵联立求解,就能解出内参矩阵K。

我的经验:至少拍15-20张不同角度的棋盘格照片。角度变化要大——左右倾斜、上下俯仰、远近移动都要有。我曾经只拍了10张,结果标定出来的焦距偏差了3%,后来补拍了几张才稳定下来。

3.2 棋盘格采集:看似简单,坑却不少

棋盘格采集,听起来就是「拍几张照片嘛」。但实际操作中,我踩过的坑可以写一本小册子了。

3.2.1 棋盘格制作要点

  • 尺寸要准:打印出来后,用尺子量一下每个格子的实际边长。打印缩放会导致尺寸偏差,我习惯在打印时关闭「适应页面」选项。
  • 格子数要奇偶搭配:比如9×6(内角点数为8×5)。这样棋盘格有明确的方向性,算法能自动识别。
  • 粘贴要平整:贴在硬纸板或亚克力板上,不能有褶皱。我见过有人贴在软布上,标定结果一塌糊涂。

3.2.2 采集规范

要求 说明 我的建议
光照均匀 避免强光直射或阴影遮挡 室内日光灯下拍摄最佳
棋盘格占画面1/3以上 太小了角点检测不准 距离相机0.5-1.5米为宜
角度变化丰富 X轴、Y轴、Z轴方向都要旋转 每个角度拍2-3张
保持棋盘格清晰 不能有运动模糊 使用三脚架或提高快门速度

避坑指南:我曾经在室外强光下采集棋盘格,结果反光导致部分角点检测失败。后来我改成室内漫反射环境,问题就解决了。另外,棋盘格不要离相机太近——太近了畸变严重,角点检测容易出错。

3.3 使用OpenCV进行标定:代码实战

OpenCV提供了完整的标定工具链。我个人习惯用Python,调试起来方便。下面是一套完整的标定流程。

3.3.1 角点检测

import cv2
import numpy as np
import glob

# 棋盘格内角点数量(列,行)
chessboard_size = (9, 6)
# 每个格子的实际尺寸(单位:mm)
square_size = 30.0

# 准备世界坐标系中的点
objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1, 2)
objp *= square_size

# 存储所有图像的点
objpoints = []  # 世界坐标系中的点
imgpoints = []  # 图像坐标系中的点

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, chessboard_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, chessboard_size, corners2, ret)
        cv2.imshow('Corners', img)
        cv2.waitKey(100)

cv2.destroyAllWindows()

注意:如果findChessboardCorners返回False,说明角点检测失败。常见原因有:棋盘格太小、光照不均匀、角度过于极端。我建议先拍一张正面的照片测试一下,确认检测成功后再批量采集。

3.3.2 执行标定

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

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)

这里输出的mtx就是内参矩阵,包含fx、fy、cx、cy。dist是畸变系数,包含k1、k2、p1、p2、k3。

为什么会输出5个畸变系数?因为OpenCV默认使用径向畸变(k1、k2、k3)和切向畸变(p1、p2)。径向畸变是镜头本身造成的,切向畸变是传感器和镜头不平行导致的。

3.4 标定结果评估:别急着用,先看看准不准

标定完不能直接拿去用。我见过有人标定完就部署,结果车道线检测偏差了5个像素——这就是没做评估的后果。

3.4.1 重投影误差

calibrateCamera返回的ret就是重投影误差。它的含义是:将世界坐标系中的角点投影回图像,与检测到的角点之间的平均像素误差。

一般来说,重投影误差小于0.5像素就算合格。如果大于1.0像素,说明标定质量有问题,需要检查采集数据。

我的经验:重投影误差0.3像素以内,标定结果就很好了。如果误差偏大,我通常会做两件事:一是剔除误差最大的几张照片,二是检查棋盘格是否平整。

3.4.2 可视化评估

# 计算每张图像的重投影误差
mean_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)
    mean_error += error

print("平均重投影误差: {:.3f} 像素".format(mean_error / len(objpoints)))

我习惯把每张图像的误差打印出来,看看有没有异常值。如果某张照片的误差明显偏大,直接删掉重新标定。

3.5 去畸变:让图像恢复「本来面目」

标定完成后,最后一步就是去畸变。说白了,就是把弯曲的直线拉直。

# 读取一张测试图像
img = cv2.imread('test_image.jpg')
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]

cv2.imshow('Undistorted', dst)
cv2.waitKey(0)

注意:去畸变后图像边缘会有黑边。使用getOptimalNewCameraMatrix可以自动裁剪掉黑边区域。但如果你需要保留完整的视野,可以把alpha参数设为0,这样会保留所有像素,但边缘会有拉伸变形。

去畸变后的图像,直线就是直线了。这对于车道线检测至关重要——你想想看,如果图像本身是弯的,检测出来的车道线怎么可能准?

3.6 本章知识体系

下面这张图,是我梳理的相机标定全流程。从棋盘格采集到最终的去畸变,每一步都有坑,每一步都有技巧。

相机标定全流程知识体系 棋盘格制作 尺寸准确、粘贴平整 图像采集 15-20张、多角度 角点检测 findChessboardCorners 执行标定 calibrateCamera 结果评估 重投影误差 < 0.5px 去畸变 undistort 不合格则重新采集 标定输出结果 内参矩阵 (fx, fy, cx, cy) | 畸变系数 (k1, k2, p1, p2, k3) | 外参 (旋转、平移) 应用:车道线检测、障碍物测距、SLAM、3D重建

标定这件事,说难不难,说简单也不简单。关键是要理解每一步的原理,知道为什么这么做,以及出了问题怎么排查。我在项目中积累的经验就是:多拍几张、多检查几次、多验证一遍。标定做好了,后面的感知算法才能跑得稳。