4. 避障数据预处理:传感器数据滤波、坐标系对齐与异常值剔除

各位同学,欢迎来到避障算法落地的关键一环——数据预处理。

说实话,很多新手一上来就急着调避障逻辑,结果发现无人机要么乱晃,要么撞墙。为什么?因为传感器喂进来的数据是“脏”的。我见过太多团队在仿真里跑得飞起,一上真机就崩,十有八九是预处理没做好。

今天我们就来聊聊,怎么把这堆原始数据“洗”干净,让避障算法吃得舒服。

4.1 传感器数据滤波:中值滤波与卡尔曼滤波

传感器数据,尤其是超声波和红外这类低成本方案,噪声大得离谱。你想想看,一个超声波测距,可能因为多径反射突然跳变几十厘米。这种毛刺数据直接喂给避障算法,飞控会以为前方突然出现一堵墙,然后猛拉杆——后果嘛,你懂的。

4.1.1 中值滤波:简单粗暴,但有效

中值滤波的原理很简单:取连续N个采样值,排序后取中间那个。它能很好地干掉孤立的尖峰噪声。

我个人习惯在超声波和红外传感器上用窗口大小为5的中值滤波。为什么是5?太小了滤不干净,太大了延迟太高。避障讲究实时性,延迟超过100ms,飞机可能已经撞上了。

// 中值滤波示例(ArduPilot风格)
float median_filter(float new_sample, float buffer[], uint8_t size) {
    float temp[size];
    memcpy(temp, buffer, size * sizeof(float));
    // 将新样本插入缓冲区尾部,移除最旧样本
    for (uint8_t i = 0; i < size - 1; i++) {
        buffer[i] = buffer[i + 1];
    }
    buffer[size - 1] = new_sample;
    // 排序取中值
    for (uint8_t i = 0; i < size - 1; i++) {
        for (uint8_t j = i + 1; j < size; j++) {
            if (temp[i] > temp[j]) {
                float t = temp[i];
                temp[i] = temp[j];
                temp[j] = t;
            }
        }
    }
    return temp[size / 2];
}
我的经验:中值滤波有个坑——窗口大小必须是奇数,否则中值在两个数之间,你得自己决定取哪个。我一般取偏大的那个,因为避障场景下“宁远勿近”,报远一点至少不会触发急刹。

4.1.2 卡尔曼滤波:更平滑,但更“重”

如果你需要更平滑的估计,比如激光雷达或视觉里程计的数据,卡尔曼滤波是更好的选择。它本质上是一个“预测+修正”的过程:根据上一时刻的状态预测当前值,再用当前测量值去修正这个预测。

我记得第一次在Pixhawk上跑卡尔曼滤波时,算力差点爆了。后来发现,对于一维距离数据,完全可以用简化版——标量卡尔曼滤波,计算量小得多。

// 一维卡尔曼滤波(简化版)
typedef struct {
    float x;  // 状态估计
    float P;  // 估计误差协方差
    float Q;  // 过程噪声协方差
    float R;  // 测量噪声协方差
} Kalman1D;

float kalman_update(Kalman1D *kf, float z) {
    // 预测
    float x_pred = kf->x;
    float P_pred = kf->P + kf->Q;
    // 更新
    float K = P_pred / (P_pred + kf->R);
    kf->x = x_pred + K * (z - x_pred);
    kf->P = (1 - K) * P_pred;
    return kf->x;
}
注意:卡尔曼滤波的Q和R参数需要调。Q设太大,滤波结果会跟着测量值乱跳;Q设太小,响应又太慢。我一般先让飞机悬停,采集一段静态数据,把R设为测量噪声的方差,Q设为一个很小的数(比如0.01),然后慢慢往上加。

4.2 坐标系对齐:从机体坐标系到NED

传感器装在飞机上,测出来的数据是相对于飞机本身的——这叫机体坐标系(Body Frame)。但避障算法需要知道障碍物在地球坐标系下的位置,也就是NED坐标系(北东地)。

说白了,你得把传感器看到的“前方1米”转换成“北方向0.5米,东方向0.8米,下方0.1米”。

4.2.1 旋转矩阵

这个转换靠的是旋转矩阵。飞机的姿态(横滚、俯仰、偏航)决定了机体坐标系和NED坐标系之间的旋转关系。

// 机体坐标系到NED坐标系的转换
// 输入:机体坐标系下的向量 (x_b, y_b, z_b)
// 输出:NED坐标系下的向量 (x_n, y_n, z_n)
// 姿态角:roll (φ), pitch (θ), yaw (ψ)

void body_to_ned(float x_b, float y_b, float z_b,
                 float roll, float pitch, float yaw,
                 float *x_n, float *y_n, float *z_n) {
    float cφ = cos(roll), sφ = sin(roll);
    float cθ = cos(pitch), sθ = sin(pitch);
    float cψ = cos(yaw), sψ = sin(yaw);

    // 旋转矩阵 R_b^n
    *x_n = (cθ*cψ)*x_b + (sφ*sθ*cψ - cφ*sψ)*y_b + (cφ*sθ*cψ + sφ*sψ)*z_b;
    *y_n = (cθ*sψ)*x_b + (sφ*sθ*sψ + cφ*cψ)*y_b + (cφ*sθ*sψ - sφ*cψ)*z_b;
    *z_n = (-sθ)*x_b + (sφ*cθ)*y_b + (cφ*cθ)*z_b;
}

核心要点:坐标系转换的精度直接取决于姿态估计的精度。如果你的飞控姿态解算有偏差,那转换后的障碍物位置也会跟着偏。我曾经遇到过因为磁罗盘没校准好,偏航角差了5度,结果避障算法总是往左边躲——后来查了半天才发现是坐标系没对齐。

4.2.2 实战中的坑

嗯,这里要注意:ArduPilot内部已经帮你做了大部分坐标系转换工作。你从AP_Proximity库读到的距离数据,默认已经是NED坐标系下的了。但如果你自己写传感器驱动,或者用第三方库,一定要确认坐标系。

我习惯在代码里加一个坐标系检查函数,打印出原始数据和转换后的数据,悬停时对比一下——如果飞机水平静止,机体坐标系和NED坐标系应该重合,转换前后数据应该一致。

4.3 异常值剔除:别让一个坏数据毁了整个避障

滤波只能平滑噪声,但挡不住传感器彻底“抽风”。比如激光雷达打到透明玻璃上,直接返回一个无效值;或者超声波被螺旋桨气流干扰,测出个负数。

这些异常值必须剔除,否则避障算法会做出荒谬的决策。

4.3.1 阈值法

最简单的方法:设定一个合理范围。比如超声波的有效测距范围是20cm到500cm,那小于20cm或大于500cm的数据直接扔掉。

// 阈值剔除
bool is_valid_range(float distance, float min_range, float max_range) {
    return (distance >= min_range) && (distance <= max_range);
}

4.3.2 一致性检查

更高级一点:结合多个传感器做一致性检查。比如,如果激光雷达说前方2米有障碍,但超声波说前方0.5米,那肯定有一个是错的。我一般会取两个传感器的加权平均,权重根据传感器的置信度来定。

传感器类型 典型有效范围 常见异常 剔除策略
超声波 20cm - 500cm 多径反射、螺旋桨干扰 阈值 + 中值滤波
红外 10cm - 150cm 阳光干扰、表面反射 阈值 + 一致性检查
激光雷达 5cm - 40m 玻璃、镜面、雨雾 阈值 + 卡尔曼滤波
视觉 30cm - 20m 光照变化、纹理缺失 置信度阈值 + 时间一致性

4.3.3 时间连续性检查

还有一种情况:数据突然跳变,但又不是完全离谱。比如上一帧测距是2米,下一帧突然变成0.5米,然后又跳回2米。这种大概率是干扰。

我常用的方法是:如果当前值和上一帧的差值超过一个阈值(比如0.3米),就暂时不信任这个数据,而是用上一帧的值代替,直到连续3帧都稳定在新值上。

// 时间连续性检查
float temporal_consistency(float current, float previous, float max_delta) {
    if (fabs(current - previous) > max_delta) {
        // 跳变过大,暂时不信任
        return previous;
    }
    return current;
}
我曾经踩过的坑:有一次在室内测试,无人机突然朝天花板猛冲。查了半天,发现是超声波传感器测到了地板反射回来的二次回波,报了个很远的距离。从那以后,我强制要求所有传感器数据必须经过“最小有效距离”检查——低于飞机自身高度的数据直接丢弃。

4.4 本章知识体系

下面这张图总结了避障数据预处理的完整流程。你可以把它当作一个检查清单,每次集成新传感器时,按这个流程走一遍,基本不会出大问题。

避障数据预处理流程 原始传感器数据 第一步:传感器数据滤波 中值滤波(孤立尖峰) | 卡尔曼滤波(平滑估计) 第二步:坐标系对齐 机体坐标系(Body Frame) → NED坐标系(北东地) 第三步:异常值剔除 阈值法 | 一致性检查 | 时间连续性检查 干净的NED坐标系数据

好了,这一章的内容就到这里。数据预处理是避障算法的基石,地基没打好,上面盖的房子再漂亮也得塌。下一章我们会把这些处理好的数据喂给避障算法,看看它们是怎么做出“向左躲”还是“向右躲”的决策的。


专注资料整理