A Python-based EMG gesture control system that reads real-time EMG signals from Arduino via serial, and triggers keyboard inputs using either rule-based logic or a machine learning model.
This project is heavily inspired by the work of Upsidedownlabs and their open-source neuroscience and EMG projects. Their hardware ecosystem and educational content played a major role in the development of this project.
Similar project video by Upside Down Labs:
Watch on YouTube
Their work on EMG sensing, BioAmp hardware, and human-computer interaction projects is highly recommended for anyone interested in biosignal processing and gesture recognition.
EMG Gesture Control connects an EMG sensor setup to your PC. It reads live muscle signals, processes them, and translates 3 specfic gestures into keyboard events. The app supports:

- fast non-ML gesture logic
- ML-based prediction with baseline calibration
- auto-switching between modes when a model is available
- live debug output in the terminal
- recording EMG data to CSV
- configurable key mappings and presets via a web UI
- Real-time EMG signal acquisition over serial
- Dual mode operation:
- Non-ML threshold-based detection
- ML model prediction pipeline
- Auto mode selects ML automatically if a model file is present
- Configurable via .env
- EMG data recording to
data/*.csv - Preset key mapping support via JSON
- Clean single-line live debug output
- Simple pywebview UI for control and settings
The system reads two EMG channels from the Arduino. Each sample is parsed from the serial stream and treated as enveloped EMG signal values.
app.py opens the serial port configured by SERIAL_PORT and BAUD_RATE. Incoming lines are decoded, split into values, and passed into the gesture processing loop.
In non-ML mode, the app applies rule-based thresholds to Envelop EMG values:
- one gesture when
env1is above a low threshold andenv2stays under a value - second gestures when
env2crosses higher thresholds - third geust
- actions are mapped to keyboard keys with a cooldown to avoid repeat triggers
This mode is fast and useful for basic EMG control without a trained model.
In ML mode, the pipeline is:
- collect a baseline from the first
BASELINE_SAMPLES - compute whether the current EMG signal deviates from baseline
- build a feature vector (
emg1,emg2) - run
model.predict(...) - map the predicted class to a keyboard action
ML mode uses joblib to load the model and pandas for feature framing.
Hardware setup Setup Tutorial
python -m pip install -r req.txtIf you prefer a requirements.txt, create one from req.txt or install directly:
python -m pip install pyserial numpy pandas joblib python-dotenv pywebview keyboardCOnfigure a .config file in the project root with the following values.
SERIAL_PORT=COM3
BAUD_RATE=115200
FORCE_MODE=auto
MODEL_PATH=models/emg_modelv0.2.pkl
BUFFER_SIZE=64
COOLDOWN_TIME=0.5
WINDOW=1
BASELINE_SAMPLES=300
DEV1=5
DEV2=40
CSV_FILENAME=data.csv
SAVE_INTERVAL=5SERIAL_PORT- the Arduino serial port, e.g.
COM3
- the Arduino serial port, e.g.
BAUD_RATE- communication speed, typically
115200
- communication speed, typically
FORCE_MODEauto→ auto-selects ML if model existsml→ force ML modenon-ml→ force rule-based mode
MODEL_PATH- path to the saved ML model file
BUFFER_SIZE- serial smoothing buffer length
COOLDOWN_TIME- minimum seconds between triggered key presses
WINDOW- ML window size for feature extraction
BASELINE_SAMPLES- number of samples collected before ML predictions begin
DEV1- baseline deviation threshold for channel 1
DEV2- baseline deviation threshold for channel 2
CSV_FILENAME- output CSV name when recording
SAVE_INTERVAL- seconds between automatic CSV flushes
Note: non-ML gesture thresholds are currently defined in app.py and can be tuned there if needed.
python app.py- Open the local UI served by
pywebview - Choose your key mappings
- Click
Start EMG
- Assign actions to physical keys through the UI
- The default mapping is:
action1→spaceaction2→leftaction3→right
- Loads a trained model from
MODEL_PATH - Uses baseline calibration
- Applies prediction on a feature window
- Best for gesture classification after training
- Uses fixed threshold logic
- Fast and lightweight
- Good for prototyping or when a model is unavailable
- Default mode
- If
MODEL_PATHexists and loads successfully, ML mode is selected - Otherwise, falls back to non-ML
This project includes a ready-made training script (Logistic_Regression_model.py) that handles the entire pipeline.
- Run the main app:
python app.py - In the web UI, enable Recording (check the recording checkbox)
- Perform each gesture multiple times while keeping the recording active
- The app will save EMG data to
data/data.csvwith this format:
timestamp,emg1,emg2,label
1735689600.1,245,123,action1
1735689600.2,250,121,action1
1735689600.3,248,125,action2
...- Stop the app after collecting samples for all desired gestures
Open data/data.csv and manually set the label column for each row:
| Label | Gesture |
|---|---|
action1 |
First gesture (e.g., fist) |
action2 |
Second gesture (e.g., open hand) |
action3 |
Third gesture (e.g., relax) |
Tip: You can also add more labels like
action4,action5etc. for additional gestures.
Run the training script:
python Logistic_Regression_model.pyThis will:
- Load data from
data/data.csv - Extract
emg1andemg2as features - Split data into 80% training / 20% testing
- Train a Logistic Regression classifier with StandardScaler
- Print accuracy score
- If
models/emg_model.pklalready exists, prompt you for a new filename - Save the model to the specified path (default:
models/emg_modelv0.3.pkl)
- Open
.configand set:FORCE_MODE=auto MODEL_PATH=models/emg_modelv0.3.pkl
- Restart
python app.py - The app will automatically load the model and switch to ML mode
| Column | Description |
|---|---|
timestamp |
Epoch seconds when sample was recorded |
emg1 |
Raw EMG value from channel 1 |
emg2 |
Raw EMG value from channel 2 |
label |
Gesture class (action1, action2, action3, etc.) |
If you want to use a different classifier, edit Logistic_Regression_model.py:
# Replace LogisticRegression with your choice:
from sklearn.ensemble import RandomForestClassifier
model = make_pipeline(
StandardScaler(),
RandomForestClassifier(n_estimators=100)
)To change the output model path, modify the last line:
joblib.dump(model, "models/your_model_name.pkl")- confirm Arduino is connected
- verify
SERIAL_PORTandBAUD_RATE - check device manager for the correct COM port
- ensure the EMG electrodes are attached correctly
- verify the Arduino sketch is outputting two numeric values per line
- check the terminal for malformed serial lines
- reduce
COOLDOWN_TIME - lower
WINDOWif ML mode feels too slow - keep serial baud rate at
115200
- make sure
joblibis installed - ensure
MODEL_PATHpoints to a valid.pkl - inspect error output when the app starts
- use
BAUD_RATE=115200 - keep
BUFFER_SIZEsmall for faster responsiveness - reduce
SAVE_INTERVALonly if you need frequent CSV writes - use
non-mlmode for the lowest latency - tune
DEV1/DEV2and baseline sample counts for your hardware setup
Contributions are welcome.
- open issues for bugs or feature requests
- submit PRs for new gesture modes, config options, or improved UI
- keep changes small and focused
- document .env updates and model requirements
Suggested license: MIT
A permissive license that allows reuse, modification, and sharing while protecting contributors.