3、相机模型与标定:针孔相机模型、内参与外参、张正友标定法简介

各位同学,咱们今天聊聊相机模型和标定。说实话,这是BEV感知里最基础、也最容易踩坑的一环。我刚开始做视觉融合的时候,就因为在标定上偷了个懒,结果后面整个系统都跑偏了。嗯,咱们一步步来。

3.1 针孔相机模型——说白了就是小孔成像

你想想看,相机是怎么把三维世界拍到二维照片上的?最经典的解释就是针孔模型。说白了,就是光线通过一个小孔,在后面的成像平面上倒着投影。

数学上怎么描述?我们通常把三维空间中的点 (X, Y, Z) 映射到图像平面上的 (u, v)。这个过程可以写成:

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

这个公式看着复杂,其实就干了两件事:

  • 外参:把世界坐标转到相机坐标(旋转R + 平移t)
  • 内参:把相机坐标投影到像素平面(矩阵K)

我在项目中遇到过一个问题:明明标定好了,但投影出来的点总是有偏差。后来发现是忽略了镜头畸变。所以这里要提醒大家,针孔模型是理想情况,实际镜头都有畸变,后面会讲。

3.2 内参矩阵——相机的“身份证”

内参矩阵K长这样:

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

这里:

  • fx, fy:焦距,单位是像素。说白了就是镜头到成像平面的距离,换算成像素个数
  • cx, cy:光心偏移。理想情况下光心在图像正中心,但实际总有偏差

我个人习惯在拿到新相机后,第一件事就是标定内参。为什么?因为内参是固定的,只要不换镜头、不变焦距,它就不变。你想想看,如果内参错了,后面所有三维重建都是错的。

小技巧: 我建议你在标定内参时,多拍几组不同角度的棋盘格照片。至少20张,覆盖视野的各个角落。我曾经只拍了10张,结果标定出来的fx和fy差了3个像素,导致测距误差大了不少。

3.3 外参——把相机放到世界坐标系里

外参解决的是“相机在哪、朝哪看”的问题。它由旋转矩阵R和平移向量t组成:

[R | t] = [r11, r12, r13, t1;
           r21, r22, r23, t2;
           r31, r32, r33, t3]

在BEV感知里,外参特别重要。因为我们要把多个相机的图像统一到同一个坐标系下(通常是车身坐标系)。

举个例子:

  • 前视相机的外参:把相机坐标转到车头方向
  • 环视相机的外参:把四个鱼眼相机都转到车身中心

这里有个坑——外参会随着时间变化。车开久了,摄像头支架可能松动,或者车身轻微变形。所以,我建议每隔一段时间重新标定一下外参。

注意: 外参和内参不同。内参标定一次基本够用,但外参需要定期维护。我曾经在项目交付前发现环视拼接有错位,排查了两天才发现是后视摄像头被磕碰了一下,外参偏了0.5度。

3.4 张正友标定法——最经典的标定方法

说到标定,就绕不开张正友标定法。这个方法1998年提出,到现在还是工业界的主流。为什么?因为它只需要一个棋盘格,操作简单,精度还高。

核心思路是这样的:

  1. 拍一组不同角度的棋盘格照片
  2. 提取棋盘格的角点坐标
  3. 利用棋盘格的物理尺寸(比如格子边长30mm)和图像坐标,求解单应性矩阵
  4. 通过多张照片的约束,解出内参和外参
  5. 最后用最小二乘法优化畸变参数

说白了,就是利用“我知道棋盘格长什么样”这个先验信息,反推出相机的参数。

我记得第一次用OpenCV实现张正友标定时,代码其实很简单:

import cv2
import numpy as np

# 准备棋盘格角点的世界坐标
pattern_size = (9, 6)  # 内角点数量
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)
# 假设格子边长30mm
objp *= 30.0

# 存储所有图片的角点
objpoints = []  # 世界坐标
imgpoints = []  # 图像坐标

# 遍历图片
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)
        imgpoints.append(corners)

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

print("内参矩阵:\n", mtx)
print("畸变系数:\n", dist)

这段代码跑完,你就得到了内参矩阵和畸变系数。嗯,就这么简单。

关键点: 张正友标定法假设棋盘格是平面(Z=0),所以只需要一个平面靶标。这也是它方便的原因——打印一张棋盘格贴在硬纸板上就行。

3.5 畸变校正——别让镜头骗了你

实际镜头都有畸变,主要是两种:

  • 径向畸变:画面边缘的直线变弯了,像鱼眼效果
  • 切向畸变:镜头和成像平面不平行,导致画面倾斜

OpenCV里用5个参数来描述畸变:k1, k2, p1, p2, k3。前两个是径向畸变,中间两个是切向畸变,最后一个k3是径向畸变的高阶项。

校正畸变的代码也很直接:

# 获取校正映射
h, w = img.shape[:2]
newcameramtx, roi = cv2.getOptimalNewCameraMatrix(mtx, dist, (w, h), 1, (w, h))

# 校正图像
dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

我曾经在项目里遇到过一个问题:校正后的图像边缘有黑边。这是因为校正后图像的有效区域变小了。解决办法是用 cv2.getOptimalNewCameraMatrixalpha 参数控制保留多少边缘像素。

避坑指南: 标定完成后,一定要做重投影误差检查。一般要求重投影误差小于0.5像素。如果误差太大,说明标定质量不行,需要重新拍照片。我曾经有一次重投影误差到了1.2像素,排查后发现是棋盘格没贴平,有褶皱。

3.6 小结

这一章咱们聊了:

  • 针孔相机模型:三维到二维的映射
  • 内参:相机的固有属性,焦距和光心
  • 外参:相机在世界坐标系中的位姿
  • 张正友标定法:用棋盘格标定内参和外参
  • 畸变校正:让图像更真实

下一章咱们会讲鱼眼相机模型和环视系统的标定。嗯,那个更有意思,因为鱼眼相机的畸变更大,标定也更讲究技巧。

记住一句话:标定是BEV感知的基石。标定做不好,后面所有工作都是白费。我见过太多项目因为标定问题返工的了。