4. 相机内参模型:针孔模型、畸变模型与内参标定

聊到BEV感知,绕不开的一个基础就是相机内参。说实话,很多做感知的同学一开始容易忽略它,觉得“反正有深度学习,模型自己能学出来”。但我在实际项目中吃过亏——外参标得再准,内参不对,BEV视角下的车道线直接歪掉。所以这一节,咱们把内参模型和标定方法彻底讲透。

4.1 针孔模型:最朴素的成像原理

针孔模型,说白了就是初中物理的小孔成像。光线穿过一个小孔,在后面的成像平面上形成倒像。数学上,我们用内参矩阵K来描述这个过程。

内参矩阵K长这样:

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

其中:

  • fx, fy:焦距在x和y方向上的像素尺度。注意,它们不一定相等,因为传感器像素可能不是正方形。
  • cx, cy:主点坐标,也就是光轴与成像平面的交点。理想情况下在图像中心,但实际会有偏移。

我记得有一次标定完发现主点偏移了十几个像素,当时没在意,结果BEV拼接时左右车道线对不齐。排查了半天,最后发现是内参没更新。嗯,从那以后我每次标定完都会看一眼主点位置。

4.2 畸变模型:镜头不是完美的

现实中的镜头不是针孔,有透镜。透镜会引入畸变,主要分两种:

4.2.1 径向畸变

径向畸变是光线经过透镜边缘时弯曲程度不同导致的。越靠近图像边缘,畸变越明显。数学上用泰勒级数展开来建模:

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

其中r是像素到主点的距离。k1, k2, k3就是径向畸变参数。一般用k1和k2就够了,鱼眼镜头才需要k3。

我的经验:标定径向畸变时,棋盘格一定要拍到图像边缘。我曾经偷懒只拍了中间区域,结果去畸变后边缘反而更扭曲了。你想想看,边缘数据都没采集到,模型怎么拟合呢?

4.2.2 切向畸变

切向畸变是镜头和成像平面不平行导致的。说白了就是装配误差。模型如下:

x_distorted = x + (2*p1*x*y + p2*(r^2 + 2*x^2))
y_distorted = y + (p1*(r^2 + 2*y^2) + 2*p2*x*y)

p1, p2就是切向畸变参数。这个畸变一般比较小,但也不能忽略。

注意:切向畸变参数如果标出来特别大(比如p1 > 0.1),说明镜头安装有问题。我遇到过一批摄像头,切向畸变参数异常大,拆开一看,镜头座没拧紧。这种硬件问题,标定是救不了的。

4.3 内参矩阵K的标定方法

标定内参,最经典的方法就是张正友标定法。我用过很多次,稳定可靠。核心思路是:拍摄不同角度的棋盘格,利用棋盘格的角点坐标,求解内参和畸变参数。

4.3.1 标定流程

  1. 采集图像:拍摄15-20张不同角度、不同位置的棋盘格图像。注意要覆盖图像的各个区域。
  2. 角点检测:用OpenCV的findChessboardCorners检测棋盘格角点。
  3. 求解内参:用calibrateCamera函数求解内参矩阵和畸变参数。
  4. 评估结果:看重投影误差,一般小于0.5像素就算合格。

代码示例:

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)

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)
print("重投影误差:", ret)

4.3.2 标定注意事项

  • 棋盘格要平整:贴在硬板上,不要有褶皱。我见过有人用A4纸打印,结果纸弯了,标定结果一塌糊涂。
  • 光照要均匀:避免反光和阴影,否则角点检测会失败。
  • 角度要多样:俯仰、偏航、滚转都要有,但不要太大(建议小于45度),否则角点检测不准。

核心要点:内参标定是BEV感知的基石。内参不准,外参标得再好也没用。我建议每3个月重新标定一次内参,尤其是车载摄像头,振动和温度变化会导致内参漂移。

4.4 知识体系结构图

下面这张图总结了本章的核心逻辑:

相机内参模型知识体系 针孔模型 畸变模型 标定方法 内参矩阵 K fx, fy, cx, cy 径向畸变 k1, k2, k3 切向畸变 p1, p2 张正友标定法 重投影误差评估 三者关系:针孔模型 + 畸变模型 → 标定 → 内参矩阵K

4.5 避坑指南

最后分享几个我踩过的坑:

  • 棋盘格尺寸要量准:每个格子的实际物理尺寸(比如30mm)要精确测量。我见过有人用标称尺寸,结果标定出来的焦距偏了5%。
  • 不要只用标定板:标定完一定要用实际场景验证。比如拍一张有直线结构的场景(大楼边缘、车道线),去畸变后看直线是否变直。
  • 多相机标定要同步:如果是多相机系统,每个相机单独标定内参,然后联合标定外参。顺序不能乱。

小技巧:标定完成后,可以用cv2.undistort对一张图像去畸变,然后手动测量图像边缘的直线是否笔直。这个方法虽然土,但很有效。

好了,内参模型和标定方法就讲到这里。记住,内参是BEV感知的“地基”,地基不稳,上层建筑再漂亮也没用。下一节我们会聊外参标定,到时候会用到今天讲的内参知识。

可可精品资料库