四、相机标定实战:使用OpenCV进行棋盘格标定
相机标定,说白了就是给相机做一次「视力矫正」。
你想想看,每台相机出厂时都有点小毛病——镜头畸变、焦距偏差、光心偏移。如果不做标定,你从三维扫描仪里拿到的点云数据,就像戴着散光眼镜看世界,全是变形的。
我个人习惯把标定分成三步走:拍棋盘 → 算参数 → 验结果。今天我们就用OpenCV把这套流程走一遍。
核心知识点
- 棋盘格标定的原理与流程
- OpenCV的cv2.findChessboardCorners()和cv2.calibrateCamera()
- 重投影误差的计算与评估
- 标定结果的保存与加载
4.1 准备工作:棋盘格与拍摄
棋盘格标定板,你可以打印出来贴在平板上。我建议用10×7的内角点棋盘(也就是9×6的格子),尺寸大一点,角点更容易被检测到。
我的经验:棋盘格一定要贴平!我曾经用亚克力板夹着打印纸,结果纸面有轻微褶皱,标定出来的畸变参数全是错的。后来换了3mm厚的铝基板,问题才解决。
拍摄时注意几点:
- 至少拍15-20张,覆盖不同角度和距离
- 棋盘格要占画面30%-70%
- 避免过度倾斜(超过45度角点检测容易失败)
- 光照均匀,不要有强反光
4.2 核心代码:角点提取与标定
直接上代码。这段代码我用了很多项目,基本是标定的「标准模板」。
import cv2
import numpy as np
import glob
# 棋盘格参数
CHECKERBOARD = (9, 6) # 内角点数
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
# 世界坐标系中的棋盘格角点坐标
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
objpoints = [] # 3D点
imgpoints = [] # 2D点
images = glob.glob('calib_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
if ret:
objpoints.append(objp)
corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
imgpoints.append(corners2)
# 可视化角点
cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
cv2.imshow('Corners', img)
cv2.waitKey(100)
cv2.destroyAllWindows()
# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("相机内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
注意:cv2.findChessboardCorners()对图像分辨率敏感。如果棋盘格太小,可以先用cv2.resize()放大图像再检测。我遇到过1080p的图检测不到,缩放到2倍就成功了。
4.3 标定结果评估:重投影误差
标定完怎么知道准不准?看重投影误差。
原理很简单:用标定出的参数,把3D角点投影回图像平面,算算和实际检测到的2D角点差了多少像素。
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)
mean_error = total_error / len(objpoints)
return mean_error
reprojection_error = compute_reprojection_error(
objpoints, imgpoints, rvecs, tvecs, mtx, dist
)
print(f"平均重投影误差: {reprojection_error:.4f} 像素")
误差多少算好?我个人经验:
| 误差范围 | 评价 | 建议 |
|---|---|---|
| < 0.3 像素 | 优秀 | 可以直接用于高精度三维扫描 |
| 0.3 - 0.8 像素 | 良好 | 大多数场景可用,检查是否有系统偏差 |
| 0.8 - 1.5 像素 | 一般 | 建议重新拍摄,检查棋盘格是否平整 |
| > 1.5 像素 | 差 | 标定失败,检查代码或硬件 |
避坑指南:我曾经遇到一次标定,误差只有0.15像素,但实际三维重建效果很差。后来发现是因为所有棋盘格图像都集中在画面中央,边缘的畸变根本没被约束到。所以拍摄时一定要让棋盘格出现在画面的各个角落。
4.4 标定文件保存与加载
标定完的参数要存下来,不然每次启动都重新标定,太傻了。
我习惯用XML格式保存,因为可读性好,也方便其他语言调用。
import cv2
# 保存标定结果
fs = cv2.FileStorage('camera_calib.xml', cv2.FILE_STORAGE_WRITE)
fs.write('camera_matrix', mtx)
fs.write('dist_coeffs', dist)
fs.write('reprojection_error', reprojection_error)
fs.release()
print("标定文件已保存到 camera_calib.xml")
加载时也很简单:
# 加载标定结果
fs = cv2.FileStorage('camera_calib.xml', cv2.FILE_STORAGE_READ)
mtx_loaded = fs.getNode('camera_matrix').mat()
dist_loaded = fs.getNode('dist_coeffs').mat()
error_loaded = fs.getNode('reprojection_error').real()
fs.release()
print("加载完成,重投影误差:", error_loaded)
小技巧:除了XML,你也可以用np.savez()保存为.npz格式,加载更快。但XML的好处是可以用文本编辑器打开查看,调试时很方便。
4.5 实战中的常见问题
做标定这么多年,我总结了几条高频踩坑点:
- 角点检测失败:试试调整cv2.findChessboardCorners()的flags参数,或者用自适应阈值预处理图像
- 标定结果不稳定:每次运行结果不一样?说明图像数量不够或角度变化太小,加到30张试试
- 畸变校正后图像边缘扭曲:这是正常的,因为镜头边缘畸变最大。可以适当裁剪边缘区域
- 重投影误差小但实际效果差:检查棋盘格是否变形,或者相机是否在标定过程中被碰过
嗯,到这里标定的核心流程就走通了。你拿着这套代码,配合一块打印的棋盘格,半小时内就能完成一次完整的相机标定。记住,标定不是一次性的活——换镜头、调焦距、甚至温度变化都可能需要重新标定。
公众号:蓝海资料掘金营,微信deep3321