相机标定原理:标定板设计、张正友标定法、内参外参求解、畸变系数估计、OpenCV标定实践
相机标定,说白了就是给相机做一次「视力矫正」。
你想想看,我们拿到的每一张图像,其实都是三维世界在二维平面上的投影。但这个投影过程并不完美——镜头有畸变,传感器有安装误差,像素也不是完美的正方形。如果不做标定,你从图像里算出来的距离、角度、三维坐标,全都是错的。
我在做双目视觉项目时,第一次标定没做好,结果重建出来的三维点云整个是扭曲的。后来老老实实重新标了一遍,效果天差地别。嗯,标定这事,真不能偷懒。
标定板设计:棋盘格与圆点阵
标定板是标定的「基准尺」。它的作用很简单:提供一个已知尺寸、已知几何关系的图案,让相机去「对答案」。
最常见的标定板有两种:
- 棋盘格标定板:黑白相间的方格,角点检测简单可靠。我个人习惯用 9×6 或 10×7 的内角点数量,方格边长 20-30mm。
- 圆点阵标定板:圆形图案,圆心定位精度理论上比角点更高。但圆点存在透视变形,需要做椭圆拟合补偿。
关键参数:标定板必须平整,打印后贴在刚性背板上。我曾经用普通A4纸打印,贴在硬纸板上,结果标定出来的焦距误差超过 5%。后来换了亚克力板 + 高精度打印,误差降到 0.3% 以内。
设计标定板时,有几点要注意:
- 棋盘格必须是奇数×偶数个格子,这样内角点才是整数个
- 方格边长要精确测量,误差控制在 0.1mm 以内
- 标定板尺寸要覆盖相机视场的 1/3 到 2/3
- 拍摄时标定板要倾斜、旋转、平移,覆盖整个图像区域
张正友标定法:从单应矩阵到内参
张正友标定法,是计算机视觉领域最经典的标定方法之一。它的核心思想很简单:用多张不同姿态的标定板图像,通过单应矩阵约束,逐步求解相机内参和外参。
为什么需要多张图像?因为一张图像只能提供 8 个自由度(单应矩阵),而相机内参有 4 个(fx, fy, cx, cy),外参有 6 个(旋转 3 + 平移 3),总共 10 个未知数。一张图像不够,至少需要 2 张,但实际中我建议拍 15-20 张。
算法的核心步骤:
- 检测角点:在每张图像上提取棋盘格内角点坐标
- 计算单应矩阵:对每张图像,建立世界坐标到像素坐标的映射
- 求解内参:利用单应矩阵的约束条件,构建线性方程组,解出内参矩阵
- 求解外参:内参已知后,每张图像的外参就迎刃而解
- 非线性优化:用 Levenberg-Marquardt 算法对所有参数做全局优化
我的经验:张正友标定法对初始值不敏感,但如果你能提供合理的初始估计(比如用相机标称焦距),优化收敛会更快。我一般先用线性解算,再迭代 10-20 次就稳定了。
内参外参求解:相机矩阵与位姿
内参矩阵描述了相机内部的几何特性:
K = [fx 0 cx]
[ 0 fy cy]
[ 0 0 1]
其中 fx, fy 是焦距(以像素为单位),cx, cy 是主点坐标(通常接近图像中心)。
外参矩阵描述了相机在世界坐标系中的位置和朝向:
[R | t]
R 是 3×3 旋转矩阵,t 是 3×1 平移向量。
求解过程其实很直观:
- 先忽略畸变,用线性方法解出内参和外参的初始值
- 然后考虑畸变,用非线性优化同时优化所有参数
- 优化目标是最小化重投影误差——也就是角点检测位置和投影位置之间的像素距离
重投影误差是衡量标定质量的核心指标。我一般要求平均误差小于 0.3 像素,如果超过 0.5 像素,说明标定有问题,需要重新拍摄。
畸变系数估计:径向与切向畸变
镜头畸变主要分两种:
- 径向畸变:光线通过镜头边缘时弯曲,导致图像边缘向内(桶形)或向外(枕形)变形
- 切向畸变:镜头和传感器不平行,导致图像倾斜
OpenCV 使用 Brown 模型来描述畸变:
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
其中 k1, k2, k3 是径向畸变系数,p1, p2 是切向畸变系数。r 是像素到主点的距离。
注意:畸变系数不是越多越好。对于普通镜头,用 k1, k2, p1, p2 四个参数就够了。k3 只在鱼眼镜头或大广角镜头下才有意义。我曾经见过有人用 8 个畸变参数,结果过拟合,标定结果反而更差。
OpenCV标定实践:从代码到结果
OpenCV 提供了完整的标定工具链。下面是我常用的标定流程:
import cv2
import numpy as np
import glob
# 1. 准备棋盘格参数
chessboard_size = (9, 6) # 内角点数量
square_size = 25.0 # 方格边长(mm)
# 2. 生成世界坐标点
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
# 3. 存储所有图像的点
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)
# 4. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
# 5. 评估标定质量
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(f"平均重投影误差: {mean_error/len(objpoints):.3f} 像素")
这段代码我用了很多年,基本没大改过。有几个细节值得注意:
cornerSubPix这一步很关键,能把角点精度从像素级提升到亚像素级- 标定图像要覆盖视场的各个区域,尤其是边缘和角落
- 图像数量建议 15-20 张,太少容易过拟合,太多也没必要
避坑指南:我曾经用 5 张图像做标定,结果重投影误差只有 0.1 像素,但实际三维重建误差很大。后来发现是过拟合了——参数刚好拟合了这 5 张图像,但泛化能力很差。所以标定图像一定要多角度、多姿态,至少 15 张以上。
标定流程全景图
下面这张图展示了相机标定的完整流程,从标定板设计到最终输出内参、外参和畸变系数:
这张图把整个标定流程串起来了。你从左上角开始,沿着箭头走一遍,就能理解每个环节的输入输出关系。注意那个虚线反馈回路——如果重投影误差太大,就得回到图像采集步骤重新拍。
好了,相机标定的核心原理就这些。说白了,标定就是解一组方程,但工程实现中有很多细节需要注意。标定板要平、图像要多、角点要准、优化要稳——把这四点做好,标定结果基本不会差。
公众号:蓝海资料掘金营,微信deep3321