中值滤波与均值滤波——传感器数据稳定的必备算法
·
DS18B20第一次读数可能是85°C,超声波测距偶尔会出现异常值,光敏电阻的ADC读数会随机跳动。传感器数据不稳定是常态,滤波算法就是用来对付这些问题的。
项目 Driver/filtering.c 里实现了两种最常用的滤波算法,这一篇完整解析它们的原理和代码实现。
什么时候需要滤波?
| 噪声类型 | 来源 | 典型表现 |
|---|---|---|
| 高频随机噪声 | 电磁干扰、电源纹波 | 读数在小范围内随机跳动 |
| 偶发脉冲干扰 | 静电、接触不良 | 偶尔出现一个极端异常值 |
| 环境渐变 | 温度漂移 | 读数有缓慢的趋势性变化 |
中值滤波擅长处理脉冲干扰(比如超声波偶尔读到的异常大值),均值滤波擅长平滑高频噪声。它们各有所长,不能互相替代。
中值滤波:抗脉冲干扰的利器
原理
取最近N次采样值,排序,取中间的那个作为输出。
原始数据:[25.2, 25.1, 85.0, 25.3, 25.2] ← 85.0是异常值
排序后: [25.1, 25.2, 25.2, 25.3, 85.0]
取中值: 25.2 ← 异常值被完美过滤!
无论异常值有多大,只要它不占多数,中值滤波都能把它过滤掉。这是均值滤波做不到的。
完整代码(项目实际代码)
#define N 5 // 滤波窗口大小(必须为奇数)
pdata float median_data_array[5] = {0}; // 环形缓冲区
pdata unsigned char median_data_index = 0; // 写入指针
pdata unsigned char median_data_count = 0; // 有效数据计数
float Median_Filter(float New_Data) {
unsigned char i, j;
float Median_Filter_Temp_Array[N] = {0}; // 排序用临时数组
float temp;
/* 第一步:新数据入队 */
median_data_array[median_data_index++] = New_Data;
median_data_index %= N; // 循环覆盖最旧数据
/* 第二步:更新有效数据计数 */
if(median_data_count < N)
median_data_count++;
/* 第三步:复制到临时数组 */
for(i = 0; i < median_data_count; i++) {
Median_Filter_Temp_Array[i] = median_data_array[i];
}
/* 第四步:冒泡排序 */
for(i = 0; i < median_data_count; i++) {
for(j = i + 1; j < median_data_count; j++) {
if(Median_Filter_Temp_Array[i] > Median_Filter_Temp_Array[j]) {
temp = Median_Filter_Temp_Array[i];
Median_Filter_Temp_Array[i] = Median_Filter_Temp_Array[j];
Median_Filter_Temp_Array[j] = temp;
}
}
}
/* 第五步:返回中值 */
return Median_Filter_Temp_Array[median_data_count / 2];
}
环形缓冲区原理
N = 5, 初始状态:
写入25.2: [25.2, 0.0, 0.0, 0.0, 0.0] index=1, count=1
写入25.1: [25.2, 25.1, 0.0, 0.0, 0.0] index=2, count=2
写入25.5: [25.2, 25.1, 25.5, 0.0, 0.0] index=3, count=3
写入25.2: [25.2, 25.1, 25.5, 25.2, 0.0] index=4, count=4
写入85.0: [25.2, 25.1, 25.5, 25.2, 85.0] index=0, count=5 ← 队列满了
再写入25.4: [25.4, 25.1, 25.5, 25.2, 85.0] index=1
↑ 覆盖了最旧的25.2
排序后: [25.1, 25.2, 25.4, 25.5, 85.0]
中值: 25.4 ← 85.0被排到末尾,不影响结果
median_data_index %= N 这个取模操作是实现环形缓冲的关键——指针到末尾后自动回到开头。
均值滤波:平滑高频噪声
原理
取最近N次采样值的平均值。代码中使用了滑动窗口均值,只需要O(1)的时间复杂度:
#define N 5
pdata unsigned char mean_data_array[N] = {0}; // 滑动窗口
pdata unsigned char mean_sum = 0; // 窗口内数据和
pdata unsigned char mean_data_index = 0; // 写入指针
pdata unsigned char mean_data_count = 0; // 有效数据计数
unsigned char Mean_Filter(unsigned char New_Data) {
/* 第一步:减去即将被覆盖的旧值 */
mean_sum -= mean_data_array[mean_data_index];
/* 第二步:写入新值 */
mean_data_array[mean_data_index] = New_Data;
/* 第三步:加上新值 */
mean_sum += mean_data_array[mean_data_index];
/* 第四步:移动指针 */
mean_data_index = (++mean_data_index) % N;
/* 第五步:更新计数 */
if(mean_data_count < N)
mean_data_count++;
/* 第六步:返回均值 */
return mean_sum / mean_data_count;
}
滑动窗口为什么高效?
传统均值滤波每次都要遍历N个数据求和,时间复杂度O(N)。滑动窗口利用上一次的和:
sum_new = sum_old - data_old + data_new
示例(N=5):
当前窗口:[10, 12, 11, 50, 13] sum=96
新数据:25
减去最旧的10,加上25:
新窗口:[12, 11, 50, 13, 25] sum = 96-10+25 = 111
均值 = 111/5 = 22.2
每个新数据只需要一次减法、一次加法和一次除法,不需要遍历数组。虽然均值滤波对异常值敏感(上面例子中85.0会拉高均值),但它计算量小,适合对实时性要求高的场景。
两种算法的对比
| 特性 | 中值滤波 | 均值滤波 |
|---|---|---|
| 数据类型 | float | unsigned char |
| 内存占用 | 约22字节 | 约8字节 |
| 计算复杂度 | O(N²)(排序) | O(1)(滑动窗口) |
| 抗脉冲干扰 | 优秀 | 差 |
| 平滑效果 | 一般 | 优秀 |
| 适用场景 | 超声波、温度 | ADC电压、光强 |
实际效果对比(输入数据流:25.2, 25.1, 85.0, 25.3, 25.2, 25.4, 25.1):
中值滤波(N=5):
25.2 → 25.15 → 25.2 → 25.3 → 25.2
(85.0被完全过滤!)
均值滤波(N=5):
25.2 → 25.15 → 37.12 → 32.92 → 25.2
(85.0的影响持续了2次!)
调度器本身就是天然滤波
有时候不需要显式的滤波算法。看一下调度器的配置:
{Read_Tem, 100, 0}, // 每100ms读一次温度
{Get_Distance, 999, 0}, // 每999ms读一次距离
采样周期本身就构成了一种低通滤波——采样间隔越远,滤除的高频噪声越多。对于大多数蓝桥杯题目来说,调度器的采样间隔已经够用了。
但如果题目明确要求"显示稳定"或者传感器噪声特别大(比如超声波),那就需要加滤波。建议:
- DS18B20温度:加中值滤波(N=5),防止85°C初始值和偶发跳变
- 超声波距离:加中值滤波(N=5),防止偶发异常值
- PCF8591光敏/电位器:均值滤波(N=3~5)就够了
补充:其他实用滤波技巧
限幅滤波
// 新值与上次值的差超过阈值就丢弃
unsigned char Limit_Filter(unsigned char new_val, unsigned char last_val,
unsigned char threshold) {
if(abs(new_val - last_val) > threshold)
return last_val; // 超限,保持旧值
return new_val;
}
一阶滞后滤波(最简单)
// output = alpha * new + (1-alpha) * last
float Low_Pass_Filter(float new_val, float last_val, float alpha) {
return alpha * new_val + (1 - alpha) * last_val;
}
// alpha = 0.1~0.3,越小滤波越强但响应越慢
一阶滞后滤波只需要保存上一次的输出值,内存占用极小(4字节),适合RAM紧张的场合。
更多推荐



所有评论(0)