找回密码
 立即注册
首页 业界区 业界 基于python mne库构造自定义fNIRS数据并可视化地形图 ...

基于python mne库构造自定义fNIRS数据并可视化地形图

撒阗奕 10 小时前
在科研中遇到需要可视化fNIRS通道的重要性,参考了网上已有的一篇文章,发现只是导入元数据进行替换来实现的,并不符合自己目标(不是需要可视化原始数据,而是需要可视化通道间重要性,每个通道值为0-1,所有通道加起来合为1),因此花了不少时间参照mne库完成了实现。
实现效果:
1、自定义每个通道的位置,无需元数据位置信息
2、自定义通道数据,可以绘制相对重要性而非元数据电压分布图
3、替换了强度标识,而非具体电压大小
代码如下:
点击查看代码
  1. import numpy as np
  2. import mne
  3. import yaml
  4. import matplotlib.pyplot as plt
  5. from mne.channels import make_dig_montage
  6. def get_1020_positions(fnirs_to_1020_mapping):
  7.     """
  8.     根据10/20国际标准体系获取fNIRS电极的具体三维坐标
  9.     参数:
  10.     ----------
  11.     fnirs_to_1020_mapping : dict
  12.         映射字典,格式: {'S1': 'FC3', 'S2': 'FCz', 'D1': 'FC1', ...}
  13.         其中键是fNIRS电极名,值是10/20系统中的标准电极名
  14.    
  15.     返回:
  16.     ----------
  17.     tuple: (source_positions, detector_positions)
  18.         - source_positions: 光源位置字典 {'S1': array([x,y,z]), ...}
  19.         - detector_positions: 探测器位置字典 {'D1': array([x,y,z]), ...}
  20.     """
  21.    
  22.     # 1. 获取标准10/20系统的所有电极位置
  23.     montage_1020 = mne.channels.make_standard_montage('standard_1020')
  24.     positions_1020 = montage_1020.get_positions()['ch_pos']
  25.    
  26.     # 2. 初始化返回字典
  27.     source_positions = {}
  28.     detector_positions = {}
  29.     missing_electrodes = []
  30.    
  31.     # 3. 遍历映射字典,获取每个电极的坐标
  32.     for fnirs_name, eeg_name in fnirs_to_1020_mapping.items():
  33.         if eeg_name in positions_1020:
  34.             position = positions_1020[eeg_name]
  35.             
  36.             # 根据电极名的首字母判断是光源还是探测器
  37.             if fnirs_name.startswith('S'):
  38.                 source_positions[fnirs_name] = position
  39.             elif fnirs_name.startswith('D'):
  40.                 detector_positions[fnirs_name] = position
  41.         else:
  42.             missing_electrodes.append((fnirs_name, eeg_name))
  43.     if missing_electrodes:
  44.         print(f"\n⚠ 警告: 以下电极不在标准10/20系统中:")
  45.         for fnirs_name, eeg_name in missing_electrodes:
  46.             print(f"  {fnirs_name} -> {eeg_name}")
  47.     return source_positions, detector_positions
  48. def create_fNIRS_data(data,config_path):
  49.     """根据numpy data创建fNIRS数据 data维度为[channels*2,times]"""
  50.     # 1. 定义通道配置,包含source和detector对应位置,以及通道定义
  51.     with open(config_path, 'r', encoding='utf-8') as f:
  52.         config = yaml.safe_load(f)
  53.     channel_config = config['channel_config']
  54.    
  55.     # 2. 获取光源和探测器的3D位置
  56.     fnirs_to_1020_mapping = config['fnirs_to_1020_mapping']
  57.     source_positions, detector_positions = get_1020_positions(fnirs_to_1020_mapping)
  58.     '''
  59.     例子:
  60.         source_positions = {
  61.         'S1': np.array([0.03, 0.08, 0.05]),   # 前额左
  62.         'S2': np.array([-0.03, 0.08, 0.05]),  # 前额右
  63.     }
  64.     detector_positions = {
  65.         'D1': np.array([0.02, 0.05, 0.05]),   # 前额左探测器
  66.         'D2': np.array([0.00, 0.05, 0.05]),   # 前额中探测器
  67.         'D3': np.array([-0.02, 0.05, 0.05]),  # 前额右探测器
  68.     }
  69.     '''
  70.     # 3. 创建通道名
  71.     ch_names = []
  72.     for src, det, _ in channel_config:
  73.         ch_names.append(f"{src}_{det} hbo")  # 760nm波长
  74.         ch_names.append(f"{src}_{det} hbr")  # 850nm波长
  75.     # n_channels = len(ch_names)
  76.     # n_times = 500
  77.     sfreq = 10.0
  78.     # 5. 创建info对象,这是mne中数据的元数据,包含基本信息,主要是数据每维度的通道名,type,要对应
  79.     ch_types = []
  80.     for ch_name in ch_names:
  81.         if 'hbo' in ch_name:
  82.             ch_types.append('hbo')  
  83.         elif 'hbr' in ch_name:
  84.             ch_types.append('hbr')  
  85.     info = mne.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
  86.     # 7. 创建montage,这是电极布局,eeg数据通道直接导入,fnirs数据通过提供3D数据也能创建
  87.     ch_pos = {**source_positions, **detector_positions}
  88.     montage = make_dig_montage(ch_pos=ch_pos, coord_frame='head')
  89.     info.set_montage(montage)
  90.     # 8. 创建Raw对象,数据+信息结构
  91.     raw = mne.io.RawArray(data, info, verbose=False)
  92.     # 创建Evoked对象用于地形图,和raw对象区别是分批次,便于取平均
  93.     evoked = mne.EvokedArray(data, info, tmin=0, comment='fNIRS data')
  94.   
  95.     return evoked,ch_names,raw, channel_config, source_positions, detector_positions
  96. def plot_fNIRS_layout(raw, source_positions, detector_positions):
  97.     """绘制fNIRS电极位置布局"""
  98.     fig, axes = plt.subplots(1, 2, figsize=(12, 5))
  99.     # 1. 2D传感器布局
  100.     ax1 = axes[0]
  101.     raw.plot_sensors(axes=ax1, show_names=False)
  102.     # 调整电极点大小:找到所有的散点集合并调整大小
  103.     for collection in ax1.collections:
  104.         if hasattr(collection, 'get_sizes'):
  105.             # 获取当前的散点大小
  106.             current_sizes = collection.get_sizes()
  107.             if len(current_sizes) > 0:
  108.                 # 设置为指定的小尺寸
  109.                 collection.set_sizes([current_sizes/5])
  110.     ax1.set_title("fNIRS sensors")
  111.     # 2. 在坐标轴上绘制光源和探测器位置
  112.     ax2 = axes[1]  
  113.     # 绘制光源
  114.     for src, pos in source_positions.items():
  115.         ax2.scatter(pos[0], pos[1], c='red', s=200, marker='o', label='source' if src == 'S1' else "")
  116.         ax2.text(pos[0], pos[1], src, fontsize=12, ha='center', va='bottom')
  117.     # 绘制探测器
  118.     for det, pos in detector_positions.items():
  119.         ax2.scatter(pos[0], pos[1], c='blue', s=200, marker='s', label='detector' if det == 'D1' else "")
  120.         ax2.text(pos[0], pos[1], det, fontsize=12, ha='center', va='bottom')
  121.     ax2.set_xlabel('X (m)')
  122.     ax2.set_ylabel('Y (m)')
  123.     ax2.set_title('source and detector position')
  124.     ax2.legend()
  125.     ax2.grid(True, alpha=0.3)
  126.     ax2.axis('equal')
  127.     plt.tight_layout()
  128.     return fig
  129. def plot_custom_topmap(evoked, ch_names, time_point=0.0, figsize=(10, 8)):
  130.     # 筛选HBO和HBR通道
  131.     hbo_picks = [i for i, ch in enumerate(ch_names) if 'hbo' in ch]
  132.     hbr_picks = [i for i, ch in enumerate(ch_names) if 'hbr' in ch]
  133.    
  134.     if not hbo_picks or not hbr_picks:
  135.         raise ValueError("未找到HBO或HBR通道")
  136.    
  137.     # 创建图形
  138.     fig, axes = plt.subplots(1, 2, figsize=figsize)
  139.    
  140.     # 绘制HBO地形图
  141.     hbo_data = evoked.data[hbo_picks, 0]  # 取第一个时间点
  142.     hbo_info = mne.pick_info(evoked.info, hbo_picks)
  143.    
  144.     im_hbo, _ = mne.viz.plot_topomap(
  145.         data=hbo_data,
  146.         pos=hbo_info,
  147.         axes=axes[0],
  148.         show=False,
  149.         cmap='RdBu_r',
  150.         vmin=-5,
  151.         vmax=5,
  152.         sensors=True,
  153.         contours=False
  154.     )
  155.     axes[0].set_title('HbO Topography', fontsize=12, fontweight='bold')
  156.     plt.colorbar(im_hbo, ax=axes[0], shrink=0.8)
  157.    
  158.     # 绘制HBR地形图
  159.     hbr_data = evoked.data[hbr_picks, 0]
  160.     hbr_info = mne.pick_info(evoked.info, hbr_picks)
  161.    
  162.     im_hbr, _ = mne.viz.plot_topomap(
  163.         data=hbr_data,
  164.         pos=hbr_info,
  165.         axes=axes[1],
  166.         show=False,
  167.         cmap='RdBu_r',
  168.         vmin=-5,
  169.         vmax=5,
  170.         sensors=True,
  171.         contours=False
  172.     )
  173.     axes[1].set_title('HbR Topography', fontsize=12, fontweight='bold')
  174.     plt.colorbar(im_hbr, ax=axes[1], shrink=0.8)
  175.    
  176.     plt.suptitle(f'fNIRS Topography at t={time_point}s', fontsize=14, fontweight='bold')
  177.     plt.tight_layout()
  178.     return fig
  179. def plot_fNIRS_topmap(data,config_path):
  180.     evoked, ch_names, raw, channel_config, source_positions, detector_positions = create_fNIRS_data(data,config_path)
  181.     # # 绘制布局
  182.     fig_layout = plot_fNIRS_layout(raw, source_positions, detector_positions)
  183.     fig_layout.savefig("fNIRS_layout.png", dpi=300, bbox_inches='tight')
  184.    
  185.     #绘制地形图
  186.     # 修改后的topomap_args
  187.     topomap_args = dict(
  188.         extrapolate='local',
  189.         sphere=(0, 0, 0, 0.115),
  190.     #   image_interp='bicubic',
  191.         contours=True,        # 禁用等高线
  192.         outlines='head',
  193.         sensors=True,
  194.         colorbar=False,
  195.         res=64,
  196.         show=False,
  197.     )
  198.    
  199.     im = evoked.plot_topomap(ch_type='hbr',
  200.                                 times=0.,         
  201.                             **topomap_args)
  202.     fig = plt.gcf()  # 获取当前图形
  203.     ax = fig.axes[0]
  204.    
  205.     # 从 axes 中找到图像对象
  206.     image_obj = None
  207.     for child in ax.get_children():
  208.         if hasattr(child, 'get_array'):
  209.             image_obj = child
  210.             break
  211.     # 创建颜色条
  212.     if image_obj is not None:
  213.         cbar = plt.colorbar(image_obj, ax=ax,
  214.                         fraction=0.046,
  215.                         pad=0.04,
  216.                         shrink=0.8)
  217.     # 移除所有数字刻度
  218.     cbar.set_ticks([])  
  219.    
  220.     # 在颜色条顶部添加"强"标签
  221.     cbar.ax.text(0.5, 1.05, 'high',  # 位置:水平居中,垂直稍上方
  222.                 transform=cbar.ax.transAxes,  # 使用相对坐标
  223.                 ha='center', va='bottom',  # 水平居中,垂直底部对齐
  224.                 fontsize=6,  # 字体大小
  225.                 fontweight='bold',  # 粗体
  226.                 color='black')
  227.    
  228.     # 在颜色条底部添加"弱"标签
  229.     cbar.ax.text(0.5, -0.05, 'low',  # 位置:水平居中,垂直稍下方
  230.                 transform=cbar.ax.transAxes,
  231.                 ha='center', va='top',  # 水平居中,垂直顶部对齐
  232.                 fontsize=6,
  233.                 fontweight='bold',
  234.                 color='black')
  235.     fig.savefig("fig_topography.png", dpi=300, bbox_inches='tight')
  236. if __name__ == "__main__":
  237.     # 创建数据
  238.     n_hbo = 36  
  239.     n_hbr = 36
  240.    
  241.     # 生成随机数据
  242.     random_hbo = np.random.rand(n_hbo, 1)  
  243.     random_hbr = np.random.rand(n_hbr, 1)  
  244.     # 归一化,使得数据为0-1
  245.     hbo_normalized = random_hbo / random_hbo.sum()
  246.     hbr_normalized = random_hbr / random_hbr.sum()
  247.    
  248.     # 合并数据,按HBO-HBR交替排列
  249.     data = np.zeros((n_hbo+n_hbr, 1))
  250.     for i in range(n_hbo):
  251.         data[2*i, 0] = hbo_normalized[i, 0]      # HBO
  252.         data[2*i + 1, 0] = hbr_normalized[i, 0]  # HBR
  253.     config_path = 'public_dataset_config.yaml'
  254.     plot_fNIRS_topmap(data,config_path)
复制代码
public_dataset_config.yaml如下:点击查看代码
  1. channel_config:
  2.   - [S2, D2, C1]  # 通道1
  3.   - [S3, D2, C2]  # 通道2
  4.   - [S3, D1, C3]  # 通道3
  5.   - [S1, D2, C4]  # 通道4
  6.   - [S1, D1, C5]  # 通道5
  7.   - [S1, D3, C6]  # 通道6
  8.   - [S5, D1, C7]  # 通道7
  9.   - [S5, D3, C8]  # 通道8
  10.   - [S4, D3, C9]  # 通道9
  11.   - [S14, D14, C10]  # 通道10
  12.   - [S14, D15, C11]  # 通道11
  13.   - [S14, D16, C12]  # 通道12
  14.   - [S8, D8, C13]  # 通道13
  15.   - [S8, D6, C14]  # 通道14
  16.   - [S8, D4, C15]  # 通道15
  17.   - [S7, D6, C16]  # 通道16
  18.   - [S7, D4, C17]  # 通道17
  19.   - [S7, D5, C18]  # 通道18
  20.   - [S9, D8, C19]  # 通道19
  21.   - [S9, D4, C20]  # 通道20
  22.   - [S9, D7, C21]  # 通道21
  23.   - [S6, D4, C22]  # 通道22
  24.   - [S6, D5, C23]  # 通道23
  25.   - [S6, D7, C24]  # 通道24
  26.   - [S10, D10, C25]  # 通道25
  27.   - [S10, D12, C26]  # 通道26
  28.   - [S10, D9, C27]  # 通道27
  29.   - [S11, D10, C28]  # 通道28
  30.   - [S11, D9, C29]  # 通道29
  31.   - [S11, D11, C30]  # 通道30
  32.   - [S13, D13, C31]  # 通道31
  33.   - [S13, D12, C32]  # 通道32
  34.   - [S13, D9, C33]  # 通道33
  35.   - [S12, D13, C34]  # 通道34
  36.   - [S12, D9, C35]  # 通道35
  37.   - [S12, D11, C36]  # 通道36
  38. fnirs_to_1020_mapping:
  39.   S1: Fpz     
  40.   S2: AF7     
  41.   S3: AF3
  42.   S4: AF8
  43.   S5: AF4
  44.   S6: C1
  45.   S7: FC3
  46.   S8: C5
  47.   S9: CP3     
  48.   S10: C2     
  49.   S11: FC4
  50.   S12: C6
  51.   S13: CP4
  52.   S14: Oz
  53.   D1: AFz     
  54.   D2: Fp1   
  55.   D3: Fp2
  56.   D4: C3  
  57.   D5: FC1   
  58.   D6: FC5
  59.   D7: CP1
  60.   D8: CP5
  61.   D9: C4     
  62.   D10: FC2   
  63.   D11: FC6  
  64.   D12: CP2   
  65.   D13: CP6
  66.   D14: POz
  67.   D15: O1
  68.   D16: O2
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册