3. 相机内参标定:针孔模型、畸变模型、张正友标定法原理与实战
相机标定,说白了就是搞清楚两件事:相机把三维世界映射到二维图像时,用了什么数学规则,以及这个规则里有哪些参数需要确定。我做了这么多年传感器融合,可以负责任地告诉你——内参标定是所有后续工作的基石。你想想看,如果相机都不知道自己长什么样,它看到的东西怎么可能和其他传感器对齐?
3.1 针孔模型:相机成像的数学抽象
针孔模型是最经典的相机成像模型。它假设光线通过一个小孔投影到成像平面上。虽然真实相机用的是透镜,但针孔模型提供了一个很好的近似起点。
数学上,针孔模型可以表示为:
s * [u, v, 1]^T = K * [R|t] * [X, Y, Z, 1]^T
其中:
- (u, v) 是图像像素坐标
- (X, Y, Z) 是世界坐标系下的三维点
- K 是内参矩阵
- [R|t] 是外参矩阵(旋转+平移)
- s 是尺度因子
内参矩阵 K 长这样:
K = [[fx, 0, cx],
[ 0, fy, cy],
[ 0, 0, 1]]
fx, fy 是焦距(以像素为单位),cx, cy 是主点坐标(光轴与成像平面的交点)。
核心理解:内参矩阵描述的是相机内部的光学几何特性。同一个相机,内参是固定的;换了相机,内参就得重新标定。
3.2 畸变模型:现实世界的修正
针孔模型是理想情况。真实相机有透镜,透镜会引入畸变。我遇到过不少新手,标定完发现投影误差很大,其实就是没处理好畸变。
畸变主要分两类:
| 畸变类型 | 产生原因 | 视觉效果 | 数学模型 |
|---|---|---|---|
| 径向畸变 | 透镜形状不完美 | 直线变弯曲(桶形/枕形) | k1, k2, k3 三个参数 |
| 切向畸变 | 透镜与成像平面不平行 | 图像倾斜/梯形失真 | p1, p2 两个参数 |
畸变校正的数学公式:
x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + [p1*(r^2 + 2*y^2) + 2*p2*x*y]
其中 r^2 = x^2 + y^2,x, y 是归一化平面坐标。
避坑指南:我曾经在标定一个广角相机时,只用了 k1, k2 两个径向畸变参数,结果图像边缘的校正效果很差。后来加上 k3 才解决问题。对于视场角大于 90° 的相机,建议使用 3 个径向畸变参数。
3.3 张正友标定法:从棋盘格到内参
张正友标定法是目前最主流的相机内参标定方法。它的核心思想是:用一张已知尺寸的棋盘格,从不同角度拍摄多张图像,然后通过数学优化求解内参和畸变参数。
为什么这个方法好用?因为它不需要知道相机相对于棋盘格的位置(外参),只需要棋盘格是平面且尺寸已知。这在实际操作中非常方便。
算法流程如下:
- 拍摄棋盘格图像:从不同角度拍摄 10-20 张图像
- 提取角点:检测棋盘格的内角点坐标
- 计算单应性矩阵:每张图像对应一个单应性矩阵 H
- 求解内参初值:利用单应性矩阵的约束条件
- 估计畸变参数:基于最小二乘法
- 全局优化:使用 Levenberg-Marquardt 算法最小化重投影误差
个人经验:我习惯拍摄 15-20 张图像,覆盖棋盘格在画面中的不同位置和角度。特别要注意让棋盘格出现在图像边缘和角落——这些区域对畸变参数的估计至关重要。
3.4 实战:使用 OpenCV 进行相机标定
下面是一个完整的标定代码示例。我建议你直接运行试试,看看效果。
import cv2
import numpy as np
import glob
# 1. 准备棋盘格参数
CHECKERBOARD = (9, 6) # 内角点数量
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 2. 准备世界坐标系中的点
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
# 3. 存储所有图像的点
objpoints = [] # 世界坐标系中的点
imgpoints = [] # 图像坐标系中的点
images = glob.glob('calibration_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 4. 查找棋盘格角点
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)
cv2.imshow('Corners', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
# 5. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
# 6. 评估标定精度
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("平均重投影误差: ", mean_error / len(objpoints))
关键指标:重投影误差通常应小于 0.5 像素。如果误差大于 1 像素,说明标定质量有问题,需要检查图像质量或重新拍摄。
3.5 标定流程与知识体系
下面这张图展示了相机内参标定的完整知识体系:
3.6 标定实战中的常见问题
我在项目中遇到过不少标定翻车的情况,这里总结几个最常见的坑:
| 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 重投影误差大 | 误差 > 1 像素 | 图像模糊/棋盘格不平整 | 重新拍摄,确保对焦清晰 |
| 畸变校正过度 | 图像边缘扭曲 | 畸变参数过多/图像不足 | 增加图像数量,减少畸变参数 |
| 主点偏移严重 | cx, cy 偏离图像中心 | 棋盘格未覆盖画面边缘 | 让棋盘格出现在画面四角 |
| 标定结果不稳定 | 每次标定结果差异大 | 图像数量不足/角度单一 | 拍摄 20+ 张,覆盖不同角度 |
曾经踩过的坑:有一次我在室内标定相机,用了 10 张图像,重投影误差只有 0.3 像素,看起来很好。结果拿到室外用,畸变校正完全不对。后来发现是室内光线太暗,棋盘格角点提取精度不够。从那以后,我标定都会在充足光线下进行,并且至少拍 15 张。
3.7 标定质量的验证方法
标定完不是就完事了。我习惯做三件事来验证标定质量:
- 重投影误差检查:所有角点的平均重投影误差应 < 0.5 像素
- 畸变校正可视化:用标定结果校正一张图像,检查直线是否变直
- 跨场景验证:在不同距离和光照条件下拍摄,验证标定结果的泛化能力
小技巧:你可以用标定结果把棋盘格图像校正后,再检测角点,计算校正后的角点误差。如果校正后的误差比校正前小很多,说明标定是有效的。
嗯,相机内参标定就讲到这里。记住,标定不是一次性工作——相机在使用过程中可能会因为温度变化、振动等原因导致内参漂移。我建议每隔一段时间重新标定一次,或者在关键任务前做一次快速验证。