Skip to content

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 stateExample command
left: left-hand clenchTurn left
right: right-hand clenchTurn right
both: both hands clenchedMove forward
rest: no clenchStop

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.

The project expects this exact channel order:

FC4, C4, CP4, C2, C1, CP3, C3, FC3

This montage brackets the left and right sensorimotor cortices:

RegionElectrodesPurpose
Right hemisphereFC4, C4, CP4, C2Sensitive to left-hand motor activity
Left hemisphereC1, CP3, C3, FC3Sensitive 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.

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 = 57
serial_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:

  1. Confirm the serial port in Windows Device Manager.
  2. Connect all electrodes in the documented order.
  3. Check that the electrodes have stable skin contact.
  4. Close any other BrainFlow program using the board.
  5. Sit comfortably with forearms supported and cables secured.

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:

  1. The terminal announces the selected hand state at 4.
  2. The countdown continues through 3 and 2 while EEG keeps streaming.
  3. At 1, the terminal displays CLENCH.
  4. The user holds that clench for one second.
  5. 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.

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:

BandModel use
0.05–5 HzSlow movement-related cortical potential features
8–30 HzBroadband sensorimotor covariance features
8–12 HzMu rhythm for FBCSP
12–16 HzUpper mu / low beta for FBCSP
16–20 HzBeta activity for FBCSP
20–26 HzBeta activity for FBCSP
26–30 HzUpper 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.

Collection writes two files into data/:

  • four_class_clench_trials_from_pause.npz contains the filtered trials used for training.
  • raw_four_class_clench_trials_from_pause.npz preserves 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:

Terminal window
python tools\trial_plotter.py

It 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.

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.

The second branch summarizes how channels vary together in the broad 8–30 Hz band:

  1. Ledoit-Wolf estimation produces a stable 8×88\times8 channel covariance matrix for each trial.
  2. The affine-invariant Riemannian mean of the training covariances provides a reference point.
  3. Each covariance is mapped to the tangent space at that mean.
  4. The upper triangle becomes a 36-value feature vector.
  5. Standardization and balanced logistic regression produce four class probabilities.

Covariance features capture spatial relationships across all channels, complementing the component powers used by FBCSP.

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:

L=EC3EC4EC3+EC4+ϵL = \frac{E_{C3} - E_{C4}}{E_{C3} + E_{C4} + \epsilon}

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 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.

Use Python 3.11 or 3.12 and run commands from the repository root:

Terminal window
git clone https://github.com/NeuroPawn/motor-imagery.git
Set-Location motor-imagery
py -3.11 -m venv .venv
.\.venv\Scripts\Activate.ps1

The pinned requirements file contains trailing semicolons. Create a clean copy before installing it:

Terminal window
Get-Content requirements_usama_env.txt |
ForEach-Object { $_.TrimEnd(';') } |
Set-Content requirements.txt
pip install -r requirements.txt

The main dependencies include BrainFlow, MNE, NumPy, SciPy, scikit-learn, Joblib, Matplotlib, PyQt5, and PyQtGraph.

Update the board_id and serial_port values in the scripts, then run the smoke test:

Terminal window
python tools\board_test.py --serial-port COM3 --duration 10

A 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:

Terminal window
python tools\Scope.py --serial-port COM3 --board-id 57

Collect a subject-specific calibration set:

Terminal window
python data_collect.py

Follow 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.npz

Then train the ensemble:

Terminal window
python model.py

Training prints the randomly selected cross-validation seed and fold-level scores for each base branch. It writes these deployment artifacts to models/:

csp_models.joblib
riem_mean.npy
baseA_fbcsp_lda.joblib
baseB_riem_lr.joblib
baseC_mrcp_active_lr.joblib
meta_lr.joblib
ensemble_meta.json

For custom locations, pass both paths explicitly:

Terminal window
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.

With the trained artifacts in place, start the detector:

Terminal window
python live_detector.py

The 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:

st=ast1+(1a)pts_t = a s_{t-1} + (1-a)p_t

The default smoothing value is 0.80 for every class. Higher values produce steadier but slower decisions. The default decision thresholds are:

ControlDefaultMeaning
Left α\alpha0.70Minimum smoothed left probability
Right β\beta0.70Minimum smoothed right probability
Both γ\gamma0.75Minimum smoothed both probability
Active Δ\Delta0.50Minimum 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.

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:

  1. Replace CLENCH with an explicit IMAGINE LEFT, IMAGINE RIGHT, or IMAGINE BOTH cue.
  2. Ask the participant to keep both hands, forearms, jaw, and shoulders motionless.
  3. Collect a completely new calibration set; do not reuse clench-trained models.
  4. Increase the number of trials because imagery effects may be weaker and more variable.
  5. Preserve the same channel order, preprocessing, and windowing in collection and live inference.
  6. 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.

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 artifacts

The 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.

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.

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.

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.

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.

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.