Motor Imagery
This tutorial walks through the complete NeuroPawn motor-imagery project: recording eight-channel sensorimotor EEG, training a stacked ensemble, and running a live four-state detector.
The implementation was developed by the MIND club at the University of Calgary. Its outputs can be mapped to a robotic car, an assistive device, or another application:
| Recorded state | Example command |
|---|---|
left: left-hand clench | Turn left |
right: right-hand clench | Turn right |
both: both hands clenched | Move forward |
rest: no clench | Stop |
What Is Motor Imagery?
Section titled “What Is Motor Imagery?”Motor imagery is the internal rehearsal of a movement without actually performing it. Imagine the sensation and effort of closing your right hand while keeping the hand still. Planning and imagining that movement engage parts of the same sensorimotor network used during physical movement.
At rest, populations of neurons around the sensorimotor cortex often oscillate together in the mu (approximately 8–12 Hz) and beta (approximately 13–30 Hz) ranges. Movement or sustained motor imagery can reduce this rhythmic power. This is called event-related desynchronization, or ERD. Power may increase again after the task, producing event-related synchronization, or ERS.
Hand-related effects are usually strongest over the opposite hemisphere:
- Right-hand movement or imagery often changes activity near C3.
- Left-hand movement or imagery often changes activity near C4.
- Bilateral activity can affect both sides at once.
These patterns vary substantially between people. A motor BCI therefore needs calibration data from the person who will use it rather than a universal set of thresholds.
Physical clenches can produce stronger cortical movement signals, but they can also introduce muscle and cable-motion artifacts. A classifier trained on those artifacts may appear accurate without measuring the intended EEG pattern. Keep the jaw, shoulders, face, and electrode cables still, and use the lightest repeatable hand movement that fits your experiment.
Place the Electrodes
Section titled “Place the Electrodes”The project expects this exact channel order:
FC4, C4, CP4, C2, C1, CP3, C3, FC3This montage brackets the left and right sensorimotor cortices:
| Region | Electrodes | Purpose |
|---|---|---|
| Right hemisphere | FC4, C4, CP4, C2 | Sensitive to left-hand motor activity |
| Left hemisphere | C1, CP3, C3, FC3 | Sensitive to right-hand motor activity |
C3 and C4 sit over the primary sensorimotor regions most often used for hand motor imagery. The frontal-central and central-parietal electrodes add neighboring spatial information that Common Spatial Patterns and covariance features can combine.
Channel order is part of the trained model. Reordering the cables after calibration changes the meaning of every spatial feature, even when all eight electrodes still produce valid EEG.
Set Up the Knight Board
Section titled “Set Up the Knight Board”The acquisition and live scripts default to BrainFlow board ID 57 on COM3. Change both values near the top of data_collect.py and live_detector.py if your board or serial port differs:
board_id = 57serial_port = "COM3"During startup, the scripts configure each of the eight channels with gain setting 12 and add each channel to the RLD drive:
for channel in range(1, 9): board.config_board(f"chon_{channel}_12") board.config_board(f"rldadd_{channel}")The chon_* values are board-specific configuration commands, not electrode names. Keep the repository defaults unless you understand the gain settings for your board. The RLD connection helps control common-mode interference, but it does not replace a consistent EEG reference montage.
Before collecting calibration data:
- Confirm the serial port in Windows Device Manager.
- Connect all electrodes in the documented order.
- Check that the electrodes have stable skin contact.
- Close any other BrainFlow program using the board.
- Sit comfortably with forearms supported and cables secured.
Understand the Trial Protocol
Section titled “Understand the Trial Protocol”Run data_collect.py to collect the calibration set. The default active schedule contains 15 trials each for left, right, and both. The script randomizes these cues so the user cannot predict the next class.
Each active trial follows this sequence:
- The terminal announces the selected hand state at
4. - The countdown continues through
3and2while EEG keeps streaming. - At
1, the terminal displaysCLENCH. - The user holds that clench for one second.
- The latest one-second filtered and raw windows are saved with the cue label.
Relax during each countdown and make each clench similar in strength and timing. Do not begin early; the saved window is the one-second interval following the CLENCH instruction.
After all active cues, the script records consecutive one-second rest windows. The number of rest windows equals the total number of active trials. With the defaults, the final dataset contains:
- 15 left-hand trials
- 15 right-hand trials
- 15 both-hands trials
- 45 rest trials
This balances active versus rest for the MRCP branch, although the four-class dataset contains more rest examples than any individual movement class. The classifiers use balanced class weights where appropriate.
Filter the Streaming EEG
Section titled “Filter the Streaming EEG”The nominal board rate is not assumed to be exact. data_collect.py estimates the sampling rate from BrainFlow timestamps, stores it as sr_ts, and uses it to set the one-second window length and filter cutoffs. Training reads the same value from the dataset, and live detection reads it from ensemble_meta.json.
Each channel passes through a fourth-order Butterworth filter bank implemented as second-order sections:
| Band | Model use |
|---|---|
| 0.05–5 Hz | Slow movement-related cortical potential features |
| 8–30 Hz | Broadband sensorimotor covariance features |
| 8–12 Hz | Mu rhythm for FBCSP |
| 12–16 Hz | Upper mu / low beta for FBCSP |
| 16–20 Hz | Beta activity for FBCSP |
| 20–26 Hz | Beta activity for FBCSP |
| 26–30 Hz | Upper beta activity for FBCSP |
The filters are stateful: each channel and band preserves its filter state as new samples arrive. Collection and live detection therefore process a continuous stream and then take the most recent one-second window. This is important because resetting an IIR filter for every trial would create edge transients that the model could learn.
The main collect-train-detect path does not apply a notch filter, detrending, baseline correction, or a software common-average reference. If you add any of these operations, add the same operation in both data_collect.py and live_detector.py, then recollect and retrain. A preprocessing mismatch makes live features incompatible with the saved model.
Inspect the Saved Dataset
Section titled “Inspect the Saved Dataset”Collection writes two files into data/:
four_class_clench_trials_from_pause.npzcontains the filtered trials used for training.raw_four_class_clench_trials_from_pause.npzpreserves the corresponding unfiltered EEG.
The filtered file contains labels, integer class_types, the seven filter-band arrays, and sampling metadata. Each band has shape (trials, 8, samples). Class IDs are fixed as 0=left, 1=right, 2=rest, and 3=both.
Use the included plotter before training:
python tools\trial_plotter.pyIt opens plots for every available band and class. Look for channels that are flat, clipped, much noisier than the others, or dominated by brief movement spikes. Similar-looking trials within a class are more useful than a few unusually large responses.
How the Ensemble Works
Section titled “How the Ensemble Works”The project combines three views of each one-second window rather than relying on one feature type.
Model A: Filter Bank Common Spatial Patterns
Section titled “Model A: Filter Bank Common Spatial Patterns”Common Spatial Patterns (CSP) learns weighted combinations of electrodes whose variance changes across labels. Applying CSP separately to mu and beta sub-bands is known as Filter Bank CSP, or FBCSP. This helps the model accommodate individual differences in the frequency where ERD or ERS is strongest.
For each of five bands, the repository creates three CSP slots named L_vs_R, B_vs_LR, and Act_vs_Rest. Each slot returns four log-power components, producing 60 features in total. MNE’s CSP implementation uses Ledoit-Wolf regularization, trace normalization, and average component power. A standardized shrinkage LDA classifier converts those features into four class probabilities.
Model B: Riemannian Covariance Features
Section titled “Model B: Riemannian Covariance Features”The second branch summarizes how channels vary together in the broad 8–30 Hz band:
- Ledoit-Wolf estimation produces a stable channel covariance matrix for each trial.
- The affine-invariant Riemannian mean of the training covariances provides a reference point.
- Each covariance is mapped to the tangent space at that mean.
- The upper triangle becomes a 36-value feature vector.
- Standardization and balanced logistic regression produce four class probabilities.
Covariance features capture spatial relationships across all channels, complementing the component powers used by FBCSP.
Model C: Slow Movement-Related Potentials
Section titled “Model C: Slow Movement-Related Potentials”Movement-related cortical potentials (MRCPs) are slow voltage shifts associated with preparing and executing movement. The third branch uses the last 0.6 seconds of each 0.05–5 Hz window and calculates, per channel:
- mean voltage
- linear slope
- minimum voltage
- average area
It also calculates a C3/C4 envelope laterality index:
This produces 33 features. A balanced logistic-regression model predicts only active versus rest, not the individual hand class. Because the repository records overt clenches, this branch can include movement execution as well as movement preparation.
The Meta-Learner
Section titled “The Meta-Learner”The stack receives nine values:
- four FBCSP/LDA probabilities
- four Riemannian/logistic-regression probabilities
- one MRCP active probability
A final logistic regression learns how to combine them into left, right, rest, and both probabilities.
Training uses five-fold out-of-fold stacking. Within each fold, the CSP bank and Riemannian mean are fit only on the training split, and base-model probabilities are generated for the held-out split. The meta-learner is tuned on these held-out predictions using log-loss. Afterward, deployment models are refit on all available trials.
This fold-specific fitting prevents the learned spatial transforms from seeing their validation trials. The final in-sample report printed after the full-data refit is only a sanity check; it is not an independent estimate of real-world accuracy.
Install the Project
Section titled “Install the Project”Use Python 3.11 or 3.12 and run commands from the repository root:
git clone https://github.com/NeuroPawn/motor-imagery.gitSet-Location motor-imagerypy -3.11 -m venv .venv.\.venv\Scripts\Activate.ps1The pinned requirements file contains trailing semicolons. Create a clean copy before installing it:
Get-Content requirements_usama_env.txt | ForEach-Object { $_.TrimEnd(';') } | Set-Content requirements.txtpip install -r requirements.txtThe main dependencies include BrainFlow, MNE, NumPy, SciPy, scikit-learn, Joblib, Matplotlib, PyQt5, and PyQtGraph.
Check the Board Connection
Section titled “Check the Board Connection”Update the board_id and serial_port values in the scripts, then run the smoke test:
python tools\board_test.py --serial-port COM3 --duration 10A successful test records samples without a board-session or serial-port exception and writes CSV files under data/board_test/. This confirms communication, not signal quality. Use the scope to inspect all channels:
python tools\Scope.py --serial-port COM3 --board-id 57Collect and Train
Section titled “Collect and Train”Collect a subject-specific calibration set:
python data_collect.pyFollow the countdowns, remain still between prompts, and clench only during the one-second action interval. Confirm that this file exists when collection finishes:
data/four_class_clench_trials_from_pause.npzThen train the ensemble:
python model.pyTraining prints the randomly selected cross-validation seed and fold-level scores for each base branch. It writes these deployment artifacts to models/:
csp_models.joblibriem_mean.npybaseA_fbcsp_lda.joblibbaseB_riem_lr.joblibbaseC_mrcp_active_lr.joblibmeta_lr.joblibensemble_meta.jsonFor custom locations, pass both paths explicitly:
python model.py ` --data "D:\EEG\calibration.npz" ` --models-dir "D:\EEG\models"If you change the model directory, set ROOT in live_detector.py to the same location before live detection.
Run Live Detection
Section titled “Run Live Detection”With the trained artifacts in place, start the detector:
python live_detector.pyThe worker warms up until it has one second of filtered EEG, then repeatedly evaluates the latest one-second sliding window. The PyQtGraph window displays dotted raw probabilities and solid exponentially smoothed probabilities for all four classes.
For each class, the exponential moving average is:
The default smoothing value is 0.80 for every class. Higher values produce steadier but slower decisions. The default decision thresholds are:
| Control | Default | Meaning |
|---|---|---|
| Left | 0.70 | Minimum smoothed left probability |
| Right | 0.70 | Minimum smoothed right probability |
| Both | 0.75 | Minimum smoothed both probability |
| Active | 0.50 | Minimum raw MRCP active probability for both |
The detector chooses the largest smoothed class probability. It emits left or right only when that class clears its threshold. It emits both only when both the class threshold and MRCP active gate pass. Every other case falls back to rest. This conservative fallback reduces unintended commands when confidence is low.
The terminal prints the base-model probabilities, MRCP active probability, meta-model probabilities, smoothed values, and final label. Use the final label, rather than a single raw probability, as the command sent to downstream hardware.
Adapt the Pipeline for Pure Motor Imagery
Section titled “Adapt the Pipeline for Pure Motor Imagery”To build a movement-free motor-imagery experiment, first decide on one consistent mental task. Kinesthetic imagery, imagining the feeling of movement from a first-person perspective, generally matches sensorimotor rhythms better than merely visualizing a hand from the outside.
Then adapt and validate the protocol:
- Replace
CLENCHwith an explicitIMAGINE LEFT,IMAGINE RIGHT, orIMAGINE BOTHcue. - Ask the participant to keep both hands, forearms, jaw, and shoulders motionless.
- Collect a completely new calibration set; do not reuse clench-trained models.
- Increase the number of trials because imagery effects may be weaker and more variable.
- Preserve the same channel order, preprocessing, and windowing in collection and live inference.
- Consider recording EMG from the forearms if verifying absence of muscle activity matters to the experiment.
One-second windows may be short for some imagery users. If you change TRIAL_LENGTH_SECS or WINDOW_SEC, make the same change throughout collection and live detection, then retrain every artifact.
Project Layout
Section titled “Project Layout”motor-imagery/|-- data_collect.py # Guided calibration recording|-- model.py # Stacked-ensemble training|-- live_detector.py # Real-time classification and UI|-- bci/ # CSP and feature utilities|-- tools/ # Board tests and signal viewers|-- experiments/ # Alternate research pipelines|-- data/ # Recorded NPZ datasets`-- models/ # Trained deployment artifactsThe experiments/linearity_* scripts use a different continuous acquisition and feature pipeline. They are optional research tools, not part of the three-step collect-train-detect workflow described above.
Troubleshooting
Section titled “Troubleshooting”The board will not connect
Section titled “The board will not connect”Confirm the COM port, close other BrainFlow programs, and rerun tools\board_test.py. Only one process can hold the serial connection at a time.
One channel is flat or much noisier
Section titled “One channel is flat or much noisier”Check electrode contact, cable placement, and the physical channel order. Use tools\Scope.py before collecting. Recollect the dataset after fixing the signal; training cannot recover information that was not recorded.
Training cannot find the dataset
Section titled “Training cannot find the dataset”Run commands from the repository root and confirm that data/four_class_clench_trials_from_pause.npz exists. Otherwise pass its full path with --data.
Live detection cannot load a model
Section titled “Live detection cannot load a model”Confirm that all seven model files and ensemble_meta.json are in the directory specified by ROOT. Train and run with the same virtual environment so Joblib can import the local bci package and compatible dependency versions.
Training scores are high but live control is unstable
Section titled “Training scores are high but live control is unstable”The full-data report is in-sample and can be optimistic. Check the fold results, collect more independent trials, reduce motion artifacts, and verify that live behavior matches calibration behavior. Increase EMA smoothing or thresholds only after checking the EEG and training protocol; controls cannot repair mismatched data.
Left and right appear reversed
Section titled “Left and right appear reversed”Verify the physical mapping against FC4, C4, CP4, C2, C1, CP3, C3, FC3. Do not swap the interpretation merely because motor activity is often contralateral; the software labels follow the cue and channel order used during calibration.
Explore the implementation, utilities, and current defaults in the NeuroPawn motor-imagery repository.
