第四节:相机标定(上)——标定板设计与制作、张正友标定法原理、OpenCV标定流程
相机标定,说白了就是给相机做一次「视力矫正」。
你想想看,我们做双目视觉,核心任务是从两幅图像里算出深度信息。但如果相机本身有畸变,或者两个相机的内参都不知道,那后面的三角测量、视差计算全是空中楼阁。我刚开始做双目项目时,就吃过这个亏——标定没做好,出来的深度图全是歪的,折腾了两周才发现是标定板尺寸写错了。
所以这一节,咱们把标定这件事彻底讲透。
4.1 标定板设计与制作
标定板是标定的「尺子」。它的质量直接决定了标定精度。
4.1.1 棋盘格 vs 圆点阵
目前主流的有两种:
- 棋盘格:黑白相间,角点检测稳定。我个人习惯用棋盘格,因为OpenCV的
findChessboardCorners非常成熟。 - 圆点阵:圆形图案,适合大视角或鱼眼镜头。但圆心检测精度受透视变形影响较大。
我的建议是:普通双目系统用棋盘格就够了。鱼眼或广角镜头,可以考虑圆点阵。
4.1.2 设计参数
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 棋盘格内角点数 | 9×6 或 11×8 | 行列数不要相等,避免方向歧义 |
| 方格尺寸 | 20mm~30mm | 根据场景距离调整,太大或太小都不好 |
| 打印精度 | 600dpi以上 | 用激光打印,别用喷墨,容易变形 |
| 底板材质 | 亚克力或铝板 | 贴平,不能有褶皱 |
我曾经用普通A4纸打印棋盘格,贴在纸箱上标定。结果因为纸面不平,标定出来的焦距偏差了3%。后来换了亚克力板,一次就过了。记住:标定板一定要平整、平整、平整。
4.1.3 制作流程
- 用OpenCV的
cv2.drawChessboardCorners生成图案,或者用棋盘格PDF模板。 - 打印后测量实际方格尺寸,用游标卡尺量三次取平均。
- 贴在刚性底板上,确保无气泡、无褶皱。
- 拍摄时,标定板要占图像面积的1/4到1/3,太远角点检测不准。
4.2 张正友标定法原理
张正友老师在1998年提出的这个方法,可以说是相机标定领域的「标准答案」。它只需要一个平面标定板,拍几张不同角度的照片,就能算出内参和外参。
为什么能这么简单?核心思想就一句话:利用平面上的点,建立世界坐标到像素坐标的映射关系。
4.2.1 数学模型
我们先把公式摆出来:
s * [u, v, 1]^T = K * [R | t] * [X, Y, 0, 1]^T
其中:
s是尺度因子K是内参矩阵(fx, fy, cx, cy)[R | t]是外参(旋转和平移)[X, Y, 0]是标定板上的点,Z=0是因为它是平面
嗯,这里要注意:因为Z=0,所以旋转矩阵的第三列其实被消掉了。这就把问题简化成了单应性矩阵的求解。
4.2.2 求解步骤
- 检测角点:从每张图像中提取棋盘格角点坐标。
- 计算单应性矩阵:每张图像都有一个H矩阵,连接世界坐标和像素坐标。
- 约束内参:利用旋转矩阵的正交性,建立关于内参的线性方程组。
- 求解内参:通过SVD分解得到内参的闭式解。
- 非线性优化:用Levenberg-Marquardt算法最小化重投影误差。
我见过很多人只做前四步,觉得重投影误差差不多就行了。其实最后一步的非线性优化非常关键。我曾经对比过,加上LM优化后,重投影误差从0.8像素降到了0.15像素。别省这一步。
4.2.3 畸变模型
实际镜头都有畸变,主要是径向畸变和切向畸变。张正友法用以下模型:
x_distorted = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + 2*p1*x*y + p2*(r^2 + 2*x^2)
y_distorted = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + p1*(r^2 + 2*y^2) + 2*p2*x*y
其中k1、k2、k3是径向畸变系数,p1、p2是切向畸变系数。一般标定5个参数就够了,k3只在鱼眼镜头里用。
4.3 OpenCV标定流程
理论讲完了,咱们直接上代码。OpenCV把张正友法封装得非常好,核心就几个函数。
4.3.1 完整标定代码
import cv2
import numpy as np
import glob
# 1. 准备世界坐标
chessboard_size = (9, 6) # 内角点数
square_size = 25.0 # 方格尺寸,单位mm
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
# 2. 存储所有图像的点
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, chessboard_size, None)
if ret:
objpoints.append(objp)
# 亚像素精确化
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()
# 3. 执行标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None)
print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
# 4. 评估精度
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("平均重投影误差: ", mean_error/len(objpoints))
4.3.2 关键参数说明
| 参数 | 作用 | 我的建议 |
|---|---|---|
| chessboard_size | 内角点数 | 9×6最常用,别用对称的 |
| square_size | 方格实际尺寸 | 用游标卡尺量,别信打印尺寸 |
| cornerSubPix | 亚像素优化 | 必须做,精度提升明显 |
| calibrateCamera | 核心标定函数 | 返回内参、畸变、外参 |
拍摄标定图像时,我一般拍15~20张。角度要覆盖:正对、左偏、右偏、上仰、下俯。千万别只拍正面的,那样解出来的内参不稳定。我见过有人拍了30张全是正面,标定结果还不如10张多角度的。
4.3.3 标定结果验证
标定完别急着用,先验证一下:
# 去畸变
img = cv2.imread('test.jpg')
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)
# 裁剪有效区域
x, y, w, h = roi
dst = dst[y:y+h, x:x+w]
cv2.imshow('undistorted', dst)
去畸变后,图像边缘的直线应该变直了。如果还有弯曲,说明畸变参数没标好。
4.4 知识体系总览
我把这一节的核心逻辑画成了流程图,方便你理解整个标定流程:
这张图把整个标定流程串起来了。从标定板制作开始,到最终输出内参和畸变系数,每一步都环环相扣。你只要按这个流程走,标定结果不会差。
- 标定板要平整,尺寸要实测,别信打印值
- 张正友法的核心是利用平面Z=0简化单应性求解
- OpenCV的calibrateCamera一步到位,但别忘了亚像素优化
- 重投影误差控制在0.2像素以内才算合格
- 多角度拍摄比多张数更重要
好了,这一节的内容就到这儿。标定是双目视觉的基石,花时间把这一步做扎实了,后面的深度估计才能稳。下一节我们继续讲双目立体标定和极线校正,那才是真正把两个相机「对齐」的关键一步。
公众号:蓝海资料掘金营,微信deep3321