3、相机内参标定实战(OpenCV):使用 cv2.findChessboardCorners、cv2.calibrateCamera,标定结果评估。

好了,终于到了动手环节。前面讲了那么多理论,什么针孔模型、畸变参数,说白了都是为了这一刻——用 OpenCV 把相机的内参算出来。

我个人习惯,拿到一个新相机,第一件事就是做内参标定。为什么?因为后面的手眼标定、三维重建,全都依赖这套参数。内参不准,后面全是白搭。

3.1 标定板准备与图像采集

先说说标定板。OpenCV 最常用的是棋盘格,也就是 cv2.findChessboardCorners 要找的那种。

棋盘格规格建议:
  • 内角点数量:建议 9×6 或 11×8(不是格子数,是内部交点)
  • 格子大小:20mm~30mm 之间,打印后实测
  • 打印材质:哑光纸,贴在硬平板上

采集图像时,我踩过一个坑——拍了 50 张,结果全是正面朝向相机的。你想想看,这样算出来的畸变参数能准吗?

正确做法:让标定板在视野内做各种姿态——倾斜、旋转、远近、上下左右。我一般拍 15~20 张就够了,但要求覆盖整个画面。

采集要求 说明
数量 15~25 张
姿态变化 倾斜角 ±30°,旋转 360°
位置分布 覆盖画面四角和中心
光照 均匀,避免反光

3.2 核心函数:cv2.findChessboardCorners

这个函数的作用,就是从图像里找到棋盘格的内角点。说白了,就是让算法去识别那些黑白格子的交点。

import cv2
import numpy as np

# 棋盘格内角点数量
pattern_size = (9, 6)

# 读取图像
img = cv2.imread('calib_01.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 查找角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)

if ret:
    print(f"找到 {len(corners)} 个角点")
    # 绘制角点用于可视化检查
    cv2.drawChessboardCorners(img, pattern_size, corners, ret)
else:
    print("未找到棋盘格角点")
注意:pattern_size 是内角点数量,不是格子数量。9×6 的棋盘格,内角点是 8×5。我见过有人搞反了,找了半天找不到角点。

找到角点后,我建议做一步亚像素精细化。OpenCV 提供了 cv2.cornerSubPix,能把角点精度从像素级提升到亚像素级。

# 亚像素精细化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners_refined = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)

嗯,这一步很多人会跳过。但我在做高精度测量项目时发现,亚像素处理能让重投影误差降低 30% 以上。别省这一步。

3.3 核心函数:cv2.calibrateCamera

角点找完了,接下来就是算内参。OpenCV 的 cv2.calibrateCamera 会帮我们完成这件事。

它的输入有两部分:

  • 世界坐标系下的点:棋盘格上每个内角点的三维坐标,假设 Z=0
  • 图像坐标系下的点:上面找到的亚像素角点
# 准备世界坐标系点
objp = np.zeros((pattern_size[0] * pattern_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)
# 如果格子大小是 25mm,乘以 25
objp *= 25.0  # 单位:mm

# 收集所有图像的角点
objpoints = []  # 世界坐标系点列表
imgpoints = []  # 图像坐标系点列表

# 遍历所有图像...
# 假设已经找到 corners_refined
objpoints.append(objp)
imgpoints.append(corners_refined)

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

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)

输出结果中,mtx 是内参矩阵,包含 fx、fy、cx、cy。dist 是畸变系数,顺序是 k1、k2、p1、p2、k3。

内参矩阵示例:
[[532.15,   0.  , 320.5 ],
 [  0.  , 531.88, 240.3 ],
 [  0.  ,   0.  ,   1.  ]]

fx ≈ fy 说明像素是正方形的,cx、cy 接近图像中心,说明镜头安装基本对中。

3.4 标定结果评估

算完了,怎么知道结果好不好?我一般看三个指标。

3.4.1 重投影误差

这是最直接的指标。把世界坐标点通过算出来的内参和外参投影回图像,算算跟实际检测到的角点差多少像素。

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)
print(f"平均重投影误差: {mean_error:.4f} 像素")

我个人的经验值:

误差范围 评价
< 0.1 像素 优秀,高精度项目可用
0.1 ~ 0.3 像素 良好,大多数场景够用
0.3 ~ 0.5 像素 一般,需要检查标定板或图像质量
> 0.5 像素 较差,建议重新采集数据

3.4.2 畸变校正效果

用算出来的畸变系数对图像做校正,看看直线是不是变直了。

# 畸变校正
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

# 对比原图和校正图
cv2.imshow('Original', img)
cv2.imshow('Undistorted', dst)

我曾经遇到一个案例,重投影误差只有 0.08 像素,但校正后的图像边缘还是有明显弯曲。后来发现是标定板本身不平整。所以,视觉检查也很重要。

3.4.3 参数合理性检查

最后,看看算出来的参数是不是符合物理常识:

  • fx、fy:应该接近理论值。比如传感器像素大小 3.45μm,焦距 12mm,那 fx ≈ 12 / 0.00345 ≈ 3478
  • cx、cy:应该在图像中心附近,偏差超过 50 像素就要警惕
  • 畸变系数:k1 通常在 -0.5~0.5 之间,太大或太小都不正常
避坑指南:如果标定结果中 fx 和 fy 差异超过 5%,或者 cx、cy 严重偏离中心,先别急着用。检查一下图像采集时标定板是否倾斜太厉害,或者有没有误标。

3.5 完整标定流程总结

来,我把整个流程串起来,画张图帮你理清思路。

相机内参标定完整流程 准备标定板 采集多姿态图像 findChessboardCorners cornerSubPix 精细化 calibrateCamera 标定 结果评估与验证 关键参数 • 内角点数量:9×6 • 格子大小:25mm • 图像数量:15~20张 评估指标 • 重投影误差 < 0.3px • 畸变校正后直线平直 • fx/fy 符合物理计算 • cx/cy 接近图像中心

这张图把整个流程串起来了。从准备标定板开始,到采集图像、找角点、精细化、标定计算,最后评估验证。每一步都有坑,每一步也都有技巧。

我个人建议,第一次做标定的时候,把每一步的中间结果都打印出来看看。角点找没找对?重投影误差多少?校正后的图像什么样?这些可视化检查,比只看数字靠谱得多。

我的习惯:标定完成后,把内参矩阵和畸变系数保存成 YAML 或 JSON 文件。下次用的时候直接加载,不用重新标定。除非换了镜头或改变了焦距。

好了,内参标定就讲到这里。记住,标定不是一次性的工作。相机在使用过程中,如果受到震动或温度变化,内参可能会漂移。定期重新标定,是个好习惯。