4. 风向数据处理:滑动平均滤波、中值滤波、卡尔曼滤波在风向处理中的应用

风向数据,说实话,是偏航控制里最让人头疼的信号之一。

为什么?因为风向本身就是一个随机性很强的物理量。你想想看,风从山坳里吹过来,被塔筒挡一下,被叶片搅一下,传到机舱顶部的风速仪时,早就不是那个"干净"的风向了。我见过太多现场,原始风向数据跳变超过30度,直接拿去用的话,偏航系统会像个没头苍蝇一样来回转。

所以,数据处理这一步,必须做。而且要做好。

今天我就把三种最常用的滤波方法掰开揉碎了讲。每种方法我都踩过坑,咱们直接说干货。

4.1 滑动平均滤波:简单但够用

滑动平均滤波,说白了就是取最近N个点的平均值。算法简单,计算量小,在嵌入式系统里跑起来毫无压力。

公式长这样:

θ_filtered(k) = (1/N) * Σ θ(k-i), i=0 to N-1

其中N是窗口长度。N越大,曲线越平滑,但延迟也越大。

我个人习惯,在陆上风电场,N取5到10就够了。海上风电,因为湍流强度大,我建议N取15到20。

我的经验: 滑动平均滤波最大的坑是——它会把真实的风向变化也平滑掉。比如阵风来的时候,风向本来应该快速变化,但滑动平均会把它"拉"成一条缓慢的曲线。偏航响应慢了,发电量就丢了。

代码实现也很直接:

// 滑动平均滤波
#define WINDOW_SIZE 10
float wind_buffer[WINDOW_SIZE];
int buffer_index = 0;
int buffer_count = 0;

float sliding_average_filter(float new_sample) {
    float sum = 0.0;
    wind_buffer[buffer_index] = new_sample;
    buffer_index = (buffer_index + 1) % WINDOW_SIZE;
    if (buffer_count < WINDOW_SIZE) buffer_count++;
    
    for (int i = 0; i < buffer_count; i++) {
        sum += wind_buffer[i];
    }
    return sum / buffer_count;
}

嗯,这里要注意:风向是0到360度的循环量。如果你直接平均,0度和359度平均出来是179.5度,但实际应该是359.5度。这个问题我当年在项目里栽过跟头。

解决办法是先把角度拆成sin和cos分量,分别平均,再用atan2算回来。

4.2 中值滤波:对付野值的利器

滑动平均滤波怕什么?怕野值。一个传感器瞬间跳变到180度,滑动平均会被带偏好一阵子。

中值滤波就不一样了。它取窗口内所有数据的中间值,对野值有天然的免疫力。

我举个例子:

原始数据:[352, 355, 358, 10, 356, 354, 357]

排序后:[10, 352, 354, 355, 356, 357, 358]

中值:355度

你看,那个10度的野值被完美过滤掉了。

我曾经踩过的坑: 中值滤波的窗口大小不能太大。N=5或N=7效果最好。窗口太大,计算量上去了,而且会丢失细节。我在一个海上项目里试过N=15,结果风向变化时的响应延迟了将近3秒,偏航系统根本跟不上。

代码实现:

// 中值滤波
#define MEDIAN_WINDOW 5

float median_filter(float new_sample) {
    static float buffer[MEDIAN_WINDOW];
    static int index = 0;
    float temp[MEDIAN_WINDOW];
    
    buffer[index] = new_sample;
    index = (index + 1) % MEDIAN_WINDOW;
    
    // 拷贝并排序
    memcpy(temp, buffer, sizeof(temp));
    // 简单的冒泡排序,N小的时候够用
    for (int i = 0; i < MEDIAN_WINDOW - 1; i++) {
        for (int j = 0; j < MEDIAN_WINDOW - 1 - i; j++) {
            if (temp[j] > temp[j+1]) {
                float t = temp[j];
                temp[j] = temp[j+1];
                temp[j+1] = t;
            }
        }
    }
    return temp[MEDIAN_WINDOW / 2];
}

中值滤波和滑动平均滤波,我一般搭配着用。先用中值滤波干掉野值,再用滑动平均做平滑。双剑合璧,效果很好。

4.3 卡尔曼滤波:动态系统的首选

前面两种方法都是静态的,它们假设风向在窗口内基本不变。但实际风向是动态变化的,有趋势,有噪声。

卡尔曼滤波就是为这种场景设计的。它能根据系统的动态模型,结合测量值,给出最优估计。

说白了,卡尔曼滤波会"猜"下一时刻的风向大概是多少,然后用实际测量值来修正这个猜测。猜得准,就少修正;猜不准,就多修正。

风向的卡尔曼滤波模型:

状态向量: x = [θ, ω]^T  (风向角,角速度)
状态方程: x(k) = A * x(k-1) + w(k-1)
测量方程: z(k) = H * x(k) + v(k)

其中:
A = [1, dt; 0, 1]
H = [1, 0]
Q = 过程噪声协方差
R = 测量噪声协方差

我个人建议,Q和R的调参是关键。Q设大了,滤波结果会跟着测量值剧烈波动;Q设小了,响应太慢。我一般先用一段离线数据跑一遍,找到合适的Q/R比值。

代码实现(简化版):

// 卡尔曼滤波 - 风向应用
typedef struct {
    float theta;  // 估计风向
    float omega;  // 估计角速度
    float P[2][2]; // 误差协方差矩阵
} KalmanState;

void kalman_init(KalmanState *ks, float init_theta) {
    ks->theta = init_theta;
    ks->omega = 0.0;
    ks->P[0][0] = 1.0; ks->P[0][1] = 0.0;
    ks->P[1][0] = 0.0; ks->P[1][1] = 1.0;
}

float kalman_update(KalmanState *ks, float z, float dt) {
    // 预测
    float theta_pred = ks->theta + ks->omega * dt;
    float omega_pred = ks->omega;
    
    // 预测协方差
    float P_pred[2][2];
    P_pred[0][0] = ks->P[0][0] + dt*ks->P[1][0] + dt*ks->P[0][1] + dt*dt*ks->P[1][1] + Q_theta;
    P_pred[0][1] = ks->P[0][1] + dt*ks->P[1][1];
    P_pred[1][0] = ks->P[1][0] + dt*ks->P[1][1];
    P_pred[1][1] = ks->P[1][1] + Q_omega;
    
    // 卡尔曼增益
    float K[2];
    float S = P_pred[0][0] + R;
    K[0] = P_pred[0][0] / S;
    K[1] = P_pred[1][0] / S;
    
    // 更新
    float innovation = z - theta_pred;
    // 处理0/360度跳变
    if (innovation > 180) innovation -= 360;
    if (innovation < -180) innovation += 360;
    
    ks->theta = theta_pred + K[0] * innovation;
    ks->omega = omega_pred + K[1] * innovation;
    
    // 更新协方差
    ks->P[0][0] = (1 - K[0]) * P_pred[0][0];
    ks->P[0][1] = (1 - K[0]) * P_pred[0][1];
    ks->P[1][0] = -K[1] * P_pred[0][0] + P_pred[1][0];
    ks->P[1][1] = -K[1] * P_pred[0][1] + P_pred[1][1];
    
    return ks->theta;
}
三种方法对比:
方法 计算量 延迟 抗野值 动态跟踪 适用场景
滑动平均 稳态风场
中值滤波 传感器异常多
卡尔曼滤波 湍流强、动态变化快

4.4 我的推荐方案

在实际项目中,我很少只用一种方法。我的惯用套路是:

  1. 第一步:中值滤波,窗口N=5,干掉传感器野值
  2. 第二步:卡尔曼滤波,做动态估计,输出平滑且响应快的风向
  3. 第三步:限幅,防止卡尔曼发散时输出异常值

这个组合拳,我在三个风场验证过,效果都很稳定。

避坑指南: 卡尔曼滤波的初始化很重要。如果初始风向和真实值差太多,滤波需要很长时间才能收敛。我建议在风机启动时,先用滑动平均跑10秒,把卡尔曼的状态初始化好,再切换到卡尔曼模式。

下面这张图,是我总结的风向数据处理流程,你可以参考:

风向数据处理流程 原始风向数据 中值滤波 (N=5) 卡尔曼滤波 限幅处理 滤波后风向

最后说一句,滤波参数不是一成不变的。不同风场,不同季节,甚至不同风速段,最优参数都不一样。我建议在风机调试阶段,留一个参数配置接口,方便现场微调。

好了,风向数据处理这块,核心就是这些。记住一个原则:滤波的目的是保留有用信息,去掉噪声,而不是把信号抹平。把握好这个度,你的偏航控制就成功了一半。

专注资料整理