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())
运行方式
- 拍摄 15~20 张不同角度、不同距离的棋盘格图像(覆盖画面边缘与中心),存入
calib_images/目录,支持.jpg或.png格式。 - 确保棋盘格在每张图像中完整可见,且内角点检测无遮挡。
- 执行脚本:
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是否与实际打印尺寸一致(用尺子量取多个格子取平均)。 - 标定结果不稳定:增加图像数量并覆盖画面四角与中心区域,避免所有图像中棋盘格都位于画面中央。