相机内参标定:张正友标定法原理、棋盘格角点检测、内参矩阵与畸变系数的计算
说到相机内参标定,这其实是整个点云与图像融合项目里,我最先踩坑的地方。你想想看,如果相机本身的内参都不准,那后面做的所有投影、关联,说白了都是空中楼阁。今天我们就来聊聊张正友标定法——这个在工业界和学术界都广泛使用的经典方法。
张正友标定法的核心思想
张正友标定法,本质上是一种介于传统标定和自标定之间的方法。它不需要你知道标定板在空间中的精确三维位置,只需要你拿着棋盘格在相机前摆几个不同的姿态,拍几张照片就行。
我个人觉得,这个方法最大的优点是:操作简单,精度够用。我在做自动驾驶项目时,经常需要在现场快速标定相机,张正友法几乎成了我的首选。
它的核心逻辑是这样的:
- 假设棋盘格平面在世界坐标系中 Z=0
- 通过角点检测,得到图像坐标系中的点
- 利用单应性矩阵,建立世界坐标与图像坐标的映射关系
- 通过多张不同姿态的图像,求解内参矩阵和畸变系数
关键点:张正友法要求标定板是平面,且我们已知棋盘格每个角点的物理尺寸(比如格子边长 30mm)。这是整个标定过程的输入前提。
棋盘格角点检测
角点检测这一步,我建议你用 OpenCV 的 cv2.findChessboardCorners()。这个函数专门为棋盘格设计,鲁棒性不错。
不过,我在实际项目中遇到过一个问题:如果棋盘格反光严重,或者光照不均匀,角点检测很容易失败。后来我总结了一个小技巧——拍摄时尽量让棋盘格占据画面 1/3 到 1/2 的面积,并且避免强光直射。
检测到角点后,还需要用 cv2.cornerSubPix() 做亚像素级别的精炼。这一步很多人会忽略,但它的精度提升非常明显。
import cv2
import numpy as np
# 棋盘格尺寸(内角点数量)
pattern_size = (9, 6) # 9列6行内角点
# 准备世界坐标系中的点
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)
# 注意:这里单位是格子边长,实际标定时需要乘以物理尺寸
# 存储所有图像的点
objpoints = [] # 世界坐标
imgpoints = [] # 图像坐标
# 读取图像并检测角点
for fname in image_files:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, pattern_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, pattern_size, corners2, ret)
我的经验:拍摄时,让棋盘格在画面中呈现不同的角度——倾斜、旋转、靠近、远离。至少拍 10-20 张,覆盖画面的各个区域。我曾经只拍了 5 张,结果标定出来的畸变系数偏差很大。
内参矩阵的计算
有了角点对应关系,接下来就是求解内参矩阵了。内参矩阵的形式是:
[fx 0 cx]
K = [ 0 fy cy]
[ 0 0 1]
其中 fx、fy 是焦距(以像素为单位),cx、cy 是主点坐标(通常接近图像中心)。
张正友法的求解过程,我简单概括一下:
- 对每张图像,计算单应性矩阵 H
- 利用 H 的约束条件,建立关于内参的线性方程组
- 通过 SVD 分解求解内参矩阵的初始值
- 用最大似然估计进行非线性优化,得到最终结果
嗯,这里要注意:单应性矩阵 H 有 8 个自由度,而每张图像提供 2 个约束。所以理论上至少需要 3 张图像才能求解全部内参。但实际中,我建议至少用 10 张以上,这样结果更稳定。
OpenCV 封装了这一切,你只需要调用:
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
畸变系数的计算
畸变主要分两种:径向畸变和切向畸变。径向畸变是镜头本身的光学特性造成的,表现为图像边缘的桶形或枕形变形。切向畸变则是镜头与成像平面不平行导致的。
张正友法通常考虑 5 个畸变系数:
| 参数 | 含义 | 典型范围 |
|---|---|---|
| k1 | 径向畸变系数 1 | -0.5 ~ 0.5 |
| k2 | 径向畸变系数 2 | -0.5 ~ 0.5 |
| p1 | 切向畸变系数 1 | -0.1 ~ 0.1 |
| p2 | 切向畸变系数 2 | -0.1 ~ 0.1 |
| k3 | 径向畸变系数 3(高阶) | -0.5 ~ 0.5 |
避坑指南:我曾经在标定一个广角相机时,发现 k3 的值异常大。后来排查发现,是因为标定图像没有覆盖画面的边缘区域。广角相机的畸变在边缘最明显,如果只拍中心区域,畸变系数根本算不准。
标定完成后,可以用 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)
知识体系总览
下面这张图,是我自己梳理的相机内参标定流程。你可以把它当作一个快速参考:
标定结果的质量评估
标定完成后,千万别急着用。我建议你做两件事来验证结果:
- 重投影误差:将世界坐标点投影到图像上,计算与检测到的角点之间的像素误差。一般小于 0.5 像素就算不错。
- 去畸变效果:观察去畸变后的图像,直线是否变直了?边缘区域是否还有明显的变形?
# 计算重投影误差
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("平均重投影误差: {:.3f} 像素".format(total_error / len(objpoints)))
一个小建议:如果重投影误差大于 1 个像素,先别急着调算法。检查一下标定板是不是平整?拍摄时有没有模糊?角点检测有没有漏检?很多时候,问题出在数据采集阶段。
好了,关于相机内参标定,核心内容就是这些。说白了,张正友法就是一个从图像到几何的桥梁——它把相机这个光学设备,变成了一个我们可以精确控制的数学模型。后面做点云投影、目标关联,全靠这个模型来对齐。
可可精品资料库