JOVANA
Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Filtering & Spatial Filters

Before any [[brain-computer-interface|brain–computer interface]] can decode your intent, the raw signal needs cleaning. Learn the two cleanups that do the heavy lifting: temporal filters that pick a frequency band, and spatial filters that combine electrodes.

Cleaning in time and in space

Imagine you are trying to hear one friend in a noisy café. You can do two very different things. First, you can listen only for the pitch of their voice and ignore the deep rumble of the air conditioner and the high hiss of the espresso machine — that is cleaning in time (by frequency). Second, you can use both ears to figure out where the voice is coming from and turn toward it — that is cleaning in space (by location).

Brain signals work the same way. A temporal filter keeps only a useful frequency range and throws away the rest. A spatial filter combines several electrodes at once to sharpen where on the scalp the signal is coming from. The two are complementary, and most electroencephalography (EEG) pipelines apply both before any decoding happens.

Temporal filters: bandpass & notch

A bandpass filter is like a doorman who only lets in guests within a certain age range: signals below a low cutoff and above a high cutoff are turned away. For motor tasks you often keep roughly 8–30 Hz, which covers the mu rhythm and the beta band — the rhythms that change when you imagine moving. Everything slower (like drift) or faster (like muscle noise) gets dropped.

A notch filter does the opposite of a bandpass over a tiny range: it removes one narrow frequency. Its main job is to kill the steady hum from mains electricity — 50 Hz in much of the world, 60 Hz in North America — which leaks into every recording. Think of it as a single, very precise mute button for that one annoying tone.

from scipy.signal import butter, filtfilt

# Design an 8-30 Hz bandpass (4th-order Butterworth).
# fs is the sampling rate in Hz; here 250 Hz.
fs = 250
b, a = butter(4, [8, 30], btype='band', fs=fs)

# filtfilt runs the filter forward AND backward,
# so the cleaned signal has zero phase delay.
clean = filtfilt(b, a, raw_eeg)  # raw_eeg: 1-D samples
An 8–30 Hz bandpass with SciPy. Using filtfilt (not lfilter) avoids shifting your signal in time.

Spatial filters: CAR & Laplacian

Some noise hits every electrode at once — a jaw clench, a power-line spike, a slow shift in the reference. Because it is shared, you can estimate it and subtract it. Common Average Reference (CAR) does exactly this: it averages all the channels to guess the shared part, then subtracts that average from each channel. What survives is the part that is genuinely different at each site.

A Laplacian filter is more local. Instead of subtracting the average of every channel, it subtracts the average of just an electrode's nearest neighbors. This is like sharpening a photo: it boosts whatever is happening right under that one electrode and suppresses the broad, smeared activity shared with its surroundings. Both CAR and the Laplacian cancel shared noise (artifact) — CAR removes globally shared junk, the Laplacian removes locally shared blur.

CSP: learning the best spatial filter

CAR and the Laplacian are fixed recipes — they use the same weights for everyone. [[common-spatial-patterns|Common spatial patterns (CSP)]] instead learns the electrode weighting from your data. Given two states to tell apart — say, imagining moving your left hand versus your right hand during motor imagery — CSP finds the combination of electrodes that makes the band-power (the energy in your chosen frequency band) as large as possible for one state and as small as possible for the other.

The result is a spatial filter tuned to maximize the contrast between exactly those two states — far more discriminating than a one-size-fits-all recipe. That power comes with a price: CSP needs labeled calibration data (recordings you have already marked as "left" or "right") to learn from. No labels, no CSP.

# CSP, conceptually (pseudocode).
# X1, X2: band-filtered trials for class 1 and class 2.
C1 = average_covariance(X1)   # how channels co-vary, class 1
C2 = average_covariance(X2)   # how channels co-vary, class 2

# Solve a generalized eigenproblem: find weight
# vectors W that maximize C1's variance while
# minimizing C2's (and vice versa).
W = generalized_eigenvectors(C1, C2)

# Keep the most extreme filters (highest + lowest
# eigenvalues); apply them to project new trials.
features = log_band_power(W @ new_trial)
CSP in essence: covariances per class, then a generalized eigenproblem yields the filters that best separate them.