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突然跳变。这些异常值如果不处理,会严重干扰后续的融合算法。
常用的方法有三种:
- 统计方法:计算均值和标准差,超过3倍标准差的数据直接扔掉。
- IQR方法:四分位距法,适合非正态分布的数据。
- 基于模型的方法:比如用卡尔曼滤波的残差来判断。
我个人最常用的是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 实战流程总结
好了,咱们把整个流程串起来。一个完整的数据预处理管线大概是这样的:
- 时间戳对齐:统一时间基准,做插值或最近邻匹配。
- 坐标系统一:把所有传感器数据转换到同一个坐标系。
- 异常值检测:用IQR或统计方法剔除坏数据。
- 缺失值插补:用线性插值或前向填充补全空洞。
每一步做完,都建议做一次可视化验证。你想想看,数据预处理就像做饭前的洗菜切菜——菜没洗干净,后面厨艺再好也白搭。
最后说一句:数据预处理没有银弹。不同的传感器、不同的场景,处理方式都不一样。我的建议是——先跑通一个简单的管线,然后根据实际效果不断迭代优化。别想着一步到位,那是不可能的。
下一章,咱们聊聊特征提取与融合策略。到时候见。