第1章:相机标定实战——张正友标定法原理、OpenCV实现与畸变校正

各位同学,欢迎来到《双目视觉在无人机定高中实战》的第一章。今天咱们聊聊相机标定这件事。

说实话,我刚开始做无人机视觉那会儿,觉得标定就是个“走过场”的步骤。随便打印张棋盘格,拍几张照片就跑流程。结果呢?飞起来高度偏差半米多,悬停都晃得厉害。后来才明白——标定不准,后面全是白干

相机标定,说白了就是给相机做“体检”。你得知道它镜头畸变有多严重,焦距到底是多少,光心偏移了多少。这些参数不搞清楚,你算出来的距离、高度全是错的。

核心结论:双目定高精度,90%取决于标定质量。剩下10%才是算法调优。

1.1 张正友标定法——为什么它成了行业标准?

张正友标定法,1998年提出,到现在二十多年了,依然是相机标定的主流方法。为什么?因为它解决了两个核心问题:

  • 不需要高精度3D标定物——你只需要打印一张棋盘格,贴在平板上就行
  • 鲁棒性好——即使照片拍得不太完美,也能给出不错的初值

我记得第一次用这个方法时,心里还嘀咕:“就一张纸?能行吗?”结果跑完流程,重投影误差0.3个像素,比我之前用专业标定板还准。

它的核心思想是这样的:

  1. 假设棋盘格在Z=0平面上
  2. 通过多张不同角度的照片,建立角点坐标与世界坐标的对应关系
  3. 用最小二乘法求解相机内参和畸变系数
  4. 最后用最大似然估计做全局优化

嗯,这里要注意:照片数量不是越多越好。我见过有人拍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 知识体系总览

为了让你对整个标定流程有个全局认识,我画了张图:

相机标定知识体系 准备标定板 拍摄多角度照片 角点检测 张正友标定法 最大似然优化 内参矩阵 畸变系数 重投影误差 畸变校正 → 双目定高 → 无人机自主飞行

这张图把整个标定流程串起来了。从准备标定板开始,到拍摄照片、角点检测,再到张正友标定法求解,最后输出内参、畸变系数和重投影误差。有了这些参数,你才能做畸变校正,进而实现双目定高。

好了,第一章的内容就到这里。标定是双目视觉的基石,花时间把这一步做扎实了,后面的工作会顺利很多。

专注资料整理