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 棋盘格制作步骤

  1. 用专业软件生成棋盘格图案(推荐OpenCV的 cv2.drawChessboardCorners
  2. 高精度打印,测量实际方格尺寸
  3. 粘贴到平整基板上,确保无气泡
  4. 测量并记录实际尺寸,精确到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,折中处理。

畸变校正后,图像边缘的直线会变直。你可以用一张有直线的场景图来验证效果。我习惯在标定完成后,拍一张有建筑或道路标线的照片,看看校正后直线是否真的变直了。

好了,摄像头内参标定这块就聊到这儿。说白了,张正友标定法就是一套「拍棋盘→找角点→算参数→校畸变」的流程。每一步都有细节,但掌握了这些,你就能做出可靠的标定结果。

下一章咱们聊聊摄像头外参标定,也就是怎么把多个摄像头对齐到同一个坐标系里。那个更有意思。