4. 相机标定实战:使用calibrateCamera计算内参和畸变系数
好,到了真正动手的环节了。
前面我们聊了那么多理论,什么针孔模型、畸变模型、棋盘格检测……说实话,这些知识如果不落地跑一遍,你很快就会忘掉。我自己带新人时最深的体会就是:标定这件事,看十遍不如亲手跑一次。
这一节,我们就用OpenCV的calibrateCamera函数,把内参矩阵和畸变系数算出来。我会带着你一步步走完整个流程,顺便聊聊我在项目中踩过的坑。
4.1 标定的核心流程
先给你一张流程图,把整个标定逻辑串起来。你心里有个谱,后面看代码就不会晕。
说白了,标定就是做一件事:找到真实世界中的点,和图像上的点之间的映射关系。calibrateCamera就是帮我们解这个映射的数学工具。
4.2 准备数据:世界坐标与图像坐标
在调用calibrateCamera之前,我们需要准备两组数据:
- objpoints:棋盘格上每个角点的世界坐标(单位:毫米)
- imgpoints:这些角点在图像上的像素坐标
这里有个关键点——世界坐标怎么定?
我习惯把棋盘格左上角第一个内角点定为原点(0,0,0),然后沿着棋盘格的行和列方向,依次增加。比如一个9×6的棋盘格(内角点),每个格子边长25mm,那么世界坐标就是:
import numpy as np
# 棋盘格参数
pattern_size = (9, 6) # 内角点数量 (列, 行)
square_size = 25.0 # 每个格子边长,单位mm
# 构建世界坐标
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
print(objp[:5])
# 输出:
# [[ 0. 0. 0.]
# [25. 0. 0.]
# [50. 0. 0.]
# [75. 0. 0.]
# [100. 0. 0.]]
4.3 角点检测与存储
接下来,我们要从每张图片中提取角点。OpenCV提供了findChessboardCorners,用起来很简单:
import cv2
import glob
# 存储所有图片的角点
objpoints = [] # 世界坐标
imgpoints = [] # 图像坐标
# 读取所有标定图片
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, pattern_size, None)
if ret:
objpoints.append(objp)
imgpoints.append(corners)
# 可视化角点(可选)
cv2.drawChessboardCorners(img, pattern_size, corners, ret)
cv2.imshow('Corners', img)
cv2.waitKey(100)
cv2.destroyAllWindows()
嗯,这里要注意几个细节:
- 图片数量:我建议至少拍15-20张。太少的话,标定结果不稳定。
- 角度变化:让棋盘格出现在画面的各个位置——左上、右下、中间、倾斜……覆盖整个视场。
- 距离变化:远近都要拍。我见过有人只拍一个距离,结果标定出来的畸变系数完全不对。
4.4 核心函数:calibrateCamera
数据准备好了,终于轮到主角登场了。
# 图像尺寸(用于初始化内参矩阵)
img_size = gray.shape[::-1] # (width, height)
# 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints,
imgpoints,
img_size,
None,
None
)
print("内参矩阵 (Camera Matrix):")
print(mtx)
print("\n畸变系数 (Distortion Coefficients):")
print(dist)
输出结果长这样:
内参矩阵 (Camera Matrix):
[[1.532e+03 0.000e+00 9.600e+02]
[0.000e+00 1.528e+03 5.400e+02]
[0.000e+00 0.000e+00 1.000e+00]]
畸变系数 (Distortion Coefficients):
[[-0.321 0.108 -0.002 0.001 0.000]]
我来解释一下这些数字的含义:
| 参数 | 含义 | 示例值 |
|---|---|---|
| fx, fy | 焦距(像素单位) | 1532, 1528 |
| cx, cy | 主点坐标(图像中心) | 960, 540 |
| k1, k2 | 径向畸变系数 | -0.321, 0.108 |
| p1, p2 | 切向畸变系数 | -0.002, 0.001 |
| k3 | 径向畸变第三项(通常为0) | 0.000 |
- fx和fy越接近,说明镜头在x和y方向的放大率越一致。
- cx和cy越接近图像中心,说明镜头安装越正。
- k1为负值表示桶形畸变(广角镜头常见),正值表示枕形畸变(长焦镜头常见)。
4.5 评估标定质量
标定完了,怎么知道结果好不好?
calibrateCamera返回的ret就是重投影误差(reprojection error)。这个值越小越好,通常小于0.5像素就算不错了。
print(f"重投影误差: {ret:.4f} 像素")
# 输出示例:重投影误差: 0.1234 像素
你也可以手动计算每张图片的重投影误差:
mean_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)
mean_error += error
print(f"平均重投影误差: {mean_error/len(objpoints):.4f} 像素")
- 角点检测是否准确?有没有漏检或误检?
- 棋盘格是否平整?有没有弯曲?
- 图片数量是否足够?角度覆盖是否全面?
4.6 保存标定结果
标定结果来之不易,一定要保存好。我习惯用numpy的.npz格式,方便下次加载:
# 保存
np.savez('calibration_result.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
# 加载
with np.load('calibration_result.npz') as data:
mtx = data['mtx']
dist = data['dist']
你想想看,下次做畸变校正时,直接加载这个文件就行了,不用重新标定。省时省力。
4.7 完整代码示例
最后,给你一个完整的标定脚本。你可以直接拿去用,改一下图片路径和棋盘格参数就行。
import cv2
import numpy as np
import glob
# 参数设置
pattern_size = (9, 6) # 内角点数量
square_size = 25.0 # 格子边长 (mm)
image_path = 'calib_images/*.jpg'
# 准备世界坐标
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 = []
imgpoints = []
# 遍历图片
images = glob.glob(image_path)
print(f"找到 {len(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)
imgpoints.append(corners)
print(f"✓ {fname} - 角点检测成功")
else:
print(f"✗ {fname} - 角点检测失败")
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
# 输出结果
print(f"\n重投影误差: {ret:.4f}")
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
# 保存结果
np.savez('calibration_result.npz', mtx=mtx, dist=dist, rvecs=rvecs, tvecs=tvecs)
print("\n标定结果已保存到 calibration_result.npz")
运行这个脚本,只要你的图片质量没问题,几分钟就能拿到标定结果。
好了,这一节的内容就到这里。你学会了如何用calibrateCamera计算内参和畸变系数,也知道了怎么评估标定质量。下一节,我们会用这些结果来做畸变校正,把变形的图像拉直。到时候你会发现——原来歪歪扭扭的图片,也能变得这么规整。
公众号:蓝海资料掘金营,微信deep3321