2. 相机标定

任务目标

使用棋盘格标定板对单目相机进行内参标定,获取相机内参矩阵 K 与畸变系数 D(k1, k2, p1, p2, k3),输出可用于后续VIO前端去畸变与投影的标定结果文件。

环境与依赖

  • Ubuntu 20.04 / 22.04,已安装 ROS(Noetic / Humble)或纯 Python 环境
  • 相机驱动可正常发布 /camera/image_raw(或本地图像序列)
  • Python 依赖:opencv-python、numpy、glob
  • 标定板:棋盘格(如 8×6 内角点,方格边长 30mm),打印后贴于平面硬板上

实现方案

本案例采用 OpenCV 的 findChessboardCorners + calibrateCamera 完成标定,不依赖 ROS 节点,直接读取本地图像目录。核心流程:读取图像 → 检测内角点 → 亚像素细化 → 全局标定 → 输出内参与畸变系数,并以重投影误差作为精度判定依据。

关键完整代码

import cv2
import numpy as np
import glob

# ===== 配置 =====
CHESSBOARD_SIZE = (8, 6)   # 内角点数 (列, 行)
SQUARE_SIZE = 0.030        # 方格边长,单位:米
IMAGE_DIR = "calib_images/"  # 存放标定图像的目录
OUTPUT_FILE = "camera_calib.npz"

# ===== 准备对象点 =====
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

obj_points = []   # 3D 点(世界坐标)
img_points = []   # 2D 点(像素坐标)
valid_images = []

# ===== 遍历图像并提取角点 =====
images = sorted(glob.glob(IMAGE_DIR + "*.jpg") + glob.glob(IMAGE_DIR + "*.png"))
if len(images) < 10:
    raise ValueError(f"至少需要 10 张有效图像,当前仅找到 {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:
        criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
        corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        obj_points.append(objp)
        img_points.append(corners_refined)
        valid_images.append(fname)
        # 可视化角点(可选)
        vis = cv2.drawChessboardCorners(img.copy(), CHESSBOARD_SIZE, corners_refined, ret)
        cv2.imshow("Corners", vis)
        cv2.waitKey(100)

cv2.destroyAllWindows()
print(f"成功提取角点的图像数: {len(valid_images)} / {len(images)}")

if len(valid_images) < 10:
    raise RuntimeError("有效图像不足 10 张,请调整拍摄角度或光照后重试")

# ===== 执行标定 =====
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    obj_points, img_points, gray.shape[::-1], None, None
)

# ===== 重投影误差 =====
total_error = 0.0
for i in range(len(obj_points)):
    proj_points, _ = cv2.projectPoints(obj_points[i], rvecs[i], tvecs[i], mtx, dist)
    error = cv2.norm(img_points[i], proj_points, cv2.NORM_L2) / len(proj_points)
    total_error += error
mean_error = total_error / len(obj_points)

# ===== 保存结果 =====
np.savez(OUTPUT_FILE, mtx=mtx, dist=dist, mean_error=mean_error)
print(f"标定结果已保存至: {OUTPUT_FILE}")
print(f"平均重投影误差: {mean_error:.4f} px")
print("内参矩阵 K:")
print(mtx)
print("畸变系数 D (k1, k2, p1, p2, k3):")
print(dist.ravel())

运行方式

  1. 拍摄 15~20 张不同角度、不同距离的棋盘格图像(覆盖画面边缘与中心),存入 calib_images/ 目录,支持 .jpg 或 .png 格式。
  2. 确保棋盘格在每张图像中完整可见,且内角点检测无遮挡。
  3. 执行脚本:python3 calibrate_camera.py

验证方法与通过条件

  • 角点提取数量:终端输出“成功提取角点的图像数”,应不少于 10 张,否则需补拍。
  • 重投影误差:平均重投影误差 mean_error 应小于 0.5 px。若大于 1.0 px,说明图像模糊、棋盘格不平整或角点检测有误,需重新拍摄。
  • 结果文件:生成 camera_calib.npz,可用以下代码快速验证去畸变效果:
import cv2
import numpy as np

data = np.load("camera_calib.npz")
mtx, dist = data["mtx"], data["dist"]

img = cv2.imread("calib_images/sample.jpg")  # 任选一张标定图
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
undistorted = cv2.undistort(img, mtx, dist, None, newcameramtx)
cv2.imwrite("undistorted_sample.jpg", undistorted)
print("去畸变结果已保存: undistorted_sample.jpg")

观察输出图像:画面边缘的直线(如门框、墙线)应恢复为直线,无明显桶形或枕形畸变残留。

常见问题

  • 角点检测失败:增大图像亮度对比度、避免反光、减小拍摄倾角。
  • 重投影误差偏大:检查 SQUARE_SIZE 是否与实际打印尺寸一致(用尺子量取多个格子取平均)。
  • 标定结果不稳定:增加图像数量并覆盖画面四角与中心区域,避免所有图像中棋盘格都位于画面中央。