1. 相机标定基础

相机标定基础 学习卡片

任务说明

本案例使用棋盘格标定板对单目相机完成内参标定。输入为不同位姿下拍摄的棋盘格图像(或直接使用下方生成的模拟图像),输出为相机内参矩阵(焦距 fx、fy,主点 cx、cy)、畸变系数(k1、k2、p1、p2、k3),并通过重投影误差评估标定质量。标定结果可直接用于后续手眼标定与视觉抓取案例。

标定原理与数据准备

采用OpenCV的cv2.calibrateCamera实现张正友标定法。核心流程为:提取棋盘格角点→亚像素细化→多视图对应点求解内参→非线性优化畸变系数。为便于独立复现,本案例提供两种图像来源:方式A使用真实相机拍摄的15张棋盘格图像(需自行采集,棋盘格规格为9×6内角点,方格边长25mm);方式B使用下方代码自动生成15张不同位姿的模拟棋盘格图像,无需真实相机即可完整跑通标定流程并验证算法正确性。

单目相机内参标定流程 标定板图像×15 角点提取 findChessboardCorners cornerSubPix 内参标定 calibrateCamera 内参+畸变 误差评估 重投影误差 输出结果 内参矩阵 K = [fx 0 cx; 0 fy cy; 0 0 1] 畸变系数 D = [k1, k2, p1, p2, k3] 重投影误差 RMS(像素)

完整实现代码

以下代码同时支持方式A(真实图像)与方式B(模拟图像)。默认运行方式B,自动生成15张640×480的模拟棋盘格图像并完成标定。若使用真实相机,将use_simulated设为False并修改image_dir路径即可。

import cv2
import numpy as np
import os

# ========== 配置参数 ==========
use_simulated = True          # True: 使用模拟图像; False: 使用真实拍摄图像
image_dir = "./calib_images"  # 真实图像目录(use_simulated=False时有效)
chessboard_size = (9, 6)      # 内角点数 (列, 行)
square_size = 25.0            # 方格边长(毫米)
num_images = 15               # 模拟图像数量
img_size = (640, 480)         # 模拟图像尺寸

# ========== 方式B:生成模拟棋盘格图像 ==========
def generate_simulated_images():
    """生成不同位姿的模拟棋盘格图像,返回图像列表"""
    images = []
    # 模拟内参(用于生成图像,标定结果应接近此值)
    true_K = np.array([[520.0, 0, 320.0],
                       [0, 520.0, 240.0],
                       [0, 0, 1.0]])
    true_dist = np.array([-0.2, 0.05, 0.0, 0.0, 0.01])
    
    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) * square_size
    
    for i in range(num_images):
        # 随机位姿(旋转向量 + 平移向量)
        rvec = np.array([np.random.uniform(-0.3, 0.3),
                         np.random.uniform(-0.3, 0.3),
                         np.random.uniform(-0.2, 0.2)])
        tvec = np.array([np.random.uniform(-50, 50),
                         np.random.uniform(-50, 50),
                         np.random.uniform(400, 600)])
        # 投影到图像平面
        imgpts, _ = cv2.projectPoints(objp, rvec, tvec, true_K, true_dist)
        imgpts = imgpts.reshape(-1, 2)
        
        # 绘制棋盘格图像
        img = np.full((img_size[1], img_size[0]), 255, dtype=np.uint8)
        # 计算每个角点对应的棋盘格方块并填充
        for row in range(chessboard_size[1]):
            for col in range(chessboard_size[0]):
                idx = row * chessboard_size[0] + col
                x, y = imgpts[idx]
                # 以角点为基准绘制小方块(简化绘制,仅用于角点检测)
                pts = np.array([[x-12, y-12], [x+12, y-12], 
                                [x+12, y+12], [x-12, y+12]], dtype=np.int32)
                if (row + col) % 2 == 0:
                    cv2.fillPoly(img, [pts], 0)
        images.append(img)
    return images

# ========== 主标定流程 ==========
def calibrate_camera(images):
    """输入棋盘格图像列表,返回内参、畸变系数和重投影误差"""
    # 准备对象点(棋盘格角点的3D坐标)
    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) * square_size
    
    objpoints = []  # 3D点
    imgpoints = []  # 2D点
    valid_images = []
    
    for img in images:
        gray = img if len(img.shape) == 2 else 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)
            corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
            objpoints.append(objp)
            imgpoints.append(corners2)
            valid_images.append(img)
    
    if len(objpoints) < 3:
        raise RuntimeError(f"有效图像不足({len(objpoints)}/3),请调整棋盘格尺寸或图像质量")
    
    # 执行标定
    ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
        objpoints, imgpoints, gray.shape[::-1], None, None)
    
    # 计算重投影误差
    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)
    
    return mtx, dist, mean_error, len(objpoints)

# ========== 执行标定 ==========
if use_simulated:
    print("正在生成模拟棋盘格图像...")
    images = generate_simulated_images()
    print(f"已生成 {len(images)} 张模拟图像")
else:
    # 方式A:读取真实图像
    images = []
    for fname in sorted(os.listdir(image_dir)):
        if fname.endswith(('.jpg', '.png', '.bmp')):
            img = cv2.imread(os.path.join(image_dir, fname))
            if img is not None:
                images.append(img)
    print(f"从目录加载 {len(images)} 张图像")

# 执行标定
mtx, dist, rms_error, used_count = calibrate_camera(images)

# ========== 输出结果 ==========
print(f"\n有效标定图像数: {used_count}")
print(f"重投影误差: {rms_error:.4f} 像素")
print("\n内参矩阵 K:")
print(mtx)
print("\n畸变系数 [k1, k2, p1, p2, k3]:")
print(dist.ravel())

运行方式与依赖

需要安装Python 3.8+、OpenCV(pip install opencv-python)和NumPy。将代码保存为camera_calibration.py,直接运行python camera_calibration.py即可。默认使用模拟图像,无需额外数据文件。

验证方法与判定标准

运行代码后观察以下输出:

  • 有效标定图像数:应≥3,模拟模式下通常为15;
  • 重投影误差:模拟模式下应小于0.1像素(因图像无噪声且位姿理想);真实图像标定通常要求小于0.5像素,若超过1像素需检查角点提取质量或剔除异常图像;
  • 内参合理性:模拟模式下fx、fy应接近520,主点接近(320,240);真实相机则与传感器分辨率及镜头焦距匹配。

判定标定通过的标准:重投影误差RMS小于0.5像素(真实图像)或小于0.1像素(模拟图像),且内参数值在合理物理范围内。若误差过大,可增加图像数量、改变标定板位姿多样性(倾斜、旋转、远近变化)后重新标定。