当前位置: 首页 > news >正文

多导睡眠五大PSG数据集统一格式化处理|SHHS

 完整五个数据集处理请见:

https://blog.csdn.net/m0_70335361/article/details/151406787?fromshare=blogdetail&sharetype=blogdetail&sharerId=151406787&sharerefer=PC&sharesource=m0_70335361&sharefrom=from_linkhttps://blog.csdn.net/m0_70335361/article/details/151406787?fromshare=blogdetail&sharetype=blogdetail&sharerId=151406787&sharerefer=PC&sharesource=m0_70335361&sharefrom=from_link本文代码已公开至:XingXingYuoos/PSG_data_prepare


一、背景介绍

睡眠医学研究中多导睡眠图(PSG)数据集的异构性导致跨研究分析困难
SHHS数据库作为公开基准数据集的价值
统一格式化处理对提高数据复用性和算法泛化能力的作用


二、数据集介绍

SHHS(Sleep Heart Health Study) 是目前世界上规模较大的多中心睡眠监测研究项目之一,旨在探索睡眠呼吸障碍(如睡眠呼吸暂停综合征)与心血管疾病之间的关系。

  • 研究时间:从 1995 年开始

  • 受试者人数:超过 6,000 名成年人

  • 数据类型:基于 多导睡眠图(PSG) 的夜间监测数据

  • 数据规模:原始 PSG 数据 + 临床/人口学信息

SHHS 提供了完整的 多导睡眠监测信号

  • EEG(脑电图):主要通道如 C4–M1

  • EOG(眼电图):左右眼动信号(ROC、LOC)

  • EMG(肌电图):下颌肌或腿部肌电

  • ECG(心电图)

  • 呼吸信号:气流、胸腹呼吸带

  • 血氧饱和度(SpO₂)

  • 打鼾、体位、脉搏 等附加指标

此外,还包括:

  • 睡眠分期标签(Wake、N1、N2、N3、REM)

  • 呼吸事件标注(呼吸暂停、低通气等)

  • 临床与人口学数据(BMI、血压、心血管疾病史等

官网提供的文件如下:

在 SHHS 访问 1 中,有 5,793 名受试者获得了原始多导睡眠图数据,在 SHHS 访问 2 中,有 2,651 名受试者获得了原始多导睡眠图数据。每个记录都有一个信号文件 (.EDF) 和两个版本的事件评分和时期分期注释 (.XML)。

EDF——从 Compumedics Profusion 导出的欧洲数据格式的信号文件。
XML (Profusion) - 从 Compumedics Profusion 导出的注释文件。
XML (NSRR) - 在EDF 编辑器和转换器工具中处理的注释文件。


三、数据集预处理

step1:导入必要的库

from mne.io import concatenate_raws, read_raw_edf
import matplotlib.pyplot as plt
import mne
import os
import numpy as np
from tqdm import tqdm
from sklearn.preprocessing import StandardScaler
import xml.etree.ElementTree as ET

step2:基础路径与配置 

dir_path_psg = '/shhs/polysomnography/edfs/shhs1'
dir_path_ann = '/shhs/polysomnography/annotations-events-profusion/shhs1'seq_dir = '/data/SHHS1/seq'
label_dir = '/data/SHHS1/labels'signal_name = ['EEG', 'EOG(L)']label2id = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 3, '5': 4, '9': 0}target_sfreq = 100
epoch_sec = 30
pack_size = 20  # 每 20 个 epoch 打包成一个序列

step3:工具函数

def step3_list_and_pair_files(dir_psg, dir_ann):psg_f_names = sorted(os.listdir(dir_psg))label_f_names = sorted(os.listdir(dir_ann))pairs = []for psg_f_name, label_f_name in zip(psg_f_names, label_f_names):if psg_f_name[:12] == label_f_name[:12]:pairs.append((psg_f_name, label_f_name))print(f"[Step 2] 共配对到 {len(pairs)} 个文件:")# print(pairs)return pairs

step4:预处理和读取EDF

def step3_prepare_dirs(seq_dir, label_dir):os.makedirs(seq_dir, exist_ok=True)os.makedirs(label_dir, exist_ok=True)print(f"[Step 3] 输出目录已就绪:\n  seq_dir={seq_dir}\n  label_dir={label_dir}")def step4_load_and_preprocess_psg(psg_path):# 读取原始raw = read_raw_edf(psg_path, preload=True, verbose='ERROR')print(f"[Step 4] 原始 info:\n{raw.info}")# 选通道raw.pick_channels(signal_name)# 重采样 & 滤波raw.resample(sfreq=target_sfreq)raw.filter(0.3, 35, fir_design='firwin')print(f"[Step 4] 预处理后 info:\n{raw.info}")# 转 DataFrame 再取值(第一列是 time,需要去掉)psg_array = raw.to_data_frame().valuespsg_array = psg_array[:, 1:]  # 去除时间列,只保留信号通道,形状: (N_samples, 2)# 标准化(逐记录)std = StandardScaler()psg_array = std.fit_transform(psg_array)# 对齐到 30s(100Hz * 30s = 3000 样本)samples_per_epoch = epoch_sec * target_sfreqcut_tail30 = psg_array.shape[0] % samples_per_epochif cut_tail30 > 0:psg_array = psg_array[:-cut_tail30, :]# reshape 成 (N_epoch, 3000, 2)psg_array = psg_array.reshape(-1, samples_per_epoch, len(signal_name))# 对齐到 20 个 epoch 的整包cut_tail20 = psg_array.shape[0] % pack_sizeif cut_tail20 > 0:psg_array = psg_array[:-cut_tail20, :,:]# 形状变换: (N_epoch, 3000, 2) -> (N_pack, 20, 3000, 2) -> (N_pack, 20, 2, 3000)psg_array = psg_array.reshape(-1, pack_size, samples_per_epoch, len(signal_name))epochs_seq = psg_array.transpose(0, 1, 3, 2)print(f"[Step 4] 预处理后数组形状:epochs_seq={epochs_seq.shape} (N_pack, {pack_size}, C={len(signal_name)}, T={samples_per_epoch})")return epochs_seq

step5:解析xml

def step5_parse_labels(xml_path, label2id, cut_tail20):labels_list = []tree = ET.parse(xml_path)root = tree.getroot()# 与原脚本一致:直接遍历 SleepStagefor child in root.iter('SleepStage'):labels_list.append(label2id[child.text])labels_array = np.array(labels_list, dtype=np.int64)# 对齐到 20 的整包(与 Step4 的切尾数量一致)if cut_tail20 > 0:labels_array = labels_array[:-cut_tail20]labels_seq = labels_array.reshape(-1, pack_size)print(f"[Step 5] 标签形状:labels_seq={labels_seq.shape} (N_pack, {pack_size})")return labels_seq

step6:保存文件

def step6_save_npys(out_seq_dir, out_label_dir, rec_id, epochs_seq, labels_seq, start_seq_idx=0, start_label_idx=0):# 建子目录seq_subdir = os.path.join(out_seq_dir, rec_id)label_subdir = os.path.join(out_label_dir, rec_id)os.makedirs(seq_subdir, exist_ok=True)os.makedirs(label_subdir, exist_ok=True)# 保存序列local_num_seqs = 0for i, seq in enumerate(epochs_seq):seq_name = os.path.join(seq_subdir, f"{rec_id}-{start_seq_idx + local_num_seqs}.npy")with open(seq_name, 'wb') as f:np.save(f, seq)local_num_seqs += 1# 保存标签local_num_labels = 0for i, label_pack in enumerate(labels_seq):label_name = os.path.join(label_subdir, f"{rec_id}-{start_label_idx + local_num_labels}.npy")with open(label_name, 'wb') as f:np.save(f, label_pack)local_num_labels += 1print(f"[Step 6] 保存完成:seq={local_num_seqs},labels={local_num_labels},rec_id={rec_id}")return local_num_seqs, local_num_labels

step7:main

if __name__ == "__main__":# Step 2: 配对 PSG 与 XMLpsg_label_f_pairs = step2_list_and_pair_files(dir_path_psg, dir_path_ann)print(f"[Step 2] 映射表:{label2id}")# Step 3: 准备输出目录step3_prepare_dirs(seq_dir, label_dir)# Step 4~6: 循环处理若干记录(与原脚本一致:前 150 个)num_seqs = 0num_labels = 0for psg_f_name, label_f_name in tqdm(psg_label_f_pairs[:150], desc="Processing SHHS1"):rec_id = psg_f_name[:12]psg_path = os.path.join(dir_path_psg, psg_f_name)xml_path = os.path.join(dir_path_ann, label_f_name)# ---- Step 4: 读取与预处理 PSG ----# 这里需要知道在对齐到 20-epoch 之前,被截去的 epoch 数;我们按与你原逻辑严格同步:# 先对齐到 30s -> 再 reshape -> 再对齐到 20 的整包# 为了获得 cut_tail20,我们复用内部逻辑:先计算 epoch 数后对齐(见下)。raw_tmp = read_raw_edf(psg_path, preload=True, verbose='ERROR')raw_tmp.pick_channels(signal_name)raw_tmp.resample(sfreq=target_sfreq)raw_tmp.filter(0.3, 35, fir_design='firwin')arr_tmp = raw_tmp.to_data_frame().values[:, 1:]samples_per_epoch = epoch_sec * target_sfreqcut_tail30 = arr_tmp.shape[0] % samples_per_epochif cut_tail30 > 0:arr_tmp = arr_tmp[:-cut_tail30, :]n_epoch = arr_tmp.shape[0] // samples_per_epochcut_tail20 = n_epoch % pack_size  # 需要在标签侧裁掉同样的 epoch 数# 正式得到 epochs_seqepochs_seq = step4_load_and_preprocess_psg(psg_path)# ---- Step 5: 解析与对齐标签 ----labels_seq = step5_parse_labels(xml_path, label2id, cut_tail20)# ---- Step 6: 保存 ----add_seqs, add_labels = step6_save_npys(seq_dir, label_dir, rec_id,epochs_seq, labels_seq,start_seq_idx=num_seqs, start_label_idx=num_labels)num_seqs += add_seqsnum_labels += add_labels# Step 7: 汇总打印print(f"[Step 7] 全部完成:保存序列 {num_seqs} 个,标签 {num_labels} 个。")

最后生成结果:

http://www.dtcms.com/a/390029.html

相关文章:

  • ZeroMQ基础
  • 【JavaGuide学习笔记】什么,Java中 native 也是一个关键字?
  • 【LWIP】STM32F429 + LWIP + DP83848 热插拔问题总结
  • RGWRados::Object::Write::_do_write_meta()
  • Shopify 集合页改造:增加 Banner 图片 + 点击加载更多功能
  • 泛函 Φ(u) = ∫[(u″)² + u² + 2f(x)u]dx − (u′(0))² 在 u(0)=u(1) 下的驻点方程与边界条件
  • JAVA高频面试题汇总:Java+ 并发 +Spring+MySQL+ 分布式 +Redis+ 算法 +JVM 等
  • 构建与运营“爬虫 IP 代理池”的方法论
  • 【文献笔记】Point Transformer
  • Linux | i.MX6ULL Modbus 移植和使用(第二十一章)
  • 几种微前端框架的沙箱策略
  • 黑盒测试:测试用例设计之边界值设计方法(边界值分析法)(上点、离点、内点)健壮性测试、单缺陷假设理论
  • 【题解】P1548 [NOIP 1997 普及组] 棋盘问题
  • scala中for推导式详细讲解
  • React学习 ---- 基础知识学习
  • C语言实现MATLAB中的Fir1带通滤波器
  • 微信小程序开发教程(十七)
  • 9月18日星期四今日早报简报微语报早读
  • SqlSugar 问题记录
  • 记一次宝塔+nginx+php8+thinkphp8多应用下某个应用报错404的问题 - nginx、php日志全无 - 无法追踪
  • Windows Server远程桌面(RDP)安全优化
  • 工具链过于分散会导致哪些问题
  • 【RAG】Youtu-GraphRAG
  • 惠普LaserJet Pro M203dn黑白激光打印机双面卡纸维修一例
  • 专题二 二叉树中的深度优先搜索
  • Git 多人协作(1)
  • 设计模式第三章(迭代器模式)
  • 网络原理(4):HTTP协议 -- HTTP请求 -- 首行(请求方法)
  • 密钥下发服务中心:双重验证 + 实时监控的轻量级密钥管理解决方案
  • 硬件 - RK3588部分(4) - 原理图 - RK806