Reading hand gestures from the muscle's glyphs — surface-EMG gesture classification for prosthetic control.
A classical-ML pipeline that classifies hand gestures from forearm surface-EMG (sEMG) signals — the same control problem faced by myoelectric prosthetic hands. Signal in, gesture out, with the methodology done honestly: no test-set leakage, class-aware metrics, and numbers you can reproduce.
Originally built as a university course project (PES University, UE23EC352B); rebuilt and extended solo — methodology fixes, reproducibility, and this write-up. Edit this line freely.
Hundreds of thousands of people in the US live with upper-limb loss. Myoelectric prostheses read the electrical activity of residual forearm muscles and translate it into hand movements. The long-standing workhorse classifier in myoelectric-control research is LDA — because it is fast, stable, and cheap to run on an embedded controller, not because it is the most accurate model on paper.
Myoglyph builds the full classical-ML pipeline around this problem — filtering, feature extraction, outlier rejection, dimensionality reduction, clustering, and three classifiers — to see where each method wins and where it loses for gesture decoding, and ends in a live simulator that streams an EMG window and fires a prediction.
6 gestures · 10 subjects · NinaPro DB2 · macro-F1 is the primary metric (the data is ~76% "rest", so accuracy alone is misleading). Test set is a held-out 15% split that is never touched by outlier cleaning or model selection.
| Classifier | CV macro-F1 (5-fold) | Test accuracy | Test macro-F1 | Inference |
|---|---|---|---|---|
| KNN (k=3) | 0.672 ± 0.006 | 88.2% | 0.686 | 0.028 ms |
| SVM-RBF (C=100, γ=auto) | 0.597 ± 0.010 | 83.6% | 0.603 | 0.401 ms |
| Decision Tree (depth=18) | 0.521 ± 0.018 | 79.7% | 0.535 | 0.000 ms¹ |
¹ Decision-Tree inference is below the timer's per-sample resolution (sub-microsecond).
Numbers are reproduced verbatim by cv_and_comparison.py → results/final_comparison.csv.
What the numbers say: KNN comes out ahead on macro-F1, with all three classifiers in a fairly tight band. The most instructive result is the SVM itself: a linear SVM reaches only macro-F1 ≈ 0.39, while the RBF kernel lifts it to ≈ 0.60 — the kernel trick earning its keep, because these EMG gesture classes are not linearly separable in this feature space. The Decision Tree is the fastest and most interpretable but generalizes worst; the RBF SVM is the most expensive at inference time, which matters for an embedded prosthetic controller. These figures are lower than the original course submission reported (e.g. KNN macro-F1 0.76 → 0.69) — that gap is exactly the inflation removed by fixing the test-set leak described below.
flowchart LR
A["Raw sEMG<br/>NinaPro DB2<br/>12 ch @ 2 kHz"] --> B["Filter<br/>20–500 Hz band-pass<br/>+ 50 Hz notch"]
B --> C["Window<br/>200 ms / 100 ms overlap"]
C --> D["Features<br/>MAV · RMS · ZCR · WL<br/>48-dim/window"]
D --> E["Outlier removal<br/>One-Class SVM<br/>(train only)"]
E --> F["Scale + PCA / LDA<br/>(fit on train)"]
F --> G["Classifiers<br/>KNN · Decision Tree · SVM-RBF"]
G --> H["5-fold CV<br/>+ held-out test"]
H --> I["Live demo"]
| Stage | Script | What it does |
|---|---|---|
| 0 | data_pipeline.py |
Load .mat, band-pass + notch filter, segment into windows |
| 1 | eda.py |
Raw waveforms, RMS envelopes, class distribution |
| 2 | feature_extraction.py |
4 time-domain features × 12 channels → 48-dim vectors |
| 3 | outlier_detection.py |
One-Class SVM / Isolation Forest artifact diagnostics (train) |
| 4 | dim_reduction.py |
PCA (95% variance) + LDA, fit on train, saved to models/ |
| 5 | clustering.py |
K-Means elbow / silhouette / ARI — unsupervised baseline |
| 6 | knn_classifier.py |
KNN with K-sweep bias–variance curves |
| 7 | decision_tree.py |
Depth-pruned tree + feature importances |
| 8 | svm_classifier.py |
SVM — linear/RBF/poly kernels, soft-margin sweep, tuning |
| 9 | cv_and_comparison.py |
Leak-free 5-fold CV + final comparison table |
| 10 | demo_simulator.py |
Live streaming-EMG classifier demo (the showpiece) |
A few representative figures:
![]() |
![]() |
![]() |
![]() |
The interactive demo runs on a fresh clone without the dataset — it ships a small
bundle of pre-classified samples (models/demo_samples.npz).
git clone https://github.com/ataylus/Myoglyph.git && cd Myoglyph
pip install -r requirements.txt
python demo_simulator.py # click a gesture → sample window replays → model's prediction showsTo reproduce the whole pipeline you need the dataset (free registration, see below):
python run_all.py # stages 0–9 + regenerates demo bundle & GIFFull instructions, including the exact data layout, are in SETUP.md.
Tested on Python 3.11; dependencies pinned in requirements.txt. An NVIDIA GPU is
optional (cuML accelerates K-Means); the SVM runs on scikit-learn for reproducibility.
NinaPro DB2 — 12-channel forearm sEMG at 2 kHz from 40 subjects performing 50
movements. Myoglyph uses the first 10 subjects, Exercise 1, and a 6-gesture
subset: rest, thumbs_up, open_hand, fist, point, pinch → 58,006 windows.
The dataset is not included here. It requires a free one-time registration at ninapro.hevs.ch and is released under a non-commercial license; there is no public mirror or automated download. See SETUP.md for the manual download + extraction steps.
- Macro-F1 over accuracy. "Rest" is ~76% of windows; a model that only ever predicts rest scores ~76% accuracy but is useless for control. Macro-F1 weights every gesture equally and is the headline metric throughout.
- No test-set leakage. Scaler, PCA, LDA, and the One-Class-SVM outlier filter are all
fit on the training split only; validation/test are merely transformed. Cross-validation
re-fits the outlier filter inside each fold (
utils.get_clean_split,train_inlier_mask). This was the single biggest fix from the original version, which cleaned outliers across all data including the test set and reported inflated metrics. - scikit-learn SVM, not GPU. cuML's GPU
SVCsilently ignoresclass_weightfor multiclass problems, which biases the model toward the rest class and yields results that don't reproduce on CPU. Correctness and reproducibility beat speed here, so the scored SVMs use scikit-learn. (cuML is still used, optionally, for K-Means.) - PCA before the SVM. Features are reduced to 95% variance (20 components) before the RBF SVM — faster, and a natural place to show dimensionality reduction feeding a kernel machine.
- Consistent feature spaces. Each classifier is cross-validated in the exact space it is trained in (KNN/DT on the 48 scaled features, SVM on PCA), so the comparison is fair and the Decision Tree's feature-importance plot refers to real EMG features.
- Within-subject, pooled split. Windows from all 10 subjects are pooled before splitting, so train and test can share a subject. This measures gesture separability, not cross-subject generalization — the harder, more realistic prosthetics setting. A leave-one-subject-out evaluation is the natural next step and would lower the numbers.
- The rest-trained outlier filter is aggressive. A One-Class SVM fit on the "rest" class flags ~13% of training windows as outliers; some of those are likely genuine but rest-dissimilar gesture windows, not just artifacts. A per-class or artifact-specific detector would be cleaner.
- Time-domain features only. MAV/RMS/ZCR/WL are cheap and embedded-friendly, but frequency-domain or AR-coefficient features typically add accuracy.
- No real-time loop. The demo replays recorded windows; a true closed-loop controller would need streaming segmentation and latency budgeting.
- 6 gestures, not 50. A deliberately tractable subset; scaling to the full DB2 gesture set is future work.
Code is released under the MIT License (see LICENSE). The NinaPro DB2 dataset is not included and is covered by its own separate non-commercial license.




