第4章:传感器标定基础

4.1 摄像头内参标定——张正友标定法

说到传感器标定,我第一个想聊的就是摄像头内参标定。你想想看,摄像头装上车以后,它看到的图像是扭曲的,像素点和真实世界的点之间有个映射关系。这个关系搞不清楚,后面的感知算法全是白搭。

张正友标定法,说白了就是用一张棋盘格照片,通过数学计算把摄像头的内参矩阵和畸变系数算出来。这个方法我用了好多年,稳定、实用,而且不需要昂贵的设备。

核心原理

摄像头成像模型可以简化为:

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

其中K就是内参矩阵:

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

fx、fy是焦距参数,cx、cy是主点偏移。这些参数决定了像素坐标和相机坐标之间的转换关系。

张正友的方法巧妙在哪呢?它假设标定板是平面(Z=0),这样就把三维问题降成了二维问题。然后通过拍摄不同角度的棋盘格照片,利用单应性矩阵来求解内参。

关键步骤:

  1. 打印一张棋盘格标定板,贴在平面上
  2. 用摄像头从不同角度拍摄15-20张照片
  3. 检测每张照片中的角点坐标
  4. 利用角点坐标和已知的棋盘格尺寸,计算单应性矩阵
  5. 通过多张照片的单应性矩阵,求解内参矩阵
  6. 用最小二乘法优化畸变系数

实际操作中的坑

我在项目中遇到过最头疼的问题,就是标定板不平整。有一次在实验室里标定得好好的,换到车上就全偏了。后来发现是标定板贴在车门上,车门本身有弧度。嗯,这里要注意:标定板一定要贴在绝对平整的表面上。

避坑指南:

  • 标定板不要太小,建议A3大小以上
  • 照片要覆盖视野的各个角落
  • 避免纯正面拍摄,要有倾斜角度
  • 光照要均匀,不要有反光

代码示例

用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)

# 存储所有照片的角点
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)

标定完成后,记得用重投影误差来评估标定质量。我一般要求重投影误差小于0.5像素,超过这个值就要重新拍了。

4.2 激光雷达与摄像头联合标定原理

摄像头和激光雷达,一个给颜色信息,一个给深度信息。但这两个传感器的坐标系不一样,得把它们对齐。联合标定要解决的就是这个问题——找到激光雷达坐标系到摄像头坐标系之间的旋转矩阵R和平移向量t。

标定原理

联合标定的核心公式很简单:

P_cam = R * P_lidar + t

P_lidar是激光雷达坐标系下的3D点,P_cam是相机坐标系下的3D点。我们需要找到R和t,让激光点云投影到图像上时,和图像中的对应物体重合。

具体怎么做呢?我习惯用标定板来做。在标定板上放几个特征点,激光雷达能扫到这些点的距离,摄像头能拍到这些点的像素位置。然后通过PnP算法求解R和t。

我的经验:

标定板最好用反光材料做,激光雷达的回波强度会更高,更容易检测到边缘。我曾经用普通纸板做过一次,结果激光点打在纸板上回波太弱,边缘检测不准,标定结果差了5个像素。

标定流程

步骤 操作 注意事项
1 放置标定板 距离传感器5-10米,角度多样化
2 采集数据 同时记录激光点云和图像
3 提取特征 检测标定板在点云和图像中的位置
4 求解外参 使用最小二乘法或RANSAC
5 验证结果 投影点云到图像,检查对齐精度

验证方法

标定完成后,怎么知道结果对不对?我一般用两种方法验证:

  1. 视觉验证:把激光点云投影到图像上,看边缘是否对齐。比如建筑物的边缘、路沿、车辆轮廓,这些特征在点云和图像中都很明显。
  2. 定量验证:计算投影误差。选几个特征点,手动标注它们在图像中的位置,和投影位置做对比。误差在2-3个像素以内就算合格。

投影验证代码:

def project_lidar_to_image(points_3d, R, t, K, dist_coeffs):
    # 将激光点转换到相机坐标系
    points_cam = (R @ points_3d.T + t.reshape(3,1)).T
    
    # 只保留相机前方的点
    mask = points_cam[:, 2] > 0
    points_cam = points_cam[mask]
    
    # 投影到图像平面
    points_2d, _ = cv2.projectPoints(
        points_cam, np.zeros(3), np.zeros(3), K, dist_coeffs
    )
    
    return points_2d.reshape(-1, 2)

常见问题

联合标定中我踩过最大的坑,是时间同步问题。激光雷达和摄像头的数据采集时间不一致,导致标定板在移动时,点云和图像对应不上。后来我加了一个硬件触发信号,让两个传感器同时采集,问题就解决了。

还有一个坑:标定板的尺寸要足够大。太小的话,激光雷达扫到的点太少,拟合出来的平面误差大。我建议标定板边长至少1米,这样激光雷达能扫到100个点以上。

重要提醒:

联合标定不是一次性的工作。传感器在车辆运行中可能会发生微小的位移,建议每3个月重新标定一次。如果车辆发生过碰撞或传感器拆装,必须立即重新标定。

好了,传感器标定基础就聊到这里。下一章我们会深入讨论激光雷达和摄像头的在线标定方法,以及如何在实车调试中快速验证标定结果。