4. 相机标定实战:OpenCV相机标定流程、calibrateCamera函数详解、标定结果评估(重投影误差)

好,咱们直接进入正题。前面讲了那么多理论,什么内参外参、畸变模型,说白了都是为了这一刻——用OpenCV把相机的“底牌”给翻出来。

我个人习惯把相机标定比作“给相机做体检”。你拿着体检报告,才知道它哪里“近视”、哪里“散光”。而OpenCV就是那个体检医生,我们只需要把数据喂给它就行。

4.1 OpenCV相机标定的完整流程

整个流程其实就五步,我当年第一次跑通时,觉得这玩意儿真没想象中那么玄乎。你跟着走一遍就明白了:

  1. 准备标定板——打印一张棋盘格,贴在硬纸板上。我建议用A4纸,别太小,不然角点检测容易翻车。
  2. 采集图像——从不同角度拍20-30张照片。注意:要让棋盘格出现在画面的各个角落,尤其是边缘。我见过有人只拍中间区域,结果标出来的畸变参数完全不对。
  3. 提取角点——用findChessboardCorners找到棋盘格的内角点。嗯,这里有个坑:棋盘格尺寸是“内角点数”,比如9x6的棋盘,内角点是8x5,别搞反了。
  4. 执行标定——调用calibrateCamera,一步到位。
  5. 评估结果——算重投影误差,看看标定质量。

核心逻辑图:下面这张图是我用SVG画的,把整个流程串起来了。你看一眼,心里就有谱了。

① 准备标定板 ② 采集多角度图像 ③ 提取棋盘格角点 ④ calibrateCamera() ⑤ 评估:重投影误差 输出:内参矩阵 + 畸变系数 + 外参 用于后续手眼标定、视觉定位 误差大?回去重拍

4.2 calibrateCamera函数详解

这个函数是整个标定的核心。说白了,它就是个“黑盒优化器”——你给它一堆2D-3D对应点,它给你算出相机参数。咱们来看看它的真面目:

retval, cameraMatrix, distCoeffs, rvecs, tvecs = cv2.calibrateCamera(
    objectPoints,    # 世界坐标系中的3D点
    imagePoints,     # 图像坐标系中的2D点
    imageSize,       # 图像尺寸 (width, height)
    cameraMatrix,    # 内参矩阵初始值(可传None)
    distCoeffs,      # 畸变系数初始值(可传None)
    flags=None,      # 标定选项
    criteria=None    # 迭代终止条件
)

我来拆解一下每个参数,都是实战中容易踩坑的地方:

参数 类型 说明 我的经验
objectPoints list of np.array 每张图的3D点,Z=0 单位用毫米,别用像素
imagePoints list of np.array 每张图的2D角点坐标 亚像素精度更好
imageSize tuple (width, height) 别传反了,是宽×高
cameraMatrix np.array (3x3) 内参矩阵 传None让函数自己算
distCoeffs np.array (1x5/1x8) 畸变系数 默认5参数就够了
flags int 标定选项 一般用默认值
criteria tuple (type, maxCount, epsilon) 建议(3, 30, 1e-6)

我的小技巧:objectPoints的生成别手写,用np.mgrid一步搞定。比如棋盘格内角点8x5,格子大小30mm:

objp = np.zeros((8*5, 3), np.float32)
objp[:, :2] = np.mgrid[0:8, 0:5].T.reshape(-1, 2) * 30.0

这样写,清晰又不容易出错。

4.3 标定结果评估:重投影误差

标定完了,怎么知道结果靠不靠谱?重投影误差就是那个“照妖镜”。

它的原理很简单:用标定出的参数,把3D点投影回图像上,算算跟实际检测到的2D点差了多少像素。这个差值越小,说明标定越准。

我一般这样算:

total_error = 0
for i in range(len(objectPoints)):
    imgpoints2, _ = cv2.projectPoints(
        objectPoints[i], rvecs[i], tvecs[i], 
        cameraMatrix, distCoeffs
    )
    error = cv2.norm(imagePoints[i], imgpoints2, cv2.NORM_L2) / len(imgpoints2)
    total_error += error

mean_error = total_error / len(objectPoints)
print(f"重投影误差: {mean_error:.4f} 像素")

误差多少算合格?我个人经验:

  • < 0.1 像素:非常优秀,工业级精度
  • 0.1 - 0.3 像素:良好,大多数场景够用
  • 0.3 - 0.5 像素:及格,需要检查标定过程
  • > 0.5 像素:不合格,重新标定吧

注意:我曾经遇到过一次,误差0.8像素,怎么调都降不下来。后来发现是标定板没贴平,有个角翘起来了。你想想看,3D点坐标都是假设在同一个平面上的,板子一翘,这个假设就破了,误差自然大。

4.4 实战中的避坑指南

做相机标定这么多年,我踩过的坑比走过的路还多。挑几个典型的说说:

  • 图像数量不是越多越好。20-30张足够,关键是角度要丰富。我见过有人拍了100张,全是同一个角度,那跟拍1张没区别。
  • 标定板要占画面1/4以上。太小了角点检测不准,太大了又拍不全。我一般控制在1/3左右。
  • 注意光照均匀。反光、阴影都会影响角点检测。我习惯在漫射光下拍,或者用偏振片。
  • 每张图都要检查角点。别偷懒,用drawChessboardCorners画出来看看。有时候检测错了你都不知道。

嗯,说到这个我想起来,有一次在工厂现场,光照条件很差,角点检测老是失败。后来我干脆用LED补光灯从侧面打光,效果立竿见影。有时候解决问题的方法就是这么简单粗暴。

好了,相机标定这块就聊到这儿。你按照这个流程走一遍,基本不会出大问题。记住:标定是后面所有视觉算法的基础,这一步马虎不得。


专注资料整理