4. 肌电特征提取:时域特征、频域特征、时频域特征

好,咱们进入正题。上一章聊了怎么把原始肌电信号弄干净,那接下来呢?

说白了,干净的信号还是太「原始」了。你没法直接拿它去控制假肢。我们需要从中提炼出一些有代表性的数值,这就是特征提取。我个人习惯把特征分成三大类:时域的、频域的、还有时频域的。咱们一个一个来。

核心思路: 特征提取就是把一段高维的肌电信号,压缩成几个低维的、能反映肌肉活动状态的关键数值。选对了特征,假肢控制就成功了一半。

4.1 时域特征:简单、直观、计算快

时域特征,就是直接在时间轴上算信号的各种统计量。好处是计算量小,嵌入式系统跑起来毫无压力。我在早期的假肢项目里,基本全靠这几个特征撑场面。

4.1.1 平均绝对值(MAV)

MAV 是最常用的特征之一。它反映了信号的整体幅值大小。肌肉收缩越用力,MAV 值就越大。

# Python 示例:计算 MAV
import numpy as np

def compute_mav(emg_signal):
    # emg_signal 是一维数组
    mav = np.mean(np.abs(emg_signal))
    return mav

# 假设你有一段 200ms 的肌电数据
sample_signal = np.random.randn(200) * 0.1  # 模拟数据
mav_value = compute_mav(sample_signal)
print(f"MAV = {mav_value:.4f}")

我的经验: MAV 对噪声比较敏感。如果信号基线没调好,MAV 会飘。我曾经遇到过因为电极接触不良,MAV 值忽大忽小,导致假肢误动作。后来我养成了一个习惯:每次采集前先测一段静息状态的 MAV,作为基线补偿。

4.1.2 均方根(RMS)

RMS 和 MAV 有点像,但它更强调信号的能量。你可以把 RMS 理解为信号的「有效值」。在控制假肢时,RMS 通常比 MAV 更稳定一些。

def compute_rms(emg_signal):
    rms = np.sqrt(np.mean(emg_signal ** 2))
    return rms

rms_value = compute_rms(sample_signal)
print(f"RMS = {rms_value:.4f}")

4.1.3 过零点数(ZC)

ZC 统计的是信号穿过零点的次数。它反映了信号的频率信息。肌肉疲劳时,信号的频率会下降,ZC 值也会变小。

def compute_zc(emg_signal):
    # 需要设置一个阈值,避免噪声引起的误判
    threshold = 0.01
    zc = 0
    for i in range(1, len(emg_signal)):
        if (emg_signal[i-1] * emg_signal[i] < 0) and \
           (abs(emg_signal[i] - emg_signal[i-1]) > threshold):
            zc += 1
    return zc

zc_value = compute_zc(sample_signal)
print(f"ZC = {zc_value}")

注意: ZC 一定要加阈值!不加阈值的话,一点点噪声就会让 ZC 值爆炸。我刚开始做的时候吃过这个亏,特征值完全没法用。

4.1.4 斜率符号变化数(SSC)

SSC 统计的是信号斜率符号变化的次数。它和 ZC 类似,但反映的是信号波形的变化剧烈程度。

def compute_ssc(emg_signal):
    threshold = 0.01
    ssc = 0
    for i in range(1, len(emg_signal)-1):
        diff1 = emg_signal[i] - emg_signal[i-1]
        diff2 = emg_signal[i+1] - emg_signal[i]
        if (diff1 * diff2 < 0) and \
           (abs(diff1) > threshold or abs(diff2) > threshold):
            ssc += 1
    return ssc

ssc_value = compute_ssc(sample_signal)
print(f"SSC = {ssc_value}")

4.2 频域特征:看信号的能量分布

时域特征虽然好用,但它丢掉了信号的频率信息。你想想看,不同的手势,肌肉放电的频率成分是不一样的。频域特征就是干这个的。

4.2.1 平均频率(MNF)

MNF 是功率谱的加权平均频率。它告诉你信号的能量主要集中在哪个频率附近。

from scipy import signal

def compute_mnf(emg_signal, fs=1000):
    # 计算功率谱密度
    freqs, psd = signal.welch(emg_signal, fs, nperseg=256)
    # 计算平均频率
    mnf = np.sum(freqs * psd) / np.sum(psd)
    return mnf

mnf_value = compute_mnf(sample_signal)
print(f"MNF = {mnf_value:.2f} Hz")

4.2.2 中值频率(MDF)

MDF 是功率谱的累积能量达到 50% 时的频率。它比 MNF 更鲁棒,不容易受极端值影响。

def compute_mdf(emg_signal, fs=1000):
    freqs, psd = signal.welch(emg_signal, fs, nperseg=256)
    cumsum = np.cumsum(psd)
    # 找到累积能量达到 50% 的频率点
    mdf = freqs[np.where(cumsum >= cumsum[-1] / 2)[0][0]]
    return mdf

mdf_value = compute_mdf(sample_signal)
print(f"MDF = {mdf_value:.2f} Hz")

避坑指南: 计算频域特征时,窗长的选择很关键。窗太长,时间分辨率低;窗太短,频率分辨率低。我一般用 256ms 的窗,配合 50% 的重叠,效果比较均衡。

4.3 时频域特征:兼顾时间和频率

时域和频域特征各有局限。时域特征丢了频率信息,频域特征丢了时间信息。那有没有办法两者兼顾?有,时频分析。

4.3.1 短时傅里叶变换(STFT)

STFT 的思路很简单:把信号切成一小段一小段,对每一段做傅里叶变换。这样你就能看到频率随时间的变化了。

def compute_stft(emg_signal, fs=1000):
    f, t, Zxx = signal.stft(emg_signal, fs, nperseg=256)
    # Zxx 是复数矩阵,取幅值
    spectrogram = np.abs(Zxx)
    return f, t, spectrogram

f, t, spec = compute_stft(sample_signal)
print(f"STFT 输出维度:频率点 {len(f)},时间点 {len(t)}")

4.3.2 小波变换(WT)

小波变换比 STFT 更灵活。它用不同尺度的小波基去匹配信号,在低频处有好的频率分辨率,在高频处有好的时间分辨率。

import pywt

def compute_wavelet(emg_signal):
    # 使用 db4 小波,进行 5 层分解
    coeffs = pywt.wavedec(emg_signal, 'db4', level=5)
    # coeffs[0] 是近似系数,coeffs[1:] 是细节系数
    return coeffs

coeffs = compute_wavelet(sample_signal)
print(f"小波分解层数:{len(coeffs)}")

注意: 时频特征虽然信息丰富,但计算量大。在嵌入式假肢上,我一般只用 STFT 的某些频带能量作为特征,不会把整个时频谱都扔给分类器。否则,算力扛不住。

4.4 知识体系总览

为了让你更直观地理解这些特征之间的关系,我画了一张图。

肌电特征提取知识体系 肌电信号特征 时域特征 MAV - 平均绝对值 RMS - 均方根 ZC - 过零点数 SSC - 斜率符号变化 频域特征 MNF - 平均频率 MDF - 中值频率 时频域特征 STFT - 短时傅里叶 WT - 小波变换 选择特征时,要考虑计算量、鲁棒性和实时性

4.5 特征选择的一些建议

特征不是越多越好。我见过有人一口气提取了 20 多个特征,结果分类效果反而变差了。为什么?因为特征之间可能存在冗余,甚至互相干扰。

  • 实时控制场景: 我建议用 MAV + RMS + ZC 就够了。计算快,效果稳定。
  • 手势识别场景: 可以加上 MNF 或 MDF,提高对不同手势的区分度。
  • 疲劳检测场景: 频域特征更合适,MDF 的下降趋势能很好地反映肌肉疲劳。
  • 复杂动作场景: 考虑用 STFT 的频带能量,或者小波系数。

我的习惯: 先选 3-5 个核心特征,跑一遍分类器看看效果。如果不够,再逐步增加。不要一上来就搞「特征工程大礼包」,那样反而容易翻车。

好了,这一章的内容就到这里。特征提取是假肢控制中承上启下的关键一步。选对了特征,后面的分类和控制就会轻松很多。下一章,咱们聊聊怎么用这些特征去识别不同的手势。

专注资料整理