MIT-BIH心电信号转图像分类数据集构建全流程与避坑指南
简介本资源是面向医学图像分析与深度学习初学者、研究者的MIT-BIH ECG心电图分类专用验证集与测试集专为图像分类模型性能评估与泛化能力验证设计。数据集基于权威MIT-BIH心电数据库构建共包含50,804张高质量PNG格式心电图图像验证集30,421张、测试集20,383张辅以1个类别映射JSON文件class_indices.json及1个可视化Python脚本支持开箱即用的分类任务评测。压缩包总计2000个文件主体为PNG图像1998个结构清晰data-val与data-test目录均采用标准ImageFolder格式按疾病类别分文件夹组织便于PyTorch/TensorFlow等框架直接加载。目前已有1450人学习下载配套类别字典与规范目录结构显著降低数据预处理门槛节省特征提取与标签对齐时间助力快速开展心律失常识别模型的验证与对比实验。1. 项目缘起当心电信号遇上图像分类最近在做一个关于心律失常自动检测的辅助诊断项目核心思路是把一维的心电信号转换成二维的图像然后扔给卷积神经网络去做分类。这个想法听起来挺酷但第一步就卡住了数据。心电数据本身不难找MIT-BIH心律失常数据库几乎是这个领域的“标准答案”但我要的是图像分类数据集而且是已经划分好验证集和测试集的。这就意味着我需要的不只是原始的.dat/.hea文件而是经过预处理、信号转图像、并且按照机器学习标准流程分割好的图像文件集合。市面上能找到的MIT-BIH资源99%都是针对信号处理的教你如何用Python的wfdb库读取信号、做滤波、提取R波。但关于如何系统地将其构建成一个用于图像分类任务的、可直接喂给PyTorch或TensorFlow的ImageFolder格式数据集资料就非常零散了。更别提验证集和测试集的划分这直接关系到模型评估的可靠性和泛化能力的判断。所以我花了差不多一周时间从零开始把MIT-BIH Arrhythmia Database“改造”成了一个标准的图像分类数据集。这个过程踩了不少坑也总结了一套还算靠谱的流程今天就把从数据下载、预处理、信号转图像、到数据集划分的完整链路以及其中的核心细节和避坑指南毫无保留地分享出来。2. 理解核心原料MIT-BIH心律失常数据库解剖在动手“烹饪”数据集之前必须彻底了解“食材”。MIT-BIH心律失常数据库MIT-BIH Arrhythmia Database发布于1980年由麻省理工学院和波士顿贝斯以色列医院联合创建它早已成为评估心律失常自动分析算法的黄金标准。2.1 数据库的基本构成与文件格式这个数据库包含48条双通道的动态心电图记录每条记录时长约30分钟。这些记录来自47位不同的受试者其中一条记录201和202来自同一位受试者。数据以特定的物理格式存储我们最常打交道的是以下几种文件.dat 文件这是二进制文件存储了实际的采样信号数据。MIT-BIH的采样频率是360 Hz即每秒采集360个点。信号通常以212格式存储这是一种为了节省空间设计的格式它将两个12位的样本来自两个导联打包成三个8位的字节。.hea 文件头文件纯文本格式。这是理解.dat文件的“说明书”。它包含了至关重要的元信息例如采样频率固定为360 Hz。信号数量通常是2MLII导联和V1导联但部分记录可能不同。信号增益用于将存储的整数值转换为实际的物理单位毫伏。ADC零点和ADC分辨率与信号值转换相关。每条记录的起始时间、患者信息等。.atr 文件注释文件同样是二进制格式。它包含了由心脏病专家手动标注的心跳类型和节律信息。这是整个数据库的精华所在是我们制作分类标签的唯一依据。标注遵循AAMI标准将心跳分为多个类别如正常搏动N、左束支传导阻滞L、右束支传导阻滞R、房性早搏A、室性早搏V等。注意直接下载的原始压缩包如mit-bih-arrhythmia-database-1.0.0.zip里通常就包含这三种文件。我们的所有工作都基于这些文件展开。2.2 从心跳标注到图像分类标签我们的目标是图像分类所以分类的单元是“一张图”而MIT-BIH标注的基本单元是“一个心跳”。因此核心任务变成了如何根据.atr文件中的心跳标注为每一个心跳或每一段心电信号生成一个分类标签并截取对应的信号段转换为图像。这里有几个关键决策点心跳类型的映射与归并AAMI标准的心跳类型有几十种但对于一个稳健的分类器我们通常需要将其归并为几个主要的临床大类。一个非常常见的映射方案是N (Normal): 正常心跳。S (Supraventricular): 室上性异位搏动包括A房性早搏、a房性逸搏、J交界性早搏等。它们起源自心室以上风险相对较低。V (Ventricular): 室性异位搏动包括V室性早搏、E室性逸搏等。起源于心室有较高的临床意义。F (Fusion): 融合搏动通常是心室融合波。Q (Unknown): 无法分类的搏动包括“/”起搏心跳等。 这种5分类N, S, V, F, Q是很多论文采用的基准方案。你需要根据你的项目目标来决定分类体系。信号段的截取窗口以一个心跳的R波峰值位置由.atr文件给出为中心向前向后各截取一段信号。窗口大小的选择需要权衡太短可能丢失P波或T波信息太长则包含过多无关信号且增加计算量。常见的选择是R峰前后各100-150个采样点对应约0.28-0.42秒这样通常能囊括一个完整的心跳波形。导联的选择MIT-BIH是双导联。通常MLII导联的波形更清晰、更标准是首选。你可以选择单导联MLII进行分析也可以尝试将双导联作为两个通道类似图像的RGB通道输入网络但这需要调整网络输入层。3. 实战构建从原始信号到标准图像数据集理解了原理我们开始动手。整个流程可以概括为下载 - 读取 - 截段 - 转图 - 保存 - 划分。我将使用Python和几个核心库来完成。3.1 环境准备与核心工具库首先确保你的环境里有这些工具# 基础数据处理和科学计算 pip install numpy pandas matplotlib # 专门用于读取MIT-BIH等生理信号的库这是最关键的工具 pip install wfdb # 图像处理库 pip install pillow # 深度学习框架以PyTorch为例用于最后的DataLoader演示 pip install torch torchvisionwfdb库是这个过程中的“瑞士军刀”它能直接解析.dat,.hea,.atr文件省去了我们手动解析二进制格式的麻烦。3.2 分步代码实现与详解假设我们已经将MIT-BIH数据库的48条记录下载到了本地./mit-bih-arrhythmia-database/目录下。步骤一读取单条记录的信号和标注import wfdb import numpy as np import matplotlib.pyplot as plt from PIL import Image import os # 定义记录路径例如第一条记录100 record_name ./mit-bih-arrhythmia-database/100 record_path ./mit-bih-arrhythmia-database/ # 使用wfdb读取记录 record wfdb.rdrecord(record_name, pn_dirmit-bih-arrhythmia-database) annotation wfdb.rdann(record_name, atr, pn_dirmit-bih-arrhythmia-database) # 查看信息 print(f信号长度: {record.sig_len}) print(f采样频率: {record.fs}) print(f信号名称: {record.sig_name}) print(f标注数量: {len(annotation.symbol)}) print(f前10个标注符号: {annotation.symbol[:10]}) print(f前10个标注位置采样点: {annotation.sample[:10]})这段代码会输出记录的基本信息和前几个心跳的标注。annotation.sample数组里存储的就是每个心跳R峰的采样点位置annotation.symbol是对应的AAMI标注符号。步骤二定义心跳类型映射和截取函数# 定义AAMI到我们目标类别的映射字典 # 这里采用常见的5分类方案 label_map { N: N, # Normal L: N, # Left bundle branch block beat (通常归为正常变体) R: N, # Right bundle branch block beat e: N, # Atrial escape beat j: N, # Nodal (junctional) escape beat A: S, # Atrial premature beat a: S, # Aberrated atrial premature beat S: S, # Supraventricular premature beat J: S, # Nodal (junctional) premature beat V: V, # Premature ventricular contraction E: V, # Ventricular escape beat F: F, # Fusion of ventricular and normal beat /: Q, # Paced beat f: Q, # Fusion of paced and normal beat Q: Q, # Unclassifiable beat # 其他不常见的符号可以忽略或归为Q } def extract_beat_segment(signal, r_peak_idx, fs360, window_before100, window_after150): 以R峰为中心截取心拍信号段。 参数: signal: 一维心电信号数组。 r_peak_idx: R峰位置的索引。 fs: 采样频率默认360Hz。 window_before: R峰前截取的采样点数。 window_after: R峰后截取的采样点数。 返回: beat_segment: 截取出的心拍信号段一维数组。 start_idx r_peak_idx - window_before end_idx r_peak_idx window_after # 处理边界情况如果R峰太靠前或太靠后无法截取完整窗口则返回None if start_idx 0 or end_idx len(signal): return None return signal[start_idx:end_idx] def plot_and_save_beat(beat_signal, save_path, dpi100): 将心拍信号绘制成图像并保存。 关键技巧去除坐标轴和边框让图像只包含波形本身。 fig, ax plt.subplots(figsize(2.56, 2.56), dpidpi) # 生成256x256像素的图像 ax.plot(beat_signal, linewidth1.5, colorblack) # 核心去除所有坐标轴和边框 ax.axis(off) ax.set_ylim([beat_signal.min() - 0.1, beat_signal.max() 0.1]) # 稍微留点边 ax.set_xlim([0, len(beat_signal)]) # 调整子图参数确保波形充满画布 fig.subplots_adjust(left0, right1, top1, bottom0) # 保存图像 fig.savefig(save_path, pad_inches0, bbox_inchestight, facecolorwhite) plt.close(fig) # 关闭图形释放内存实操心得plot_and_save_beat函数中的ax.axis(off)和fig.subplots_adjust是关键。这能确保我们保存的是一张“干净”的波形图而不是带有坐标轴的图表。这对于CNN模型至关重要因为坐标轴和刻度线会成为无意义的干扰特征。dpi和figsize共同决定了输出图像的像素尺寸这里是2.56英寸*100dpi256像素。步骤三遍历所有记录生成图像数据集现在我们编写主循环处理48条记录生成图像并保存到以类别命名的文件夹中。# 配置参数 data_dir ./mit-bih-arrhythmia-database/ output_dir ./mit_bih_ecg_images/ window_before 100 window_after 150 selected_channel 0 # 选择MLII导联通常是第0通道 # 创建输出目录结构按类别分文件夹 class_names [N, S, V, F, Q] for cls in class_names: os.makedirs(os.path.join(output_dir, cls), exist_okTrue) # 用于统计的字典 beat_count {cls: 0 for cls in class_names} skipped_beats 0 # 获取所有记录名排除扩展名 record_names [f.split(.)[0] for f in os.listdir(data_dir) if f.endswith(.hea)] record_names list(set(record_names)) # 去重 for r_name in record_names: try: print(f处理记录: {r_name}) # 读取信号和标注 record wfdb.rdrecord(os.path.join(data_dir, r_name)) annotation wfdb.rdann(os.path.join(data_dir, r_name), atr) # 提取选择的导联信号 ecg_signal record.p_signal[:, selected_channel] # 遍历所有标注 for i, (sample, symbol) in enumerate(zip(annotation.sample, annotation.symbol)): # 映射标签 mapped_label label_map.get(symbol, None) # 只处理我们定义的类别 if mapped_label is None or mapped_label not in class_names: continue # 截取心拍段 beat extract_beat_segment(ecg_signal, sample, window_beforewindow_before, window_afterwindow_after) if beat is None: skipped_beats 1 continue # 生成唯一文件名使用记录名和R峰位置 filename f{r_name}_{sample:06d}.png save_path os.path.join(output_dir, mapped_label, filename) # 绘制并保存图像 plot_and_save_beat(beat, save_path) # 计数 beat_count[mapped_label] 1 except Exception as e: print(f处理记录 {r_name} 时出错: {e}) continue print(\n 数据生成完成 ) print(各类别心拍数量统计:) for cls, count in beat_count.items(): print(f {cls}: {count}) print(f因边界问题跳过的心拍数: {skipped_beats})运行这段代码后你会在./mit_bih_ecg_images/目录下得到N/,S/,V/,F/,Q/五个文件夹里面装满了对应的PNG格式心电图像。一个初步的图像分类数据集就成型了。4. 数据集划分的科学构建可靠的验证集与测试集有了原始的图像数据接下来是最关键也最容易出错的一步划分训练集、验证集和测试集。很多新手会直接在整个数据集上随机划分这在医学数据上尤其是MIT-BIH这种特定数据库上是严重的方法错误会导致结果虚高、不可信。4.1 为什么不能简单随机划分MIT-BIH数据库的48条记录来自47位病人。不同病人的心电特征差异巨大个体生理差异、疾病状态、噪声类型等。如果我们在所有心跳图像中随机划分那么很可能同一个病人的心跳既出现在训练集又出现在验证集和测试集。模型在训练时已经“见过”这位病人的特征模式在测试时自然表现很好但这并不是真正的泛化能力而是“记住了病人”。这被称为“病人间数据泄露”是医学图像/信号分析中最常见的陷阱之一。正确的做法是按记录即按病人进行划分。确保用于训练、验证、测试的心跳来自完全不同的病人子集。4.2 按记录划分的具体策略与代码实现MIT-BIH数据库本身有一个非官方的、但被广泛接受的按记录划分建议旨在确保各类心跳在训练和测试集中都有代表性。一个经典的划分是DS1 (训练集): 记录编号: 101, 106, 108, 109, 112, 114, 115, 116, 118, 119, 122, 124, 201, 203, 205, 207, 208, 209, 215, 220, 223, 230DS2 (测试集): 记录编号: 100, 103, 105, 111, 113, 117, 121, 123, 200, 202, 210, 212, 213, 214, 219, 221, 222, 228, 231, 232, 233, 234这个划分通常称为“Inter-Patient”划分被大量学术论文采用作为基准。我们可以在此基础上从DS1中再分出一部分作为验证集。import shutil from sklearn.model_selection import train_test_split # 定义官方建议的划分按记录号 ds1_records [101, 106, 108, 109, 112, 114, 115, 116, 118, 119, 122, 124, 201, 203, 205, 207, 208, 209, 215, 220, 223, 230] ds2_records [100, 103, 105, 111, 113, 117, 121, 123, 200, 202, 210, 212, 213, 214, 219, 221, 222, 228, 231, 232, 233, 234] # 我们的所有记录 all_records ds1_records ds2_records # 最终划分目标目录 final_dataset_dir ./mit_bih_final_dataset/ splits [train, val, test] for split in splits: for cls in class_names: os.makedirs(os.path.join(final_dataset_dir, split, cls), exist_okTrue) # 步骤1: 将DS2直接作为测试集 test_records ds2_records # 步骤2: 从DS1中划分训练集和验证集例如 80%训练20%验证 # 为了保持类别分布可以使用分层抽样但这里按记录划分已经隐含了这一点。 train_records, val_records train_test_split(ds1_records, test_size0.2, random_state42) print(f训练集记录数: {len(train_records)}) print(f验证集记录数: {len(val_records)}) print(f测试集记录数: {len(test_records)}) # 定义一个函数根据记录名将图像文件复制到对应的划分文件夹 def copy_images_by_record(source_image_dir, record_list, target_split): source_image_dir: 之前生成的 ./mit_bih_ecg_images/ 目录 record_list: 属于该划分的记录名列表 target_split: train, val, 或 test for cls in class_names: class_dir os.path.join(source_image_dir, cls) image_files [f for f in os.listdir(class_dir) if f.endswith(.png)] for img_file in image_files: # 从文件名中提取记录名文件名格式: 记录名_R峰位置.png record_name_from_file img_file.split(_)[0] if record_name_from_file in record_list: src_path os.path.join(class_dir, img_file) dst_path os.path.join(final_dataset_dir, target_split, cls, img_file) shutil.copy2(src_path, dst_path) # 执行复制 print(正在复制训练集图像...) copy_images_by_record(output_dir, train_records, train) print(正在复制验证集图像...) copy_images_by_record(output_dir, val_records, val) print(正在复制测试集图像...) copy_images_by_record(output_dir, test_records, test) print(数据集划分完成)4.3 划分后的数据统计与类别不平衡问题划分完成后务必检查每个集合中各个类别的样本数量。import collections def count_samples(split_dir): split_path os.path.join(final_dataset_dir, split_dir) count {} total 0 for cls in class_names: cls_dir os.path.join(split_path, cls) num len([f for f in os.listdir(cls_dir) if f.endswith(.png)]) count[cls] num total num return count, total train_counts, train_total count_samples(train) val_counts, val_total count_samples(val) test_counts, test_total count_samples(test) print( 数据集统计 ) print(f训练集总计: {train_total}) for cls in class_names: print(f {cls}: {train_counts[cls]} ({train_counts[cls]/train_total:.2%})) print(f\n验证集总计: {val_total}) for cls in class_names: print(f {cls}: {val_counts[cls]} ({val_counts[cls]/val_total:.2%})) print(f\n测试集总计: {test_total}) for cls in class_names: print(f {cls}: {test_counts[cls]} ({test_counts[cls]/test_total:.2%}))你大概率会发现严重的类别不平衡。例如“N”正常类别的样本数可能是“V”室性早搏的数十倍甚至上百倍。这是心电数据的固有特性也是模型训练的一大挑战。应对策略在损失函数上处理使用加权交叉熵损失Weighted CrossEntropyLoss给少数类别更高的权重。在数据加载上处理使用加权随机采样WeightedRandomSampler确保每个batch内各类别样本出现的概率接近。数据增强对少数类别的图像进行适度的数据增强如小幅度的横向拉伸压缩模拟心率变化、添加轻微噪声等但需谨慎避免改变心跳的临床形态特征。5. 集成到深度学习框架创建PyTorch DataLoader数据集准备好了划分也完成了最后一步就是将其包装成深度学习框架能直接使用的形式。以PyTorch为例我们需要创建一个自定义的Dataset类。import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image class MITBIHImageDataset(Dataset): MIT-BIH 心电图像数据集类 def __init__(self, split_dir, transformNone): 参数: split_dir: 划分目录如 ./mit_bih_final_dataset/train/ transform: 可选的图像变换组合 self.split_dir split_dir self.transform transform # 收集所有图像路径和标签 self.image_paths [] self.labels [] self.class_to_idx {cls: idx for idx, cls in enumerate(class_names)} # 将类别名映射为索引 for cls in class_names: cls_dir os.path.join(split_dir, cls) if not os.path.exists(cls_dir): continue for img_name in os.listdir(cls_dir): if img_name.endswith(.png): self.image_paths.append(os.path.join(cls_dir, img_name)) self.labels.append(self.class_to_idx[cls]) def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img_path self.image_paths[idx] label self.labels[idx] # 使用PIL打开图像转换为灰度L模式或RGB image Image.open(img_path).convert(L) # 转换为单通道灰度图 if self.transform: image self.transform(image) return image, label # 定义图像预处理变换 # 注意我们的图像已经是“干净”的波形图预处理应尽量简单 train_transform transforms.Compose([ transforms.Grayscale(num_output_channels1), # 确保是单通道 transforms.Resize((224, 224)), # 调整到网络输入尺寸如224x224 transforms.ToTensor(), # 转换为Tensor并归一化到[0,1] # 可以添加归一化但需要预先计算数据集的均值和标准差 # transforms.Normalize(mean[0.5], std[0.5]) ]) val_test_transform transforms.Compose([ transforms.Grayscale(num_output_channels1), transforms.Resize((224, 224)), transforms.ToTensor(), # transforms.Normalize(mean[0.5], std[0.5]) ]) # 创建数据集实例 train_dataset MITBIHImageDataset(./mit_bih_final_dataset/train/, transformtrain_transform) val_dataset MITBIHImageDataset(./mit_bih_final_dataset/val/, transformval_test_transform) test_dataset MITBIHImageDataset(./mit_bih_final_dataset/test/, transformval_test_transform) print(f训练集大小: {len(train_dataset)}) print(f验证集大小: {len(val_dataset)}) print(f测试集大小: {len(test_dataset)}) # 创建DataLoader # 为了解决类别不平衡可以定义加权采样器 from torch.utils.data import WeightedRandomSampler # 计算每个样本的权重与所属类别的样本数成反比 train_labels [label for _, label in train_dataset] class_counts torch.bincount(torch.tensor(train_labels)) class_weights 1. / class_counts.float() sample_weights class_weights[train_labels] train_sampler WeightedRandomSampler(sample_weights, len(sample_weights), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplertrain_sampler) # 使用采样器时不要用shuffle val_loader DataLoader(val_dataset, batch_size32, shuffleFalse) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse) # 现在train_loader, val_loader, test_loader 就可以直接用于模型训练和评估了。至此一个源自MIT-BIH心律失常数据库、经过规范预处理、按病人划分了验证集和测试集、并封装成PyTorch DataLoader的心电图像分类数据集就完全构建成功了。你可以直接将其用于训练ResNet、EfficientNet等图像分类模型进行心律失常的自动分类研究。整个过程的核心在于理解数据背后的医学逻辑按病人划分和工程细节干净的图像生成避免数据泄露才能得到可信的模型评估结果。本文还有配套的精品资源点击获取