第三节:相机标定实战——用OpenCV搞定棋盘格标定
好,咱们直接进入正题。
相机标定,说白了就是给内窥镜的“眼睛”做一次精准的视力检查。你得知道它看东西时,画面是怎么扭曲的,颜色是怎么偏移的。这一节,我就带你手把手用OpenCV完成棋盘格标定,拿到内参和畸变系数这两个核心参数。
3.1 为什么非得用棋盘格?
你可能要问:标定板那么多,为什么大家都用棋盘格?
原因其实很简单——棋盘格的角点检测算法非常成熟,OpenCV里直接封装好了。你想想看,黑白相间的格子,角点位置明确,亚像素精度也能做到很高。我在项目中试过圆点标定板,精度确实也不错,但棋盘格胜在稳定、好上手。
3.2 准备工作
你需要准备:
- 一张打印好的棋盘格标定板(建议 9×6 内角点,格子大小 20mm×20mm)
- 内窥镜或普通摄像头
- Python 3.8+ 和 OpenCV 4.x
嗯,这里要注意:棋盘格的内角点数量,指的是内部交叉点的个数。比如 9×6 的棋盘,内部角点就是 8×5。别搞反了,我第一次标定时就数错了,结果跑了半天数据全废了。
3.3 核心代码实现
咱们直接上代码。我会把每一步拆开讲清楚。
import cv2
import numpy as np
import glob
# 1. 准备棋盘格参数
chessboard_size = (8, 5) # 内角点数量
square_size = 20.0 # 格子边长,单位mm
# 2. 生成世界坐标系中的点
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)
objp *= square_size
# 3. 存储所有图像的点
objpoints = [] # 世界坐标系点
imgpoints = [] # 图像坐标系点
# 4. 读取标定图像
images = glob.glob('calib_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 5. 查找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
if ret:
objpoints.append(objp)
# 6. 亚像素精确化
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)
# 7. 可视化角点(可选)
cv2.drawChessboardCorners(img, chessboard_size, corners2, ret)
cv2.imshow('Corners', img)
cv2.waitKey(100)
cv2.destroyAllWindows()
# 8. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
3.4 代码逐段解析
这段代码我用了很多次,每次用都会微调。咱们挑几个关键点说说。
3.4.1 世界坐标点的生成
np.mgrid 这行代码,很多人第一次看会懵。其实它就是生成一个网格,每个交叉点对应一个 (x, y, 0) 坐标。z 轴设为0,因为标定板是平面的。我习惯把格子尺寸乘上去,这样内参里的焦距单位就是毫米,方便后续计算。
3.4.2 角点检测与亚像素优化
cv2.findChessboardCorners 返回的角点坐标是整数像素级别的。但实际应用中,我们需要亚像素精度。所以后面用 cv2.cornerSubPix 做一次精细化。这个步骤不能省,否则标定结果会偏。
3.5 标定结果解读
标定完成后,你会得到两个核心数据:
| 参数 | 含义 | 典型值(示例) |
|---|---|---|
| 内参矩阵 mtx | 包含焦距 fx, fy 和主点 cx, cy | [[fx, 0, cx], [0, fy, cy], [0, 0, 1]] |
| 畸变系数 dist | 包含 k1, k2, p1, p2, k3 | [k1, k2, p1, p2, k3] |
内参矩阵里的 fx 和 fy,理想情况下应该很接近。如果差太多,说明镜头可能有装配误差。畸变系数里,k1 和 k2 是径向畸变,p1 和 p2 是切向畸变。内窥镜的径向畸变通常很大,k1 可能是负的,表示桶形畸变。
3.6 保存与加载标定结果
标定一次,终身受用。我习惯把结果存成 .npz 文件,方便后续加载。
# 保存
np.savez('calibration_data.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
# 加载
data = np.load('calibration_data.npz')
mtx = data['mtx']
dist = data['dist']
这样下次直接用,不用重新标定。不过要注意,如果换了镜头或者调整了焦距,必须重新标定。
3.7 常见问题与避坑
- 角点检测失败: 检查棋盘格是否完整出现在画面中,光照是否均匀。我建议用漫射光源,避免强光直射。
- 标定误差大: 重投影误差超过 0.5 像素就算偏大了。这时候要检查图像数量是否足够,姿态是否多样。
- 畸变校正后图像边缘有黑边: 这是正常的,因为校正后图像需要重新映射。可以用
cv2.getOptimalNewCameraMatrix调整。
3.8 小结
这一节我们完成了棋盘格标定的完整流程。你拿到了内参和畸变系数,这是后续畸变校正和色彩还原的基础。下一节,我会带你用这些参数做实际的图像校正,看看效果到底怎么样。
记住:标定不是一次性的工作。每次设备有变动,都要重新标定。这是保证精度的底线。