GiaPhaZZZ/NeuralDrive

GitHub: GiaPhaZZZ/NeuralDrive

一个开源脑机接口框架,通过 EEG 信号采集、深度学习分类和嵌入式硬件驱动,将人类意图实时解码为无人地面载具的运动控制指令。

Stars: 0 | Forks: 0

# 🤖 NEURAL DRIVE ⚡ **Neural Drive** 是一个开源的脑机接口(BCI)框架,旨在消除人类生物电位与机械执行之间的鸿沟。该平台通过放置在额头上方的电极拦截原始微伏信号,捕捉神经电(EEG)特征。系统通过轻量级卷积神经网络 pipeline 处理这些输入信号,将用户的意图直接解码为实时的 command 协议,从而远程控制无人地面载具(UGV)。 最初的实现方案展示了如何仅利用面部肌肉激活和特定的认知状态,实时操控比例模型机器人底盘的方向。 Neural Drive 的长远愿景是让人机自动化得以普及。想象一下这样的世界:人类的意图由 AI 驱动的机器人完美映射出来,为先进的控制辅助、物理康复以及深空宇宙探索的下一个前沿奠定基础。 ![EEG 采集系统](/meta/record_map.png) ## Command 矩阵与载具驱动 当前的分类模型可以区分 **四个神经 command 向量**,每一个都映射到遥控车辆的一个电机动作: | Command | 录制标签 | 动作 | 执行配置 | | :--- | :--- | :--- | :--- | | **`FORWARD_1`** | `short` | 短距离脉冲 | 电机正向旋转 1 秒(精密调整)。 | | **`FORWARD_2`** | `long` | 长距离行进 | 电机正向旋转 3 秒(快速移动)。 | | **`BACKWARD_1`** | `extra` | 短距离后退 | 电机反向旋转 1 秒(微小调整)。 | | **`BACKWARD_2`** | `back` | 长距离后退 | 电机反向旋转 3 秒(避开障碍物)。 | 右侧列(`short` / `long` / `extra` / `back`)是模型预测的字面字符串,也是数据集和代码中通用的文件夹名称——在阅读本文档其余部分时,请记住这个精确的拼写。 ## 🌌 核心支柱与任务目标 * **认知自动化:** 消除手动接口的阻力。你只需思考,AI-机器人生态系统就会负责执行物理操作。 * **增强人类能力:** 通过非侵入式神经技术突破人类的物理极限,为面向极端环境和宇宙探索的工具铺平道路。 * **高吞吐量遥测:** 构建于专门的 10 kHz 流数据摄取 pipeline 之上,确保从突触到硬件执行实现近乎零延迟。 ## 思考如何转化为运动 —— 端到端 pipeline 本代码库包含在三个不同硬件(一块 Arduino UNO、一台笔记本电脑和一块 ESP32-S3)上运行的五个阶段。每个阶段都会生成一个供下一阶段使用的 artifact: ``` EEG headset/electrodes │ analog µV signal ▼ arduino/EEG_arduino_record.ino (Arduino UNO, 10 kHz ADC sampling, USB-serial) │ 2-byte framed samples over serial (230,400 baud) ▼ record/data_acquisition.py (laptop GUI: live view + 4 s capture + filtering) │ one filtered run_N.npy per recording (you sort these by hand into class folders) ▼ data_process/prepare_dataset.py (offline batch job) │ one 224×224 spectrogram .png per recording, sorted into class folders ▼ train/train.ipynb (offline training) │ eeg_classify.pth (trained EfficientNetV2-S weights) ▼ live.py (laptop GUI: live capture + inference + BLE send) │ single ASCII command byte '1'-'4' over Bluetooth LE ▼ arduino/esp32s3_controller.ino (ESP32-S3: BLE peripheral + L298N motor driver) │ GPIO pulses ▼ Motor / vehicle actuation ``` `data_acquisition.py` 和 `prepare_dataset.py`/`train.ipynb` 对于每个数据集只需运行一次(用于收集测试数据并训练模型)。`live.py` 是唯一在实际实时使用期间运行的脚本,并且它重新实现了与训练期间完全相同的滤波和频谱图代码,以确保模型在生产环境中看到的数据与其训练时的数据相匹配。 ## 仓库映射 —— 每个文件的作用 | 路径 | 运行平台 | 作用 | | :--- | :--- | :--- | | `arduino/EEG_arduino_record.ino` | Arduino UNO | 裸机 Timer1 ISR,对模拟 EEG 前端进行采样,并通过串口流式传输带有 2 字节帧的样本数据。 | | `arduino/esp32s3_controller.ino` | ESP32-S3 | BLE GATT 外设,负责接收单字节 command 并据此驱动 L298N 电机驱动器和状态 LED。 | | `record/data_acquisition.py` | 笔记本电脑 (Python) | Tkinter GUI,用于连接 Arduino、查看实时波形,并将带有标签的 4 秒测试数据作为 `.npy` 捕获/保存。 | | `data_process/prepare_dataset.py` | 笔记本电脑 (Python) | 批处理脚本,将每个记录的 `.npy` 测试数据转换为 224×224 的频谱图 `.png`,并按类别进行组织。 | | `train/train.ipynb` | 笔记本电脑/GPU (Python) | 在频谱图图像上训练 EfficientNetV2-S 分类器,并保存最佳的 checkpoint、训练曲线和混淆矩阵。 | | `live.py` | 笔记本电脑 (Python) | 实时 GUI:捕获 4 秒的测试数据,进行滤波,构建频谱图,使用训练好的模型对其进行分类,并(可选)通过 BLE 将生成的 command 自动发送到 ESP32-S3。 | | `meta/record_map.png` | — | 参考接线图:BioAmp EXG Pill + 电极 → Arduino UNO → USB → 笔记本电脑。 | ## 硬件层 采集端(参见 `meta/record_map.png`)是一块 Arduino UNO,搭配 Upside Down Labs **BioAmp EXG Pill** 前端放大器,连接佩戴在头皮/前额的 1–3 个干电极。Pill 的输出接入 Uno 的模拟输入引脚 `A0`。 `EEG_arduino_record.ino` 配置 Timer1 大约每 100 µs 触发一次中断(`interrupt_Number = 198` → 单通道下约为 10 kHz),在中断内部读取 `analogRead(A0)`,并将每个 10 位的 ADC 值打包成**两个串口字节**:第一个字节的最高位设为 `1`(帧标记)并携带高位数据,第二个字节的最高位为 `0` 并携带低 7 位数据。`loop()` 仅在循环缓冲区填满时,通过 `Serial.write()` 尽可能快地将其排空。该 sketch 还接受内联串口命令(`c:N;` 用于设置通道数,`p:1;` 用于在引脚 5 上触发同步脉冲)——目前 Python 端仅使用设置通道数的命令(`c:1;`,即单通道)。尽管 pipeline 的其余部分目前默认为单通道,但该 sketch 在硬件/固件上最多支持 6 个通道(`A0`-`A5`)。 执行端是一块运行 `esp32s3_controller.ino` 的 **ESP32-S3**。它公开了一个 BLE 服务/特征(`SERVICE_UUID` / `CHARACTERISTIC_UUID`,均为硬编码),通过引脚 `IN3`/`IN4` 驱动 L298N H 桥(仅支持前进/后退——未连接 `ENA` PWM 引脚,因此电机始终以全速运行),并为每个 command 点亮不同颜色的单个 NeoPixel。每个 command(`'1'`–`'4'`)在返回空闲状态之前会阻塞固定的 `delay()`(1 秒或 3 秒);如果 BLE 客户端在 command 执行中途断开连接,电机将被强制停止。 ![遥控系统](/meta/controller_map.png) ## 阶段 1 —— 数据采集 (`record/data_acquisition.py`) 一个 Tkinter + Matplotlib 的 GUI,执行以下操作: 1. 列出串口并以 230,400 的波特率建立连接,发送 `c:1;` 使 Arduino 进入单通道模式。 2. 解析传入的 2 字节数据帧(`parse_byb_stream`)并将原始 ADC 计数转换为微伏,以 9 位 ADC 中点为中心,按 BioAmp 增益进行缩放(`raw_to_uv`,使用 `ADC_MAX=511`、`ADC_VREF=5.0`、`BYB_GAIN=974.0`)。 3. 连续运行**有状态的因果滤波链**(`LiveFilterPipeline`:1 Hz 高通 → 40 Hz 低通 → 50/60 Hz 陷波,全部为在 GUI ticks 之间保留状态的 biquad IIR),纯粹用于滚动的 2 秒实时显示。 4. 点击“START RECORD”时,缓冲 `N_CAPTURE = 41,000` 个原始样本(4 秒数据加上两侧各 500 个样本的填充),然后运行 `finalize_filter`:这是相同滤波链的**零相位**(`sosfiltfilt`/`filtfilt`)版本——DC 去除、1 Hz 高通、40 Hz 低通、陷波——并裁剪掉两端的 500 个样本填充,确保保存的数组恰好是 `N_RECORD = 40,000` 个样本(10 kHz 下 4 秒)。设置填充是为了专门吸收 IIR 预热瞬态,否则它会破坏测试数据的起始部分。 5. 弹出保存/废弃提示框;“SAVE”会将最终确定的 1 维 µV 数组写入 `run.npy`,如果基础名称已存在,则自动递增为 `run_1.npy`、`run_2.npy` 等。 **重要的手动步骤:** 录制器 GUI 没有类别选择控件。保存后,你需要自行负责将每个 `run_N.npy` 移动/重命名到正确的类别子文件夹中(参见下文的数据集布局)——脚本不知道也不会询问你刚才执行的是四个动作中的哪一个。 ## pipeline 其余部分预期的数据集布局 `prepare_dataset.py`(以及下游的所有环节)要求录制的数据已经按如下方式整理好: ``` BCI/data/ ├── long/ *.npy (FORWARD_2 trials) ├── short/ *.npy (FORWARD_1 trials) ├── extra/ *.npy (BACKWARD_1 trials) └── back/ *.npy (BACKWARD_2 trials) ``` 任何文件名中包含子字符串 `raw` 的文件都会被明确跳过,因此,如果你想在处理过的测试数据旁边保留任何原始/未滤波的备份,请为它们预留这个子字符串,以免它们混入数据集中。 ## 阶段 2 —— 频谱图生成 (`data_process/prepare_dataset.py`) 对于 `BCI/data//` 中的每个 `.npy` 文件,该脚本会: 1. 加载数组(同时处理普通的 1 维电压数组和 2 维 `[times, voltage]` 数组)。 2. 使用 `FS=250` 截取 `T_START=20 s` 到 `T_END=120 s` 的窗口。 3. 计算频谱图(`scipy.signal.spectrogram`,`nperseg=min(256, len//2)`,80% 重叠)。 4. 将其渲染为一张 224×224 像素的无边框图像(使用 `jet` 颜色映射,功率以 dB 显示对数刻度,频率轴截断至 0–45 Hz),并将其保存到 `BCI/spectrogram_data//.png`。 ### ⚠️ 在更改 `FS`、`RECORD_SEC` 或窗口常量之前,请务必阅读本节内容 在这个阶段内置了一个**尺度不匹配**的问题,重要的是理解它,而不是盲目地去“修复”它: - 硬件/`data_acquisition.py`/`live.py` 实际上是以 **`FS = 10,000 Hz`** 的频率采样和存储数据,且每次测试只有 **4 秒**长(40,000 个样本)。 - `prepare_dataset.py` 将加载的每个数组视为以 **`FS = 250 Hz`** 采样的数据,并截取 **20 秒–120 秒**的窗口——应用到 40,000 个样本的数组上,这只是意味着“第 5,000 到 30,000 个样本”(即 4 秒测试数据中大约 0.5 秒到 3.0 秒的部分),而不是真正的第 20 到 120 秒。 - `live.py` 中的 `eeg_to_spectrogram_image()` 在相同的 40,000 个样本缓冲区上使用了完全相同的常量(`SPEC_FS=250`、`T_START=20`、`T_END=120`),因此**训练和推理在内部是彼此一致的**——模型训练时和后续输入时,使用的是相同的缩放错误的切片/频率轴解释。这就是为什么尽管标签在绝对意义上是“错误”的,该系统依然能够实现端到端工作的原因。 - 风险仅仅在于你是否只在**某一个**文件中更改了 `FS`、`RECORD_SEC`、`SPEC_FS`、`T_START` 或 `T_END`,而没有在另一个文件(`prepare_dataset.py` ↔ `live.py`/`data_acquisition.py`)中进行同步更改。请将它们视为横跨这三个文件的匹配集合,而不是相互独立的单文件选项。 ## 阶段 3 —— 模型训练 (`train/train.ipynb`) 一个单 cell 的 notebook,执行以下操作: - 扫描 `BCI/spectrogram_data/{extra,short,long,back}/*.png` 并构建分层的 **70 / 10 / 20** 训练/验证/测试集划分(使用 `sklearn.train_test_split`,设置了随机种子)。 - 仅应用 `Resize(224,224) → ToTensor → Normalize(ImageNet mean/std)`——**没有进行数据增强**。 - 从在 ImageNet 上预训练的 `torchvision.models.efficientnet_v2_s` 构建模型,并将分类头替换为 `Dropout(0.3) → Linear(in_features, 4)`。 - 使用 `AdamW(`lr=1e-4`,`weight_decay=1e-4`)和 `CosineAnnealingLR` 训练 **`NUM_EPOCHS = 5`** 个 epoch,batch size 为 16,并在验证准确率提升时保存 checkpoint。 - 输出:`eeg_classify.pth`(最佳权重)、`history.json`、`training_curves.png`(损失/准确率/LR)和 `test_evaluation.png`(预留的 20% 测试集上的分类报告 + 混淆矩阵)。 `NUM_EPOCHS=5` 是一个快速演示设置,而不是调优后的值——如果准确率不理想,这是你首先需要增加的参数之一(参见下方的快速参考表)。 ## 阶段 4 —— 实时推理 + 电机控制 (`live.py`) 这是你在实时操作期间实际运行的脚本。它结合了: - **串口读取器** —— 具有与 `data_acquisition.py` 相同的组帧/解析/µV 转换/实时滤波代码。 - **录制器** —— 具有与 `data_acquisition.py` 相同的 4 秒捕获和零相位 `finalize_filter`。 - **频谱图构建器**(`eeg_to_spectrogram_image`) —— 与 `prepare_dataset.py` 的切片/绘图逻辑相同,但渲染为内存中的 PIL 图像而不是文件,然后进行垂直翻转(低频在底部)并缩放至 `224×224`。 - **模型加载器**(`load_model`/`build_model`) —— 重建相同的 `efficientnet_v2_s` 架构(这次使用 `weights=None`,因为训练好的 checkpoint 无论如何都会覆盖所有参数)并加载 `eeg_classify.pth`。 - **分类器**(`classify`) —— 应用推理时的 transform(`Resize → ToTensor → Normalize`,与训练时统计参数相同),并返回预测的类别以及 `CLASSES = ["extra", "short", "long", "back"]` 上的完整 softmax 分布。 - **BLEManager** —— 在后台线程中运行其自己的 `asyncio` 事件循环(通过 `bleak`),以便 BLE I/O 永远不会阻塞 Tkinter 主循环;暴露了线程安全的 `connect()`/`disconnect()`/`send_command()`。 GUI 内部的操作流程:连接 EEG 串口 → 通过 BLE 连接到 ESP32 → 点击 **RECORD & CLASSIFY** → 4 秒后显示频谱图和类别概率 → 如果勾选了“Auto-send to motor”,相应的 command 字节(`CLASS_TO_CMD`)会立即写入 BLE 特征;否则,**SEND TO MOTOR** 按钮将被激活,以便手动确认后再发送。 默认的 BLE 目标(`BLE_ADDRESS = "E0:72:A1:AA:13:85"`)是某一块特定的 ESP32-S3 开发板的 MAC 地址,**必须进行更改**以匹配你自己的开发板(或者通过 `--ble` 传递),并且 `BLE_CHAR_UUID` 必须始终与编译进 `esp32s3_controller.ino` 的 `CHARACTERISTIC_UUID` 保持一致。 ## 快速参考 —— 在哪里修改什么 | 你想要更改的内容 | 编辑此处 | 备注 | | :--- | :--- | :--- | | 采样率 / 测试长度 / 边缘填充 | `record/data_acquisition.py` **和** `live.py` 中的 `FS`、`RECORD_SEC`、`EDGE_TRIM` | 必须相互匹配;在同时修改 `prepare_dataset.py` 的 `FS`/窗口常量之前,请参阅上文的采样率警告。 | | 陷波器默认值(电源嗡嗡声) | `LiveFilterPipeline(...)` 中的 `notch_hz` 默认值 / 任一 Python GUI 中 GUI 的“NOTCH”下拉菜单 | 根据你的交流电源频率,选择使用 50 Hz(欧盟/亚洲)或 60 Hz(美国)。 | | 频谱图外观(颜色映射、频率范围、图像尺寸) | `prepare_dataset.py`(`cmap`、`ax.set_ylim`、`TARGET_SIZE`)**和** `live.py` 中的 `eeg_to_spectrogram_image` | 保持两者同步——模型是在 `prepare_dataset.py` 生成的任何内容上进行训练的。 | | EEG 通道数 | `EEG_arduino_record.ino` 中的 `numberOfChannels` 逻辑;`data_acquisition.py`/`live.py` 中发送的 `c:1;` 命令 | 硬件/固件最多支持 6 个通道;目前 Python 的解析/`raw_to_uv` 默认为单通道。 | | 类别集 / 标签 | `prepare_dataset.py` 中的 `SUBFOLDERS`、`train.ipynb` 中的 `CLASSES`、`live.py` 中的 `CLASSES`/`CLASS_TO_CMD` | 这三个列表必须保持相同的顺序和拼写,因为模型的输出索引只是此列表中的一个位置。 | | 模型架构 / 超参数 | `train.ipynb` 中的 `build_model`、`BATCH_SIZE`、`NUM_EPOCHS`、`LR`、`WEIGHT_DECAY` | `NUM_EPOCHS=5` 是最小的演示值;若要获得生产级质量的模型,请提高该值(并考虑添加数据增强)。 | | 训练/验证/测试集划分比例 | `train.ipynb` 中的 `split_samples()` | 目前为 70/10/20,按类别分层。 | | BLE 设备地址 / 特征 | `live.py` 中的 `BLE_ADDRESS`、`BLE_CHAR_UUID`;`esp32s3_controller.ino` 中的 `SERVICE_UUID`、`CHARACTERISTIC_UUID` | ESP32 sketch 和 `live.py` 之间的 UUID 必须完全匹配。 | | 电机 command → 动作映射 / 时序 | `esp32s3_controller.ino` 中的 `switch(currentCommand)` 块;`live.py` 中的 `CLASS_TO_CMD` | 持续时间是硬编码的 `delay()` 调用;command 是阻塞的(在 `delay()` 期间发送的新 command 会到达,但在当前的 command 完成之前,switch 不会执行)。 | | 串口波特率 | `EEG_arduino_record.ino` 中的 `Serial.begin(230400)`;两个 Python GUI 中的 `serial.Serial(port, 230400, ...)` 调用 | 两端必须匹配。 | | 数据集/输出路径 | `prepare_dataset.py` 中的 `DATA_DIR`、`OUTPUT_DIR`;`train.ipynb` 中的 `DATA_ROOT`、`SAVE_PATH`、`HISTORY_PATH`、`PLOT_PATH`、`EVAL_PATH` | 全部相对于你启动脚本的位置。 | ## 设置与运行顺序 1. 按照 `meta/record_map.png` 所示,将 BioAmp EXG Pill + 电极连接到 Arduino UNO,并将 `arduino/EEG_arduino_record.ino` 烧录进去。 2. 按照 `arduino/esp32s3_controller.ino` 中的定义,将 L298N 电机驱动器和 NeoPixel 连接到 ESP32-S3,烧录程序,并记下它的 BLE MAC 地址。 3. 安装 Python 依赖项(见下文)并运行 `record/data_acquisition.py`。重复执行四个心理/手势任务中的每一个,每次保存一个测试数据,然后手动将生成的 `.npy` 文件分类整理到 `BCI/data/{long,short,extra,back}/` 中。 4. 运行 `data_process/prepare_dataset.py` 将每个测试数据转换为频谱图,存放在 `BCI/spectrogram_data/{long,short,extra,back}/` 下。 5. 运行 `train/train.ipynb` 训练分类器;它将生成 `eeg_classify.pth` 以及训练/评估图表。 6. 运行 `live.py --model path/to/eeg_classify.pth --ble `,连接到 EEG 串口并通过 BLE 连接到 ESP32,然后开始对实时测试数据进行分类。 ## 依赖项 Python:`numpy`、`scipy`、`matplotlib`、`pillow`、`torch`、`torchvision`、`scikit-learn`、`seaborn`、`pyserial`、`bleak`、`tkinter`(通常随 Python 一起捆绑)。 Arduino 库:`BLEDevice`/`BLEServer`/`BLEUtils`(内置于 ESP32 Arduino 核心中)和 `Adafruit_NeoPixel`。
标签:凭据扫描, 机器人控制, 物联网, 神经网络, 脑机接口, 脑电图分析, 自动驾驶导航, 逆向工具