第1章:相机标定实战——张正友标定法原理、OpenCV实现与畸变校正
各位同学,欢迎来到《双目视觉在无人机定高中实战》的第一章。今天咱们聊聊相机标定这件事。
说实话,我刚开始做无人机视觉那会儿,觉得标定就是个“走过场”的步骤。随便打印张棋盘格,拍几张照片就跑流程。结果呢?飞起来高度偏差半米多,悬停都晃得厉害。后来才明白——标定不准,后面全是白干。
相机标定,说白了就是给相机做“体检”。你得知道它镜头畸变有多严重,焦距到底是多少,光心偏移了多少。这些参数不搞清楚,你算出来的距离、高度全是错的。
核心结论:双目定高精度,90%取决于标定质量。剩下10%才是算法调优。
1.1 张正友标定法——为什么它成了行业标准?
张正友标定法,1998年提出,到现在二十多年了,依然是相机标定的主流方法。为什么?因为它解决了两个核心问题:
- 不需要高精度3D标定物——你只需要打印一张棋盘格,贴在平板上就行
- 鲁棒性好——即使照片拍得不太完美,也能给出不错的初值
我记得第一次用这个方法时,心里还嘀咕:“就一张纸?能行吗?”结果跑完流程,重投影误差0.3个像素,比我之前用专业标定板还准。
它的核心思想是这样的:
- 假设棋盘格在Z=0平面上
- 通过多张不同角度的照片,建立角点坐标与世界坐标的对应关系
- 用最小二乘法求解相机内参和畸变系数
- 最后用最大似然估计做全局优化
嗯,这里要注意:照片数量不是越多越好。我见过有人拍50张,结果重投影误差反而大了。为什么?因为照片角度太接近,信息冗余。我个人习惯是拍15-20张,覆盖不同角度和距离。
1.2 使用OpenCV进行标定——手把手实操
好,理论说完了,咱们直接上代码。我用的是OpenCV 4.x版本,Python接口。
先准备棋盘格。我建议用9×6的内角点棋盘,格子大小30mm。为什么?因为9×6的角点数量适中,既不会太少导致约束不足,也不会太多导致检测困难。
我的经验:打印棋盘格时,一定要用激光打印机,喷墨的容易洇墨,角点检测会出问题。另外,贴在硬纸板上,不要贴在泡沫板上——我吃过这个亏,泡沫板会变形。
下面是标定代码的核心部分:
import cv2
import numpy as np
import glob
# 棋盘格参数
CHECKERBOARD = (9, 6) # 内角点数量
SQUARE_SIZE = 30.0 # 格子大小(mm)
# 准备世界坐标系中的点
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)
objp *= SQUARE_SIZE
# 存储所有图片的角点
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, CHECKERBOARD, 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, 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)
print("重投影误差:", ret)
这段代码跑完,你会得到四个关键输出:
- mtx:内参矩阵,包含fx、fy、cx、cy
- dist:畸变系数,包含k1、k2、p1、p2、k3
- rvecs、tvecs:每张图片的旋转和平移向量
- ret:重投影误差,这是评估标定质量的关键指标
1.3 标定结果评估——怎么知道标得好不好?
拿到结果后,别急着用。先做评估。我一般看三个指标:
| 指标 | 合格标准 | 说明 |
|---|---|---|
| 重投影误差 | < 0.5像素 | 角点检测精度和标定质量的综合反映 |
| 内参稳定性 | 多次标定偏差 < 1% | 换一组照片重新标定,看结果是否一致 |
| 畸变校正效果 | 直线变直,边缘无扭曲 | 肉眼观察校正后的图像 |
重投影误差怎么算?说白了就是:你把世界坐标点投影到图像上,和实际检测到的角点位置比一比,看差了多少像素。这个值越小越好。
我曾经遇到过一个案例:重投影误差0.8像素,我觉得“差不多就行了”。结果双目定高时,误差直接飙到5厘米。后来重新标定,把误差降到0.3像素,高度误差就控制在1厘米以内了。
避坑指南:如果你发现重投影误差很大(>1像素),先别急着调算法。检查一下:
- 棋盘格是否平整?有没有褶皱?
- 照片是否清晰?有没有运动模糊?
- 角点检测是否准确?有没有漏检或误检?
我曾经花了两天时间调算法,最后发现是棋盘格没贴平——教训深刻。
1.4 畸变校正——让图像回归真实
标定完成后,畸变校正就很简单了。OpenCV提供了两个函数:
# 方法一:直接校正
undistorted = cv2.undistort(img, mtx, dist, None, mtx)
# 方法二:先计算映射表,再重映射(适合视频流)
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, mtx,
(width, height), cv2.CV_32FC1)
undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)
方法二效率更高,适合实时处理。无人机上我都是用方法二,因为每帧都调用undistort太慢了。
校正后的图像,边缘的桶形畸变或枕形畸变会被消除。你可以对比一下校正前后的图像:
- 校正前:直线是弯的,尤其是图像边缘
- 校正后:直线变直,但图像边缘可能会有裁剪
嗯,这里有个细节:校正后的图像边缘会被裁剪掉一部分。这是因为畸变校正需要重新采样,边缘区域的信息会丢失。如果你需要保留完整的视场角,可以调整alpha参数:
# alpha=0:裁剪最多,保留有效像素
# alpha=1:保留所有像素,但边缘会有黑色区域
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist,
(width, height), 1,
(width, height))
我个人习惯用alpha=0,因为边缘像素本来畸变就严重,留着反而影响精度。
1.5 知识体系总览
为了让你对整个标定流程有个全局认识,我画了张图:
这张图把整个标定流程串起来了。从准备标定板开始,到拍摄照片、角点检测,再到张正友标定法求解,最后输出内参、畸变系数和重投影误差。有了这些参数,你才能做畸变校正,进而实现双目定高。
好了,第一章的内容就到这里。标定是双目视觉的基石,花时间把这一步做扎实了,后面的工作会顺利很多。