
OpenBCI-Python与OpenBCI:实时脑电信号采集实战
一、引言:Python在BCI开发中的优势
Python已经成为脑机接口开发的主流语言,原因如下:
- 丰富的科学计算库:NumPy、SciPy、Matplotlib等
- 强大的机器学习框架:TensorFlow、PyTorch、Scikit-learn
- 活跃的社区支持:大量教程和开源项目
- 跨平台兼容性:Windows、macOS、Linux
本文将详细介绍如何使用Python与OpenBCI进行实时脑电信号采集和处理。
二、环境准备
2.1 安装依赖库
bash
# 核心库
pip install numpy scipy matplotlib pandas
# BrainFlow
pip install brainflow
# 高级EEG分析
pip install mne pyriemann
# 可视化
pip install seaborn plotly
# 实时绘图
pip install pyqtgraph pyside2
2.2 验证安装
python
import numpy as np
import matplotlib.pyplot as plt
import brainflow
print(f"NumPy版本: {np.__version__}")
print(f"BrainFlow版本: {brainflow.__version__}")
print("环境配置完成!")
三、基础数据采集
3.1 完整采集流程
python
from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds
import time
import numpy as np
import matplotlib.pyplot as plt
# 1. 配置设备参数
params = BrainFlowInputParams()
params.serial_port = "COM3" # Windows串口
# params.serial_port = "/dev/ttyUSB0" # Linux串口
# 2. 初始化设备
board_id = BoardIds.CYTON_BOARD.value
board = BoardShim(board_id, params)
try:
# 3. 准备会话
board.prepare_session()
# 4. 开始数据流
board.start_stream()
print("开始采集数据...")
# 5. 采集数据(10秒)
time.sleep(10)
# 6. 获取数据
data = board.get_board_data()
# 7. 停止数据流
board.stop_stream()
board.release_session()
print(f"采集完成!数据形状: {data.shape}")
# 8. 处理数据
eeg_channels = BoardShim.get_eeg_channels(board_id)
sampling_rate = BoardShim.get_sampling_rate(board_id)
print(f"EEG通道索引: {eeg_channels}")
print(f"采样率: {sampling_rate} Hz")
# 9. 绘制波形
plt.figure(figsize=(12, 8))
for i, ch in enumerate(eeg_channels[:4]):
plt.subplot(4, 1, i+1)
plt.plot(data[ch][:500])
plt.title(f"Channel {ch+1}")
plt.ylabel("μV")
plt.grid(True)
plt.xlabel("Samples")
plt.tight_layout()
plt.show()
except Exception as e:
print(f"采集过程中发生错误: {e}")
if board.is_prepared():
board.release_session()
3.2 数据格式说明
python
# BrainFlow返回的数据格式
# 二维数组:行 = 通道,列 = 采样点
# 获取各类通道
eeg_channels = BoardShim.get_eeg_channels(board_id) # EEG通道
accel_channels = BoardShim.get_accel_channels(board_id) # 加速度计通道
timestamp_channel = BoardShim.get_timestamp_channel(board_id) # 时间戳通道
marker_channel = BoardShim.get_marker_channel(board_id) # 标记通道
# 提取数据
eeg_data = data[eeg_channels, :] # EEG数据
accel_data = data[accel_channels, :] # 加速度计数据
timestamps = data[timestamp_channel, :] # 时间戳
markers = data[marker_channel, :] # 标记
print(f"EEG数据: {eeg_data.shape}")
print(f"时间戳: {timestamps.shape}")
四、实时数据处理
4.1 实时波形显示
python
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from collections import deque
# 参数配置
fs = 250
buffer_size = 500
channels_to_display = 4
# 初始化缓冲区
buffers = [deque(maxlen=buffer_size) for _ in range(channels_to_display)]
# 设置绘图
fig, axes = plt.subplots(channels_to_display, 1, figsize=(12, 8))
lines = []
for i in range(channels_to_display):
line, = axes[i].plot([], [], lw=1)
lines.append(line)
axes[i].set_ylim(-100, 100)
axes[i].set_xlim(0, buffer_size)
axes[i].grid(True)
axes[i].set_ylabel(f"Ch{i+1} (μV)")
axes[-1].set_xlabel("Samples")
# 更新函数
def update(frame):
# 获取最新数据(10个采样点)
current_data = board.get_current_board_data(10)
# 更新缓冲区
eeg_channels = BoardShim.get_eeg_channels(board_id)
for i in range(channels_to_display):
ch_data = current_data[eeg_channels[i]]
buffers[i].extend(ch_data)
# 更新曲线
lines[i].set_data(range(len(buffers[i])), buffers[i])
return lines
# 启动设备
params = BrainFlowInputParams()
params.serial_port = "COM3"
board = BoardShim(board_id, params)
board.prepare_session()
board.start_stream()
# 启动动画
ani = animation.FuncAnimation(fig, update, interval=50, blit=True)
try:
plt.show()
except KeyboardInterrupt:
board.stop_stream()
board.release_session()
print("采集停止")
4.2 实时频谱分析
python
from scipy.signal import welch
import numpy as np
class RealTimeSpectrumAnalyzer:
def __init__(self, fs=250, window_size=2):
self.fs = fs
self.window_size = window_size
self.buffer = deque(maxlen=int(fs * window_size))
def add_data(self, data):
"""添加新数据"""
self.buffer.extend(data)
def compute_spectrum(self):
"""计算功率谱密度"""
if len(self.buffer) < self.fs:
return None, None
data = np.array(self.buffer)
freqs, psd = welch(data, fs=self.fs, nperseg=256)
return freqs, psd
# 使用示例
analyzer = RealTimeSpectrumAnalyzer()
# 在实时循环中
while True:
current_data = board.get_current_board_data(50)
analyzer.add_data(current_data[1]) # 通道1
freqs, psd = analyzer.compute_spectrum()
if freqs is not None:
print(f"α波功率: {np.mean(psd[(freqs>=8) & (freqs<=13)])}")
time.sleep(0.1)
五、信号预处理
5.1 滤波操作
python
from scipy.signal import butter, filtfilt
def butter_bandpass(lowcut, highcut, fs, order=5):
"""设计巴特沃斯带通滤波器"""
nyquist = 0.5 * fs
low = lowcut / nyquist
high = highcut / nyquist
b, a = butter(order, [low, high], btype='band')
return b, a
def butter_bandpass_filter(data, lowcut, highcut, fs, order=5):
"""应用带通滤波"""
b, a = butter_bandpass(lowcut, highcut, fs, order=order)
y = filtfilt(b, a, data)
return y
def butter_notch_filter(data, freq, fs, order=4):
"""设计陷波滤波器(去除工频干扰)"""
nyquist = 0.5 * fs
notch_freq = freq / nyquist
b, a = butter(order, [notch_freq - 0.01, notch_freq + 0.01], btype='bandstop')
y = filtfilt(b, a, data)
return y
# 使用示例
filtered_data = butter_bandpass_filter(raw_data, 0.5, 50, fs=250)
filtered_data = butter_notch_filter(filtered_data, 50, fs=250)
5.2 伪迹去除
python
import numpy as np
def detect_eye_blink(eeg_data, threshold=150):
"""检测眼电伪迹(眨眼)"""
blink_indices = np.where(np.abs(eeg_data) > threshold)[0]
return blink_indices
def remove_artifacts(eeg_data, blink_indices, window_size=100):
"""去除伪迹(用前后均值替换)"""
cleaned_data = eeg_data.copy()
for idx in blink_indices:
# 获取周围数据
start = max(0, idx - window_size)
end = min(len(eeg_data), idx + window_size)
# 计算前后均值
before_mean = np.mean(cleaned_data[start:idx]) if start < idx else cleaned_data[idx]
after_mean = np.mean(cleaned_data[idx+1:end]) if idx+1 < end else cleaned_data[idx]
# 线性插值
cleaned_data[idx] = (before_mean + after_mean) / 2
return cleaned_data
# 使用示例
blink_indices = detect_eye_blink(eeg_data[0])
cleaned_data = remove_artifacts(eeg_data[0], blink_indices)
六、特征提取
6.1 频域特征
python
from scipy.signal import welch
def extract_frequency_features(eeg_data, fs=250):
"""提取频域特征"""
freqs, psd = welch(eeg_data, fs=fs, nperseg=256)
# 定义频段
bands = {
'delta': (1, 4),
'theta': (4, 8),
'alpha': (8, 13),
'beta': (13, 30),
'gamma': (30, 50)
}
features = {}
for band, (low, high) in bands.items():
mask = (freqs >= low) & (freqs <= high)
features[band] = np.mean(psd[mask])
# 计算比值特征
features['alpha/beta'] = features['alpha'] / (features['beta'] + 1e-10)
features['theta/alpha'] = features['theta'] / (features['alpha'] + 1e-10)
return features
# 使用示例
features = extract_frequency_features(eeg_data[0])
print("频域特征:")
for key, value in features.items():
print(f" {key}: {value:.4e}")
6.2 时域特征
python
def extract_time_domain_features(eeg_data):
"""提取时域特征"""
features = {}
# 基本统计特征
features['mean'] = np.mean(eeg_data)
features['std'] = np.std(eeg_data)
features['var'] = np.var(eeg_data)
features['min'] = np.min(eeg_data)
features['max'] = np.max(eeg_data)
features['range'] = np.max(eeg_data) - np.min(eeg_data)
# 高阶统计特征
features['skewness'] = np.mean((eeg_data - features['mean']) ** 3) / (features['std'] ** 3 + 1e-10)
features['kurtosis'] = np.mean((eeg_data - features['mean']) ** 4) / (features['std'] ** 4 + 1e-10)
# 过零率
features['zero_crossing_rate'] = np.sum(np.diff(np.sign(eeg_data)) != 0) / len(eeg_data)
# RMS值
features['rms'] = np.sqrt(np.mean(eeg_data ** 2))
return features
# 使用示例
time_features = extract_time_domain_features(eeg_data[0])
print("时域特征:")
for key, value in time_features.items():
print(f" {key}: {value:.4f}")
七、完整实战项目
7.1 脑电信号实时监测系统
python
import time
import numpy as np
import matplotlib.pyplot as plt
from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds
from collections import deque
class EEGMonitor:
def __init__(self, board_id, port, fs=250):
self.board_id = board_id
self.port = port
self.fs = fs
self.board = None
self.eeg_channels = []
self.features_history = deque(maxlen=100)
def connect(self):
"""连接设备"""
params = BrainFlowInputParams()
params.serial_port = self.port
self.board = BoardShim(self.board_id, params)
self.board.prepare_session()
self.board.start_stream()
self.eeg_channels = BoardShim.get_eeg_channels(self.board_id)
print("设备连接成功")
def disconnect(self):
"""断开连接"""
if self.board:
self.board.stop_stream()
self.board.release_session()
print("设备断开连接")
def get_features(self, window_size=2):
"""获取特征"""
data = self.board.get_current_board_data(int(self.fs * window_size))
# 计算各通道频段功率
features = []
for ch in self.eeg_channels:
ch_data = data[ch]
# 滤波
from scipy.signal import butter, filtfilt
nyquist = 0.5 * self.fs
b, a = butter(4, [0.5/nyquist, 50/nyquist], btype='band')
ch_data = filtfilt(b, a, ch_data)
# 计算频谱
from scipy.signal import welch
freqs, psd = welch(ch_data, fs=self.fs, nperseg=256)
# 提取频段功率
bands = [(1,4), (4,8), (8,13), (13,30), (30,50)]
for low, high in bands:
mask = (freqs >= low) & (freqs <= high)
features.append(np.mean(psd[mask]))
return np.array(features)
def evaluate_state(self):
"""评估心理状态"""
features = self.get_features()
# 计算平均频段功率
delta = np.mean(features[::5])
theta = np.mean(features[1::5])
alpha = np.mean(features[2::5])
beta = np.mean(features[3::5])
gamma = np.mean(features[4::5])
# 计算状态指标
alpha_beta_ratio = alpha / (beta + 1e-10)
theta_alpha_ratio = theta / (alpha + 1e-10)
# 判断状态
if alpha_beta_ratio > 2:
state = "放松"
color = "green"
elif alpha_beta_ratio > 1:
state = "中等"
color = "yellow"
else:
state = "紧张"
color = "red"
return {
'state': state,
'color': color,
'alpha/beta': alpha_beta_ratio,
'theta/alpha': theta_alpha_ratio,
'bands': {'delta': delta, 'theta': theta, 'alpha': alpha, 'beta': beta, 'gamma': gamma}
}
# 使用示例
if __name__ == "__main__":
monitor = EEGMonitor(BoardIds.CYTON_BOARD.value, "COM3")
try:
monitor.connect()
while True:
state_info = monitor.evaluate_state()
print(f"状态: {state_info['state']}")
print(f"α/β比值: {state_info['alpha/beta']:.2f}")
print(f"频段功率: {state_info['bands']}")
print("-" * 40)
time.sleep(1)
except KeyboardInterrupt:
monitor.disconnect()
print("程序退出")
7.2 数据记录与回放
python
import numpy as np
import os
class DataRecorder:
def __init__(self, save_dir="data"):
self.save_dir = save_dir
os.makedirs(save_dir, exist_ok=True)
def save_data(self, data, filename=None):
"""保存数据"""
if filename is None:
filename = f"eeg_data_{int(time.time())}.npy"
filepath = os.path.join(self.save_dir, filename)
np.save(filepath, data)
print(f"数据已保存到: {filepath}")
return filepath
def load_data(self, filepath):
"""加载数据"""
return np.load(filepath)
def save_features(self, features, filename=None):
"""保存特征"""
if filename is None:
filename = f"features_{int(time.time())}.csv"
filepath = os.path.join(self.save_dir, filename)
import pandas as pd
df = pd.DataFrame(features)
df.to_csv(filepath, index=False)
print(f"特征已保存到: {filepath}")
return filepath
# 使用示例
recorder = DataRecorder()
recorder.save_data(data, "experiment_001.npy")
八、可视化技巧
8.1 实时动态图表
python
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# 创建实时更新的图表
fig = make_subplots(rows=2, cols=1,
subplot_titles=("EEG波形", "频段功率"))
# 初始化数据
time_data = np.arange(0, 500)
eeg_data = np.zeros(500)
band_power_data = [0.2, 0.15, 0.4, 0.15, 0.1]
bands = ['delta', 'theta', 'alpha', 'beta', 'gamma']
# 添加轨迹
fig.add_trace(go.Scatter(x=time_data, y=eeg_data,
mode='lines', name='EEG'), row=1, col=1)
fig.add_trace(go.Bar(x=bands, y=band_power_data,
name='频段功率'), row=2, col=1)
# 更新布局
fig.update_layout(height=600, width=800, title="实时脑电监测")
fig.update_xaxes(title_text="样本", row=1, col=1)
fig.update_yaxes(title_text="电压 (μV)", row=1, col=1)
fig.update_xaxes(title_text="频段", row=2, col=1)
fig.update_yaxes(title_text="功率", row=2, col=1)
# 显示图表(在Jupyter环境中)
fig.show()
8.2 头部拓扑图
python
import matplotlib.pyplot as plt
import numpy as np
def plot_head_topography(data, electrodes):
"""绘制头部拓扑图"""
# 电极位置(简化版10-20系统)
positions = {
'Fp1': (-0.8, 0.9), 'Fp2': (0.8, 0.9),
'F3': (-0.6, 0.5), 'F4': (0.6, 0.5),
'C3': (-0.6, 0), 'C4': (0.6, 0),
'P3': (-0.6, -0.5), 'P4': (0.6, -0.5),
'O1': (-0.4, -0.9), 'O2': (0.4, -0.9),
'Fz': (0, 0.5), 'Cz': (0, 0), 'Pz': (0, -0.5)
}
fig, ax = plt.subplots(figsize=(8, 8))
# 绘制头部轮廓
circle = plt.Circle((0, 0), 1, color='gray', fill=False, linewidth=2)
ax.add_artist(circle)
# 绘制电极
for electrode, (x, y) in positions.items():
if electrode in electrodes:
idx = electrodes.index(electrode)
value = data[idx]
# 颜色映射
color = plt.cm.viridis(value / np.max(data))
ax.scatter(x, y, s=200, c=[color], edgecolors='black')
ax.text(x, y+0.05, electrode, ha='center', fontsize=8)
# 添加颜色条
sm = plt.cm.ScalarMappable(cmap='viridis',
norm=plt.Normalize(vmin=np.min(data), vmax=np.max(data)))
sm.set_array([])
plt.colorbar(sm, ax=ax, label='活动强度')
ax.set_xlim(-1.2, 1.2)
ax.set_ylim(-1.2, 1.2)
ax.set_title('脑电活动拓扑图')
plt.show()
# 使用示例
electrodes = ['Fp1', 'Fp2', 'F3', 'F4', 'C3', 'C4', 'P3', 'P4', 'O1', 'O2', 'Fz', 'Cz', 'Pz']
activity_data = np.random.rand(len(electrodes))
plot_head_topography(activity_data, electrodes)
九、常见问题解决
9.1 串口权限问题
bash
# Linux系统下添加用户到dialout组
sudo usermod -aG dialout $USER
# 重启后生效
9.2 实时性能优化
python
# 优化建议:
# 1. 使用较小的缓冲区
# 2. 减少不必要的计算
# 3. 使用多线程/多进程
# 4. 降低采样率(如果允许)
# 示例:使用多线程
import threading
class DataProcessor(threading.Thread):
def __init__(self, board):
super().__init__()
self.board = board
self.running = True
def run(self):
while self.running:
data = self.board.get_current_board_data(100)
# 处理数据...
time.sleep(0.05)
def stop(self):
self.running = False
9.3 数据同步问题
python
# 使用时间戳同步
timestamps = data[BoardShim.get_timestamp_channel(board_id)]
# 计算实际采样率
actual_fs = len(timestamps) / (timestamps[-1] - timestamps[0])
print(f"实际采样率: {actual_fs:.2f} Hz")
十、总结
Python为BCI开发提供了强大的工具链,从数据采集到实时处理再到可视化,都有成熟的解决方案。
10.1 核心技能总结
- ✅ BrainFlow数据采集
- ✅ 实时数据处理
- ✅ 信号滤波与伪迹去除
- ✅ 特征提取(频域和时域)
- ✅ 数据可视化
- ✅ 状态评估
10.2 下一步计划
在下一篇文章中,我们将深入学习信号预处理技术,包括滤波、去噪和伪迹去除的高级方法。
**继续探索Python在BCI开发中的应用!**下一篇我们将学习信号预处理技术。
本文是《OpenBCI从入门到精通》系列的第7篇。
关键字:Python、OpenBCI、脑电采集、实时处理、信号分析
