基于python mne库构造自定义fNIRS数据并可视化地形图
在科研中遇到需要可视化fNIRS通道的重要性,参考了网上已有的一篇文章,发现只是导入元数据进行替换来实现的,并不符合自己目标(不是需要可视化原始数据,而是需要可视化通道间重要性,每个通道值为0-1,所有通道加起来合为1),因此花了不少时间参照mne库完成了实现。
实现效果:
1、自定义每个通道的位置,无需元数据位置信息
2、自定义通道数据,可以绘制相对重要性而非元数据电压分布图
3、替换了强度标识,而非具体电压大小
代码如下:
点击查看代码
import numpy as np
import mne
import yaml
import matplotlib.pyplot as plt
from mne.channels import make_dig_montage
def get_1020_positions(fnirs_to_1020_mapping):
"""
根据10/20国际标准体系获取fNIRS电极的具体三维坐标
参数:
----------
fnirs_to_1020_mapping : dict
映射字典,格式: {'S1': 'FC3', 'S2': 'FCz', 'D1': 'FC1', ...}
其中键是fNIRS电极名,值是10/20系统中的标准电极名
返回:
----------
tuple: (source_positions, detector_positions)
- source_positions: 光源位置字典 {'S1': array([x,y,z]), ...}
- detector_positions: 探测器位置字典 {'D1': array([x,y,z]), ...}
"""
# 1. 获取标准10/20系统的所有电极位置
montage_1020 = mne.channels.make_standard_montage('standard_1020')
positions_1020 = montage_1020.get_positions()['ch_pos']
# 2. 初始化返回字典
source_positions = {}
detector_positions = {}
missing_electrodes = []
# 3. 遍历映射字典,获取每个电极的坐标
for fnirs_name, eeg_name in fnirs_to_1020_mapping.items():
if eeg_name in positions_1020:
position = positions_1020[eeg_name]
# 根据电极名的首字母判断是光源还是探测器
if fnirs_name.startswith('S'):
source_positions[fnirs_name] = position
elif fnirs_name.startswith('D'):
detector_positions[fnirs_name] = position
else:
missing_electrodes.append((fnirs_name, eeg_name))
if missing_electrodes:
print(f"\n⚠ 警告: 以下电极不在标准10/20系统中:")
for fnirs_name, eeg_name in missing_electrodes:
print(f" {fnirs_name} -> {eeg_name}")
return source_positions, detector_positions
def create_fNIRS_data(data,config_path):
"""根据numpy data创建fNIRS数据 data维度为[channels*2,times]"""
# 1. 定义通道配置,包含source和detector对应位置,以及通道定义
with open(config_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
channel_config = config['channel_config']
# 2. 获取光源和探测器的3D位置
fnirs_to_1020_mapping = config['fnirs_to_1020_mapping']
source_positions, detector_positions = get_1020_positions(fnirs_to_1020_mapping)
'''
例子:
source_positions = {
'S1': np.array([0.03, 0.08, 0.05]), # 前额左
'S2': np.array([-0.03, 0.08, 0.05]), # 前额右
}
detector_positions = {
'D1': np.array([0.02, 0.05, 0.05]), # 前额左探测器
'D2': np.array([0.00, 0.05, 0.05]), # 前额中探测器
'D3': np.array([-0.02, 0.05, 0.05]), # 前额右探测器
}
'''
# 3. 创建通道名
ch_names = []
for src, det, _ in channel_config:
ch_names.append(f"{src}_{det} hbo") # 760nm波长
ch_names.append(f"{src}_{det} hbr") # 850nm波长
# n_channels = len(ch_names)
# n_times = 500
sfreq = 10.0
# 5. 创建info对象,这是mne中数据的元数据,包含基本信息,主要是数据每维度的通道名,type,要对应
ch_types = []
for ch_name in ch_names:
if 'hbo' in ch_name:
ch_types.append('hbo')
elif 'hbr' in ch_name:
ch_types.append('hbr')
info = mne.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
# 7. 创建montage,这是电极布局,eeg数据通道直接导入,fnirs数据通过提供3D数据也能创建
ch_pos = {**source_positions, **detector_positions}
montage = make_dig_montage(ch_pos=ch_pos, coord_frame='head')
info.set_montage(montage)
# 8. 创建Raw对象,数据+信息结构
raw = mne.io.RawArray(data, info, verbose=False)
# 创建Evoked对象用于地形图,和raw对象区别是分批次,便于取平均
evoked = mne.EvokedArray(data, info, tmin=0, comment='fNIRS data')
return evoked,ch_names,raw, channel_config, source_positions, detector_positions
def plot_fNIRS_layout(raw, source_positions, detector_positions):
"""绘制fNIRS电极位置布局"""
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 1. 2D传感器布局
ax1 = axes[0]
raw.plot_sensors(axes=ax1, show_names=False)
# 调整电极点大小:找到所有的散点集合并调整大小
for collection in ax1.collections:
if hasattr(collection, 'get_sizes'):
# 获取当前的散点大小
current_sizes = collection.get_sizes()
if len(current_sizes) > 0:
# 设置为指定的小尺寸
collection.set_sizes([current_sizes/5])
ax1.set_title("fNIRS sensors")
# 2. 在坐标轴上绘制光源和探测器位置
ax2 = axes[1]
# 绘制光源
for src, pos in source_positions.items():
ax2.scatter(pos[0], pos[1], c='red', s=200, marker='o', label='source' if src == 'S1' else "")
ax2.text(pos[0], pos[1], src, fontsize=12, ha='center', va='bottom')
# 绘制探测器
for det, pos in detector_positions.items():
ax2.scatter(pos[0], pos[1], c='blue', s=200, marker='s', label='detector' if det == 'D1' else "")
ax2.text(pos[0], pos[1], det, fontsize=12, ha='center', va='bottom')
ax2.set_xlabel('X (m)')
ax2.set_ylabel('Y (m)')
ax2.set_title('source and detector position')
ax2.legend()
ax2.grid(True, alpha=0.3)
ax2.axis('equal')
plt.tight_layout()
return fig
def plot_custom_topmap(evoked, ch_names, time_point=0.0, figsize=(10, 8)):
# 筛选HBO和HBR通道
hbo_picks = [i for i, ch in enumerate(ch_names) if 'hbo' in ch]
hbr_picks = [i for i, ch in enumerate(ch_names) if 'hbr' in ch]
if not hbo_picks or not hbr_picks:
raise ValueError("未找到HBO或HBR通道")
# 创建图形
fig, axes = plt.subplots(1, 2, figsize=figsize)
# 绘制HBO地形图
hbo_data = evoked.data[hbo_picks, 0] # 取第一个时间点
hbo_info = mne.pick_info(evoked.info, hbo_picks)
im_hbo, _ = mne.viz.plot_topomap(
data=hbo_data,
pos=hbo_info,
axes=axes[0],
show=False,
cmap='RdBu_r',
vmin=-5,
vmax=5,
sensors=True,
contours=False
)
axes[0].set_title('HbO Topography', fontsize=12, fontweight='bold')
plt.colorbar(im_hbo, ax=axes[0], shrink=0.8)
# 绘制HBR地形图
hbr_data = evoked.data[hbr_picks, 0]
hbr_info = mne.pick_info(evoked.info, hbr_picks)
im_hbr, _ = mne.viz.plot_topomap(
data=hbr_data,
pos=hbr_info,
axes=axes[1],
show=False,
cmap='RdBu_r',
vmin=-5,
vmax=5,
sensors=True,
contours=False
)
axes[1].set_title('HbR Topography', fontsize=12, fontweight='bold')
plt.colorbar(im_hbr, ax=axes[1], shrink=0.8)
plt.suptitle(f'fNIRS Topography at t={time_point}s', fontsize=14, fontweight='bold')
plt.tight_layout()
return fig
def plot_fNIRS_topmap(data,config_path):
evoked, ch_names, raw, channel_config, source_positions, detector_positions = create_fNIRS_data(data,config_path)
# # 绘制布局
fig_layout = plot_fNIRS_layout(raw, source_positions, detector_positions)
fig_layout.savefig("fNIRS_layout.png", dpi=300, bbox_inches='tight')
#绘制地形图
# 修改后的topomap_args
topomap_args = dict(
extrapolate='local',
sphere=(0, 0, 0, 0.115),
# image_interp='bicubic',
contours=True, # 禁用等高线
outlines='head',
sensors=True,
colorbar=False,
res=64,
show=False,
)
im = evoked.plot_topomap(ch_type='hbr',
times=0.,
**topomap_args)
fig = plt.gcf() # 获取当前图形
ax = fig.axes[0]
# 从 axes 中找到图像对象
image_obj = None
for child in ax.get_children():
if hasattr(child, 'get_array'):
image_obj = child
break
# 创建颜色条
if image_obj is not None:
cbar = plt.colorbar(image_obj, ax=ax,
fraction=0.046,
pad=0.04,
shrink=0.8)
# 移除所有数字刻度
cbar.set_ticks([])
# 在颜色条顶部添加"强"标签
cbar.ax.text(0.5, 1.05, 'high', # 位置:水平居中,垂直稍上方
transform=cbar.ax.transAxes, # 使用相对坐标
ha='center', va='bottom', # 水平居中,垂直底部对齐
fontsize=6, # 字体大小
fontweight='bold', # 粗体
color='black')
# 在颜色条底部添加"弱"标签
cbar.ax.text(0.5, -0.05, 'low', # 位置:水平居中,垂直稍下方
transform=cbar.ax.transAxes,
ha='center', va='top', # 水平居中,垂直顶部对齐
fontsize=6,
fontweight='bold',
color='black')
fig.savefig("fig_topography.png", dpi=300, bbox_inches='tight')
if __name__ == "__main__":
# 创建数据
n_hbo = 36
n_hbr = 36
# 生成随机数据
random_hbo = np.random.rand(n_hbo, 1)
random_hbr = np.random.rand(n_hbr, 1)
# 归一化,使得数据为0-1
hbo_normalized = random_hbo / random_hbo.sum()
hbr_normalized = random_hbr / random_hbr.sum()
# 合并数据,按HBO-HBR交替排列
data = np.zeros((n_hbo+n_hbr, 1))
for i in range(n_hbo):
data[2*i, 0] = hbo_normalized[i, 0] # HBO
data[2*i + 1, 0] = hbr_normalized[i, 0] # HBR
config_path = 'public_dataset_config.yaml'
plot_fNIRS_topmap(data,config_path)
点击查看代码
channel_config:
- [S2, D2, C1] # 通道1
- [S3, D2, C2] # 通道2
- [S3, D1, C3] # 通道3
- [S1, D2, C4] # 通道4
- [S1, D1, C5] # 通道5
- [S1, D3, C6] # 通道6
- [S5, D1, C7] # 通道7
- [S5, D3, C8] # 通道8
- [S4, D3, C9] # 通道9
- [S14, D14, C10] # 通道10
- [S14, D15, C11] # 通道11
- [S14, D16, C12] # 通道12
- [S8, D8, C13] # 通道13
- [S8, D6, C14] # 通道14
- [S8, D4, C15] # 通道15
- [S7, D6, C16] # 通道16
- [S7, D4, C17] # 通道17
- [S7, D5, C18] # 通道18
- [S9, D8, C19] # 通道19
- [S9, D4, C20] # 通道20
- [S9, D7, C21] # 通道21
- [S6, D4, C22] # 通道22
- [S6, D5, C23] # 通道23
- [S6, D7, C24] # 通道24
- [S10, D10, C25] # 通道25
- [S10, D12, C26] # 通道26
- [S10, D9, C27] # 通道27
- [S11, D10, C28] # 通道28
- [S11, D9, C29] # 通道29
- [S11, D11, C30] # 通道30
- [S13, D13, C31] # 通道31
- [S13, D12, C32] # 通道32
- [S13, D9, C33] # 通道33
- [S12, D13, C34] # 通道34
- [S12, D9, C35] # 通道35
- [S12, D11, C36] # 通道36
fnirs_to_1020_mapping:
S1: Fpz
S2: AF7
S3: AF3
S4: AF8
S5: AF4
S6: C1
S7: FC3
S8: C5
S9: CP3
S10: C2
S11: FC4
S12: C6
S13: CP4
S14: Oz
D1: AFz
D2: Fp1
D3: Fp2
D4: C3
D5: FC1
D6: FC5
D7: CP1
D8: CP5
D9: C4
D10: FC2
D11: FC6
D12: CP2
D13: CP6
D14: POz
D15: O1
D16: O2

浙公网安备 33010602011771号