3、坐标系与标定:世界坐标系、相机坐标系、图像坐标系、传感器联合标定基础

说实话,坐标系和标定这块,是很多刚入行同学的第一道坎。

我记得我刚做感知那会儿,总觉得标定就是个「配准」的事。后来在实车上栽了跟头——明明算法跑得好好的,换了个车就全乱套了。排查了三天,最后发现是标定参数差了0.1度。嗯,0.1度,在50米外就是将近10厘米的偏差。你说吓不吓人?

3.1 四个坐标系,一个都不能少

自动驾驶里,我们天天跟四个坐标系打交道。说白了,就是回答四个问题:

  • 世界坐标系:车在哪?路在哪?——全局定位
  • 相机坐标系:相机看到了什么?——以镜头为中心
  • 图像坐标系:像素落在哪?——照片上的位置
  • 像素坐标系:具体是哪个像素?——整张图的坐标

我习惯把世界坐标系想象成「上帝视角」。你想想看,高精地图、GPS、规划路径,全是在这个世界坐标系里玩的。而相机坐标系呢,就是相机自己的小世界——光心是原点,Z轴朝前,X轴朝右,Y轴朝下。

关键点:世界坐标系到相机坐标系,本质上是刚体变换。旋转+平移,6个自由度。我见过有人把旋转矩阵写反了,结果车道线全飞到天上去了。

3.2 从3D到2D:投影那点事

相机坐标系到图像坐标系,这一步叫「透视投影」。公式很简单:

// 相机坐标系 (Xc, Yc, Zc) -> 图像坐标系 (x, y)
x = f * Xc / Zc
y = f * Yc / Zc

这里f是焦距。为什么是除法?因为近大远小啊。我在项目中遇到过一个问题:用长焦镜头时,远处物体投影到图像上几乎不动,导致测距误差巨大。后来我们加了个约束——只对30米内的目标做精确测距。

再下一步,图像坐标系到像素坐标系。这一步就是加个偏移和缩放:

u = x / dx + u0
v = y / dy + v0

dx、dy是每个像素的物理尺寸,u0、v0是主点偏移。说白了,就是把连续坐标变成离散的像素格子。

我的小技巧:标定相机时,记得检查主点(u0, v0)是否在图像中心附近。如果偏差超过10个像素,说明镜头装配有问题。我曾经遇到过一台相机,主点偏了30个像素,结果车道线检测一直偏右——排查了两天才发现是硬件问题。

3.3 联合标定:让传感器说同一种语言

单个相机标定好办,棋盘格拍几张就完事。但多传感器联合标定,才是真正的硬骨头。

我做过一个项目,车上装了4个环视相机、1个前向长焦、1个激光雷达。每个传感器都有自己的坐标系。你想想看,如果它们说的不是同一种语言,融合出来的结果能靠谱吗?

联合标定的核心就一句话:找一个公共参考系,把所有传感器都对齐到它上面

常用的方法有两种:

  • 目标法:放一个标定板(比如棋盘格),让所有传感器都能看到它。然后解算相对位姿。
  • 自然特征法:利用场景中的角点、边缘等特征,做自动配准。适合在线标定。
方法 精度 适用场景 我踩过的坑
目标法 高(<0.1°) 产线、实验室 标定板反光导致角点检测失败
自然特征法 中等(0.3°~0.5°) 路测、在线校准 动态场景下特征匹配容易漂移

注意:联合标定不是一劳永逸的。车辆行驶中的振动、温度变化,都会导致标定参数漂移。我建议每3个月或每1万公里做一次复标。曾经有客户反馈车道线检测不准,查到最后发现是标定参数已经漂了半年没更新。

3.4 实战:一个标定流程的骨架

下面是我个人习惯用的标定流程,你可以参考:

  1. 准备标定板:棋盘格尺寸要精确到毫米级。我一般用A1大小的板子,格子边长30mm。
  2. 采集数据:让标定板出现在视野的不同位置。至少20张图,覆盖远近、左右、俯仰。
  3. 角点检测:用OpenCV的findChessboardCorners。注意亚像素精度。
  4. 单目标定:算内参(焦距、主点、畸变系数)。
  5. 双目标定:算外参(旋转矩阵、平移向量)。
  6. 验证:把标定结果投影到图像上,看重投影误差。我要求误差小于0.5像素。
// 一个简单的单目标定代码片段(Python风格)
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) * 30  # 30mm格子

# 遍历所有图片,检测角点
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)

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

这段代码我用了不下百次。但要注意,如果标定板不平整,或者光照不均匀,角点检测会失败。我建议在自然光下拍摄,避免强光直射。

3.5 避坑指南

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

  • 坑一:标定板太小。我曾经用A4纸打印棋盘格,结果远距离根本看不清角点。后来换了A1板子,问题解决。
  • 坑二:忽略畸变。有些同学标定完只用了内参,忘了畸变校正。结果图像边缘的物体位置全错了。记住,畸变系数一定要用。
  • 坑三:外参初始化不对。联合标定时,如果初始位姿猜得太离谱,优化算法会收敛到局部最优。我习惯先用CAD模型给个粗略值,再让算法微调。
  • 坑四:时间同步。标定完发现相机和激光雷达的数据对不上——原来是时间戳没对齐。嗯,这个坑我踩了两次才长记性。

坐标系和标定,说白了就是给传感器「校准眼睛」。眼睛准了,后面的感知算法才能站得住脚。我见过太多项目,算法调来调去效果不好,最后发现是标定参数有问题。所以,别嫌麻烦,把标定做扎实了,后面能省一大半心。