4. 数据预处理:时间戳同步、坐标变换、去畸变、点云滤波与降采样

数据预处理,说白了就是给原始数据「洗澡」。你采集回来的数据,时间对不上、坐标不统一、图像有畸变、点云全是噪点…… 直接喂给算法,结果肯定一塌糊涂。我刚开始做自动驾驶时,就吃过这个亏——数据没对齐,回放出来的轨迹直接飞到了马路牙子上。所以这一章,咱们把预处理这五个硬骨头啃下来。

数据预处理核心流程 原始多模态数据 时间戳同步 坐标变换 去畸变 点云滤波 降采样 对齐后的干净数据 处理步骤 输出结果

4.1 时间戳同步——让所有传感器「说同一种时间」

多传感器融合的第一步,就是时间对齐。相机、激光雷达、IMU、GPS,每个设备都有自己的时钟,漂移个几毫秒,在高速场景下就是几米的误差。我个人习惯用 硬件同步 + 软件插值 双保险。

  • 硬件同步:通过PPS信号或IEEE 1588协议,让所有传感器共享同一个时钟源。这是最可靠的方式,但需要硬件支持。
  • 软件插值:当硬件同步不可用时,根据时间戳线性插值。比如激光雷达10Hz,相机20Hz,把相机帧插到激光雷达的时间轴上。
💡 我的经验: 我在项目中遇到过GPS时间戳跳变的情况,原因是GPS信号短暂丢失后恢复时,时间戳回跳了100ms。后来我加了异常检测——如果相邻时间戳差超过阈值,直接丢弃该帧,避免污染整个序列。
# 伪代码:基于最近邻的时间戳匹配
def sync_sensors(lidar_ts, camera_ts):
    matched = []
    for lt in lidar_ts:
        # 找到与lt最接近的相机时间戳
        idx = np.argmin(np.abs(camera_ts - lt))
        if abs(camera_ts[idx] - lt) < 5e-3:  # 5ms阈值
            matched.append((lt, camera_ts[idx]))
    return matched

4.2 坐标变换——把数据塞进同一个「世界坐标系」

每个传感器都有自己的坐标系。激光雷达是三维直角坐标,相机是像素坐标,IMU是机体坐标系。你想想看,如果不做变换,算法根本不知道「前方障碍物」在哪个位置。

我一般维护一个 坐标变换树(TF树),把所有传感器都挂到车体坐标系下,再统一转到世界坐标系。核心就是四个变换:

  • 传感器 → 车体:外参标定得到的旋转平移矩阵。
  • 车体 → 世界:根据GPS/IMU推算的位姿。
  • 像素 ↔ 相机坐标系:内参矩阵 + 畸变参数。
  • 时间同步后的动态变换:考虑车辆运动,对每个点做运动补偿。
⚠️ 注意:坐标变换一定要用齐次坐标(4x4矩阵),否则平移和旋转混在一起容易出错。我曾经因为忘记转齐次坐标,调试了整整两天……
# 使用齐次矩阵进行坐标变换
def transform_point(point, tf_matrix):
    # point: [x, y, z, 1]
    return tf_matrix @ point

4.3 去畸变——让图像和点云「恢复真实形状」

相机镜头有径向畸变和切向畸变,激光雷达在运动过程中也会产生点云畸变(因为扫描一圈需要时间,车在动)。去畸变就是把这些扭曲拉回来。

  • 图像去畸变:用标定得到的内参和畸变系数,对每个像素重新映射。OpenCV的 cv2.undistort() 一步搞定。
  • 点云去畸变:利用IMU或里程计数据,把每个点的时间戳对应的位姿算出来,然后补偿到起始时刻。说白了就是「运动补偿」。
⚠️ 避坑指南: 我曾经在点云去畸变时,直接用线性插值代替IMU数据,结果弯道场景下点云出现了「拖尾」现象。后来老老实实用了高频IMU,效果才正常。记住:运动补偿的精度直接取决于位姿插值的精度。

4.4 点云滤波——把「脏数据」筛出去

激光雷达点云里有很多噪点:地面反射、雨雾、传感器本身的异常点。不滤波的话,后续的障碍物检测会多出无数假目标。我常用的滤波器有:

滤波器作用适用场景
直通滤波保留指定范围(如Z轴 -1.5~3m)去除地面和过高物体
体素滤波用体素网格降采样 + 去噪均匀化点云密度
统计滤波去除离群点(基于邻域距离统计)去除孤立噪点
半径滤波如果某点周围点数少于阈值则剔除去除稀疏噪点

我个人习惯先做直通滤波,把明显不相关的区域切掉,再用统计滤波去除离群点。这样计算量小,效果也干净。

4.5 降采样——给点云「瘦身」

64线激光雷达一帧可能有几十万个点,直接处理太慢了。降采样不是随便丢点,而是要在保持几何结构的前提下减少点数。最常用的是 体素降采样:把空间分成小立方体,每个立方体只保留一个重心点。

import open3d as o3d

pcd = o3d.io.read_point_cloud("frame.pcd")
# 体素降采样,体素边长0.1m
downsampled = pcd.voxel_down_sample(voxel_size=0.1)
print(f"原始点数: {len(pcd.points)}, 降采样后: {len(downsampled.points)}")
💡 小技巧: 体素大小不是固定的。近处场景用0.05m,远处用0.2m,既能保证近处精度,又能减少远处冗余点。我在高速场景下常用自适应体素,效果比固定体素好不少。

好了,数据预处理这五步走完,你的数据就「干净」了。下一阶段就可以放心地喂给真值系统或者回放引擎。嗯,这里要再啰嗦一句:预处理的质量直接决定闭环验证的可信度,千万别图省事跳过某一步。