相机标定完全指南:内参标定、双目标定与相机-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^2k1, k2, k3 为径向畸变系数,p1, p2 为切向畸变系数。

2.3 张正友标定法核心思想

  1. 假设标定板在 Z=0 的平面上,单应矩阵 H 描述图像与标定板平面的映射
  2. 利用 H 的约束条件建立关于内参的方程组
  3. 至少需要 3 张以上不同角度的标定图像
  4. 通过求解方程组 + 非线性优化(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

总结

本文要点回顾:

  1. 标定的本质:求解相机的内参矩阵 K 和畸变系数,以及不同传感器之间的外参 R、T
  2. 张正友标定法:使用棋盘格标定板,至少 3 张不同角度的图像,推荐 15-25 张
  3. 重投影误差:是评估标定质量的核心指标,理想值 < 0.3 像素,合格值 < 0.5 像素
  4. 双目标定:在单目标定基础上,额外求解左右相机之间的 R、T
  5. 相机-IMU 标定:使用 Kalibr 工具,需要充分旋转、保持标定板可见、固定牢靠
  6. 标定结果应用:去畸变、坐标变换、立体匹配、VIO/SLAM 系统初始化

参考资料

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐