3、数据采集与预处理:传感器数据滤波与对齐

各位同学,大家好。今天我们来聊聊温室环境里一个特别实在的话题——数据采集与预处理。

说实话,很多刚入行的朋友觉得传感器数据嘛,拿来就用呗。我在项目里吃过这个亏。有一次在山东的番茄大棚,传感器读数跳得跟心电图似的,直接导致灌溉系统误动作。嗯,从那以后,我养成了一个习惯:数据到手,先滤波,再对齐,最后才敢用

这一节,我们就来拆解三个核心动作:数据滤波、异常值剔除、时间戳对齐

3.1 传感器数据为什么需要滤波?

你想想看,温室里的传感器暴露在高温、高湿、强电磁干扰的环境下。一个温湿度探头,可能因为风机启动瞬间的电压波动,读数就跳变几度。这种毛刺数据,如果不处理,边缘计算节点就会做出错误决策。

我个人习惯把滤波分为两类:

  • 硬件滤波:在传感器端加RC低通滤波电路,简单粗暴,但效果有限。
  • 软件滤波:在边缘计算节点上通过算法处理,灵活可控,是我们今天的主角。

核心原则:滤波不是把真实信号抹平,而是把噪声和干扰去掉。保留趋势,剔除毛刺。

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

中值滤波是我在温室项目里用得最多的方法之一。为什么?因为它对脉冲噪声(比如传感器突然跳变)的抑制效果特别好。

原理:取连续N个采样值,排序后取中间值作为当前输出。

举个例子,温度传感器连续5次采样:25.1, 25.3, 35.8, 25.2, 25.0。明显35.8是异常值。排序后:25.0, 25.1, 25.2, 25.3, 35.8。取中间值25.2,完美剔除了干扰。

代码实现也很简单:

// 中值滤波示例(C语言,适用于边缘计算节点)
#define FILTER_WINDOW 5

float median_filter(float new_sample) {
    static float buffer[FILTER_WINDOW];
    static int index = 0;
    float temp[FILTER_WINDOW];
    float median;
    
    // 环形缓冲区
    buffer[index] = new_sample;
    index = (index + 1) % FILTER_WINDOW;
    
    // 拷贝并排序
    memcpy(temp, buffer, sizeof(temp));
    // 冒泡排序(窗口小,效率可接受)
    for (int i = 0; i < FILTER_WINDOW - 1; i++) {
        for (int j = 0; j < FILTER_WINDOW - i - 1; j++) {
            if (temp[j] > temp[j+1]) {
                float t = temp[j];
                temp[j] = temp[j+1];
                temp[j+1] = t;
            }
        }
    }
    
    median = temp[FILTER_WINDOW / 2];
    return median;
}

经验之谈:窗口大小选3或5就够了。窗口太大,信号延迟明显,温室控制会滞后。我曾经试过窗口7,结果温度变化响应慢了半拍,补光策略全乱了。

3.3 卡尔曼滤波:更平滑,更智能

中值滤波虽然好,但它有个问题——对缓慢漂移的噪声效果一般。这时候,卡尔曼滤波就派上用场了。

说白了,卡尔曼滤波是一种最优估计算法。它根据上一时刻的状态和当前时刻的观测值,做一个加权平均,权重由噪声的统计特性决定。

在温室场景下,我主要用它来处理二氧化碳浓度光照强度这类变化相对平缓、但测量噪声较大的信号。

简化版的一维卡尔曼滤波公式:

// 一维卡尔曼滤波(适用于边缘计算)
typedef struct {
    float Q;  // 过程噪声协方差
    float R;  // 测量噪声协方差
    float x;  // 估计值
    float P;  // 估计误差协方差
    float K;  // 卡尔曼增益
} KalmanFilter;

void kalman_init(KalmanFilter *kf, float init_value) {
    kf->Q = 0.01;   // 根据传感器特性调整
    kf->R = 0.1;    // 根据传感器手册设定
    kf->x = init_value;
    kf->P = 1.0;
}

float kalman_update(KalmanFilter *kf, float measurement) {
    // 预测
    kf->P = kf->P + kf->Q;
    
    // 更新
    kf->K = kf->P / (kf->P + kf->R);
    kf->x = kf->x + kf->K * (measurement - kf->x);
    kf->P = (1 - kf->K) * kf->P;
    
    return kf->x;
}

注意:卡尔曼滤波的参数Q和R需要根据实际传感器调试。Q设太大,滤波结果会跟随噪声;R设太大,响应会变迟钝。我一般先看传感器手册,再现场微调。

3.4 异常值剔除:别让坏数据污染决策

滤波能平滑噪声,但遇到传感器彻底故障或者通信丢包,滤波也无能为力。这时候需要异常值剔除

我常用的方法有两种:

  1. 3σ原则:假设数据服从正态分布,超出均值±3倍标准差的数据视为异常。
  2. 箱线图法:基于四分位数,超出Q1-1.5IQR或Q3+1.5IQR的数据剔除。

在边缘计算节点上,我推荐用滑动窗口+3σ的组合。因为温室环境变化慢,窗口内数据基本稳定。

// 滑动窗口异常值剔除
#define WINDOW_SIZE 10

int is_outlier(float new_value, float *window, int count) {
    if (count < 3) return 0;  // 数据太少,不判断
    
    float sum = 0, mean, std;
    for (int i = 0; i < count; i++) sum += window[i];
    mean = sum / count;
    
    float sq_sum = 0;
    for (int i = 0; i < count; i++) {
        sq_sum += (window[i] - mean) * (window[i] - mean);
    }
    std = sqrt(sq_sum / count);
    
    // 3σ判断
    if (fabs(new_value - mean) > 3 * std) {
        return 1;  // 异常
    }
    return 0;
}

避坑指南:我曾经在夏季高温大棚里,温度正常波动就很大。3σ阈值设得太死,把正常的高温数据也剔除了,导致降温系统启动延迟。后来我把阈值放宽到4σ,同时结合变化率判断,效果好了很多。

3.5 时间戳对齐:多传感器融合的基础

温室里通常有多个传感器:温度、湿度、光照、CO₂、土壤水分……它们采样频率可能不同。有的1秒一次,有的10秒一次。如果不做时间戳对齐,融合出来的数据就是乱的。

我常用的对齐策略:

方法 适用场景 优点 缺点
最近邻插值 采样频率差异不大 计算量小 精度一般
线性插值 变化平缓的信号 精度较高 需要额外计算
时间戳同步 所有传感器统一时钟 最准确 需要硬件支持

在边缘计算节点上,我推荐线性插值。因为温室环境变化慢,线性插值足够用,而且计算量适中。

// 线性插值对齐时间戳
// 已知t1时刻的值为v1,t2时刻的值为v2
// 求t时刻的值(t1 <= t <= t2)
float linear_interpolate(float t1, float v1, float t2, float v2, float t) {
    if (fabs(t2 - t1) < 0.001) return v1;  // 防止除零
    float ratio = (t - t1) / (t2 - t1);
    return v1 + ratio * (v2 - v1);
}

我的习惯:在项目里,我会给每个传感器数据包打上本地时间戳(边缘节点的时间)。然后统一以1秒为基准,对所有传感器做线性插值对齐。这样后续的融合算法就不用操心时间不一致的问题了。

3.6 实战建议:滤波与对齐的流水线

最后,我分享一下在温室项目里常用的数据处理流水线:

  1. 原始数据 → 2. 异常值剔除(3σ滑动窗口) → 3. 中值滤波(窗口5) → 4. 卡尔曼滤波(可选,用于CO₂等信号) → 5. 时间戳对齐(线性插值到1秒基准) → 6. 存入本地数据库

这个流水线在STM32和树莓派上都跑过,效果稳定。你想想看,如果跳过这些步骤,直接用原始数据做决策,那跟闭着眼睛开车有什么区别?

好了,这一节的内容就到这里。下一节我们会讲边缘计算节点的数据存储与本地决策,到时候会用到今天讲的这些预处理数据。记得动手把代码跑一遍,有问题随时交流。