3. 相机模型与标定:针孔相机模型、畸变校正、内参外参标定、多相机联合标定

各位同学,欢迎来到第三章。这一章,我们聊聊相机——BEV感知系统的“眼睛”。

说实话,很多做感知的同学,一上来就扎进网络结构里,觉得相机标定是“脏活累活”。但我得说,这个想法很危险。我见过太多项目,模型调得天花乱坠,最后因为标定精度不够,BEV视角下目标位置直接偏了半米。嗯,那感觉,就像你瞄准了靶心,结果枪管是歪的。

所以,这一章我们踏踏实实把相机模型和标定讲透。你想想看,只有把“眼睛”校准了,后面的感知才有意义。

3.1 针孔相机模型:从3D世界到2D像素

先问个问题:一个三维空间里的点,是怎么跑到我们相机传感器上的?

最经典的模型,就是针孔相机模型。说白了,它描述了一个“小孔成像”的过程。光线穿过一个小孔,在后面的成像平面上形成一个倒立的像。我们把这个过程数学化,就得到了相机的内参和外参。

内参,是相机“自己”的属性。比如焦距、主点位置(光轴和成像平面的交点)、像素的物理尺寸。它决定了从“相机坐标系”到“像素坐标系”的映射关系。

外参,是相机在“世界”中的位置和姿态。它描述了从“世界坐标系”到“相机坐标系”的旋转和平移。

整个流程可以概括为:

  1. 世界坐标系相机坐标系(靠外参的旋转R和平移t)
  2. 相机坐标系图像物理坐标系(靠内参的焦距f)
  3. 图像物理坐标系像素坐标系(靠主点偏移和像素尺寸)

用公式表达就是:

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

其中,K是内参矩阵,[R|t]是外参矩阵,s是尺度因子。

核心要点:内参矩阵K是3x3的,外参矩阵[R|t]是3x4的。两者相乘,就是一个3x4的投影矩阵P。这个P,就是连接3D世界和2D像素的桥梁。

3.2 畸变校正:别让镜头“欺骗”了你

理想很丰满,现实很骨感。针孔模型是理想情况,但真实的镜头是有畸变的。尤其是我们做BEV感知,经常用广角镜头或鱼眼镜头,畸变非常明显。

畸变主要分两种:

  • 径向畸变:光线在镜头边缘弯曲得更厉害。表现为“桶形畸变”(画面中间凸起)或“枕形畸变”(画面中间凹陷)。
  • 切向畸变:镜头和成像平面不平行导致的。表现为画面“倾斜”。

怎么校正?我们通常用多项式模型来描述畸变。对于径向畸变,用r=0处的泰勒展开来近似:

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)

对于切向畸变:

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]

这里的k1, k2, k3, p1, p2就是畸变参数。

我的经验:我曾经在一个项目中,用了畸变很大的鱼眼镜头,但只用了k1和k2两个参数去校正。结果画面边缘的直线还是弯的。后来加了k3,效果才明显改善。所以,如果你的镜头畸变很大,建议把k3也标定进去。

3.3 内参外参标定:如何得到这些参数?

参数有了,怎么得到它们?这就是标定的工作。

最经典的方法是张正友标定法。你只需要打印一张棋盘格,用相机从不同角度拍几张照片,算法就能自动解算出内参和畸变参数。

流程大致如下:

  1. 拍摄多张棋盘格图像(建议10-20张,覆盖不同角度和距离)
  2. 检测每张图像中的角点
  3. 利用角点坐标和已知的棋盘格物理尺寸,求解单应性矩阵
  4. 利用单应性矩阵的约束,求解内参
  5. 利用内参和单应性矩阵,求解外参
  6. 用最大似然估计优化所有参数

代码实现上,OpenCV的cv2.calibrateCamera()函数直接封装了整个过程。我习惯这样用:

import cv2
import numpy as np

# 准备棋盘格角点的世界坐标
objp = np.zeros((6*9, 3), np.float32)
objp[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2)

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

# 遍历图像
for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, corners = cv2.findChessboardCorners(gray, (9, 6), None)
    if ret:
        objpoints.append(objp)
        imgpoints.append(corners)

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

# mtx 就是内参矩阵,dist 就是畸变系数

避坑指南:我曾经在户外标定时,光照不均匀导致角点检测失败。后来我改用高对比度的棋盘格,并在拍摄时保证光照均匀。另外,棋盘格一定要贴平,不能有褶皱,否则标定出来的参数会有系统性偏差。

3.4 多相机联合标定:让所有“眼睛”对齐

做BEV感知,通常需要多个相机(比如环视4个或6个)。每个相机都有自己的内参和外参。但我们需要的是所有相机在同一个世界坐标系下的统一表达。

多相机联合标定,就是解决这个问题。它要找到每个相机相对于一个“基准坐标系”的外参。

常用的方法有两种:

  • 基于标定物的方法:在多个相机共视区域内放置标定物(比如大棋盘格或三维标定架),同时拍摄,然后通过特征点匹配,解算出相机之间的相对位姿。
  • 基于自然特征的方法:利用场景中的自然特征点(比如SIFT、ORB特征),通过多视图几何(如本质矩阵、基础矩阵)来估计相机之间的相对位姿。这种方法更灵活,但精度可能不如标定物法。

我个人更推荐基于标定物的方法,尤其是在量产项目中,精度可控,鲁棒性好。

具体步骤:

  1. 将标定物放置在多个相机的共视区域
  2. 所有相机同步拍摄一张图像
  3. 在每个图像中检测标定物的特征点
  4. 利用特征点的2D-3D对应关系,求解每个相机的外参
  5. 以其中一个相机为基准,将所有相机的外参统一到同一个坐标系下

举个例子,假设我们有前、后、左、右四个环视相机。我们通常以车辆中心为世界坐标系原点,然后标定出每个相机相对于这个原点的[R|t]。

关键点:多相机联合标定的精度,直接决定了BEV拼接的平滑度和目标定位的准确性。我见过一个项目,因为左右相机的外参标定差了1度,结果BEV图像中车道线在拼接处出现了明显的错位。所以,这一步值得花时间。

好了,这一章的内容就到这里。相机模型和标定是BEV感知的基石,看似基础,但细节很多。希望你能动手实践一下,用OpenCV跑一遍标定流程,感受一下从“模糊”到“清晰”的过程。

下一章,我们会聊聊如何将多个相机的图像投影到BEV视角下,也就是“逆透视变换”。到时候见。