Real-time Decoding#

RTDecode wraps an mne.decoding (CSP, or Scaler + Vectorizer) and scikit-learn classifier sklearn.pipeline.Pipeline for real-time single-trial classification — e.g. motor-imagery BCI control, or any other paradigm where each acquisition window should be classified live.

Unlike the config-driven NF modalities in Real-time Feature Modalities, a decoder is a Python object you fit offline on labelled calibration epochs, then attach to a live session with set_decoder():

calibration epochs  ->  RTDecode.fit(X, y)  ->  RTStream.set_decoder()
live window  ->  RTStream.record_main()  ->  RTDecode.predict_proba()  ->  nf_data["decode"]

Fitting a decoder offline#

X is a 3D array of labelled calibration epochs (n_epochs, n_channels, n_times) — from a dedicated calibration block, a prior session, or a subset of trials held out from the current one. y is one label per epoch (numeric or string; string labels such as "left"/"right" are supported and give more readable decoder.classes_).

from mne_rt import RTDecode

X_cal = ...  # shape (n_epochs, n_channels, n_times)
y_cal = ...  # shape (n_epochs,), e.g. ["left", "right", "left", ...]

decoder = RTDecode(info=epochs_info, spatial_filter="csp", n_components=4)
decoder.fit(X_cal, y_cal)

Two spatial_filter choices are available:

  • "csp" (default) — CSP, log-variance of the most discriminative spatial filters. Well suited to oscillatory paradigms such as motor imagery; requires at least two classes.

  • "scaler"Scaler (channel-type-aware standardisation) + Vectorizer. A simpler, filter-agnostic alternative when CSP’s oscillatory-power assumption doesn’t fit the decoding target (e.g. ERP decoding).

Any scikit-learn classifier can be passed via estimator= (default LogisticRegression()); use one supporting predict_proba if you plan to attach the decoder to a live session (see below).


Attaching and running it live#

A fitted decoder is attached with set_decoder(), then requested like any other modality via modality=["decode"]:

from mne_rt import RTStream

nf = RTStream(subject_id="sub01", montage="easycap-M1", data_type="eeg")
nf.connect_to_lsl(...)  # or connect_to_array(...) for LSL-free testing/demos
nf.set_decoder(decoder)
nf.record_main(modality=["decode"], winsize=2.0)

proba_right = nf.nf_data["decode"]  # one probability per window

record_main queries predict_proba() once per acquisition window and reports the probability of decoder.classes_[class_index] (class_index defaults to 1, configurable via config_methods.yml’s decode section or modality_params). The value flows through the same pipeline as every other modality — EMA smoothing, z-scoring, reward protocols, plotting, and BIDS export — which is why the "decode" modality always reports a continuous probability rather than a discrete predicted label: a discrete label doesn’t survive smoothing/z-scoring meaningfully. Use predict() directly (outside record_main) if you need the discrete label.

Important

winsize in record_main() must match the epoch duration X_cal was fit on, and the channel count and order must match too — a channel-count mismatch raises a clear error at record_main() start, but a same-count reordering is not detected and will silently degrade predictions.


Validating a decoder before deploying it#

RTDecode.pipeline is a plain scikit-learn sklearn.pipeline.Pipeline, so any scikit-learn model-selection tool works directly on it — cross-validate before attaching a decoder to a live session:

from sklearn.model_selection import ShuffleSplit, cross_val_score

cv = ShuffleSplit(n_splits=5, test_size=0.2, random_state=0)
scores = cross_val_score(decoder.pipeline, X_cal, y_cal, cv=cv)
print(f"Accuracy: {scores.mean():.2f} +/- {scores.std():.2f}")

For CSP decoders, the learned spatial patterns can be inspected the same way as in MNE’s own decoding tutorials:

csp = decoder.pipeline.named_steps["csp"]
csp.plot_patterns(epochs_info, components=range(csp.n_components))

See Real-time motor imagery decoding with CSP for a complete worked example (PhysioNet motor imagery, CSP, cross-validation, and a live connect_to_array() streaming demo).

For deeper offline validation before deploying a decoder — beyond a single cross-validated accuracy number — MNE’s own decoding examples gallery covers techniques not wrapped by RTDecode but useful at the calibration stage:


See also#

  • Real-time Feature Modalities — the full set of config-driven NF feature modalities, including Decode.

  • CLImne-rt demo-decode runs this whole workflow end-to-end from the command line.

  • LearnedCombiner — for blending an already fitted regressor over reduced scalar NF features (rather than raw windows) into a combined feedback score.