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("未找到棋盘格角点")
找到角点后,我建议做一步亚像素精细化。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 之间,太大或太小都不正常
3.5 完整标定流程总结
来,我把整个流程串起来,画张图帮你理清思路。
这张图把整个流程串起来了。从准备标定板开始,到采集图像、找角点、精细化、标定计算,最后评估验证。每一步都有坑,每一步也都有技巧。
我个人建议,第一次做标定的时候,把每一步的中间结果都打印出来看看。角点找没找对?重投影误差多少?校正后的图像什么样?这些可视化检查,比只看数字靠谱得多。
好了,内参标定就讲到这里。记住,标定不是一次性的工作。相机在使用过程中,如果受到震动或温度变化,内参可能会漂移。定期重新标定,是个好习惯。