3、深度相机标定实战:棋盘格标定板制作、使用OpenCV进行单目/双目相机标定、标定结果评估与重投影误差分析
相机标定,说白了就是给相机做一次「视力矫正」。你想想看,镜头天生就有畸变,像素也不是完美的正方形,这些误差不消除,后面的三维重建全是白搭。我在做第一个深度相机项目时,就吃过这个亏——标定没做好,重建出来的物体像哈哈镜里的效果,歪歪扭扭的。
这一章,咱们就手把手把标定这件事做扎实。从做棋盘格开始,到单目、双目标定,再到评估结果,一条龙走完。
3.1 棋盘格标定板制作——别在这步偷懒
棋盘格是标定的「基准尺」。它的精度直接决定了标定质量。我个人习惯用A4纸打印,但有个坑:打印出来的尺寸和理论尺寸可能有偏差。
制作要点:
- 格子数: 内角点数量建议 9×6 或 11×8(宽×高)。奇数×偶数,这样棋盘有明确的方向性。
- 格子大小: 20mm~30mm 比较合适。太小了相机远了看不清,太大了近处拍不全。
- 材质: 打印后贴在硬纸板或亚克力板上,保证平整。我见过有人贴在海报上,结果标定板是弯的,标定结果直接废了。
- 保存: 打印多份,备着。标定过程中板子容易弄脏或折皱。
嗯,这里要强调一下:标定板不要太小。我曾经为了便携做了个很小的棋盘格,结果相机稍微远一点就检测不到角点了,白白浪费了半天时间。
3.2 单目标定——用OpenCV搞定内参和畸变
单目标定,就是求相机的内参矩阵和畸变系数。OpenCV 提供了 cv2.calibrateCamera(),但准备工作要做好。
你需要拍摄 15~20 张不同角度、不同距离的棋盘格照片。注意:
- 棋盘格要占画面 1/4 以上
- 角度要丰富:正对、倾斜、旋转、远近都要有
- 不要只拍中心区域,边缘也要覆盖到
核心代码长这样:
import cv2
import numpy as np
# 棋盘格内角点数量(宽,高)
pattern_size = (9, 6)
# 实际格子边长,单位mm
square_size = 25.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)
objp *= square_size
# 存储所有图片的角点
objpoints = [] # 3D点
imgpoints = [] # 2D点
images = [...] # 你的图片路径列表
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
objpoints.append(objp)
# 亚像素精确化
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 可视化
cv2.drawChessboardCorners(img, pattern_size, corners2, ret)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
findChessboardCorners 经常找不到角点,试试把图片转成灰度图后做一下直方图均衡化。我在光线不均匀的场景下用过,成功率提升不少。
3.3 双目标定——让两个相机「对齐」
双目标定比单目多了一步:求两个相机之间的旋转矩阵和平移向量。说白了,就是知道左眼和右眼之间的相对位置关系。
OpenCV 用 cv2.stereoCalibrate() 搞定。前提是:你已经完成了左右相机的单目标定。
# 假设已经得到左右相机的内参和畸变
ret, K1, D1, K2, D2, R, T, E, F = cv2.stereoCalibrate(
objpoints, # 世界坐标点
imgpoints_left, # 左图角点
imgpoints_right, # 右图角点
cameraMatrix1, # 左相机内参
distCoeffs1, # 左相机畸变
cameraMatrix2, # 右相机内参
distCoeffs2, # 右相机畸变
image_size,
criteria=criteria,
flags=cv2.CALIB_FIX_INTRINSIC
)
这里有个关键参数 flags。我建议第一次标定时用 cv2.CALIB_FIX_INTRINSIC,只优化外参。如果效果不好,再放开内参一起优化。
3.4 标定结果评估——重投影误差是硬指标
标定完怎么知道好不好?看重投影误差。简单说,就是把世界坐标系中的棋盘格角点,通过标定结果投影回图像,看看和实际检测到的角点差多少像素。
OpenCV 提供了 cv2.projectPoints() 来计算:
def compute_reprojection_error(objpoints, imgpoints, rvecs, tvecs, mtx, dist):
total_error = 0
total_points = 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
total_points += len(imgpoints2)
return total_error / len(objpoints)
mean_error = compute_reprojection_error(objpoints, imgpoints, rvecs, tvecs, mtx, dist)
print(f"平均重投影误差: {mean_error:.4f} 像素")
一般来说:
| 误差范围 | 评价 |
|---|---|
| < 0.3 像素 | 优秀,可以用于高精度重建 |
| 0.3 ~ 0.8 像素 | 良好,大多数场景够用 |
| > 0.8 像素 | 需要重新标定,检查图片质量 |
3.5 核心流程一览
我把整个标定流程画成了流程图,方便你对照着做:
3.6 避坑指南——我踩过的那些坑
标定这件事,看起来简单,做起来全是细节。我把自己踩过的坑列出来,你遇到了直接对照:
- 角点检测失败: 棋盘格反光、图片模糊、格子太小。解决办法:调整光照,用哑光纸打印,保证对焦清晰。
- 重投影误差大: 先检查标定板是否平整。我曾经用亚克力板夹着打印纸,结果板子本身有弧度,误差死活下不来。
- 双目标定后立体校正不对: 左右相机的图片没有同步。用双目相机自带的触发模式,或者后期手动筛选时间戳接近的图片对。
- 标定结果不稳定: 每次标定出来的内参都不一样。说明图片数量不够或者角度不够丰富。加到20张以上,覆盖画面各个角落。
好了,标定这块就讲到这里。你按照这个流程走一遍,基本不会出大问题。记住:标定是三维重建的基石,这块花的时间,后面都会加倍还给你。