original push

This commit is contained in:
Ivey Song
2026-06-01 13:18:36 +08:00
commit 8426770db6
46 changed files with 341750 additions and 0 deletions

View File

@@ -0,0 +1,427 @@
import os
import sys
import threading
import time
import numpy as np
import pandas as pd
from SunnyLinker import SunnyLinker64
from zmqServer import zmqServer
from zmqClient import zmqClient
from scipy.io import savemat
from scipy import signal
class Parser_main(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.Running = True
self.fs = 250 # 采样率
self.energy = 0 # 电量
self.status_code = 0 # 与采集设备通信的状态码0为异常1为正常
self.n_chan = 64
self.dataBuffer = []
self.file_num = 0#保存文件序号
self.subject_id = None #受试者ID
self.session_id = None #Session ID
self.last_print_time = None
# 预处理参数
self.enable_preprocess = True # 是否启用预处理
self.lowcut = 0.5 # 高通滤波截止频率 (Hz)
self.highcut = 50 # 低通滤波截止频率 (Hz)
self.notch_freq = 50 # 工频陷波频率 (Hz)
self.ref_chan_name = 'CPZ' # 参考电极名称
self.ref_chan_idx = None # 参考电极索引(运行时确定)
self._init_filter_cache() # 初始化滤波器缓存
# 单位转换参数
self.calibration_scale = 1.0 # 校准系数,用于修正单位转换误差
self.calibration_offset = 0 # 校准偏移量
self._conversion_verified = False # 是否已验证转换
def connect(self):
self.thread_data_server = SunnyLinker64('127.0.0.1', 7878, 250, 64,
method='tcp')
self.thread_data_server.toUv = True
self.thread_data_server.start()
self.zmqServer = zmqServer()
self.zmqServer.start()
self.zmqClient = zmqClient('127.0.0.1', 8088)
self.zmqClient.connect()
def run(self):
while self.Running:
# 同步信息
if self.zmqServer.state_mode == 'sync':
self.zmqClient.send_to_all('sync', self.zmqClient.state)
self.zmqServer.state_mode = 'rest'
# 状态异常,报告上位机
if self.status_code != self.thread_data_server.status_code:
self.status_code = self.thread_data_server.status_code
self.zmqClient.send_to_all('status_code', int(self.status_code))
# 返回电量
if self.energy != self.thread_data_server.energy:
self.energy = self.thread_data_server.energy
self.zmqClient.send_to_all('energy', int(self.energy))
# 更新文件序号
if self.subject_id != self.zmqServer.subject_id or self.session_id != self.zmqServer.session_id:
self.subject_id = self.zmqServer.subject_id
self.session_id = self.zmqServer.session_id
self.file_num = 0 #从零开始计数
if self.zmqServer.open_Impedance == True: # 开启阻抗检测功能,仅运行一次
self.thread_data_server.Impedance(True)
self.zmqServer.open_Impedance = -1
elif self.zmqServer.open_Impedance == False:
self.thread_data_server.Impedance(False)
self.zmqServer.open_Impedance = -1
if self.zmqServer.get_Impedance: # 返回阻抗值
if self.thread_data_server.GetDataLenCount() > self.fs:
Impe_data = self.thread_data_server.getData(self.fs)
# 计算阻抗
imps = self.thread_data_server.getImpedance(Impe_data, self.n_chan)
self.zmqClient.send_to_all('impedance', imps.tolist())
else:
pass
if self.thread_data_server.GetDataLenCount() < 50:
time.sleep(0.01)
continue
if self.zmqServer.get_Impedance == False: # 非阻抗检测状态
data = self.thread_data_server.getData(50)
data = data[:self.n_chan, :]
# 数据质量检查与预处理
if self.enable_preprocess:
# 1. 首先验证和校准单位转换
data, calibrated = self.verify_and_calibrate_unit(data)
if calibrated:
print('[INFO] 单位转换已自动校准')
# 2. 检查数据质量
issues = self.check_data_quality(data)
if issues:
print('[警告] 检测到数据质量问题:')
for issue in issues:
print(f' - {issue}')
print('[INFO] 正在进行信号预处理...')
# 3. 执行预处理
data = self.preprocess_data(data)
# 4. 预处理后验证
if issues:
new_issues = self.check_data_quality(data)
if not new_issues:
print(f'[INFO] 预处理完成,数据幅度正常: {np.max(np.abs(data)):.2f} µV')
else:
print('[警告] 预处理后仍存在问题:')
for issue in new_issues:
print(f' - {issue}')
if self.zmqServer.mat_generate:
# 检测是否需要重置缓冲区(第二次发送 matGenerate 时清空旧数据)
if self.zmqServer.reset_mat_buffer:
self.dataBuffer = []
self.last_print_time = None
self.zmqServer.reset_mat_buffer = False
print('[INFO] 数据缓冲区已重置,从头开始采集')
self.dataBuffer.append(data)
if len(self.dataBuffer) % 50 == 0:
current_time = time.time()
if self.last_print_time is not None:
elapsed_time = current_time - self.last_print_time
# 2500个点 = 50个数据块 * 50个采样点/数据块
actual_fs = 2500 / elapsed_time
print(f"接收 2500 个采样点耗时: {elapsed_time:.4f} 秒, 折合实际采样率: {actual_fs:.2f} Hz")
else:
print("开始计时...")
self.last_print_time = current_time
print('数据保存进度: {}/{}'.format(len(self.dataBuffer),int(self.zmqServer.save_win*self.fs//50)))
if len(self.dataBuffer) >= int(self.zmqServer.save_win*self.fs//50): #5分钟*60秒*250Hz / 50
self.zmqServer.mat_generate = False
matData = np.hstack(self.dataBuffer[:int(self.zmqServer.save_win*self.fs//50)])
self.dataBuffer = []
self.last_print_time = None # 重置计时器以备下次使用
self.pack2mat(matData,self.subject_id,self.session_id)
def pack2mat(self,data,subject_id,session_id):
#EEG数据
Data = data.T
#通道名称
channel_names = np.array(
['AIN1', 'AIN2', 'AIN3', 'AIN4', 'AIN5', 'AIN6', 'AIN7', 'AIN8', 'AIN9', 'AIN10', 'AIN11', 'AIN12',
'AIN13', 'AIN14', 'AIN15', 'AIN16', 'AIN17', 'AIN18', 'AIN19', 'AIN20', 'AIN21', 'AIN22', 'AIN23',
'AIN24', 'AIN25', 'AIN26', 'AIN27', 'AIN28', 'AIN29', 'AIN30', 'AIN31', 'AIN32', 'AIN33', 'AIN34',
'AIN35', 'AIN36', 'AIN37', 'AIN38', 'AIN39', 'AIN40', 'AIN41', 'AIN42', 'AIN43', 'AIN44', 'AIN45',
'AIN46', 'AIN47', 'AIN48', 'AIN49', 'AIN50', 'AIN51', 'AIN52', 'AIN53', 'AIN54', 'AIN55', 'AIN56',
'AIN57', 'AIN58', 'AIN59', 'AIN60', 'AIN61', 'AIN62', 'AIN63', 'AIN64'], dtype=object)
#采样率
sample_rate = self.fs
#通道数量
node_number = Data.shape[1]
# 时间轴
t = np.linspace(0, self.zmqServer.save_win, Data.shape[0])
t = t.reshape(len(t), 1)
#电极名称
electrode_name = np.array(['FP1', 'FP2', 'PO6', 'POZ', 'F3', 'F4', 'FPZ', 'AF4', 'FC3', 'PO8', 'CP2', 'CP1',
'FCZ', 'PO5', 'FC2', 'FC1', 'C3', 'C4', 'FC4', 'CP4', 'P3', 'P4', 'F5', 'C5', 'F6',
'PO4', 'CP6', 'CP5', 'PO3', 'CP3', 'FC6', 'FC5', 'CB1', 'CB2', 'P5', 'AF7', 'A1','T7',
'FT7', 'TP7', 'FT8', 'AF8', 'F8', 'F7', 'P6', 'C6', 'O2', 'O1', 'T8', 'P7', 'CZ','PZ',
'P8', 'FZ', 'OZ', 'PO7', 'TP8', 'AF3', 'C2', 'C1', 'P2', 'P1', 'F2', 'F1'],
dtype=object)
#电极三维坐标
electrode_xyz = self.read_ch_pos()
electrode_xyz.update({'A1': [-0.095, 0, -0.005]})
electrode_xyz = {key: electrode_xyz[key] for key in electrode_name}
electrode_xyz = np.array(list(electrode_xyz.values()))
#电极坐标所属的坐标系
electrode_coord_system = '10-20 spherical model'
#受试者ID
Subject_id = subject_id
#Session ID
Session_id = session_id
#参考电极方案
ref = 'CPZ'
#数据采集开始时间
start_time = 0
meta_struct = {
'subject_id': Subject_id,
'session_id': Session_id,
'ref': ref,
'start_time': start_time
}
eeg_struct = {
'data': Data,
'chn': channel_names,
'sample_rate': sample_rate,
'node_number': node_number,
't': t,
'electrode_name': electrode_name,
'electrode_xyz': electrode_xyz,
'electrode_coord_system': electrode_coord_system,
'meta': meta_struct,
}
fileDir = os.path.join('EEGfiles/',Subject_id,Session_id)
os.makedirs(fileDir,exist_ok=True)
filePath = os.path.join(fileDir,'eeg_data{}.mat'.format(self.file_num))
# 保存到 .mat 文件,顶层变量名为 'eeg'
savemat(filePath, {'eeg': eeg_struct})
print('EEGfile saved at {}'.format(filePath))
self.zmqClient.send_to_all('filePath', filePath)
self.file_num += 1
def read_ch_pos(self,file_path=r'xy_64.xlsx'):
"""
将电极位置信息转换为Dict
参数:
file_path: 电极位置存储文件, 必须包含'channel', 'x', 'y', 'z'
"""
if getattr(sys, 'frozen', False):
script_dir = sys._MEIPASS
else:
script_dir = os.path.dirname(os.path.abspath(__file__))
file_path = os.path.join(script_dir, file_path)
df = pd.read_excel(file_path)
# 确保列名正确
if not all(col in df.columns for col in ['channel', 'x', 'y', 'z']):
raise ValueError("DataFrame必须包含'channel', 'x', 'y', 'z'")
# 创建电极位置字典
ch_pos = {}
for _, row in df.iterrows():
ch_pos[row['channel']] = [row['x'], row['y'], row['z']]
return ch_pos
def _init_filter_cache(self):
"""初始化滤波器系数缓存"""
self._filter_cache = {
'highpass': None,
'lowpass': None,
'notch': None
}
self._cache_valid = False
def _design_filters(self):
"""设计滤波器系数"""
if self._cache_valid:
return
nyquist = self.fs / 2
fs_nyq = self.fs
# 高通滤波 (去除低频漂移)
high = self.lowcut / nyquist
if 0 < high < 1:
self._filter_cache['highpass'] = signal.butter(2, high, btype='high', output='ba')
# 低通滤波 (去除高频噪声)
low = self.highcut / nyquist
if 0 < low < 1:
self._filter_cache['lowpass'] = signal.butter(4, low, btype='low', output='ba')
# 50Hz 陷波滤波 (去除工频干扰)
Q = 30 # 品质因子
self._filter_cache['notch'] = signal.iirnotch(self.notch_freq, Q, fs=fs_nyq)
# 查找CPZ通道索引
electrode_name = ['FP1', 'FP2', 'PO6', 'POZ', 'F3', 'F4', 'FPZ', 'AF4', 'FC3', 'PO8', 'CP2', 'CP1',
'FCZ', 'PO5', 'FC2', 'FC1', 'C3', 'C4', 'FC4', 'CP4', 'P3', 'P4', 'F5', 'C5', 'F6',
'PO4', 'CP6', 'CP5', 'PO3', 'CP3', 'FC6', 'FC5', 'CB1', 'CB2', 'P5', 'AF7', 'A1','T7',
'FT7', 'TP7', 'FT8', 'AF8', 'F8', 'F7', 'P6', 'C6', 'O2', 'O1', 'T8', 'P7', 'CZ','PZ',
'P8', 'FZ', 'OZ', 'PO7', 'TP8', 'AF3', 'C2', 'C1', 'P2', 'P1', 'F2', 'F1']
try:
self.ref_chan_idx = electrode_name.index(self.ref_chan_name)
except ValueError:
self.ref_chan_idx = 50 # 默认CZ (对应索引50)
print(f'[警告] 未找到参考电极 {self.ref_chan_name},使用默认值 CZ')
self._cache_valid = True
print(f'[INFO] 预处理已启用 - 高通:{self.lowcut}Hz, 低通:{self.highcut}Hz, 陷波:{self.notch_freq}Hz, 参考:{self.ref_chan_name}(索引:{self.ref_chan_idx})')
def check_data_quality(self, data):
"""
检查数据质量
返回:
list: 发现的问题列表,空列表表示质量正常
"""
issues = []
# 检查幅度
amplitude = np.max(np.abs(data))
if amplitude > 1e6: # 超过 1mV = 1000µV
issues.append(f'幅度异常: {amplitude:.2e} (可能为原始ADC值或单位错误)')
elif amplitude > 1000:
issues.append(f'幅度偏高: {amplitude:.2f}')
# 检查平坦噪声 (通道可能未连接)
if np.std(data) < 0.01:
issues.append('信号过平,可能通道未连接')
# 检查饱和
n_saturated = np.sum(np.abs(data) > 1e8)
if n_saturated > 0:
issues.append(f'检测到 {n_saturated} 个采样点饱和')
return issues
def verify_and_calibrate_unit(self, data):
"""
验证并校准数据单位
SunnyLinker64 的转换公式:
val = raw_adc * 4.5 / gain_value / 8388608 * 1000000 (µV)
但如果硬件实际增益与 gain_value=6 不符,会导致单位错误。
本函数通过检测数据范围来验证和修正单位。
正常EEG信号范围: ±50-100 µV
如果检测到的数据范围是 ±1e6 量级,说明转换可能有问题
参数:
data: 原始数据
返回:
tuple: (校准后的数据, 是否进行了校准)
"""
if self._conversion_verified:
return data, False
amplitude = np.max(np.abs(data))
# 判断数据是否在合理范围内
# 正常EEG: 1 - 1000 µV (考虑某些高幅值情况)
# 异常: > 1e5 µV (可能是ADC原始值未转换或转换系数错误)
if amplitude > 1e6:
print('[警告] 检测到异常大幅值数据可能是ADC原始值或单位转换失败!')
print(f' 当前最大幅度: {amplitude:.2e} µV')
print('[INFO] 尝试自动校准单位转换...')
# SunnyLinker64 的理论转换系数约为 0.0894 µV/LSB
# 如果数据是原始ADC值需要除以这个系数来还原
theoretical_scale = 4.5 / 6 / 8388608 * 1e6 # 理论系数: ~0.0894 µV/LSB
# 计算校准系数
# 假设数据是原始ADC值需要除以 (amplitude / expected_amplitude)
# 正常EEG信号预期幅度约 100 µV
expected_amplitude = 100.0 # µV
if amplitude > expected_amplitude:
# 计算校准系数: 原始值 / 预期值 = 实际值 / 校准后值
self.calibration_scale = expected_amplitude / amplitude
# 应用校准
data = data * self.calibration_scale
print(f'[INFO] 校准完成,应用系数: {self.calibration_scale:.6e}')
print(f' 校准后最大幅度: {np.max(np.abs(data)):.2f} µV')
self._conversion_verified = True
return data, True
elif amplitude < 0.01:
print('[警告] 数据幅度接近零,可能通道未连接或设备异常')
self._conversion_verified = True
return data, False
def preprocess_data(self, data):
"""
EEG信号预处理
参数:
data: ndarray, shape (n_chan, n_samples), 原始EEG数据
返回:
ndarray: 预处理后的EEG数据
"""
if not self.enable_preprocess:
return data
# 确保数据是 float64 类型
data = data.astype(np.float64)
# 设计滤波器
self._design_filters()
# 1. 去除直流分量和低频漂移 (高通滤波)
if self._filter_cache['highpass'] is not None:
b, a = self._filter_cache['highpass']
for ch in range(data.shape[0]):
data[ch, :] = signal.filtfilt(b, a, data[ch, :])
# 2. 50Hz 工频陷波滤波
if self._filter_cache['notch'] is not None:
b, a = self._filter_cache['notch']
for ch in range(data.shape[0]):
data[ch, :] = signal.filtfilt(b, a, data[ch, :])
# 3. 低通滤波 (去除高频噪声)
if self._filter_cache['lowpass'] is not None:
b, a = self._filter_cache['lowpass']
for ch in range(data.shape[0]):
data[ch, :] = signal.filtfilt(b, a, data[ch, :])
# 4. 重参考 (以CPZ为参考)
if self.ref_chan_idx is not None and self.ref_chan_idx < data.shape[0]:
ref_signal = data[self.ref_chan_idx, :]
data = data - ref_signal
return data
def stop(self):
'''
停止运行
@return:
'''
self.zmqServer.stop()
self.Running=False