Module 3: EEG Data Collection - Workbook
Welcome to Module 3! This workbook covers the practical aspects of EEG data collection, from understanding hardware specifications to real-time data acquisition. You'll learn about different EEG systems, proper electrode application techniques, and how to ensure high-quality recordings. This module bridges the gap between theory and hands-on EEG recording.
Learning Objectives:
• Compare different EEG hardware systems and specifications
• Master electrode application and impedance optimization
• Implement real-time data acquisition using modern protocols
• Design experiments with proper controls and timing
• Troubleshoot common recording problems
Topic 1: EEG Hardware Overview
Compare different types of EEG systems and their specifications:
| Specification |
Consumer-Grade |
Research-Grade |
Clinical-Grade |
| Number of channels |
|
32-256 |
|
| Sampling rate (Hz) |
128-256 |
|
200-1000 |
| ADC resolution (bits) |
|
24 |
16-24 |
| Input impedance |
>1 MΩ |
|
>100 MΩ |
| Noise level (μVrms) |
2-5 |
|
<0.5 |
| Price range ($) |
200-2000 |
10k-100k |
|
Calculate the voltage resolution for different ADC specifications:
def calculate_voltage_resolution(adc_bits, voltage_range):
"""
Calculate the smallest detectable voltage change
Parameters:
- adc_bits: ADC resolution in bits
- voltage_range: total input range in volts (e.g., ±250mV = 0.5V total)
"""
# Number of discrete levels
levels = 2 **
# Voltage per bit
resolution_volts = voltage_range /
# Convert to microvolts
resolution_uv = resolution_volts *
return resolution_uv
# Examples for different systems
consumer_res = calculate_voltage_resolution(16, 0.5) # 16-bit, ±250mV
research_res = calculate_voltage_resolution( , 0.5) # 24-bit, ±250mV
print(f"Consumer system resolution: {consumer_res:.3f} μV")
print(f"Research system resolution: {research_res:.3f} μV")
# What's the minimum ADC bits needed to detect 0.1 μV changes with ±250mV range?
min_bits =
Select all TRUE statements about EEG hardware:
Match electrode types with their characteristics and use cases:
1. Ag/AgCl wet electrodes
2. Active electrodes
3. Dry electrodes
4. Saline electrodes
5. Subdermal needles
A. Built-in preamplification
B. No conductive gel needed
C. Standard for long recordings
D. Quick setup, moderate quality
E. Intraoperative monitoring
Match: 1-___, 2-___, 3-___, 4-___, 5-___
Complete the code to select appropriate hardware settings:
def select_hardware_settings(recording_type):
"""
Recommend hardware settings based on recording type
"""
settings = {
'sampling_rate': None,
'filter_settings': {},
'electrode_type': None,
'reference': None
}
if recording_type == 'ERP':
# Event-related potentials need good temporal resolution
settings['sampling_rate'] = # Hz (minimum 250)
settings['filter_settings'] = {
'highpass': , # Hz (very low for slow components)
'lowpass': 100, # Hz
'notch': None # Often skip notch for ERPs
}
settings['electrode_type'] = 'Ag/AgCl wet'
settings['reference'] = ' ' # Common for ERPs
elif recording_type == 'BCI':
# Brain-computer interface - real-time processing
settings['sampling_rate'] = # Hz (balance quality/speed)
settings['filter_settings'] = {
'highpass': 0.5,
'lowpass': , # Hz (focus on control signals)
'notch': 60 # Remove interference
}
settings['electrode_type'] = ' ' # For quick setup
settings['reference'] = 'CAR' # Common average reference
elif recording_type == 'Sleep':
# Long duration, full spectrum needed
settings['sampling_rate'] = # Hz (minimum for all stages)
settings['filter_settings'] = {
'highpass': 0.3,
'lowpass': 35,
'notch': 60
}
settings['electrode_type'] = ' ' # Comfortable for long recordings
return settings
Topic 2: Electrode Application & Impedance
Create a step-by-step protocol for proper electrode application:
Electrode Application Checklist
- Clean electrode sites with
- Gently abrade skin using
- Apply conductive gel/paste - amount:
- Press electrode firmly for seconds
- Check impedance - target: < kΩ
- Secure with tape/cap/net
- Re-check impedances after minutes
Impedance troubleshooting guide:
| Impedance Reading |
Likely Cause |
Solution |
| >50 kΩ |
|
Add more gel, check contact |
| Unstable/jumping |
Movement or poor contact |
|
| Very low (<1 kΩ) |
|
Check for salt bridge |
| Asymmetric (L vs R) |
Different prep quality |
|
Implement impedance checking and visualization:
import numpy as np
import matplotlib.pyplot as plt
class ImpedanceChecker:
def __init__(self, electrode_names, target_impedance=5):
self.electrodes = electrode_names
self.target = target_impedance # kΩ
self.history = {name: [] for name in electrode_names}
def measure_impedance(self, amplifier_connection):
"""
Measure impedance for all electrodes
"""
impedances = {}
for electrode in self.electrodes:
# Send test signal (usually 10-30 Hz)
test_freq = # Hz
test_current = # nA (typical)
# Measure voltage response
# Z = V / I (Ohm's law)
measured_voltage = amplifier_connection.inject_current(
electrode, test_current, test_freq
)
# Calculate impedance in kΩ
impedance_ohms = measured_voltage / (test_current * 1e-9)
impedance_kohms = impedance_ohms /
impedances[electrode] = impedance_kohms
self.history[electrode].append(impedance_kohms)
return impedances
def get_quality_indicators(self, impedances):
"""
Categorize impedance quality
"""
quality = {}
for electrode, z in impedances.items():
if z < self.target:
quality[electrode] = ' ' # Good
elif z < self.target * 2:
quality[electrode] = 'acceptable'
elif z < self.target * 4:
quality[electrode] = ' ' # Needs work
else:
quality[electrode] = 'poor'
return quality
def suggest_fixes(self, impedances):
"""
Suggest specific fixes for high impedance
"""
suggestions = {}
for electrode, z in impedances.items():
if z > self.target * 4:
suggestions[electrode] = [
"Re-prep with more aggressive abrasion",
"Check for hair interference",
"Replace electrode if damaged"
]
elif z > self.target * 2:
suggestions[electrode] = [
"Add more conductive gel",
"Press electrode more firmly",
"Clean area with "
]
elif z > self.target:
suggestions[electrode] = [
"Minor adjustment needed",
"Wait for gel to settle"
]
return suggestions
def plot_impedance_map(self, impedances):
"""
Create visual impedance map
"""
# This would create a topographical plot
# Color coding: Green (<5kΩ), Yellow (5-10kΩ), Red (>10kΩ)
pass
# Common impedance problems and solutions
impedance_problems = {
'all_high': {
'cause': ' ', # Ground electrode issue
'solution': 'Check ground electrode connection and impedance'
},
'increasing_over_time': {
'cause': ' ', # Gel drying
'solution': 'Add more gel or use different gel type'
},
'sudden_spike': {
'cause': 'Electrode lifted or moved',
'solution': ' '
}
}
What test frequency is typically used for impedance measurement?
Topic 3: Real-time Data Acquisition
Implement real-time data streaming using LSL:
from pylsl import StreamInfo, StreamOutlet, StreamInlet, resolve_stream
import numpy as np
import time
class EEGStreamer:
def __init__(self, device_name, n_channels, sampling_rate):
"""
Initialize LSL stream for EEG data
"""
# Create stream info
self.info = StreamInfo(
name=' ', # Stream name
type=' ', # Data type (EEG)
channel_count=n_channels,
nominal_srate=sampling_rate,
channel_format=' ', # float32
source_id=device_name
)
# Add channel labels
channels = self.info.desc().append_child("channels")
channel_names = [f'Ch{i+1}' for i in range(n_channels)]
for c in channel_names:
channels.append_child("channel") \
.append_child_value("label", c) \
.append_child_value("unit", " ") \
.append_child_value("type", "EEG")
# Create outlet
self.outlet = StreamOutlet(self.info)
def stream_data(self, eeg_buffer):
"""
Send data chunk through LSL
"""
# Check if anyone is listening
if self.outlet.have_consumers():
# Send sample
self.outlet. (eeg_buffer.tolist())
def add_markers(self, marker_stream):
"""
Send event markers synchronized with EEG
"""
# Create marker outlet
marker_info = StreamInfo(
name=' ',
type='Markers',
channel_count=1,
nominal_srate=0, # Irregular sampling
channel_format=' ', # string
source_id='marker_stream'
)
marker_outlet = StreamOutlet(marker_info)
return marker_outlet
class EEGReceiver:
def __init__(self, stream_name=None):
"""
Connect to LSL stream and receive data
"""
# Resolve EEG stream
print("Looking for EEG stream...")
streams = resolve_stream('type', ' ')
if len(streams) == 0:
raise RuntimeError("No EEG stream found")
# Create inlet
self.inlet = StreamInlet(streams[0])
# Get stream info
info = self.inlet.info()
self.n_channels = info. ()
self.srate = info.nominal_srate()
print(f"Connected to {info.name()} - {self.n_channels} channels @ {self.srate} Hz")
def receive_chunk(self, max_samples=1024):
"""
Receive a chunk of data
"""
# Pull chunk
chunk, timestamps = self.inlet. (max_samples=max_samples)
if chunk:
return np.array(chunk), np.array(timestamps)
else:
return None, None
def receive_continuous(self, duration, callback=None):
"""
Continuously receive data for specified duration
"""
start_time = time.time()
all_data = []
all_timestamps = []
while (time.time() - start_time) < duration:
chunk, timestamps = self.receive_chunk()
if chunk is not None:
all_data.append(chunk)
all_timestamps.append(timestamps)
# Process in real-time if callback provided
if callback:
callback(chunk, timestamps)
# Concatenate all data
if all_data:
data = np. (all_data, axis=0)
timestamps = np.concatenate(all_timestamps)
return data, timestamps
else:
return None, None
Match LSL concepts with their purposes:
| LSL Component |
Purpose |
| StreamInfo |
____________________ |
| StreamOutlet |
____________________ |
| StreamInlet |
____________________ |
| Timestamp |
____________________ |
Use BrainFlow for device-agnostic data acquisition:
from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds
from brainflow.data_filter import DataFilter, FilterTypes, DetrendOperations
class BrainFlowEEG:
def __init__(self, board_type='SYNTHETIC'):
"""
Initialize BrainFlow for EEG acquisition
"""
# Set up board parameters
self.params = BrainFlowInputParams()
# Select board
if board_type == 'SYNTHETIC':
self.board_id = BoardIds. .value
elif board_type == 'OPENBCI':
self.board_id = BoardIds.CYTON_BOARD.value
self.params.serial_port = ' ' # COM port
elif board_type == 'MUSE':
self.board_id = BoardIds. .value
# Create board object
self.board = BoardShim(self.board_id, self.params)
# Get board info
self.sampling_rate = BoardShim.get_sampling_rate(self.board_id)
self.eeg_channels = BoardShim.get_eeg_channels(self.board_id)
self.timestamp_channel = BoardShim.get_timestamp_channel(self.board_id)
def start_stream(self, buffer_size=450000):
"""
Start data acquisition
"""
# Prepare session
self.board. ()
# Start stream with buffer
self.board.start_stream(buffer_size)
print(f"Streaming started - {len(self.eeg_channels)} channels @ {self.sampling_rate} Hz")
def get_data(self, num_samples=None):
"""
Get data from the buffer
"""
if num_samples:
# Get specific number of samples
data = self.board. (num_samples)
else:
# Get all available data
data = self.board.get_board_data()
return data
def apply_filters(self, data):
"""
Apply real-time filters to data
"""
# Detrend to remove DC offset
for channel in self.eeg_channels:
DataFilter.detrend(data[channel], DetrendOperations. .value)
# Bandpass filter
for channel in self.eeg_channels:
DataFilter.perform_bandpass(
data[channel],
self.sampling_rate,
, # Low freq
, # High freq
4, # Order
FilterTypes.BUTTERWORTH.value,
0 # No ripple
)
# Notch filter for powerline
for channel in self.eeg_channels:
DataFilter.perform_bandstop(
data[channel],
self.sampling_rate,
, # Center frequency - 60 Hz
4.0, # Width
4, # Order
FilterTypes.BUTTERWORTH.value,
0
)
return data
def stop_stream(self):
"""
Stop acquisition and release resources
"""
self.board. ()
self.board.release_session()
# Real-time processing example
def process_chunk(eeg_chunk, channels):
"""
Process EEG data in real-time
"""
# Calculate instantaneous power
alpha_channels = []
for ch_idx in channels:
# Extract alpha band (8-13 Hz)
alpha_power = DataFilter.get_band_power(
eeg_chunk[ch_idx],
sampling_rate,
, # Alpha range
)
alpha_channels.append(alpha_power)
return np.mean(alpha_channels)
Topic 4: Experimental Design for EEG
Design a complete EEG experiment with proper timing and controls:
import numpy as np
import pandas as pd
from psychopy import visual, core, event
class EEGExperiment:
def __init__(self, subject_id, condition):
self.subject_id = subject_id
self.condition = condition
self.results = []
# Timing parameters (in seconds)
self.timing = {
'fixation': , # Pre-stimulus baseline
'stimulus': , # Stimulus duration
'response_window': , # Max response time
'iti': (1.0, 2.0), # Inter-trial interval (jittered)
'break_every': # Trials between breaks
}
# Experimental parameters
self.n_blocks = 4
self.trials_per_block = 50
self.conditions = ['congruent', 'incongruent']
def create_trial_list(self):
"""
Create balanced trial list with proper randomization
"""
trials = []
for block in range(self.n_blocks):
block_trials = []
# Create equal number of each condition
for condition in self.conditions:
n_trials = self.trials_per_block // len(self.conditions)
block_trials.extend([condition] * n_trials)
# Randomize within block
np.random. (block_trials)
# Add to main list with block info
for i, trial_type in enumerate(block_trials):
trial = {
'block': block + 1,
'trial': i + 1,
'condition': trial_type,
'iti': np.random.uniform(*self.timing['iti'])
}
trials.append(trial)
return pd.DataFrame(trials)
def send_trigger(self, trigger_code, lsl_outlet=None):
"""
Send trigger to EEG system
"""
# Trigger codes
triggers = {
'experiment_start': 1,
'block_start': 10,
'fixation': 20,
'stimulus_congruent': ,
'stimulus_incongruent': ,
'response_correct': 40,
'response_incorrect': 41,
'response_miss': 42,
'block_end': 90,
'experiment_end': 99
}
if lsl_outlet:
# Send via LSL
lsl_outlet.push_sample([trigger_code])
else:
# Send via parallel port (if available)
# parallel.setData(trigger_code)
pass
# Log trigger
timestamp = core.getTime()
return {'trigger': trigger_code, 'timestamp': timestamp}
def run_trial(self, trial_info, window, lsl_outlet):
"""
Run a single trial with proper timing
"""
trial_data = trial_info.copy()
# Fixation period (baseline)
fixation = visual.TextStim(window, text='+')
fixation.draw()
window.flip()
self.send_trigger( , lsl_outlet) # Fixation trigger
core.wait(self.timing['fixation'])
# Present stimulus
if trial_info['condition'] == 'congruent':
stimulus = visual.TextStim(window, text='< < < < <')
trigger = self.triggers['stimulus_congruent']
else:
stimulus = visual.TextStim(window, text='< < > < <')
trigger = self.triggers['stimulus_incongruent']
stimulus.draw()
window.flip()
stim_onset = core.getTime()
self.send_trigger(trigger, lsl_outlet)
# Collect response
response = event.waitKeys(
maxWait=self.timing['response_window'],
keyList=['left', 'right'],
timeStamped=core.Clock()
)
# Process response
if response:
key, rt = response[0]
trial_data['response'] = key
trial_data['rt'] = rt * 1000 # Convert to ms
# Check accuracy
correct_response = 'left' if '<' in trial_info['condition'] else 'right'
trial_data['correct'] = (key == correct_response)
# Send response trigger
if trial_data['correct']:
self.send_trigger( , lsl_outlet)
else:
self.send_trigger(41, lsl_outlet)
else:
# No response
trial_data['response'] = 'miss'
trial_data['rt'] = None
trial_data['correct'] = False
self.send_trigger(42, lsl_outlet)
# Inter-trial interval
window.flip() # Clear screen
core.wait(trial_info['iti'])
return trial_data
# Artifact control strategies
artifact_controls = {
'eye_movements': {
'strategy': 'Central fixation cross',
'implementation': ' '
},
'muscle_tension': {
'strategy': ' ',
'implementation': 'Regular breaks, comfortable seating'
},
'alpha_contamination': {
'strategy': 'Keep eyes open',
'implementation': ' '
}
}
What is the recommended minimum ITI for EEG experiments?
Implement proper event marking and timing verification:
class EventMarkerSystem:
def __init__(self):
self.marker_log = []
self.sync_pulses = []
def create_marker_scheme(self):
"""
Design a comprehensive marker scheme
"""
marker_scheme = {
# Experiment structure
'exp_start': 1,
'exp_end': 2,
'block_start': 10,
'block_end': 11,
# Trial events
'trial_start': 20,
'fixation_onset': 21,
'stimulus_onset': 30,
'stimulus_offset': 31,
'response_window_start': 40,
'response_window_end': 41,
# Stimulus types (30-39)
'stim_category_A': 32,
'stim_category_B': 33,
'stim_target': 35,
'stim_distractor': 36,
# Responses (50-59)
'response_correct': 50,
'response_incorrect': 51,
'response_miss': 52,
'response_early': 53,
# Special events
'break_start': 90,
'break_end': 91,
'sync_pulse': , # For timing verification
'artifact_detected': 99
}
return marker_scheme
def send_marker(self, marker_code, timestamp=None):
"""
Send marker with timing verification
"""
if timestamp is None:
timestamp = time.time()
# Record in log
self.marker_log.append({
'code': marker_code,
'timestamp': timestamp,
'sample': None # Will be filled by EEG system
})
# Send to multiple systems
self._send_to_lsl(marker_code)
self._send_to_parallel(marker_code)
# For critical markers, send verification pulse
if marker_code in [30, 50, 51, 52]: # Stimulus/response markers
time.sleep(0.001) # 1ms delay
self._send_to_parallel( ) # Clear/sync pulse
def verify_timing(self, eeg_timestamps):
"""
Check marker timing accuracy
"""
timing_errors = []
for i, marker in enumerate(self.marker_log[:-1]):
next_marker = self.marker_log[i + 1]
# Calculate expected vs actual intervals
expected_interval = next_marker['timestamp'] - marker['timestamp']
# Find markers in EEG data
marker_sample = self._find_marker_in_eeg(marker['code'], eeg_timestamps)
next_sample = self._find_marker_in_eeg(next_marker['code'], eeg_timestamps)
if marker_sample and next_sample:
actual_interval = (next_sample - marker_sample) / # sampling_rate
# Calculate timing error
error = abs(actual_interval - expected_interval) * 1000 # ms
if error > : # Threshold in ms
timing_errors.append({
'marker1': marker['code'],
'marker2': next_marker['code'],
'error_ms': error
})
return timing_errors
def generate_sync_pattern(self, duration=10):
"""
Generate synchronization pattern for offline alignment
"""
# Create unique pattern of pulses
sync_times = []
patterns = [100, 200, 100, 300, 100, 100, 200] # ms intervals
current_time = 0
for interval in patterns:
current_time += interval / 1000 # Convert to seconds
sync_times.append(current_time)
return sync_times
# Timing considerations
timing_best_practices = {
'stimulus_duration': {
'minimum': , # ms - at least 2 screen refreshes
'recommended': 500, # ms
'reason': 'Ensure reliable ERPs'
},
'marker_precision': {
'requirement': ' ms', # 1-2ms precision needed
'method': 'Hardware triggers or LSL timestamps'
},
'jitter': {
'purpose': ' ',
'range': '±500ms from mean ITI'
}
}
Video Demo Notes: Proper Electrode Application
Follow along with the video demonstration and complete this practical checklist:
Pre-Application Setup
- Measure head circumference: _____ cm
- Identify anatomical landmarks:
- Nasion located
- Inion located
- Preauricular points located
- Calculate 10-20 positions
- Mark Cz (vertex) position
Application Steps
- Clean Cz area with:
- Abrasion method used:
- Amount of gel applied:
- Initial impedance: _____ kΩ
- Final impedance: _____ kΩ
- Time to achieve good impedance: _____ minutes
Good
<5 kΩ
OK
5-10 kΩ
Poor
>10 kΩ
Target
< kΩ
Common mistakes to avoid (check all that apply):
Diagnose and solve these common recording problems:
| Problem |
Observed Signal |
Diagnosis |
Solution |
| 50/60 Hz everywhere |
Large amplitude sine wave |
|
Check ground, move away from power sources |
| Flat line on one channel |
No signal variation |
Disconnected electrode |
|
| Slow drift on all channels |
Baseline wandering |
|
Check reference, apply high-pass filter |
| Intermittent spikes |
Random high amplitude |
|
Re-seat electrode, check for movement |
| Clipping/saturation |
Signal hits maximum |
Input range exceeded |
|
Describe your systematic approach to diagnosing an unknown EEG problem:
Answer Key
Check your answers after completing all exercises.
Exercise 1: Consumer: 1-14 channels, 12-16 bits, 20k-200k$;
Research: 500-5000 Hz, >10 MΩ, <1 μVrms
Code: adc_bits, levels, 1e6, 24, 22 bits minimum
True statements: 2, 3, 4, 5
Exercise 2: Match: 1-C, 2-A, 3-B, 4-D, 5-E
Code: ERP: 500-1000, 0.1, linked ears; BCI: 256, 30-40, dry/active;
Sleep: 256, Ag/AgCl wet
Exercise 3: 70% alcohol, prep gel/sandpaper, pea-sized, 10-30, 5-10, 5
Table: Poor contact, Secure electrode, Gel bridge, Re-prep both sides
Exercise 4: 10-30, 10-100, 1000, excellent, poor, alcohol,
Ground issue, Gel drying, Re-secure
Test frequency: 10-30 Hz
Exercise 5: EEG-[device], EEG, float32, microvolts, push_sample,
EEG-Markers, string, EEG, channel_count, pull_chunk, vstack
LSL components: Metadata, Send data, Receive data, Synchronization
Exercise 6: SYNTHETIC_BOARD, COM3/ttyUSB0, MUSE_S_BOARD, prepare_session,
get_current_board_data, CONSTANT, 1.0, 50.0, 60.0, stop_stream, 8.0, 13.0
Exercise 7: 0.5-1.0, 0.5-1.0, 2.0, 30, shuffle, 30, 31, 20, 40
Artifact controls: Eye tracking, Relaxation instructions, Engaging task
Recommended ITI: 1000-2000 ms
Exercise 8: 100, 255, sampling_rate, 10, 33.33, 1-2, Prevent habituation
Best practices: 33.33 ms, 1-2 ms precision
Exercise 9: 70% alcohol, prep gel, pea-sized, 5
All mistakes listed are common and should be avoided
Exercise 10: Poor grounding, Re-connect/check cable, DC offset/sweat,
Loose connection, Reduce gain/check impedances
Congratulations! 🎉
You've completed Module 3 on EEG Data Collection! You now understand hardware specifications, electrode application techniques, real-time data acquisition protocols, and experimental design principles for high-quality EEG recordings.
Next step: Move on to Module 4 - Advanced EEG Processing