ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

sEMG手势识别源码实战:从原始信号到TMA激活图与实时分类

2026/10/1 13:40:13 拓冰建站 浏览量
sEMG手势识别源码实战:从原始信号到TMA激活图与实时分类 简介基于多通道表面肌电图(sEMG)信号的颞肌激活图实时手势识别项目面向生物医学信号处理与机器学习初学者及研究者提供从数据采集、预处理、特征提取到模型训练与实时识别的完整源代码。项目采用Jupyter Notebook作为主要开发环境涵盖SVM、随机森林等传统分类器及CNN、LSTM等深度学习模型并配有Myo臂带等硬件接口示例便于理解信号与手势之间的映射关系。资源共73个文件以csv数据文件、py脚本、h5训练模型、ipynb示例笔记为主另有png可视化结果与mat数据压缩包仅5.6MB目录结构清晰。已有158人学习下载适合希望深入理解sEMG信号处理全流程并构建实际应用系统的开发者。通过该资源可直接运行训练与识别流程获取tsne、Amap等激活图可视化结果以及完整的数据处理与建模脚本。1. 多通道sEMG手势识别的源码包从原始信号到TMA激活图的实时识别链路才是这份源代码的干货所在把 Myo 臂带套上小臂、在 Jupyter Notebook 里第一次跑通基于多通道表面肌电图sEMG信号的颞肌激活图实时手势识别算法的源代码时我意识到真正值钱的不是“识别率不错”这句话而是从原始信号到激活图、再到分类器这一整条链路的工程细节。这个 sEMG-Hand-Gesture-Recognition-master 压缩包属于后者它不是一个丢给你一个训练好的黑匣子的 demo而是一个能自己采集、自己训练、自己实时跑的 Python 项目源码。包内 models 目录下已经躺着 subject_1001、subject_1002 两个训练好的模型notebooks 里有直接可运行的 gesture_recog_tma.ipynbsrc 下则分好了 tma、gsp、record_training_data.py、train_model.py、recognize_real_time_hand_gestures.py 这些模块。适合看腻了“sEMG深度学习”论文、想在自己电脑上把 10 个手势串起来的人。2. 从裸信号到激活图TMA 预处理链路与核心参数怎么落2.1 先搞清楚 TMA 到底是什么不是颞肌是“时间上的激活”先说一个命名坑摘要里写的“颞肌激活图TMA”我一开始真以为是贴在太阳穴上的颞肌电极。但翻完 src/ 下的代码就知道在这个手势识别场景里 TMA 是 Temporal Muscle Activation 的缩写Temporal 修饰的是 Activation指的是“时间维度上的肌肉激活图”——把多通道 sEMG 按时间轴切成窗口标记出每个通道什么时候被激活、激活强度如何最后得到一张横轴是时间窗口、纵轴是通道的激活矩阵。翻译成“颞肌”是把 temporal 当解剖学词了它在信号处理语境里就是“时序”的意思。这个识别方向本身没问题因为 Myo 臂带的 8 个通道围绕小臂一圈不同手势在对应通道上出现的激活模式存在稳定差异。项目里 figures 目录下的 onsets.png 就是这条链路最直观的产物原始 sEMG 波形上叠加了每通道的激活起始点标注Amap_Final.png 则是把整段手势的激活状态铺成一张图。你后面用 record_training_data.py 采集数据时这两张图就是用来检查“信号采得干不干净”的照妖镜。raw1.png 是某一段原始信号的落盘预览both1.png 通常是原始信号与预处理后信号的对照图用来确认滤波没有把激活边界搞乱。2.2 预处理链路4 阶 Butterworth 带通 零相位滤波 逐通道归一化sEMG 的频谱能量主要集中在 20~200 Hz但原始信号里混着基线漂移、50 Hz 电源工频干扰、电极移位产生的直流偏置。直接把这样一段波形送去分类器模型会去学工频噪声而不是手势模式。所以预处理的目标很明确只保留肌肉活动频段再去基线漂移把各通道幅值拉齐。下面是一段在我自己复现时按项目场景写的预处理代码对应 src 里 train_model.py 和 plot_the_real_time_emg_signal.py 都会用到的同类链路import numpy as np from scipy import signal def preprocess_emg(raw, fs200, lowcut20.0, highcut200.0): raw: 形状为 (n_samples, n_channels) 的原始 sEMG 阵列 fs: Myo 臂带 sEMG 采样率官方默认 200 Hz # 4 阶 Butterworth 带通用 sos 形式而非 ba 形式数值稳定性更好 sos signal.butter(4, [lowcut, highcut], btypebandpass, fsfs, outputsos) filtered signal.sosfiltfilt(sos, raw, axis0) # 零相位手势段不发生位移 # 去基线漂移长时间采集时静息电平会缓慢变化 filtered filtered - filtered.mean(axis0) # 逐通道幅度归一化发力大小因人因时不同统一到 [-1, 1] scale np.max(np.abs(filtered), axis0) scale[scale 0] 1.0 return filtered / scale这里有几个参数值得较真。阶数取 4 是常见折中阶数高带外衰减更陡但滤波器延迟加大实时识别时会让“你手已经动了信号还没跟上”。我用 sosfiltfilt 而不是 lfilter是因为它有零相位特性能保证手势的 onset 位置不被相位失真拉偏代价是会对整段信号做两次滤波非实时场景无所谓实时场景下一般改用 sosfilt并在代码里接受几帧延迟。归一化选最大值而不是标准差是为了保留“发力越大、激活越明显”的相对强度信息如果按标准差压握拳和放松的幅值差异会被抹掉TMA 的激活阈值就不太好设。2.3 TMA 构建整流、滑窗能量、阈值激活三步把波形变成图TMA 不是预处理之后直接出来的它需要对每个通道单独做激活检测。工程上我按三步看这段源码第一步整流取绝对值把负半周翻正第二步滑动窗算局部平均能量常见用 MAV即平均绝对值第三步拿能量与该通道静息基线比较超过阈值记 1否则记 0。我在复现时按 src/tma 模块的思路写了这么一个简化版def build_tma(emg, win_len40, step10, thr_ratio0.5): n_ch emg.shape[1] n_frames 1 (emg.shape[0] - win_len) // step tma np.zeros((n_frames, n_ch), dtypenp.int8) # 基线取信号前 5% 静息段的幅度均值加个极小值防止除 0 base_line np.mean(np.abs(emg[:int(emg.shape[0] * 0.05), :]), axis0) 1e-6 for i in range(n_frames): seg emg[i * step : i * step win_len, :] energy np.mean(np.abs(seg), axis0) tma[i, :] (energy thr_ratio * base_line).astype(np.int8) return tma逻辑说明每一行 tma 对应一个时间窗口每一列对应一个 sEMG 通道值 1 表示该通道在这个窗口内处于激活状态。这样一整段手势就压缩成了一张低维的 0/1 图后面分类器吃的是这张图而不是原始波形抗噪能力强不少。参数方面win_len 在 40 帧、step 在 10 帧时形态学上比较稳。win_len 太短比如 10 帧激活区间会碎成一片噪声点太长大于 80 帧则会抹掉相邻手势之间的分界。thr_ratio 我从 0.4 试到 0.6最终留在 0.5阈值太低会把静息抖动也算成激活太高则抓不住轻握手这类低幅值手势。注意 Myo 臂带戴得越紧静息基线越高换一次佩戴位置最好重新标定一次基线这是项目里最容易翻车的地方。3. 从TMA到分类样本窗口切割、标签对齐与分类器选型3.1 手势时长不稳定滑窗加重叠是标准做法一段手势从“开始动”到“停在目标姿势”短的不到 500 ms长的能拖到 1.5 秒。直接把整个手势切成一个样本喂给分类器样本数量太少且长度不齐。项目里 figures 目录下的 gesture10.png 展示的就是某个手势在屏幕上被识别并绘制出来的效果但训练时不能依赖这种人工截断。常见做法是固定窗口长度加重叠滑动比如窗口 300 ms滑动步长 50 ms。这样一段 1 秒的手势能派生出一批样本数据量翻好几倍而且相邻样本属于同一手势训练集的类别分布也更均匀。我一般把窗口长度设在 200~400 ms步长设在 25~100 ms 之间窗口越大单样本信息越充分但窗口太大会跨到前后手势导致标签错位。具体参数可以按 Myo 的 200 Hz 采样率换算300 ms 就是 60 个采样点步长 50 ms 就是 10 个采样点。录制手势时每个手势重复 20 次用滑窗切完以后样本数通常够一个传统分类器用不需要强行上深度学习。3.2 标签对齐是最大的“玄学”onset 标注不要全信录制训练数据时最影响最终识别率的不是网络结构而是标签到底对齐到哪一帧。项目里 onsets.png 把每个通道的激活起始点画在了原始波形上看着一目了然但这里有个隐蔽问题不同肌肉通道的 onset 之间有先后。比如握拳这个动作指浅屈肌先激活尺侧腕屈肌后跟上二者相差可能有 30~80 ms。如果按第一个通道的 onset 作为手势起点TMA 图左边缘会带着半截“未激活”区域如果按最后一个通道的 onset又会切掉部分激活信息。我的处理习惯是录制时每个手势动作前留 2 秒静息动作完成后停 0.5 秒再做下一个标签记录的是动作指令下发的时间戳而不是手真正开始发力的时间戳。训练时对每个样本做 20 帧以内的随机时间平移让分类器对“激活边界稍微偏左偏右”不敏感。这是很多人复现后识别率忽高忽低的原因——不是算法问题是标签对齐漂移。3.3 分类器选型传统分类器足够t-SNE 先帮你看一眼数据src 里 train_model.py 的作用就是训练分类器并落到 models 目录。对这个项目来说SVM、随机森林这类传统分类器处理 TMA 矩阵足够TMA 已经是高度压缩的结构化特征不需要 CNN 再去自动提特征。KNN 也能跑但样本量上来以后预测延迟会随距离计算量增长实时识别时不推荐。训练前一定要看 t-SNE 可视化也就是 figures/tsne.png 的来历把每个样本的特征向量降到 2 维按手势类别着色。如果不同类别的点基本聚成互不重叠的团说明 TMA 特征分得开如果两团压在一起用手性很强说明这两类手势在信号层面本就相似需要调窗口长度或换特征。我踩过的坑就是先不看 t-SNE直接训练出 95% 训练集准确率放到实时上一塌糊涂——后来发现两个手势在 t-SNE 上压根就叠在一起原来源码包的采集脚本里这两个手势定义得太接近。分类器选型上还有一点多分类 SVM 默认是 one-vs-one 还是 one-vs-rest不同版本 scikit-learn 行为有差异。项目里如果不是显式设置你在推理时拿到的 decision_function 含义可能和你期望的不一样。我一般显式用SVC(kernelrbf, decision_function_shapeovr)至少保证预测接口在复现时稳定。4. 从 record_training_data.py 到 train_model.py一份能照着跑的通关流程4.1 环境搭建Jupyter Notebook 与 requirements.txt 先对齐拿到压缩包第一步不是解压而是先看 requirements.txt 写了哪些版本。项目能在 Jupyter Notebook 里跑但本机环境乱七八糟的话最容易在 import 阶段就翻车。我一般用 conda 单独建一个环境Python 版本固定在 3.9避免 scipy 和 matplotlib 在版本上互掐。conda create -n semg_gesture python3.9 conda activate semg_gesture pip install numpy scipy pandas matplotlib scikit-learn notebook安装完先不要急着打开整个项目单独新建终端跑一遍jupyter notebook确认服务能启动、浏览器能打开、notebook 内核选择的是 semg_gesture 环境。这一步能过滤掉一大批“找不到指定程序”式的问题。很多人在 Windows 上双击图标启动 Jupyter 后发现 import scipy 报错基本都是因为启动的是全局环境而不是 conda 环境。requirements.txt 里如果已经固定了版本直接用pip install -r requirements.txt也行但注意它可能包含 GPU 相关包没有 N 卡的机器会白装一堆东西所以我更倾向于按上面四条手动装。4.2 录制训练数据每个手势重复 20 次静息段必须留够record_training_data.py 是数据采集入口。它要处理的活儿很单调提示你做一个手势、保持一秒、放松、换下一个然后把原始 sEMG 信号和对应标签写到本地。常见做法是每个手势重复 20 到 30 次10 个手势就是 200 到 300 条原始记录滑窗之后样本量完全够用。录制时有三条我踩出来的规矩。第一每两个手势之间至少留 2 秒静止这 2 秒会成为 TMA 基线计算的依据基线脏了后面的激活阈值全部失真。第二动作要自然发力不要为了“让信号明显”故意紧绷故意紧绷会让模型在真实场景下识别率直接腰斩。第三采集中途如果发现 armband 松动立刻停下来重新戴正再从头录戴着歪掉的臂带录 10 分钟等于生产了一批带偏置的垃圾数据。代码层面采集脚本通常会把每个手势的数据和标签分开存。按下第一个手势键之前先静置 1 秒这不是仪式感是为了让脚本截取一段纯静息段来算基线。4.3 训练与模型落盘models 下按 subject 分开存不是多此一举训练入口是 train_model.py。它读入录制好的原始数据走一遍预处理和 TMA 构建然后滑窗切样本、训练分类器最后把模型文件写进 models 目录。压缩包里已经有 subject_1001、subject_1002 两个目录说明原项目作者按受试者编号分开保存模型这是 sEMG 项目里非常正确的习惯。sEMG 信号天然是用户相关的电极在手臂上的位置、皮肤阻抗、肌肉发达程度、发力习惯都因人而异。跨用户直接复用模型往往效果很差分开保存才能保证“谁的数据训练谁用”的逻辑清晰。我自己的习惯是模型文件名里带上采样日期、受试者编号和动作集版本比如subject_1001_gesture10_20250115.pkl这样训练集发生变化时能一眼看出哪个模型该被替换。from sklearn import svm import joblib # clf 是已经用 TMA 特征和标签拟合好的分类器 model_path models/subject_1001/gesture10.pkl joblib.dump(clf, model_path) print(fmodel saved to {model_path}, classes{clf.classes_})逻辑说明joblib 是 scikit-learn 推荐的模型持久化方式比 pickle 更能处理好大规模 numpy 数组。保存时把 classes_ 一并带上实时识别时才能把预测结果映射回手势名称。注意每次训练前都要检查训练样本里是否包含全部 10 个手势类别缺一个类别模型也不会报错但推理时那个手势永远识别不出来。5. 实时识别与常见问题排查五个真实遇到的坑实时识别入口是 recognize_real_time_hand_gestures.py它会一边接收 Myo 的实时数据流一边按固定窗口做 TMA 推理并绘制结果。plot_real_time_difference_signal.py 和 plot_the_real_time_emg_signal.py 则负责把实时信号和通道差分信号画出来辅助调试。这一章把我在复现过程中遇到的五个问题按“现象 → 原因 → 解决”写出来。5.1 现象Myo 连接成功后数据流仍然抽搐波形断断续续原因Myo 臂带通过蓝牙传输 sEMG 数据电脑自带蓝牙和 Myo 接收器冲突或周围 2.4 GHz 设备干扰严重时数据包会丢。代码收到的是不连续的时间序列滑窗切出的样本缺了一段TMA 看起来会有诡异的激活空洞。解决优先用 Myo 官方接收器而不是电脑内置蓝牙采集时让手机、微波炉这类 2.4 GHz 设备离远一点。代码层面在 record 和 recognize 脚本里都对时间戳做连续性检查如果相邻数据间隔超过 50 ms就把当前窗口丢弃而不是强行补零。我还会同时跑 plot_real_time_difference_signal.py差分信号的抖动幅度如果突然变大说明这一帧数据质量不可信。5.2 现象实时识别界面卡顿手势已经做完识别结果半秒后才出来原因Matplotlib 绘图和推理在同一个线程里抢 CPU推理本身没多慢但每帧都刷新画布会拖垮主循环。这是新手最容易忽略的实时性瓶颈。解决把绘图频率降下来比如每推理 5 次才刷新一次图像更彻底的方案是数据采集、特征推理、界面绘制各开一个线程中间用队列传递结果。我在项目里把推理窗口做成滑动队列只有队列满时才计算一次 TMA其余时间只推入新数据计算量直接降了一个量级。5.3 现象训练时准确率很高换个人戴臂带识别率跌到一半以下原因这就是前面说的用户相关问题。训练集来自 subject_1001测试时不重新采集而是直接复用模型跨用户泛化基本都要翻车。解决换人必须走“采集 → 预处理 → 训练 → 落盘”的完整流程生成新的 subject 目录。如果想省事可以用原模型参数做迁移学习比如冻结 SVM 前几层支持向量的思路但这在 sklearn 里并不直观我一般直接重新训练耗时也就几分钟。“换人之前先重新训练”这个习惯能帮你避免九成玄学问题。5.4 现象两个手势在训练时能分开实时识别时经常混淆原因实时数据里手势的执行节奏和训练录制不一致比如录制时是慢速握拳实时使用时会自然加快。TMA 激活区间被压缩或拉伸分类器没见过这种形态。解决录制训练数据时故意变化节奏快慢各录一半训练前对 TMA 做时间轴拉伸增强把每个样本在时间维度上伸缩到 0.8~1.2 倍等效扩充数据集。项目里如果没内置这个增强可以自己在数据加载时写个简单线性插值代码量不大但提升明显。5.5 现象Jupyter Notebook 里 import 项目模块报 ModuleNotFoundError原因notebook 的工作目录和 src 目录不在一起Python 的 sys.path 里没有项目根目录导致 import tma、import gsp 失败。Windows 上还常见“找不到指定程序”式错误那是 Jupyter 用了错误环境的 Python 内核。解决在 notebook 第一个 cell 里写两行路径设置。import sys sys.path.append(../src)代码说明把项目 src 目录加进解释器搜索路径notebook 就能找到 tma、gsp 这些内部模块。注意路径是相对 notebook 文件位置的解压位置移动后要重新检查。如果用了 conda 环境记得每次重启 notebook 内核后重新执行这两个 cellsys.path 不会自动保留。6. 不算结束用三个额外动作把模型鲁棒性再压一压模型能跑通只是起点我一般会再花一晚上做三件事让它更扛得住真实使用。第一件事是打开 confidence 输出把实时识别中预测概率低于 0.6 的帧单独收集起来回放原始信号看是标签错位还是动作本身不规范。第二件事是做一次时间扰动验证把测试集的 TMA 在时间轴上整体平移 5 到 10 帧再重新评估模型如果准确率掉得厉害说明模型过度依赖激活边界的位置而不是激活模式。第三件事是检查 Amap_Final.png 的手势模式是否和预期一致比如握拳应集中在屈肌通道上展拳应看到背侧通道激活如果激活图乱成一团不要急着调分类器先回去看预处理和基线。这三件事做完模型的边界在哪、什么动作会骗过它心里大概就有数了。我在复现这个项目后养成了一个习惯每次换样例或换配戴方式都强制走一遍“预处理对照图 → t-SNE → 短期验证 → 重新训练”的流程不忙着调参。很多看似玄学的识别失败其实是前期数据采集环节埋的雷。希望这份经手过一轮的野路子经验能帮你少走一段弯路。本文还有配套的精品资源点击获取