4、空间同步(标定):相机内参标定、激光雷达与相机外参标定、多雷达标定

空间同步,说白了就是让不同传感器「说同一种语言」。

你想想看,相机看到的是像素,激光雷达看到的是点云,毫米波雷达看到的是目标列表。如果它们各自用各自的坐标系说话,那融合就是个笑话。我刚开始做多传感器融合时,就吃过这个亏——相机和雷达各自跑得挺好,一融合就乱套,后来才发现是外参偏了0.5度。嗯,0.5度在10米外就是将近10厘米的偏差,对于目标跟踪来说,这误差已经大到没法忍了。

4.1 相机内参标定

相机内参标定,是所有标定的第一步。为什么?因为相机的外参是相对于相机坐标系来说的,而相机坐标系又依赖于内参。内参不准,外参就是空中楼阁。

内参标定说白了就是求四个东西:焦距(fx, fy)、光心(cx, cy)和畸变系数(k1, k2, p1, p2, k3)。我个人习惯用棋盘格标定板,OpenCV的cv2.calibrateCamera()函数就能搞定。

核心步骤:

  1. 打印一张棋盘格标定板(建议用A3纸,格子大小5cm以上)
  2. 从不同角度拍摄15-20张照片(注意:覆盖边缘和角落)
  3. 用OpenCV提取角点,进行标定
  4. 检查重投影误差,一般要小于0.5像素
import cv2
import numpy as np

# 棋盘格参数
CHECKERBOARD = (9, 6)  # 内角点数量
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# 准备世界坐标系中的点
objp = np.zeros((CHECKERBOARD[0] * CHECKERBOARD[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:CHECKERBOARD[0], 0:CHECKERBOARD[1]].T.reshape(-1, 2)

objpoints = []  # 3D点
imgpoints = []  # 2D点

images = glob.glob('calib_images/*.jpg')

for fname in images:
    img = cv2.imread(fname)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    ret, corners = cv2.findChessboardCorners(gray, CHECKERBOARD, None)
    
    if ret:
        objpoints.append(objp)
        corners2 = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1), criteria)
        imgpoints.append(corners2)

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

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

避坑指南:我曾经用手机拍标定板,结果发现重投影误差一直降不下去。后来才发现是照片分辨率太高,角点提取不稳定。建议把图片缩放到1000x800左右再标定,效果会好很多。

4.2 激光雷达与相机外参标定

激光雷达和相机的外参标定,说白了就是求一个旋转矩阵R和一个平移向量t,把激光雷达坐标系下的点云投影到相机图像上。

我常用的方法有两种:

  • 基于标定板的方法:在标定板上提取激光雷达的线特征和相机的角点特征,然后求解PnP问题
  • 基于自然特征的方法:利用场景中的边缘、角点等自然特征进行匹配

我个人更推荐第一种,因为稳定、可重复。具体做法是这样的:

  1. 放置一个棋盘格标定板(最好用亚克力板,表面平整)
  2. 激光雷达扫描标定板,提取板上的点云边缘
  3. 相机拍摄标定板,提取角点
  4. 用最小二乘法求解R和t
# 伪代码:激光雷达点云投影到图像
def project_lidar_to_image(points_3d, R, t, camera_matrix, dist_coeffs):
    # 将激光雷达点转换到相机坐标系
    points_cam = R @ points_3d.T + t.reshape(3, 1)
    
    # 投影到图像平面
    points_2d, _ = cv2.projectPoints(
        points_cam.T, 
        np.zeros((3,1)), 
        np.zeros((3,1)), 
        camera_matrix, 
        dist_coeffs
    )
    return points_2d

注意:激光雷达的点云是稀疏的,标定板上的点可能只有几十个。我曾经遇到过标定板太小,激光雷达扫不到足够点的情况。建议标定板尺寸至少30cm x 30cm,并且放在距离传感器3-5米的位置。

4.3 多雷达标定

多雷达标定,说白了就是让多个激光雷达「对齐」到同一个坐标系下。这在自动驾驶中很常见——车顶一个128线主雷达,车头两个补盲雷达,车尾再来一个。

多雷达标定有两种思路:

方法 原理 适用场景 精度
直接法 利用公共视场中的标定物,直接求解相对位姿 雷达之间有重叠视场 高(< 2cm)
间接法 先各自标定到车体坐标系,再推导相对关系 雷达之间无重叠视场 中等(< 5cm)

我个人习惯用直接法,因为精度更高。具体做法是:

  1. 在公共视场中放置多个标定物(比如三个不同高度的圆柱体)
  2. 每个雷达分别提取标定物的中心点
  3. 用ICP或SVD分解求解相对位姿
# 伪代码:多雷达标定中的SVD求解
def solve_relative_pose(points_src, points_tgt):
    # 去中心化
    centroid_src = np.mean(points_src, axis=0)
    centroid_tgt = np.mean(points_tgt, axis=0)
    
    points_src_centered = points_src - centroid_src
    points_tgt_centered = points_tgt - centroid_tgt
    
    # SVD分解
    H = points_src_centered.T @ points_tgt_centered
    U, S, Vt = np.linalg.svd(H)
    R = Vt.T @ U.T
    
    # 确保旋转矩阵行列式为+1
    if np.linalg.det(R) < 0:
        Vt[-1, :] *= -1
        R = Vt.T @ U.T
    
    t = centroid_tgt - R @ centroid_src
    return R, t

经验之谈:多雷达标定最容易出问题的地方是「时间同步」。我曾经遇到过两个雷达的扫描频率不一样(一个10Hz,一个20Hz),导致标定结果时好时坏。后来加了时间戳对齐,问题就解决了。建议在标定前先确认所有雷达的时钟是否同步。

4.4 标定结果验证

标定完了,怎么知道准不准?我一般做三件事:

  • 可视化验证:把激光雷达点云投影到图像上,看边缘是否对齐。如果车身边缘和图像中的边缘偏差超过3个像素,就要重新标定。
  • 距离验证:在已知距离的位置放一个目标,看融合后的距离误差。一般要求误差小于10cm。
  • 重复性验证:拆装传感器后重新标定,看结果是否一致。如果两次标定结果偏差超过1度,说明安装结构有问题。

核心要点总结:

  • 相机内参标定是基础,重投影误差要小于0.5像素
  • 激光雷达与相机外参标定,建议用标定板法,稳定可靠
  • 多雷达标定,优先用直接法,精度更高
  • 标定完成后一定要做验证,别偷懒
空间同步(标定)知识体系 相机内参标定 激光雷达与相机外参标定 多雷达标定 棋盘格标定板 角点提取 + 重投影误差 标定板法 / 自然特征法 PnP求解 + 点云投影验证 直接法 / 间接法 SVD分解 + ICP配准 标定结果验证 可视化验证 距离验证 重复性验证

好了,空间同步这块就聊到这儿。标定这东西,说白了就是「差之毫厘,谬以千里」。我见过太多项目因为标定没做好,后面融合算法写得再好也白搭。所以,别嫌麻烦,每一步都做扎实了,后面才能省心。

可可精品资料库