第4章:摄像头标定——针孔相机模型、畸变模型与张正友标定法

各位同学,今天我们来聊聊摄像头标定。说实话,这是整个传感器融合里最“接地气”的一步。你想想看,摄像头拍出来的图像,如果不做标定,那误差能大到让你怀疑人生。我当年刚入行时,就吃过这个亏——装了个摄像头,自以为图像挺清晰,结果一测距离,偏差十几厘米。后来才发现,是畸变没处理好。

好,咱们一步步来。先搞清楚摄像头是怎么“看”世界的。

4.1 针孔相机模型——摄像头成像的“理想国”

针孔相机模型,说白了就是小孔成像。光线穿过一个小孔,在后面的感光面上形成倒像。这个模型虽然简单,但它是所有标定算法的基础。

数学上,我们用四个坐标系来描述这个过程:

  • 世界坐标系:真实物体在三维空间中的位置 (X, Y, Z)
  • 相机坐标系:以摄像头光心为原点 (Xc, Yc, Zc)
  • 图像坐标系:成像平面上的物理位置 (x, y)
  • 像素坐标系:图像上以像素为单位的坐标 (u, v)

从世界坐标到像素坐标,本质上就是做两次变换:

  1. 外参:把世界坐标转到相机坐标(旋转 + 平移)
  2. 内参:把相机坐标投影到像素平面(焦距、主点偏移)

公式长这样(别怕,理解意思就行):

s * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T

其中 K 就是内参矩阵:

K = [fx, 0, cx;
     0, fy, cy;
     0,  0,  1]

fx、fy 是焦距(像素单位),cx、cy 是主点偏移。这些值,就是我们要标定出来的东西。

我的经验: 实际项目中,fx 和 fy 往往不相等。别以为它们应该一样——镜头加工有公差,传感器也不是完美正方形。我见过有人硬把 fx 设成 fy,结果标定误差直接翻倍。

4.2 畸变模型——现实世界的“扭曲”

针孔模型是理想情况。现实中的镜头,尤其是广角镜头,会引入畸变。主要有两种:

4.2.1 径向畸变

说白了就是“桶形”或“枕形”失真。离图像中心越远,变形越明显。数学上用泰勒级数来建模:

x_corrected = x * (1 + k1*r^2 + k2*r^4 + k3*r^6)
y_corrected = y * (1 + k1*r^2 + k2*r^4 + k3*r^6)

r 是像素到图像中心的距离。k1、k2、k3 就是径向畸变系数。一般 k3 只在鱼眼镜头里用,普通镜头标 k1、k2 就够了。

避坑指南: 我曾经在标定一个广角摄像头时,只标了 k1、k2,结果图像边缘还是歪的。后来加上 k3,效果立竿见影。所以,如果你的镜头视角超过 120°,建议把 k3 也标上。

4.2.2 切向畸变

这个是因为镜头和传感器不平行造成的。比如镜头装歪了,或者传感器贴歪了。公式:

x_corrected = x + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_corrected = y + [p1*(r^2 + 2*y^2) + 2*p2*x*y]

p1、p2 就是切向畸变系数。说实话,这个值一般很小,但如果你做高精度测量,比如车道线检测,忽略它就会出问题。

4.3 张正友标定法——经典中的经典

张正友教授在 1998 年提出的这个方法,到现在还是工业界的主流。为什么?因为它只需要拍几张棋盘格照片,就能把内参、外参、畸变系数全算出来。

核心思路是这样的:

  1. 拍一张棋盘格照片,提取角点
  2. 假设棋盘格在 Z=0 平面上,建立单应性矩阵 H
  3. 利用多张照片的 H 矩阵,求解内参 K
  4. 用最小二乘法优化畸变系数
  5. 最后用最大似然估计做全局优化

嗯,这里要注意:棋盘格必须拍不同角度、不同位置。我见过有人只拍了 5 张,结果标定出来的焦距偏差 5%。我个人习惯至少拍 15-20 张,覆盖图像的各个区域。

关键点: 棋盘格要占图像面积的 20%-80%。太小了角点检测不准,太大了边缘畸变信息不够。我一般用 9x6 的内角点棋盘格,格子大小 30mm。

4.4 OpenCV 标定实践——动手干吧

理论说完了,咱们直接上代码。OpenCV 的 cv::calibrateCamera() 函数封装了张正友标定法,用起来很简单。

4.4.1 准备棋盘格图像

先拍一组棋盘格照片。注意:

  • 光照均匀,不要有反光
  • 棋盘格在画面中占不同位置
  • 倾斜角度 15°-45° 之间
  • 至少 10 张,建议 15-20 张

4.4.2 提取角点

import cv2
import numpy as np

# 棋盘格内角点数量
pattern_size = (9, 6)
# 世界坐标系中的角点坐标(假设 Z=0)
objp = np.zeros((pattern_size[0]*pattern_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:pattern_size[0], 0:pattern_size[1]].T.reshape(-1, 2)

# 存储所有图像的世界坐标和像素坐标
objpoints = []  # 3D 点
imgpoints = []  # 2D 点

for fname in image_files:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, corners = cv2.findChessboardCorners(gray, pattern_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, pattern_size, corners2, ret)
        cv2.imshow('img', img)
        cv2.waitKey(500)
cv2.destroyAllWindows()

4.4.3 执行标定

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

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)
print("重投影误差:", ret)

输出结果中,mtx 就是内参矩阵,dist 是畸变系数(顺序是 k1, k2, p1, p2, k3)。ret 是重投影误差,一般小于 0.5 像素就算不错了。

我的习惯: 标定完后,我会用 cv2.undistort() 矫正一张图,看看边缘的直线是不是变直了。如果还有弯曲,说明标定不够好,需要重新拍图。

4.4.4 图像去畸变

# 方法一:直接矫正
undistorted = cv2.undistort(img, mtx, dist, None, newcameramtx)

# 方法二:先计算映射表,再重映射(适合视频流)
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w,h), 1, (w,h))
mapx, mapy = cv2.initUndistortRectifyMap(mtx, dist, None, newcameramtx, (w,h), 5)
undistorted = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

方法二效率更高,适合实时处理。我一般在量产代码里用 remap,因为只需要算一次映射表。

4.5 知识体系总览

下面这张图,是我自己整理的摄像头标定知识结构。你可以把它当成一张“地图”,随时回来看看。

摄像头标定知识体系 针孔相机模型 畸变模型 张正友标定法 OpenCV实践 4个坐标系 世界→相机→图像→像素 内参矩阵 K fx, fy, cx, cy 径向畸变 k1, k2, k3 切向畸变 p1, p2 棋盘格角点提取 单应性矩阵 H 最小二乘优化 最大似然估计 calibrateCamera() undistort() remap() 重投影误差评估 标定流程:拍图 → 提取角点 → 计算内参 → 优化畸变 → 去畸变 重投影误差 < 0.5 像素为合格

4.6 标定结果验证

标定完了,怎么知道好不好?我一般看三个指标:

指标 合格标准 说明
重投影误差 < 0.5 像素 角点投影回图像的平均偏差
去畸变后直线度 边缘直线无明显弯曲 肉眼观察棋盘格边缘是否变直
标定结果稳定性 多次标定偏差 < 2% 换一组照片再标,看内参是否一致
注意: 重投影误差不是越小越好。我见过有人标到 0.05 像素,结果实际用起来反而不好。为什么?因为过拟合了。标定数据本身有噪声,误差太小反而说明模型把噪声也学进去了。0.3-0.5 像素是比较健康的范围。

好了,摄像头标定这部分就讲到这里。记住一句话:标定是传感器融合的基石。这一步做不好,后面所有算法都是空中楼阁。下次咱们聊激光雷达标定,那个又是另一番天地了。


公众号:蓝海资料掘金营,微信deep3321