从棋盘格到内参矩阵:OpenCV相机标定实战指南

当你第一次尝试用OpenCV进行相机标定时,是否遇到过这些困惑:为什么fx和fy不一样?棋盘格到底要拍多少张才够?亚像素优化真的有必要吗?本文将用一套完整的Python代码流程,带你避开新手常踩的坑,真正理解相机标定背后的原理。

1. 准备工作:棋盘格与拍摄技巧

1.1 棋盘格的选择与打印

棋盘格是相机标定的基础工具,但很多人在第一步就犯了错。关键不在于棋盘格本身,而在于内角点的数量。如果你使用11x8的棋盘格,实际内角点是10x7(行列各减1)。这个数字需要在代码中明确指定。

推荐使用calib.io生成棋盘格:

  • 选择checkerboard类型
  • 设置rows和columns为实际棋盘格的行列数
  • checker width设置每个格子的实际物理尺寸(如2mm)

打印时务必选择100%比例,并用游标卡尺测量实际打印尺寸是否准确。我曾遇到因打印机自动缩放导致标定失败的案例,最终发现打印出的格子实际为1.95mm而非标称的2mm。

1.2 拍摄技巧与常见误区

拍摄棋盘格照片时,新手常犯三个错误:

  1. 拍摄角度单一:所有照片都是正面拍摄,缺乏视角变化
  2. 数量不足:通常需要15-20张不同角度的照片
  3. 模糊或过曝:确保棋盘格线条清晰可见

实际操作建议:

  • 将棋盘格平放在桌面,从不同高度和角度拍摄
  • 尝试倾斜棋盘格(30°-60°)
  • 确保棋盘格占画面1/3以上面积
  • 避免强光反射导致的过曝
# 示例:批量读取棋盘格图像
image_files = glob.glob('calibration_images/*.jpg')
print(f"找到{len(image_files)}张标定图片")

2. 角点检测与亚像素优化

2.1 基础角点检测

OpenCV提供了多种角点检测方法,findChessboardCornersSB是较新且稳定的选择:

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCornersSB(gray, (w, h), None)
if ret:
    cv2.drawChessboardCorners(img, (w, h), corners, ret)
    cv2.imshow('Corners', img)
    cv2.waitKey(500)

常见问题排查:

  • 如果返回ret=False,检查棋盘格是否完整可见
  • 确认输入的(w, h)是内角点数量而非棋盘格格子数
  • 尝试调整图像对比度或使用直方图均衡化

2.2 亚像素优化的必要性

原始角点坐标是整数像素级的,亚像素优化可以将精度提升到0.1像素级别。这对于高精度应用(如线激光测量)至关重要:

criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)

参数说明:

  • (11, 11):搜索窗口大小
  • (-1, -1):死区半径(-1表示自动计算)
  • criteria:最大迭代30次或精度达到0.001时停止

3. 相机标定核心流程

3.1 世界坐标系的建立

标定需要建立棋盘格的三维世界坐标。假设棋盘格在Z=0平面,每个格子大小为2mm:

objp = np.zeros((w*h, 3), np.float32)
objp[:, :2] = np.mgrid[0:w, 0:h].T.reshape(-1, 2) * 2.0  # 2mm格子

3.2 执行标定计算

收集足够多的3D-2D点对应后,调用calibrateCamera

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None
)

输出参数解读:

  • mtx:内参矩阵 [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]
  • dist:畸变系数 [k1, k2, p1, p2, k3]
  • rvecs, tvecs:每张图片的旋转和平移向量

3.3 为什么fx和fy不同?

理想情况下fx=fy,但实际中可能因以下原因不同:

  1. 像素不是完美的正方形
  2. 镜头存在非对称畸变
  3. 图像传感器安装倾斜

两者差异通常在1%以内。如果差异过大(如超过5%),可能需要检查标定过程是否正确。

4. 结果验证与应用

4.1 重投影误差分析

重投影误差是评估标定质量的关键指标:

mean_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)
    mean_error += error
print(f"平均重投影误差: {mean_error/len(objpoints):.2f}像素")

误差小于0.5像素通常认为标定质量良好。

4.2 内参矩阵的实际应用

内参矩阵用于将像素坐标转换为归一化相机坐标:

def pixel_to_normalized(u, v, mtx):
    fx, fy = mtx[0, 0], mtx[1, 1]
    cx, cy = mtx[0, 2], mtx[1, 2]
    x = (u - cx) / fx
    y = (v - cy) / fy
    return (x, y)

这个转换是许多高级应用(如线激光平面标定)的基础。例如,在激光三角测量中,我们需要将激光线上的像素点转换为三维空间中的射线方向。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐