第一章:相机标定技术——张正友标定法
相机标定,说白了就是给相机做「体检」。
我入行那会儿,第一次拿到工业相机拍出来的图像,边缘全是弯的,心里直犯嘀咕:这镜头是不是坏了?后来才明白,这不是坏了,是广角镜头天生的畸变在作怪。你想想看,光线经过一堆透镜,怎么可能还走直线?
所以,标定的核心任务就两个:算出相机的内参(焦距、主点、畸变系数),再算出外参(旋转、平移)。有了这些,我们才能把图像里的像素坐标,还原成真实世界的几何关系。
1.1 张正友标定法——为什么它成了行业标准?
张正友老师在1998年提出的这个方法,我个人认为是相机标定领域最优雅的解决方案之一。为什么这么说?
- 只需要一个平面棋盘格——不需要精密的3D标定块,打印一张纸就行。
- 操作简单——从不同角度拍十几张照片,算法自动解算。
- 精度够用——重投影误差能控制在0.1像素以内,对绝大多数项目来说绰绰有余。
我记得第一次在产线上部署标定程序时,工人师傅问我:「这玩意儿准吗?」我让他随便拍了8张不同角度的棋盘格照片,跑完标定一看,重投影误差0.08像素。师傅竖了个大拇指,从此再没质疑过。
核心思想:张正友法假设标定板是平面(Z=0),通过单应性矩阵建立2D图像点与3D世界点的对应关系,然后利用约束条件求解内参和外参。
1.2 标定板设计——棋盘格 vs 圆点
选哪种标定板?这问题我经常被问到。直接说结论:
| 类型 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 棋盘格 | 角点检测稳定,亚像素精度高 | 边缘模糊时容易丢角点 | 室内、光照均匀、高精度需求 |
| 圆点阵列 | 对畸变不敏感,中心定位准 | 透视变形后椭圆检测复杂 | 大广角、鱼眼镜头、光照变化大 |
我个人习惯用棋盘格,因为OpenCV的findChessboardCorners函数太成熟了,几乎不用调参。但如果你用的是超广角镜头(比如视场角>120°),我建议你试试圆点标定板。为什么?因为棋盘格在图像边缘的角点会被严重拉长,检测算法容易失败。我曾经在一个项目里被这个问题折磨了两天,换成圆点后一次通过。
设计建议:
- 棋盘格尺寸:7×10或8×11个内角点,方格边长20-30mm
- 圆点阵列:5×7或7×9个圆点,直径10-15mm,间距2倍直径
- 打印时用哑光纸,避免反光
- 贴在刚性平板上,不要弯曲
1.3 OpenCV标定流程——从拍照到出结果
嗯,这里要注意,标定流程看似简单,但每一步都有坑。我直接给你看核心代码,然后逐行解释。
import cv2
import numpy as np
import glob
# 1. 准备世界坐标系中的点
# 假设棋盘格内角点数为 (9, 6),方格边长 25mm
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
# 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)
# 3. 查找棋盘格角点
ret, corners = cv2.findChessboardCorners(gray, pattern_size, None)
if ret:
objpoints.append(objp)
# 4. 亚像素精细化
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)
# 5. 可视化(可选)
cv2.drawChessboardCorners(img, pattern_size, corners2, ret)
cv2.imshow('Corners', img)
cv2.waitKey(100)
cv2.destroyAllWindows()
# 6. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
这段代码我用了不下百次,有几个关键点你得注意:
- 角点检测失败? 检查图像是否过曝或过暗,棋盘格是否占画面1/4以上。
- 亚像素精细化不能省——直接跳过这步,标定精度会掉一个数量级。
- 图像数量——我建议至少15张,覆盖不同角度和距离。别偷懒,我曾经只拍了8张,结果标定出来的焦距偏差了3%。
1.4 标定结果评估——重投影误差
标定完了,怎么知道结果好不好?最直观的指标就是重投影误差。
简单说,就是把标定板上的3D点,用算出来的内参和外参投影回图像,看看跟实际检测到的2D点差多少像素。这个差值越小,说明标定越准。
经验阈值:
- 优秀:< 0.1 像素
- 良好:0.1 - 0.3 像素
- 可接受:0.3 - 0.5 像素
- 需要重做:> 0.5 像素
计算重投影误差的代码也很简单:
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 += 1
return total_error / total_points
rms = compute_reprojection_error(objpoints, imgpoints, rvecs, tvecs, mtx, dist)
print(f"重投影误差: {rms:.4f} 像素")
避坑指南:
- 我曾经遇到过重投影误差0.05像素,但实际矫正效果很差的情况。后来发现是标定板没贴平,产生了微小的弯曲。所以,标定板的平面度比你想的重要得多。
- 如果误差突然变大,先检查是不是有图像没检测到角点,或者标定板在图像边缘被严重遮挡。
- 不要只看平均值,建议逐张图像查看误差分布。如果某张图的误差明显偏大,果断删掉重新标定。
1.5 知识体系总览
下面这张图是我自己整理的标定技术框架,你可以把它当作本章的「地图」:
这张图把整个标定流程串起来了。你从「张正友标定法」出发,左边准备标定板,右边采集图像,中间走完核心算法流程,最后用重投影误差来验收成果。每一步都有讲究,但整体逻辑很清晰。
我的个人习惯:每次标定完,我都会把内参矩阵和畸变系数保存成一个YAML文件,方便后续调用。同时,我会保留几张没参与标定的图像做验证,看看矫正效果是否真的OK。这一步虽然简单,但能帮你发现很多隐藏问题。
公众号:蓝海资料掘金营,微信deep3321