4、时域特征提取:峰值检测、过零率、短时能量、波形因子计算
好,咱们进入第四章。时域特征提取。
说实话,在消防报警这个领域,时域特征是最直观的。你想想看,一个火灾探测器采集到的信号,本质上就是一条随时间变化的曲线。峰值有多高?波动有多快?能量有多大?这些信息,全藏在时域里。
我个人习惯,做嵌入式移植时,先把时域特征算准了,再谈频域。为什么?因为时域计算简单、实时性好,对MCU的算力要求低。你总不能指望一个Cortex-M0内核的探测器去跑FFT吧?
4.1 峰值检测
峰值检测,说白了就是找信号的最大值。但这里有个坑——你要找的是真实峰值,不是噪声尖峰。
我在项目中遇到过这种情况:一个烟雾探测器,采样率1kHz,环境中有空调吹风造成的扰动。直接取最大值,结果误报率飙升。后来怎么解决的?加了一个滞回比较和最小间隔约束。
核心思路:
- 设定一个阈值,只有超过阈值的点才参与峰值判定
- 记录峰值后,设定一个“死区时间”,期间不检测新峰值
- 用滑动窗口内的局部最大值代替全局最大值
代码实现上,我建议用状态机。嗯,这样效率最高。
// 峰值检测 - 状态机实现
typedef enum {
PEAK_IDLE,
PEAK_RISING,
PEAK_FALLING
} peak_state_t;
float peak_detect(float sample, float threshold, uint32_t *dead_ticks) {
static peak_state_t state = PEAK_IDLE;
static float current_peak = 0.0f;
if (*dead_ticks > 0) {
(*dead_ticks)--;
return 0.0f; // 死区中,不输出峰值
}
switch (state) {
case PEAK_IDLE:
if (sample > threshold) {
current_peak = sample;
state = PEAK_RISING;
}
break;
case PEAK_RISING:
if (sample > current_peak) {
current_peak = sample; // 继续上升,更新峰值
} else if (sample < current_peak * 0.9f) {
// 下降超过10%,认为峰值已过
state = PEAK_FALLING;
}
break;
case PEAK_FALLING:
if (sample < threshold) {
state = PEAK_IDLE;
*dead_ticks = 50; // 50个采样点的死区
return current_peak; // 输出峰值
}
break;
}
return 0.0f;
}
小技巧:死区时间要根据采样率和信号特性来调。我一般先设50个采样点,然后看效果微调。消防报警信号变化慢,死区可以设大一点。
4.2 过零率
过零率,就是信号穿过零点的次数。这个特征在消防报警里特别有用——你想想,火灾初期的阴燃阶段,烟雾浓度缓慢上升,信号波动很小,过零率很低。但一旦出现明火,气流扰动加剧,过零率会突然升高。
计算过零率,公式很简单:
ZCR = (1 / N) * Σ |sign(x[n]) - sign(x[n-1])|
其中sign是符号函数,N是窗口长度。
但实际嵌入式实现时,有个细节要注意——直流偏置。如果信号有直流分量,零点就不在0了。我曾经吃过这个亏,一个传感器输出有1.2V的偏置,直接算过零率,结果全是0。
避坑指南:计算过零率前,一定要先做直流滤波。可以用一个简单的IIR高通滤波器,截止频率设0.5Hz就够了。
我常用的实现方式:
// 过零率计算 - 带直流滤波
typedef struct {
float prev_sample;
float dc_offset;
float alpha; // 滤波系数,通常0.99
} zcr_ctx_t;
float calc_zcr(zcr_ctx_t *ctx, float sample, int window_size) {
static int count = 0;
static float zcr_sum = 0.0f;
// 直流滤波
ctx->dc_offset = ctx->alpha * ctx->dc_offset + (1 - ctx->alpha) * sample;
float ac_signal = sample - ctx->dc_offset;
// 过零检测
if (ctx->prev_sample * ac_signal < 0) {
zcr_sum += 1.0f;
}
ctx->prev_sample = ac_signal;
count++;
if (count >= window_size) {
float zcr = zcr_sum / window_size;
count = 0;
zcr_sum = 0.0f;
return zcr;
}
return -1.0f; // 窗口未满,返回无效值
}
4.3 短时能量
短时能量,反映的是信号在一小段时间内的强度。消防报警里,这个特征用来区分“有信号”和“没信号”。比如,一个感烟探测器,正常环境下短时能量很低,一旦有烟雾进入,散射光信号增强,能量值会跳变。
计算公式:
E = Σ x[n]²
嗯,就这么简单。但嵌入式实现时,平方运算对定点MCU来说有点费劲。我建议用绝对值求和代替平方求和,效果差不多,但速度快很多。
| 方法 | 计算量(每采样点) | 精度损失 | 推荐场景 |
|---|---|---|---|
| 平方求和 | 1次乘法 + 1次加法 | 无 | 有FPU的MCU |
| 绝对值求和 | 1次条件判断 + 1次加法 | 约5%~10% | 无FPU的低端MCU |
| 查表法 | 1次查表 + 1次加法 | 取决于表精度 | RAM充足的场景 |
我的经验:在STM32F0这类Cortex-M0芯片上,用绝对值求和做短时能量,一个窗口(256点)的计算时间不到0.5ms,完全满足实时性要求。
// 短时能量 - 绝对值求和实现
float short_term_energy(int16_t *buffer, int len) {
uint32_t energy = 0;
for (int i = 0; i < len; i++) {
// 绝对值求和,避免乘法
if (buffer[i] < 0) {
energy += (uint32_t)(-buffer[i]);
} else {
energy += (uint32_t)buffer[i];
}
}
return (float)energy / len; // 归一化
}
4.4 波形因子
波形因子,这个稍微有点抽象。它定义为有效值(RMS)与平均绝对值(MAV)的比值。
波形因子 = RMS / MAV
有什么用呢?它反映了信号的“形状”。正弦波的波形因子是1.11,方波是1.0,白噪声大约是1.25。在消防报警里,不同火源产生的信号波形不同——阴燃火信号偏正弦波,明火信号偏随机噪声。通过波形因子,可以初步判断火源类型。
计算时要注意:RMS需要开平方,这在嵌入式里是个大开销。我建议用牛顿迭代法或者查表法来算平方根,别用标准库的sqrt,太慢了。
优化技巧:如果只是要波形因子,其实可以不用算真正的RMS。用“平方和”代替RMS,用“绝对值和”代替MAV,比值不变。这样省掉了开平方运算。
// 波形因子 - 优化版(避免开平方)
float waveform_factor(int16_t *buffer, int len) {
uint32_t sum_sq = 0; // 平方和
uint32_t sum_abs = 0; // 绝对值和
for (int i = 0; i < len; i++) {
int32_t val = buffer[i];
sum_sq += (uint32_t)(val * val); // 平方
if (val < 0) {
sum_abs += (uint32_t)(-val);
} else {
sum_abs += (uint32_t)val;
}
}
// 波形因子 = sqrt(sum_sq / N) / (sum_abs / N)
// 简化后 = sqrt(sum_sq) / sum_abs * sqrt(N)
// 但为了省掉sqrt,我们直接返回平方和与绝对值和的比值
// 这个比值与真正的波形因子是单调对应的
return (float)sum_sq / (float)(sum_abs * sum_abs / len);
}
注意:上面这个优化版返回的不是真正的波形因子,而是一个单调映射值。如果你需要精确的波形因子值(比如要和标准库对比),还是得老老实实算RMS。但在嵌入式报警算法里,我们只关心相对变化,单调映射完全够用。
4.5 综合应用:一个完整的时域特征提取流程
好了,四个特征都讲完了。在实际项目中,我通常把它们组合成一个特征向量,输入到分类器里。
举个例子,一个典型的消防报警算法流程:
- 每256个采样点(约256ms)计算一次特征
- 提取峰值、过零率、短时能量、波形因子
- 将四个特征归一化到0~1范围
- 输入到阈值判断或简单分类器
- 连续3个窗口都超过阈值,才触发报警
为什么要连续3个窗口?嗯,防止误报。我曾经遇到过,一个电焊工人在探测器旁边干活,单次峰值很高,但持续不到1秒。如果单窗口触发,就会误报。加上连续判断,误报率降低了90%。
最后提醒一句:时域特征虽然简单,但组合起来威力不小。别小看这四个参数,调好了,能解决80%的火灾探测问题。剩下的20%,才需要上频域和机器学习。
下一章,咱们聊聊频域特征提取。那个更有意思。