4. 数据预处理与清洗:时间戳对齐、坐标系统一、异常值检测与剔除、缺失值插补

各位同学,欢迎来到第四讲。

上一章我们聊了传感器选型和数据采集。数据是拿到了,但说实话,那会儿的数据基本没法直接用。为什么?因为每个传感器都有自己的“脾气”——采样频率不一样、坐标系各玩各的、偶尔还给你蹦出几个离谱的异常值。

我刚开始做多模态融合的时候,就吃过这个亏。把激光雷达和摄像头的数据直接怼进模型,结果定位精度一塌糊涂。排查了两天,最后发现是时间戳差了200毫秒。嗯,200毫秒,车都跑出去好几米了。

所以今天这章,咱们就把数据预处理这件事彻底讲透。

4.1 时间戳对齐:让所有传感器“说同一种时间”

多模态系统里,传感器各有各的时钟。摄像头可能是30fps,激光雷达是10Hz,IMU是200Hz。它们的时间基准不一样,直接融合就是灾难。

核心思路就两条:硬同步和软同步。

  • 硬同步:硬件层面给一个统一的触发信号。比如用GPS的PPS(秒脉冲)去同步所有传感器。精度高,但硬件成本也高。
  • 软同步:软件层面做插值或最近邻匹配。适合低成本方案。

我个人习惯的做法

如果条件允许,优先做硬同步。实在不行,就用“线性插值+最近邻”的组合拳。具体来说,以最低帧率的传感器为基准,对其他传感器的时间戳做线性插值,找到最接近的那个数据帧。

举个例子,激光雷达10Hz(每100ms一帧),摄像头30Hz(每33ms一帧)。对齐时,以激光雷达的时间戳为基准,去摄像头数据里找时间差最小的那一帧。

def align_timestamps(lidar_ts, cam_ts, cam_data):
    """
    以激光雷达时间戳为基准,对齐摄像头数据
    lidar_ts: 激光雷达时间戳列表
    cam_ts: 摄像头时间戳列表
    cam_data: 摄像头数据列表
    """
    aligned_data = []
    for lt in lidar_ts:
        # 找时间差最小的摄像头帧
        idx = np.argmin(np.abs(np.array(cam_ts) - lt))
        aligned_data.append(cam_data[idx])
    return aligned_data

避坑指南:我曾经遇到过一个问题——两个传感器的时间戳虽然对齐了,但它们的时钟漂移率不一样。比如一个走快了0.1%,半小时后就差了180ms。解决办法是定期做一次时钟同步校准,或者用滑动窗口动态调整偏移量。

4.2 坐标系统一:别让“左右”搞反了

每个传感器都有自己的坐标系。激光雷达通常是“前-左-上”,摄像头是“右-下-前”,IMU又是另一套。如果不统一,融合出来的结果就是“四不像”。

标准做法:定义一个世界坐标系(比如车体坐标系),然后把所有传感器的数据都转换到这个坐标系下。

转换公式其实不复杂,就是旋转矩阵加平移向量:

P_world = R * P_sensor + T

其中R是旋转矩阵,T是平移向量。这两个参数需要通过标定得到。

传感器 原始坐标系 转换到车体坐标系
激光雷达 前-左-上 旋转+平移
摄像头 右-下-前 旋转+平移
IMU 右-前-上 旋转+平移

注意:坐标系转换最容易出错的点是“旋转方向”。比如绕Z轴旋转90度,是顺时针还是逆时针?我建议每次转换后都做一次可视化验证——把点云投影到图像上,看看边缘是否对齐。肉眼一看就知道对不对。

4.3 异常值检测与剔除:把“坏数据”揪出来

传感器不是完美的。偶尔会蹦出几个离谱的值——比如激光雷达打到玻璃上反射出负数,或者IMU突然跳变。这些异常值如果不处理,会严重干扰后续的融合算法。

常用的方法有三种

  1. 统计方法:计算均值和标准差,超过3倍标准差的数据直接扔掉。
  2. IQR方法:四分位距法,适合非正态分布的数据。
  3. 基于模型的方法:比如用卡尔曼滤波的残差来判断。

我个人最常用的是IQR方法,因为它对数据分布不敏感,鲁棒性好。

def remove_outliers_iqr(data):
    """
    使用IQR方法剔除异常值
    """
    Q1 = np.percentile(data, 25)
    Q3 = np.percentile(data, 75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    return data[(data >= lower_bound) & (data <= upper_bound)]

我遇到过的一个坑:有一次做激光雷达点云处理,用统计方法剔除异常值,结果把远处的真实点也给剔了。后来发现是因为点云密度不均匀——近处点密,远处点稀。解决办法是分区域做异常值检测,不同距离用不同的阈值。

4.4 缺失值插补:数据丢了怎么办?

传感器偶尔会丢帧。比如摄像头被遮挡了一瞬间,或者激光雷达的某个扫描线没收到回波。这时候数据就出现了“空洞”。

插补方法的选择取决于数据特性

  • 线性插值:适合变化平缓的数据,比如温度、位置。
  • 样条插值:适合有曲线特征的数据,比如轨迹。
  • 最近邻插值:适合离散数据,比如分类标签。
  • 基于模型的插值:比如用卡尔曼滤波预测缺失值。

我建议优先用线性插值,简单高效。如果数据变化剧烈,再考虑样条插值。

def linear_interpolate(data, missing_indices):
    """
    对缺失位置做线性插值
    data: 原始数据(含NaN)
    missing_indices: 缺失值的位置索引
    """
    known_indices = np.where(~np.isnan(data))[0]
    known_values = data[known_indices]
    interpolated = np.interp(missing_indices, known_indices, known_values)
    return interpolated

一个小技巧:如果缺失值连续出现很多个(比如连续丢了10帧),线性插值就不太靠谱了。这时候我会用“前向填充”——用最后一个有效值填充后面的缺失值,直到新的有效数据出现。虽然粗糙,但在实时系统中很实用。

4.5 实战流程总结

好了,咱们把整个流程串起来。一个完整的数据预处理管线大概是这样的:

  1. 时间戳对齐:统一时间基准,做插值或最近邻匹配。
  2. 坐标系统一:把所有传感器数据转换到同一个坐标系。
  3. 异常值检测:用IQR或统计方法剔除坏数据。
  4. 缺失值插补:用线性插值或前向填充补全空洞。

每一步做完,都建议做一次可视化验证。你想想看,数据预处理就像做饭前的洗菜切菜——菜没洗干净,后面厨艺再好也白搭。

最后说一句:数据预处理没有银弹。不同的传感器、不同的场景,处理方式都不一样。我的建议是——先跑通一个简单的管线,然后根据实际效果不断迭代优化。别想着一步到位,那是不可能的。

下一章,咱们聊聊特征提取与融合策略。到时候见。