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 异常值剔除:别让坏数据污染决策
滤波能平滑噪声,但遇到传感器彻底故障或者通信丢包,滤波也无能为力。这时候需要异常值剔除。
我常用的方法有两种:
- 3σ原则:假设数据服从正态分布,超出均值±3倍标准差的数据视为异常。
- 箱线图法:基于四分位数,超出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 实战建议:滤波与对齐的流水线
最后,我分享一下在温室项目里常用的数据处理流水线:
- 原始数据 → 2. 异常值剔除(3σ滑动窗口) → 3. 中值滤波(窗口5) → 4. 卡尔曼滤波(可选,用于CO₂等信号) → 5. 时间戳对齐(线性插值到1秒基准) → 6. 存入本地数据库
这个流水线在STM32和树莓派上都跑过,效果稳定。你想想看,如果跳过这些步骤,直接用原始数据做决策,那跟闭着眼睛开车有什么区别?
好了,这一节的内容就到这里。下一节我们会讲边缘计算节点的数据存储与本地决策,到时候会用到今天讲的这些预处理数据。记得动手把代码跑一遍,有问题随时交流。