4. 标定基础理论

好,咱们进入真正的动手环节了。前面几章我们把相机模型、畸变原理、标定板设计都捋了一遍,现在终于要拿OpenCV干活了。这一章我会带着你走完单目标定的完整流程——从棋盘格角点检测,到调用calibrateCamera,再到评估标定结果好不好。

说实话,标定这件事,理论看十遍不如亲手跑一遍。我当年第一次标定车载环视摄像头时,以为调个API就完事了,结果重投影误差大得离谱,排查了两天才发现是角点检测时棋盘格图像没拍好。嗯,这些坑我会在下面一一给你指出来。

4.1 棋盘格角点检测:findChessboardCorners

角点检测是标定的第一步,也是最容易出问题的一步。说白了,就是让OpenCV在图像里找到棋盘格上那些黑白方块的交点。

核心API就两个:

ret, corners = cv2.findChessboardCorners(image, patternSize, flags)
corners2 = cv2.cornerSubPix(gray, corners, winSize, zeroZone, criteria)

参数说明:

参数含义我的建议
image输入图像(灰度或彩色)直接传灰度图,省去内部转换开销
patternSize棋盘格内角点数,如(9,6)注意是内角点,不是格子数!
flags检测算法标志一般用CALIB_CB_ADAPTIVE_THRESH就够了
winSize亚像素搜索窗口半尺寸我习惯用(5,5),太小不稳定
criteria迭代终止条件(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
我曾经踩过的坑: patternSize传成了棋盘格的行列数(10,7),而不是内角点数(9,6)。OpenCV不会报错,但角点位置全偏了,标定出来的内参完全不能用。记住:内角点数 = 格子数 - 1。

检测到角点后,我强烈建议你用cv2.drawChessboardCorners把角点画出来看看。肉眼确认一下,比什么都靠谱。

cv2.drawChessboardCorners(image, patternSize, corners, ret)
cv2.imshow('Corners', image)

4.2 calibrateCamera函数详解

角点检测完了,就该进入核心环节了。calibrateCamera这个函数,说白了就是求解相机内参矩阵和畸变系数的最小二乘优化问题。

函数签名长这样:

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objectPoints, imagePoints, imageSize,
    cameraMatrix, distCoeffs,
    flags=None, criteria=None
)

输出参数解读:

  • ret:重投影误差的RMS值,越小越好。我一般要求<0.5像素才算合格。
  • mtx:相机内参矩阵,3x3,包含fx, fy, cx, cy。
  • dist:畸变系数向量,默认5个参数(k1,k2,p1,p2,k3)。
  • rvecs, tvecs:每张图片的旋转向量和平移向量,标定板相对于相机的外参。
个人经验: 我习惯在调用calibrateCamera之前,先把cameraMatrix初始化为单位矩阵,distCoeffs初始化为全零。这样优化起点比较中性,收敛更快。

flags参数我常用的几个:

标志作用什么时候用
cv2.CALIB_FIX_K3固定k3为0图片数量少时,防止过拟合
cv2.CALIB_ZERO_TANGENT_DIST固定p1,p2为0确认镜头没有切向畸变时
cv2.CALIB_FIX_PRINCIPAL_POINT固定主点(cx,cy)已知主点在图像中心时

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

标定完了,怎么知道结果好不好?最直观的指标就是重投影误差。

原理很简单:用标定得到的内参和外参,把世界坐标系中的棋盘格角点投影回图像平面,算一下投影点和实际检测到的角点之间的距离。这个距离的平均值就是重投影误差。

OpenCV提供了现成的函数:

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

print(f"平均重投影误差: {total_error/len(objectPoints):.4f} 像素")
我的判断标准:
  • < 0.3像素:优秀,可以用于高精度环视拼接
  • 0.3 - 0.5像素:良好,满足大部分车载应用
  • 0.5 - 1.0像素:及格,需要检查标定图片质量
  • > 1.0像素:不合格,重新采集数据吧

为什么会重投影误差大?我遇到过的情况有:

  • 标定图片模糊,角点检测不准
  • 棋盘格没有覆盖整个视野
  • 图片数量太少(我建议至少15-20张)
  • 标定板平面度不够(车载环视常用亚克力板,容易变形)

4.4 标定流程全景图

下面这张图把整个单目标定流程串起来了,你可以对照着走一遍:

单目摄像头标定完整流程 采集标定板图像 findChessboardCorners cornerSubPix 亚像素细化 calibrateCamera 求解 计算重投影误差 误差 < 阈值? 是 → 保存内参/畸变 否 → 重新采集/调整 关键:每张图片的棋盘格姿态要不同——倾斜、旋转、远近都要覆盖

4.5 完整代码示例

最后,给你一个可以直接跑的完整脚本。这是我个人习惯的写法,注释都写清楚了:

import cv2
import numpy as np
import glob

# 棋盘格参数
CHECKERBOARD = (9, 6)  # 内角点数
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# 准备世界坐标系中的点
objp = np.zeros((CHECKERBOARD[0]*CHECKERBOARD[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1,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, CHECKERBOARD, None)
    
    if ret:
        objpoints.append(objp)
        corners2 = cv2.cornerSubPix(gray, corners, (5,5), (-1,-1), criteria)
        imgpoints.append(corners2)
        
        cv2.drawChessboardCorners(img, CHECKERBOARD, corners2, ret)
        cv2.imshow('img', img)
        cv2.waitKey(100)

cv2.destroyAllWindows()

# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None)

print(f"内参矩阵:\n{mtx}")
print(f"畸变系数:\n{dist}")
print(f"重投影误差: {ret:.4f} 像素")
注意: 标定图片不要用JPEG压缩太狠的,压缩伪影会影响角点检测精度。我习惯用PNG或者低压缩率的JPEG。

好了,这一章的内容就到这儿。你拿着上面的代码和流程,找一组标定图片跑一遍,很快就能上手。标定这件事,动手做一次比看十遍书都管用。