简介:一套面向医学图像分割任务的心脏左心房切片数据集,按轴位面、冠状面、矢状面三个方向将3D数据切分为2D图像,并为每个切面准备独立的images与masks目录,mask中0为背景、1为心脏,适合用于分割模型训练、验证及算法对比。包内共2000个文件,以1998张PNG图像为主,配套1个Python可视化脚本和1个JSON配置文件,脚本可随机抽取样本并生成原图、GT、GT叠加效果三视图,帮助研究者直观检查标签质量。压缩包整体约93.88MB,目录划分清楚,便于直接接入现有训练流程。资源上线以来已有365人学习浏览,适用于医学图像处理初学者或需要快速获取多切面基准数据的算法工程师。
1. 医学图像分割数据集:为什么左心房切片要先分清轴位、冠状、矢状
做医学图像分割的人拿到一份现成数据集,第一反应通常是直接丢进模型训练,但心脏这种器官的3D数据在切分为2D切片时,切面方向直接决定了模型能看到什么结构。这份数据集把左心房3D数据分别从轴位面、冠状面、矢状面切出来,每个切面独立成目录,配套mask标签和可视化脚本,省掉了自己写dicom解析和切片脚本的功夫。适合正在做分割模型入门、需要一份带标准标签的2D医学图像数据来验证网络结构,或者想对比不同切面方向对分割精度影响的人。值得先说清楚的是,这份数据集的mask只有两个类别——1是心脏、0是背景,属于典型二分类分割任务,不是多器官标注,评估指标和loss选择都该按二分类来设计。
2. 数据集内部结构与标签文件:先把目录和命名规则摸透
2.1 三个切面的文件组织方式
压缩包解压后,根目录下是dataset.json和一堆png文件,文件名形如la_007_162.png。la是left atrium左心房的缩写,007大概率是序列或患者编号,162是切片序号。x轴目录下是320×320分辨率,1351张图对应1351张mask;y轴是320×130,1151对;z轴是320×130,828对。这里值得注意:三个切面的分辨率并不一致,x轴是正方形,y轴和z轴是长方形。这个差异不是错误,而是原始3D体数据的体素间距在不同方向上不一致导致的。实际做数据加载时,不能直接用一个固定的resize参数套三个目录,否则会破坏器官的纵横比。
dataset.json的内容没有在文件列表里展开,但按这个数据集的常见组织方式,它通常记录的是类别名、类别索引和各切面的样本路径映射。建议训练前先读一下这个文件里的键名,确认是COCO结构还是简单字典结构,因为不同格式对应不同的数据加载器写法。
2.2 mask数值与二分类的对应关系
数据集说明里明确写了mask中1为心脏、0为背景,这意味着不需要做one-hot编码的前处理,直接读灰度图,像素值为1的区域就是左心房。但有个隐藏细节:png读进来如果是三通道,或者灰度值范围被归一化到0到255,那1这个值几乎看不见。常见做法是在数据加载时用np.array(Image.open(mask_path).convert('L'))强制转单通道灰度,然后做一次(mask == 1).astype(np.uint8)的阈值化,把非0非1的杂散像素全部归零。这个操作看似多余,实际能避免很多训练时的loss震荡。
import numpy as np from PIL import Image def load_mask(mask_path): # 强制转灰度,避免三通道读入导致 shape 变成 (H, W, 3) mask = np.array(Image.open(mask_path).convert('L')) # 二值化:只保留像素值为1的区域,其余全部视为背景 mask = (mask == 1).astype(np.uint8) return mask这段代码里convert('L')是关键,如果mask png本身是8bit灰度,不转也能读,但一旦遇到某些标注工具导出的三通道mask就会出问题。mask == 1严格匹配像素值1,而不是大于0,因为如果标注过程有抗锯齿,边缘可能出现0.5之类的小数,直接大于0判定会把边缘噪声当器官。参数上唯一需要关注的是路径的拼接方式,train和val的mask目录结构必须一致。
2.3 数据划分与训练集/验证集的坑
文件列表里没有单独的train.txt或val.txt,这意味着划分方式需要自己处理。常见做法是按8:2或9:1切分,但切分时有个容易踩的坑:同一切片的不同切面之间不是独立样本。la_007编号下的x轴切片和y轴切片来自同一个心脏的3D体数据,如果随机划分导致同一个la编号的样本同时出现在训练集和验证集,验证指标会虚高。常见做法是按la编号分组划分,例如把所有la编号排序后,取前80%的编号作为训练集,后20%作为验证集,而不是直接对文件列表做随机采样。
3. 从PNG到模型输入:数据加载与预处理的标准流程
3.1 数据增强策略:医学图像该做什么不该做什么
医学图像分割和自然图像分割最大的差别在于,翻转和旋转不能乱用。左心房在胸腔里的位置虽然有一定对称性,但水平翻转会让左右心房互换,如果只用水平翻转做增强,模型会学到位置先验而不是形状先验。常用的增强组合是:小角度旋转(±15度以内)、随机缩放(0.9到1.1倍)、亮度对比度扰动,再加一点弹性形变。像RandomHorizontalFlip这类操作建议直接关掉,或者只在明确知道左右对称不敏感时使用。
import albumentations as A train_transform = A.Compose([ A.RandomRotate90(p=0.3), A.Rotate(limit=15, p=0.5), A.RandomScale(scale_limit=0.1, p=0.5), A.RandomBrightnessContrast(brightness_limit=0.05, contrast_limit=0.05, p=0.3), A.ElasticTransform(alpha=1, sigma=50, p=0.2), ]) val_transform = A.Compose([])RandomRotate90和Rotate同时用是因为前者补黑边方式不同,两者叠加能增加变换多样性。RandomScale的scale_limit=0.1表示缩放幅度限制在正负10%,超过这个范围会让心房直径变化失真。ElasticTransform的sigma控制形变平滑度,50比较温和。验证集不增强的原因是要保证评估指标可复现,任何随机性都会让同一模型在不同次验证时得到不同mIoU。
3.2 自定义Dataset读取动作的核心实现
写PyTorch的Dataset时,除了刚才的mask处理,还要注意图像的像素值范围。原始png读进来是0到255的uint8,模型输入通常要归一化到0到1或按ImageNet的均值和方差标准化。医学图像推荐用简单的x / 255归一化,不要套ImageNet的mean和std,因为CT和MRI的像素分布和自然图像完全是两回事。
import torch from torch.utils.data import Dataset class CardiacSliceDataset(Dataset): def __init__(self, image_paths, mask_paths, transform=None): self.image_paths = image_paths self.mask_paths = mask_paths self.transform = transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image = np.array(Image.open(self.image_paths[idx]).convert('L')) mask = load_mask(self.mask_paths[idx]) if self.transform: aug = self.transform(image=image, mask=mask) image, mask = aug['image'], aug['mask'] # HWC -> CHW,并转 float32 image = torch.from_numpy(image).unsqueeze(0).float() / 255.0 mask = torch.from_numpy(mask).unsqueeze(0).float() return image, maskunsqueeze(0)把(H, W)变成(1, H, W),加上的通道维代表单通道灰度。mask保持float是因为BCEWithLogitsLoss要求输入和target都是float类型,不能用long。/255.0在这里是必须的,如果enhance了亮度再除以255,范围依然是0到1。
3.3 Loss与评价指标的选择逻辑
二分类分割任务,Dice Loss或BCEWithLogitsLoss是主力选择。左心房在切片中的占比通常不大,如果x轴切片中心房只占整张图的5%以下,直接用BCE会偏向预测背景,Dice Loss对正负样本不敏感的特性就体现出来了。实现上,Dice Loss的分母加smooth项防止除零:
def dice_loss(pred, target, smooth=1.0): pred = torch.sigmoid(pred) intersection = (pred * target).sum(dim=(2, 3)) union = pred.sum(dim=(2, 3)) + target.sum(dim=(2, 3)) dice = (2.0 * intersection + smooth) / (union + smooth) return 1.0 - dice.mean()dim=(2, 3)是在H和W维度上求和,batch和channel维度保留。这个实现假设输入是(N, 1, H, W)的logits,target是(N, 1, H, W)的二值图。smooth取1.0是为了在mask全黑(没有心脏的切片)时避免分母为0。训练时常见做法是把Dice Loss和BCE按7:3加权组合,纯Dice Loss收敛慢,纯BCE对小目标不友好。
4. 可视化脚本:把原图、GT、叠加蒙版一次看清
4.1 脚本的运行逻辑与输出
可视化脚本解决的是样本质量问题——训练前花10分钟扫一遍GT,能避免训练两天后发现标签错位的惨剧。脚本的逻辑是随机抽一张图,生成三张子图:原始切片、GT掩码、GT在原图上的半透明叠加,然后保存到当前目录。这一步对核对mask是否对齐、切片是否反色、心房位置是否合理都很有用。
import matplotlib.pyplot as plt from PIL import Image import numpy as np import os, random def visualize_sample(image_path, mask_path, save_path='visualize.png'): image = np.array(Image.open(image_path).convert('L')) mask = np.array(Image.open(mask_path).convert('L')) mask = (mask == 1).astype(np.uint8) overlay = image.copy() overlay[mask == 1] = 255 # 把GT区域置白 fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(image, cmap='gray') axes[0].set_title('Original') axes[1].imshow(mask, cmap='gray') axes[1].set_title('GT Mask') axes[2].imshow(image, cmap='gray') axes[2].imshow(mask, cmap='jet', alpha=0.4) axes[2].set_title('Overlay') for ax in axes: ax.axis('off') plt.savefig(save_path, bbox_inches='tight', dpi=150)overlay[mask == 1] = 255是把心脏区域全部提亮,肉眼看边界非常直观。第三张子图用的是alpha=0.4的jet伪彩色叠加,GT区域会显示成红色系渐变,适合检查边缘轮廓是否贴合原图。运行后如果在当前目录看到visualize.png,说明数据路径和mask读取都没有问题。这里有个实用技巧:把脚本里随机抽图改成按la编号顺序抽图,每20张抽一张,就能快速覆盖整个数据集的标注质量。
4.2 从可视化到数据清洗:能发现哪些问题
跑一遍可视化脚本最常见的发现是三类问题:一是mask边缘有孤立噪声点,这些点不是心脏组织,需要做形态学开运算处理;二是部分切片的GT明显偏移,通常是因为原始3D标注在插值切分时产生了对齐偏差;三是某些切片里完全没有心脏区域,这些样本对训练没有贡献,但会让Dice Loss的smooth起作用,保留与否影响不大。可视化脚本的价值就在于把这些肉眼能识别的问题在训练前暴露出来,而不是等模型训完看badcase再回头查数据。
5. 避坑手册:这份数据集最容易翻车的五个细节
5.1 现象:y轴和z轴图像直接resize到正方形后,模型效果明显变差
原因:y轴和z轴原图是320×130,直接resize到320×320会把心房拉伸成椭圆,形状先验被破坏。解决:统一resize到320×256或保持原有宽高比后padding。具体做法是取短边130,缩放到目标短边,再对长边做中心裁剪或补零。
5.2 现象:训练时loss出现NaN
原因:mask在读取时不是二值图,某些像素值是255而不是0和1,导致Dice Loss的target超出预期范围,反向传播时梯度爆炸。解决:在load_mask里加一行mask = np.where(mask > 0, 1, 0),把所有非零像素统一置1,而不是只判断mask == 1。
5.3 现象:验证集mIoU很高,但实际推理效果差
原因:划分数据时没有按la编号分组,同一个心脏的相邻切片同时进了训练集和验证集,数据泄露导致指标虚高。解决:按la_007中的007编号做group split,确保同一编号的所有切片只出现在一个集合里。验证时用y轴数据做测试更能反映泛化能力,因为y轴和z轴分辨率不同,能检验模型的尺度鲁棒性。
5.4 现象:可视化脚本里GT在原图上看不清,边缘发虚
原因:png的mask经过了JPEG式压缩或标注软化,边缘有灰度过渡带。解决:用scipy.ndimage.binary_opening做一次开运算,kernel大小取3×3,可以去掉单像素噪声同时保留边缘细节。这个操作对mask做一次就够了,不要在训练时反复做,会引入随机性。
from scipy import ndimage mask = ndimage.binary_opening(mask, structure=np.ones((3, 3))).astype(np.uint8)5.5 现象:三个切面的数据用同一套超参数训练,x轴收敛正常,y轴和z轴欠拟合
原因:x轴是320×320,空间信息更丰富;y轴和z轴是320×130,长宽比悬殊,下采样后有效信息密度不同。解决:给y轴和z轴单独设置batch size和learning rate,一般做法是batch size减半、learning rate调低到原来的0.8倍,并单独记录最优checkpoint,不要三个切面共用一个最优模型。
6. 进阶用法:用可视化脚本做切面一致性校验与交叉验证
可视化脚本除了抽检单张样本,还可以改造成跨切面对齐检查工具。左心房的同一个解剖位置会同时出现在x轴的第n层、y轴的第m层和z轴的第k层,只是截面形状不同。把三个切面中间层的GT画在同一个画布上并排对比,能直观看出三维标注在三个方向上的投影是否自洽。具体操作是把visualize_sample改成接收三个图路径,用plt.subplot(2, 3, i)排布,让同一row显示原始图、GT、叠加图。这个检查值得做,因为3D标注切分成2D切片时,如果原始标注在某个方向上存在偏移,只有跨切面对比才能看出来,单看一个方向很难发现。
另一个更实用的做法是把可视化脚本输出直接当验证集的badcase筛选器。训练完成后,拿验证集的推理结果输出到目录,用脚本的叠加模式把预测和GT画在一起。对比时我有一个血泪经验:不要只看mIoU数值,mIoU 0.85的模型可能在心房边界处有系统性外扩,这种问题在数值上不明显,但可视化一眼就能看出来。从那以后我每次训练完都强制走一遍这个可视化流程,抽三个切面各10张图保存下来。评价一个医学分割模型能不能用,我只看两件事:边界是否贴合、是否有孤立小洞。这两点靠数值看不出来,靠可视化最直接。
如果想把这份数据集跑得更深,可以再做一个切面融合实验:用三个切面分别训练三个模型,推理时对每个体素取三个模型预测的均值。这个思路本质上是在模拟3D分割的效果,不需要改网络结构,只改数据加载和推理逻辑。均值融合通常比单一方向模型提升2到4个点的Dice,算是在这份2D切片数据上榨取更高精度的性价比方案。希望这些细节能帮你在复现这条数据时少踩几个坑。
本文还有配套的精品资源,点击获取