第1章:相机模型与标定
各位同学,欢迎来到BEV感知的第一站。说实话,很多做感知的同学一上来就扑向网络模型,结果发现数据对齐都做不好。我当年也踩过这个坑——模型跑得飞起,但投影到BEV空间全是歪的。后来才明白,相机模型和标定才是整个感知系统的地基。
这一章,我们就来把这个地基打牢。
1.1 针孔相机模型
先聊聊最经典的针孔相机模型。说白了,它就是描述「三维世界怎么投影到二维图像」的数学工具。
你想想看,光线从物体出发,穿过相机的小孔,落在成像平面上。这个过程中,物体的大小和距离成反比——这就是最基本的透视投影。
数学上,我们用四个坐标系来描述这个过程:
- 世界坐标系:你定义的三维空间,单位是米
- 相机坐标系:以相机光心为原点,Z轴指向镜头前方
- 图像坐标系:在成像平面上,单位是毫米
- 像素坐标系:最终图像上的行列索引,单位是像素
整个投影公式长这样:
s * [u, v, 1]^T = K * [R | t] * [X, Y, Z, 1]^T
其中K是内参矩阵,[R|t]是外参矩阵。s是尺度因子,因为深度信息在投影过程中丢失了。
核心要点:内参描述相机本身的属性(焦距、主点),外参描述相机在世界中的位置和朝向。
1.2 畸变模型
理想很丰满,现实很骨感。真实的镜头不是完美的针孔,光线经过透镜会发生折射,这就产生了畸变。
我在项目中遇到过最典型的情况:用广角相机拍车道线,结果图像边缘的直线全变成了弧线。这就是径向畸变在作怪。
畸变主要分两类:
- 径向畸变:光线弯曲程度随距离光心距离变化。桶形畸变(图像边缘向外凸)和枕形畸变(向内凹)都属于这一类。
- 切向畸变:镜头和成像平面不平行导致的。这个在量产车上比较常见,因为装配公差难免。
数学上,我们用多项式来矫正畸变:
x_corrected = x * (1 + k1*r^2 + k2*r^4 + k3*r^6) + [2*p1*x*y + p2*(r^2 + 2*x^2)]
y_corrected = y * (1 + k1*r^2 + k2*r^4 + k3*r^6) + [p1*(r^2 + 2*y^2) + 2*p2*x*y]
嗯,这里要注意:k1、k2、k3是径向畸变系数,p1、p2是切向畸变系数。r是像素点到光心的距离。
我的经验:对于自动驾驶常用的摄像头(HFOV 60-120度),一般用k1、k2、p1、p2四个参数就够了。k3只在鱼眼镜头(HFOV > 150度)时才需要。
1.3 相机标定原理
标定,说白了就是求解内参、外参和畸变系数。为什么需要标定?因为出厂参数不够准,而且车辆使用过程中镜头位置可能发生微变。
我记得有一次,一辆测试车在颠簸路面上跑了一整天,回来发现BEV感知的拼接出现了明显的错位。排查了半天,原来是相机支架松动了,外参变了。从那以后,我养成了定期标定的习惯。
标定的核心思想很简单:
- 拍摄已知尺寸的标定板(通常是棋盘格)
- 提取图像中的角点坐标
- 建立3D-2D对应关系
- 用优化算法求解相机参数
避坑指南:我曾经用一张A4纸打印的棋盘格做标定,结果因为纸张不平整,标定出来的内参偏差很大。后来换了亚克力板背光的标定板,精度才上来。标定板的平整度直接影响结果,别省这个钱。
1.4 张正友标定法
这是目前最主流的标定方法,由张正友教授在1998年提出。它的巧妙之处在于:不需要知道相机相对于标定板的精确位置,只需要拍摄不同角度的棋盘格照片。
为什么能这样?因为棋盘格的角点都在一个平面上,而且格子大小已知。这就提供了足够的几何约束。
算法流程大致如下:
- 拍摄10-20张不同角度的棋盘格照片
- 提取每张照片的角点坐标
- 用闭式解估计内参和每张照片的外参
- 用最大似然估计优化所有参数(包括畸变系数)
我个人习惯用OpenCV实现,代码非常简洁:
import cv2
import numpy as np
# 准备棋盘格尺寸
pattern_size = (9, 6) # 内角点数量
square_size = 0.025 # 格子边长,单位米
# 准备世界坐标系中的点
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)
objp *= square_size
# 存储所有图片的角点
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)
imgpoints.append(corners)
# 标定
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
关键参数:棋盘格的内角点数量是 (列数-1) × (行数-1)。比如9×6的棋盘格,内角点是8×5=40个。别搞错了,我见过有人数了半天格子数不对。
1.5 标定工具使用
实际工程中,我们很少手写标定代码。常用的工具包括:
| 工具 | 适用场景 | 特点 |
|---|---|---|
| OpenCV | 单目标定 | 灵活,可集成到pipeline |
| MATLAB Camera Calibrator | 快速验证 | 可视化好,操作简单 |
| Kalibr | 多相机+IMU联合标定 | 支持多种标定板,精度高 |
| Apollo标定工具 | 自动驾驶整车标定 | 集成度高,支持在线标定 |
我个人推荐初学者先用MATLAB工具跑一遍,直观感受标定过程。等理解了原理,再切换到OpenCV做自动化。
标定时的注意事项:
- 照片要覆盖整个视场角,特别是边缘区域
- 标定板倾斜角度在30-60度之间,别太平也别太陡
- 光照要均匀,避免反光
- 至少拍15张,我一般拍20-25张
小技巧:标定完成后,一定要做重投影误差检查。如果平均误差超过0.5像素,说明标定质量有问题。我一般要求控制在0.3像素以内。
知识体系总览
下面这张图总结了本章的核心逻辑,从相机模型到标定实践的完整链路:
从这张图可以看得很清楚:相机模型是理论基础,畸变模型是现实修正,标定原理是求解方法,张正友法是具体算法,工具是落地手段。一环扣一环,缺一不可。
好了,这一章的内容就到这里。标定这件事,看起来简单,但做精了不容易。我建议你找一块棋盘格,亲手跑一遍标定流程,看看重投影误差是多少。实践出真知。