相机标定(上):标定板的选择与张正友标定法
各位同学,咱们今天聊聊相机标定。说实话,这是双目视觉里最基础、也最容易被忽视的一环。我见过不少项目,算法调得再好,标定没做好,最后精度就是上不去。嗯,咱们从标定板开始说起。
一、标定板怎么选?棋盘格 vs 圆点阵
标定板的选择,说白了就是给相机找个“尺子”。你想想看,相机要测量三维空间,总得有个已知尺寸的参照物吧?
棋盘格标定板
这是最常用的。黑白格子交替,角点检测非常稳定。我个人习惯用棋盘格,因为OpenCV对它的支持最成熟。
- 优点:角点检测精度高,算法成熟,代码几乎不用改
- 缺点:需要保证棋盘格在图像中足够大,否则角点容易丢失
- 适用场景:大多数工业检测场景,尤其是高精度测量
我的经验:棋盘格尺寸别选太小。我做过一个项目,用10mm的格子标定500mm视野的相机,结果角点检测总是跳。后来换成20mm的格子,一次就过了。
圆点阵标定板
圆点阵在某些场景下比棋盘格更优。为什么?因为圆心的亚像素定位精度理论上比角点更高。
- 优点:圆心定位精度高,对畸变不敏感
- 缺点:需要保证圆点完整,边缘被裁切就麻烦了
- 适用场景:大畸变镜头、广角相机
注意:圆点阵标定板对光照更敏感。我曾经在一条产线上调试,车间灯光一变化,圆点检测就出问题。后来加了遮光罩才解决。
二、张正友标定法原理
张正友标定法,说白了就是“用平面标定板来求解相机参数”。为什么不用三维标定块?因为平面标定板好做、好放、好移动。
核心思想其实很简单:
- 我们已知标定板上每个点的三维坐标(世界坐标系)
- 相机拍到了这些点的二维坐标(图像坐标系)
- 通过多张不同角度的照片,建立方程组,求解相机内参和外参
数学上,它把标定板平面设为Z=0,这样三维到二维的映射就简化成了单应性矩阵。嗯,这里要注意,单应性矩阵有8个自由度,一张照片只能提供8个方程,所以至少需要3张不同角度的照片才能求解全部参数。
关键点:张正友标定法假设镜头畸变模型是径向畸变(k1, k2)和切向畸变(p1, p2)。实际项目中,我一般只标定k1和k2,p1和p2影响很小,标了反而容易过拟合。
三、单目标定的步骤
咱们直接上步骤,不绕弯子:
- 准备标定板:打印棋盘格,贴在平整的硬板上
- 采集图像:从不同角度拍10-20张照片
- 检测角点:用OpenCV的findChessboardCorners
- 亚像素优化:用cornerSubPix提高精度
- 标定计算:调用calibrateCamera
- 评估结果:看重投影误差,一般小于0.5像素就算合格
避坑指南:我曾经为了省事只拍了5张照片,结果标定出来的焦距偏差了3%。后来老老实实拍了15张,误差降到0.2像素。记住,照片数量不是越多越好,但少于10张风险很大。
四、代码实现(OpenCV)
直接上代码,我习惯用Python,因为调试方便。C++版本逻辑一样,只是语法不同。
import cv2
import numpy as np
import glob
# 1. 准备标定板参数
chessboard_size = (9, 6) # 内角点数量
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 = [] # 图像坐标系点
images = glob.glob('calib_images/*.jpg')
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 4. 检测角点
ret, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
if ret:
objpoints.append(objp)
# 5. 亚像素优化
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, chessboard_size, corners2, ret)
cv2.imshow('img', img)
cv2.waitKey(500)
cv2.destroyAllWindows()
# 6. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
print("相机内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
# 7. 评估重投影误差
total_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)
total_error += error
print("平均重投影误差: ", total_error / len(objpoints))
重要提醒:代码中的square_size一定要填实际物理尺寸,单位保持一致。我见过有人忘了乘以square_size,结果标定出来的焦距完全不对,还以为是相机坏了。
五、核心知识体系
下面这张图是我自己总结的,把本章的核心逻辑串起来了。你仔细看看,标定这件事其实就三个环节:准备、计算、验证。
嗯,这张图把整个流程串起来了。你从左边开始,准备阶段选好标定板、拍好照片,中间计算阶段用OpenCV跑一遍,最后验证阶段检查误差。每一步都有坑,但按这个流程走,基本不会出大问题。
最后说一句:标定这件事,看起来简单,但细节决定成败。我见过太多项目,算法调得天花乱坠,最后发现是标定板没贴平。所以,别嫌麻烦,每一步都做扎实了。