相机标定完全指南
·
相机标定完全指南:内参标定、双目标定与相机-IMU 外参标定
摘要: 本文覆盖相机标定的完整知识链:从数学基础到 OpenCV 实现,涵盖单目内参标定(张正友法)、双目标定、相机-IMU 外参标定(Kalibr),附完整 Python 代码与常见问题排查。
适用读者:计算机视觉 / SLAM / 自动驾驶方向的开发者 | 难度:中级 | 预计阅读时间:30 分钟
文章目录
一、前言
相机镜头在制造和装配过程中不可避免地引入畸变,导致图像中的直线变弯曲、距离比例失真。标定的目标就是求解相机的内参(焦距、主点、畸变系数)和外参(相机之间的相对位姿),为后续的三维重建、立体匹配、SLAM 等任务提供精确的几何模型。
| 标定类型 | 目标 | 典型应用场景 |
|---|---|---|
| 内参标定 | 求焦距、主点、畸变系数 | 去畸变、三维重建 |
| 外参标定(双目) | 求两个相机之间的 R、T | 立体匹配、测距 |
| 外参标定(相机-IMU) | 求相机与 IMU 之间的 R、T | VIO、SLAM、自主导航 |
二、标定前的数学基础
2.1 相机模型(针孔模型)
三维世界点 P_w = [X, Y, Z]^T 到二维像素点 p = [u, v]^T 的投影关系:
s * [u, v, 1]^T = K * [R | T] * [X, Y, Z, 1]^T
其中内参矩阵 K:
K = [fx 0 cx]
[ 0 fy cy]
[ 0 0 1]
fx, fy:焦距(单位:像素)cx, cy:主点坐标(通常接近图像中心)
2.2 畸变模型
径向畸变(桶形/枕形):
x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)
切向畸变(装配偏差导致):
x_distorted = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_distorted = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]
其中 r^2 = x^2 + y^2,k1, k2, k3 为径向畸变系数,p1, p2 为切向畸变系数。
2.3 张正友标定法核心思想
- 假设标定板在 Z=0 的平面上,单应矩阵 H 描述图像与标定板平面的映射
- 利用 H 的约束条件建立关于内参的方程组
- 至少需要 3 张以上不同角度的标定图像
- 通过求解方程组 + 非线性优化(Levenberg-Marquardt)得到所有参数
三、单目相机内参标定
3.1 准备标定板
- 棋盘格(OpenCV 原生支持):建议 7x6 或 9x6 个内角点
- 圆点阵列(对称/非对称):OpenCV 也支持
- 打印后贴在平整的硬板上(弯曲会引入系统误差)
- 测量每个方格的物理边长(mm),记录备用
3.2 采集标定图像
- 固定相机焦距(手动对焦更佳,避免自动对焦改变内参)
- 拍摄 15~25 张有效图像
- 关键要求:
- 标定板在画面中的位置、角度、距离要尽量多样化
- 覆盖画面的各个区域(尤其是边缘,因为畸变主要在边缘)
- 标定板完整出现在画面中,图像清晰、光照均匀
3.3 完整标定流程
准备标定板 → 采集图像 → 检测角点 → 亚像素细化 → 求解参数 → 评估误差 → 保存结果
四、单目标定完整 Python 实现
4.1 项目目录结构
camera_calibration/
├── calibration_images/ # 标定图片目录
├── calibration_result.yaml # 标定结果
├── generate_board.py # 生成棋盘格
├── capture_images.py # 采集标定图片
├── calibrate.py # 执行标定
└── undistort.py # 畸变校正验证
4.2 生成棋盘格(可选)
import cv2
import numpy as np
size = 140 # 每个格子的像素边长
board_size = 10
canvas = np.zeros((size * board_size, size * board_size, 1), np.uint8)
for i in range(size * board_size):
for j in range(size * board_size):
if (int(i / size) + int(j / size)) % 2 != 0:
canvas[j, i] = 255
cv2.imwrite("chessboard.png", canvas)
print("棋盘格图片已生成 chessboard.png,打印并贴平后使用。")
4.3 采集标定图片
import cv2
import os
os.makedirs("./calibration_images", exist_ok=True)
camera = cv2.VideoCapture(0)
idx = 0
print("按 j 保存图片,按 q 退出")
while True:
ret, img = camera.read()
if not ret:
break
cv2.imshow("img", img)
key = cv2.waitKey(1) & 0xFF
if key == ord("j"):
idx += 1
path = f"./calibration_images/img{idx}.jpg"
cv2.imwrite(path, img)
print(f"已保存: {path}")
elif key == ord("q"):
break
camera.release()
cv2.destroyAllWindows()
4.4 执行标定
import numpy as np
import cv2
import glob
import yaml
import os
# ==================== 配置参数 ====================
CHESSBOARD_SIZE = (7, 6) # 内角点数量(列,行)
SQUARE_SIZE_MM = 25.0 # 方格边长(mm)
CALIB_DIR = "calibration_images"
RESULT_PATH = "calibration_result.yaml"
# ==================================================
def main():
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 1. 构造世界坐标点(Z=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_MM
objpoints = []
imgpoints = []
# 2. 遍历图片检测角点
images = glob.glob(os.path.join(CALIB_DIR, "*.jpg"))
if not images:
print(f"错误:{CALIB_DIR}/ 下未找到 .jpg 图片")
return
print(f"找到 {len(images)} 张图片,开始处理...")
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)
corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
imgpoints.append(corners)
cv2.drawChessboardCorners(img, CHESSBOARD_SIZE, corners, ret)
cv2.imshow("corners", img)
cv2.waitKey(300)
else:
print(f"警告:{os.path.basename(fname)} 未检测到角点,已跳过")
cv2.destroyAllWindows()
print(f"有效图片: {len(objpoints)} 张")
if len(objpoints) < 10:
print(f"警告:有效图片不足 10 张,标定可能不准确")
# 3. 执行标定
print("正在标定...")
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
# 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
mean_error = total_error / len(objpoints)
# 5. 保存结果
data = {
"camera_matrix": mtx.tolist(),
"dist_coefficients": dist.tolist(),
"reprojection_error": float(mean_error),
"chessboard_size": list(CHESSBOARD_SIZE),
"square_size_mm": SQUARE_SIZE_MM,
}
with open(RESULT_PATH, "w") as f:
yaml.dump(data, f, default_flow_style=None)
# 6. 打印结果
print(f"\n{'='*40}")
print(f"重投影误差 (RMS): {mean_error:.4f} 像素")
print(f"内参矩阵 K:\n{mtx}")
print(f"畸变系数 [k1,k2,p1,p2,k3]:\n{dist.ravel()}")
print(f"{'='*40}")
print(f"结果已保存至 {RESULT_PATH}")
if __name__ == "__main__":
main()
4.5 畸变校正验证
import cv2
import numpy as np
import yaml
with open("calibration_result.yaml", "r") as f:
calib = yaml.safe_load(f)
mtx = np.array(calib["camera_matrix"])
dist = np.array(calib["dist_coefficients"])
img = cv2.imread("your_distorted_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.imwrite("undistorted.jpg", dst)
print("校正完成 -> undistorted.jpg")
五、双目相机标定
5.1 原理
双目标定在单目标定的基础上,额外求解左右相机之间的旋转 R 和平移 T(即外参)。这样可以把两个相机的坐标系统一到一起,实现立体匹配和深度估计。
5.2 流程
左相机内参标定 -> 右相机内参标定 -> 双目联合标定 -> 得到 R, T
5.3 关键代码
import cv2
import numpy as np
# 假设 objpoints, imgpoints_l, imgpoints_r 已经准备好
# (左右相机同时拍摄同一组标定板,角点一一对应)
# 1. 分别标定左右相机
_, mtx_l, dist_l, rvecs_l, tvecs_l = cv2.calibrateCamera(
objpoints, imgpoints_l, img_size, None, None
)
_, mtx_r, dist_r, rvecs_r, tvecs_r = cv2.calibrateCamera(
objpoints, imgpoints_r, img_size, None, None
)
# 2. 双目联合标定
flags = cv2.CALIB_FIX_INTRINSIC # 固定已标定好的内参
ret, mtx_l, dist_l, mtx_r, dist_r, R, T, E, F = cv2.stereoCalibrate(
objpoints, imgpoints_l, imgpoints_r,
mtx_l, dist_l, mtx_r, dist_r,
img_size, criteria=criteria, flags=flags
)
print(f"双目基线平移 T (mm):\n{T}")
print(f"双目旋转矩阵 R:\n{R}")
# 3. 立体校正(可选,用于后续立体匹配)
R1, R2, P1, P2, Q, _, _ = cv2.stereoRectify(
mtx_l, dist_l, mtx_r, dist_r, img_size, R, T
)
六、相机-IMU 外参标定(以 Kalibr 为例)
6.1 核心思想
让相机和 IMU 同时观测同一个运动,然后通过优化找到它们之间的刚体变换关系(旋转 R + 平移 T)。
+----------+ +----------+
| IMU |<-- R,T -->| 相机 |
+----------+ +----------+
IMU 说:我测到设备向左转了 30 度
相机说:我看到标定板向右动了
算法说:根据两边的观测,你们之间差了 1 厘米平移、5 度旋转
6.2 准备工作
标定板
用 AprilTag 标定板(Kalibr 原生支持),每个 tag 的物理尺寸是已知的。
相机内参配置文件 camchain.yaml
cam0:
camera_model: pinhole
intrinsics: [fx, fy, cx, cy]
distortion_model: radial-tangential
distortion_coeffs: [k1, k2, p1, p2]
resolution: [width, height]
rostopic: /camera/image_raw
内参来自前面第四节的单目标定结果。
IMU 噪声参数文件 imu.yaml
imu0:
Model: calibrated
rostopic: /imu/data
update_rate: 200.0 # Hz
accelerometer:
noise_density: 0.01 # m/s^2/sqrt(Hz)(查 datasheet)
random_walk: 0.0002 # m/s^3/sqrt(Hz)
gyroscope:
noise_density: 0.000175 # rad/s/sqrt(Hz)
random_walk: 2.8e-06 # rad/s^2/sqrt(Hz)
T_i_b: !!opencv-matrix
rows: 4; cols: 4; dt: d
data: [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]
6.3 录制数据
# 固定好相机+IMU,录制两个 topic
rosbag record /camera/image_raw /imu/data -o calibration_data
数据采集要求:
| 要求 | 说明 |
|---|---|
| 充分旋转 | IMU 对旋转敏感,绕三轴快速旋转,精度最高 |
| 适当平移 | 帮助确定尺度因子 |
| 标定板在视野内 | 相机必须一直看到标定板 |
| 动作快速 | 保证 IMU 信号有足够信噪比 |
| 固定牢靠 | 相机和 IMU 之间不能有松动 |
推荐动作序列:
动作 1:绕各轴快速旋转(激发 IMU)
动作 2:沿各轴平移移动(确定尺度)
动作 3:保持标定板在相机视野中
6.4 运行 Kalibr
kalibr_calibrate_imu_camera \
--bag calibration_data.bag \
--cam camchain.yaml \
--imu imu.yaml \
--target april_6x6.yaml
6.5 解读结果
Kalibr 输出的变换矩阵描述的是 IMU -> 相机 的变换:
# IMU 到相机的旋转矩阵
extrinsicRotation: !!opencv-matrix
data: [0.9999, -0.0075, 0.0076, ...]
# IMU 到相机的平移向量
extrinsicTranslation: !!opencv-matrix
data: [-0.0082, 0.0109, -0.0021] # 单位:米
填入 VINS-Mono 等 VIO 系统:
extrinsicRotation: !!opencv-matrix
rows: 3; cols: 3; dt: d
data: [0.9999, -0.0075, 0.0076,
0.0075, 0.9999, 0.0032,
-0.0076, -0.0032, 0.9999]
extrinsicTranslation: !!opencv-matrix
rows: 3; cols: 1; dt: d
data: [-0.0082, 0.0109, -0.0021]
七、结果评估指标
| 指标 | 含义 | 合格标准 |
|---|---|---|
| 重投影误差 | 3D 点投影回 2D 的平均像素偏差 | < 0.5 像素(理想 < 0.3) |
| Kalibr 回环误差 | 优化收敛后的残差 | 越小越好,观察是否收敛 |
| 去畸变视觉检查 | 矫正后直线是否变直 | 直观判断 |
八、常见问题排查
8.1 内参标定
| 问题 | 原因 | 解决 |
|---|---|---|
| 角点检测失败 | 标定板不平、光照不均、图像模糊 | 检查打印质量,改善光照 |
| 重投影误差过大 | 有效图片太少、覆盖不够全面 | 增加图片数量和角度多样性 |
| 焦距 fx, fy 差距大 | 镜头安装倾斜 | 重新装配或接受这个结果 |
| 畸变校正后边缘拉伸严重 | k3 系数被固定了 | 启用 CALIB_RATIONAL_MODEL |
8.2 相机-IMU 外参标定
| 问题 | 后果 | 解决 |
|---|---|---|
| 晃动太慢 | IMU 信号弱,标不准 | 快速、大幅度旋转 |
| 标定板没在视野里 | 相机部分丢失观测 | 全程保持标定板可见 |
| 相机和 IMU 没固定牢 | 松动导致外参不是常数 | 硬性刚体连接 |
| 时间没同步 | 两边数据对不上 | 使用硬件同步或 NTP 对时 |
| IMU 噪声参数不准 | 优化权重错误 | 查准确的 datasheet 值 |
九、标定方法对比
| 方法 | 工具 | 输入 | 输出 | 适用场景 |
|---|---|---|---|---|
| 张正友标定法 | OpenCV | 棋盘格图片 | 相机内参 + 畸变 | 单目/双目内参 |
| 双目立体标定 | OpenCV | 左右同步图片 | 双目外参 R, T | 双目测距 |
| Kalibr | ROS | Rosbag(图像+IMU) | 相机-IMU 外参 R, T | VIO / SLAM |
| ALICE | C++ | 标定板图片序列 | 相机-激光雷达外参 | 激光雷达融合 |
十、标定结果应用速查
import numpy as np
import cv2
# 加载标定结果
K = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) # 内参
dist = np.array([k1, k2, p1, p2, k3]) # 畸变
R_c_i = np.array([...]) # IMU->相机 旋转 (3x3)
t_c_i = np.array([...]) # IMU->相机 平移 (3x1)
# 1. 去畸变
undistorted = cv2.undistort(image, K, dist)
# 2. 世界坐标 -> 像素坐标
pixel = K @ (R @ world_point + t)
# 3. IMU 坐标 -> 相机坐标
p_camera = R_c_i @ p_imu + t_c_i
总结
本文要点回顾:
- 标定的本质:求解相机的内参矩阵 K 和畸变系数,以及不同传感器之间的外参 R、T
- 张正友标定法:使用棋盘格标定板,至少 3 张不同角度的图像,推荐 15-25 张
- 重投影误差:是评估标定质量的核心指标,理想值 < 0.3 像素,合格值 < 0.5 像素
- 双目标定:在单目标定基础上,额外求解左右相机之间的 R、T
- 相机-IMU 标定:使用 Kalibr 工具,需要充分旋转、保持标定板可见、固定牢靠
- 标定结果应用:去畸变、坐标变换、立体匹配、VIO/SLAM 系统初始化
参考资料
更多推荐



所有评论(0)