医学图像分割数据集解析与UNet实战:从数据准备到训练避坑
发布时间:2026/10/5 1:35:30 作者:尧图编辑部 阅读量:1,286

简介面向医学图像分割研究与教学场景这份肺部感染COVID图像分割数据集提供约2500张256×256分辨率的肺部影像及对应感染区域标注。数据基于DRIVE数据集扩充而来所有标签均为前景像素值255的二值mask格式规整便于直接观察和输入深度学习模型。资源已划分训练集1864张、验证集466张、测试集583张每个子集均包含images与masks目录结构清晰可无缝用于模型训练、调参与最终评估也适合教学演示和算法对比实验。包内共2000个文件以png图片为主另含1个txt说明文件与1个Python可视化脚本脚本可随机抽取一张样本将原始图片、GT标签以及GT在原图上的蒙板效果并排展示并保存帮助使用者快速核对标注质量与分割效果。压缩包大小约80.6MB已有403人学习适合医学图像分割入门者、科研人员及需要标准COVID感染区域数据的算法工程师。1. 这套医学图像分割数据集先把“数据准备”这件事说透做医学图像分割的同行应该都有同感模型结构反而不是瓶颈真正折磨人的是数据。这套肺部感染COVID分割数据集一共约2500张256×256的图像和对应标签把原始图片和mask分好了训练集、验证集、测试集还附赠一个可视化脚本——下载下来就能直接开工不用自己写脚本去配对、清洗、校验。适合正在做医学影像分割、想拿真实病灶数据跑基线实验的研究生也适合入门语义分割、想知道“别人家的数据集长什么样”的工程师。结合我拆过DRIVE扩充数据集的经历这里先给个结论这套数据的mask是前景为255的二值图肉眼直接可读比一堆json格式标注好处理得多。2. 了解数据集的真实布局三个子集与标签文件的设计逻辑2.1 目录结构与文件命名规则拿到手先看目录。常见做法是解压后分成train、val、test三个大文件夹每个文件夹下再放images和masks两个子目录。图片和mask主文件名一一对应比如covid_2521.png对应covid_2521.png的mask没有额外的json或xml标注文件减少了一层解析成本。COVID_Segmentation_Dataset/ ├── train/ │ ├── images/ # 1864张 │ └── masks/ # 1864张 ├── val/ │ ├── images/ # 466张 │ └── masks/ # 466张 └── test/ ├── images/ # 583张 └── masks/ # 583张这里要留意一个细节数据集的划分是在源头做好的训练集、验证集、测试集的比例约为 6.5:1.6:2而不是常见的8:1:1。测试集比例明显偏大可能是考虑到医学场景里要单独留出一批“模型没见过的患者”做最终评估。实际训练时验证集已经够用了测试集不要反复去碰否则会削弱最终指标的说服力。2.2 mask的像素设计为什么0和255比0和1更好用标签图的像素值只有两种背景是0感染区域是255。很多入门教程喜欢把mask归一化到0和1但这份数据直接用255表示前景有一个实际好处——可以用图像查看器直接打开mask肉眼确认病灶标注质量不用转成0-255再显示。结合DRIVE数据集扩充的经验这种处理方式直接复用了“血管分割”的标注思路后续做数据增强时稍微小心一点就行。import cv2 import numpy as np mask_path train/masks/covid_2521.png mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) print(唯一像素值, np.unique(mask)) print(感染区域占比, (mask 255).sum() / mask.size)逻辑说明先以灰度模式读入mask统计所有像素值确认是否只有0和255两种再计算感染区域占整张图的比例。这一小段代码建议在拿到数据的第一个晚上就跑一遍用来筛选异常标注。参数上不需要额外设置只要保证mask路径正确。若发现某张mask出现第三种像素值说明该图标注不规范要么剔除要么修复。这个比例还能帮你判断模型训练时是否需要class weight——如果感染区域普遍只有几个百分点交叉熵损失基本会被背景主导。2.3 可视化脚本先看标签再谈训练和许多纯数据包不一样这套资源还带了一个图像分割可视化脚本随机抽一张数据把原始图、GT mask、GT叠加在原图上的效果三张图拼在一起保存到当前目录。这一步建议在配置模型之前先执行重点确认标注边界和真实的感染区域是否重合。python visualize.py执行后会生成一张类似visualization_result.png的图片。脚本读入的是随机抽取的样本所以每次运行结果不同。这么做有个好处数据集本身不带json或xml标注视觉检查就是最直接的质检方式。我一般会连续跑几次多抽几张看看标注的边界质量如果感染区域边缘出现大块锯齿或明显缺漏就是标注工具有问题后续针对低质量样本做清洗即可。3. 从数据到训练加载器、预处理与UNet基线3.1 写一个干净的PyTorch Dataset类自动配对文件和mask数据集的目录结构很规整写加载器不需要绕路。常见做法是直接遍历images目录再根据同名规则去masks里找对应文件顺序不需要管文件名配对即可。import os from torch.utils.data import Dataset from PIL import Image import torchvision.transforms as T class COVIDDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone): self.img_dir img_dir self.mask_dir mask_dir self.img_names sorted(os.listdir(img_dir)) self.transform transform def __len__(self): return len(self.img_names) def __getitem__(self, idx): img_name self.img_names[idx] img_path os.path.join(self.img_dir, img_name) mask_path os.path.join(self.mask_dir, img_name) # 同名规则 image Image.open(img_path).convert(RGB) mask Image.open(mask_path).convert(L) if self.transform: image self.transform(image) mask self.transform(mask) mask (mask 127).float() # 255 - 1 return image, mask逻辑说明img_name直接用原文件名去masks目录中查找不依赖额外配对表mask转成单通道灰度后通过阈值127把255变成1.0送入模型时是标准的二分类标签。这里的sorted是对文件做排序保证多次训练时数据顺序稳定。若你对数据增强有要求可以将transform参数传入Compose实例但要特别注意mask必须使用和image完全相同的几何变换种子否则会出现图像和标签错位的情况。3.2 数据增强几何变换和强度变换要分开处理分割任务的增强比分类麻烦几何变换旋转、翻转、缩放必须同步作用于image和mask而亮度、对比度、高斯噪声这类强度变换只能作用于image。常见做法是把这两类变换拆开实现避免误伤标签。import random import torchvision.transforms.functional as TF class SegTransform: def __call__(self, image, mask): # 随机水平翻转 if random.random() 0.5: image TF.hflip(image) mask TF.hflip(mask) # 随机旋转10度 angle random.uniform(-10, 10) image TF.rotate(image, angle, fill0) mask TF.rotate(mask, angle, fill0) # 亮度抖动只作用于原图 image TF.adjust_brightness(image, random.uniform(0.9, 1.1)) return image, mask逻辑说明旋转和翻转对mask同步执行填充值设为0表示旋转产生的空白区域补背景亮度抖动只对image做不影响标签的语义。注意mask在旋转时fill0很关键如果默认填充0其实正好是背景像素不用额外操心。若你手上有ElasticTransform这类形变增强建议给医学数据配上但代价是一旦形变幅度过大标注边界会失真需要根据视觉检查结果逐步调整幅度。3.3 UNet基线参数直接照搬这个配置对于256×256输入UNet是分割任务最稳妥的起点。以下配置在我自己的实验里跑通多次训练约60个epoch能到0.85以上的Dice。医学分割的损失函数建议用Dice Loss和BCE的加权组合光用交叉熵会偏向背景。import torch import torch.nn as nn class DiceBCELoss(nn.Module): def __init__(self, weight_bce0.5, smooth1e-6): super().__init__() self.weight_bce weight_bce self.smooth smooth def forward(self, pred, mask): pred torch.sigmoid(pred) bce nn.functional.binary_cross_entropy(pred, mask) inter (pred * mask).sum() dice 1 - (2 * inter self.smooth) / (pred.sum() mask.sum() self.smooth) return self.weight_bce * bce (1 - self.weight_bce) * dice参数说明weight_bce0.5表示BCE和Dice各占一半smooth1e-6防止分母为0。训练时batch size取8用Adam优化器初始学习率设为1e-4配合余弦退火或ReduceLROnPlateau。输入图片无需额外resize因为数据本身已是256×256直接用原始分辨率训练能在一定程度上避免插值造成的病灶边界模糊。训练完成后用测试集单独跑一次Dice和IoU作为最终汇报指标。3.4 训练流程的TensorBoard跟踪分割训练不看 loss 曲线基本等于盲飞。在训练循环里插入可视化回调每若干个epoch把原图、GT、预测mask一起写到TensorBoard里。这一招能快速定位“loss下降但分割结果完全不对”的典型问题。比如网络把所有像素都预测成背景时loss可能仍然在下降因为背景占了多数只有从可视化里才能看出模型在偷懒。from torch.utils.tensorboard import SummaryWriter import torchvision writer SummaryWriter(runs/covid_unet) # 假设已有固定的验证样本 val_image, val_mask pred_mask torch.sigmoid(model(val_image.unsqueeze(0))) 0.5 grid torchvision.utils.make_grid( torch.cat([val_image, val_mask, pred_mask], dim2) ) writer.add_image(val_sample, grid, epoch)逻辑说明把原图、GT、预测在宽度方向上拼接成一张图写进TensorBoard3个epoch看一次即可。torch.cat的dim2表示在高度维度做拼接因为此时张量形状是(C, H, W)。这样一张图就能同时呈现三列内容排查模式坍塌非常直观。4. 避坑手册数据校验、类别不平衡与加载器陷阱4.1 文件名配对失败训练时mask和image对不上现象训练损失异常低验证Dice几乎为0可视化显示预测结构和GT完全错位。常见原因是加载器使用了zip(os.listdir(images), os.listdir(masks))一旦目录内顺序不一致或部分文件名不匹配就会把不同样本的image和mask配在一起。原因医学数据集常由多批数据合并而来文件顺序经常被打乱或者中间有个别mask文件缺失导致两个目录列表长度相同但内容不对应。解决严格使用“同名配对”策略即__getitem__里用img_name在mask_dir中寻找同名文件。拿到数据第一件事跑一遍下面这段校验脚本img_names set(os.listdir(train/images)) mask_names set(os.listdir(train/masks)) assert img_names mask_names, 存在未配对的图片或mask4.2 感染区域太小模型把所有像素预测成背景现象训练过程中loss一直在降但测试集Dice却只有0.1左右查看预测mask几乎全黑。原因感染区域在原图中占比很小负样本背景占绝对多数模型只要全部预测为背景就能获得一个看起来不错的loss值。解决改用Dice Loss或Focal Loss并配合class weight。医学分割里这是一个经典难题。实践中我会先在验证集上统计每个batch里前景像素占比如果普遍低于5%直接上Dice Loss门槛。另一种补充手段是过采样数据增强对感染区域较大的样本加大旋转、缩放幅度让网络更频繁见到真实的病灶形态。4.3 mask在数据增强时被插值成非0/255的中间值现象可视化预测时一切正常但计算指标时发现mask值出现127、63这类灰色像素导致二值化结果不稳定。原因用了torchvision.transforms.Resize之后再读mask或者忘记对mask单独设置InterpolationMode.NEAREST默认的BILINEAR会把255和0插值成灰色过渡值。解决对mask做resize时必须用最近邻插值并且阈值统一按大于127设为前景。这是从DRIVE扩充数据集踩坑中沉淀下来的习惯凡是mask一律用InterpolationMode.NEAREST凡是图像如果需要可微增强用BILINEAR。代码示例如下from torchvision.transforms import InterpolationMode mask_transform T.Resize((256, 256), interpolationInterpolationMode.NEAREST) image_transform T.Resize((256, 256), interpolationInterpolationMode.BILINEAR)4.4 训练和验证的数据分布不一致指标虚高现象训练Dice稳定在0.92测试集突然掉到0.61差得离谱。原因验证集和训练集之间存在隐藏的相似样本或者验证集划分与训练集来自同一个设备、同一个患者时段模型实际上记住了图像特征而不是病灶特征。解决这套数据已经划分好了不需要自己再做随机划分但使用时要克制评估次数。测试集仅在最终验收时跑一次。如果自己要交叉验证建议按患者或图像来源分组切分而不是纯随机切分。尤其是COVID肺部感染数据同一患者不同帧的相似度远高于不同患者纯随机切分会让验证集指标虚高10个百分点以上。4.5 加载慢是常事但读不到数据通常是路径问题现象程序报FileNotFoundError但日志里显示路径明明存在。原因Windows和Linux的路径分隔符差异或者训练脚本的工作目录与数据集解压位置不一致。开会时特别容易在相对路径上翻车。解决在Dataset类里把路径打印一遍然后用os.path.abspath重新拼接更省事的方式是在项目根目录建一个config.py集中管理所有路径常量。路径建议写绝对路径或基于项目根目录的相对路径避免在sys.path里绕来绕去。5. 用这套数据验证分割模型从指标到细节的一个收尾习惯数据真正值钱的地方在于“模型在测试集上到底能不能用”。我给自己定的标准是训练结束后必须手动检查至少10张来自测试集中间的样本把预测mask、GT和原图并排看一遍光看Dice和IoU数字不算数。肺部的感染区域边缘通常不规则Dice高但边缘偏移量大的情况经常出现。逐个样本评估时我会额外保存每个测试样本的Dice拉出一个直方图找到那些Dice异常低的样本。如果这些样本集中在某个亮度范围或某种解剖结构上那就说明训练数据里缺乏对应形态的病灶需要针对性补数据或再做一次数据增强。除了Dice我还会记录两个容易被忽略的数字预测mask里小于50像素的孤立区域数量以及大块感染区域被预测成多个碎片的情况。前者可以用经典连通域分析滤掉后者如果频繁出现说明网络对连续区域的分割能力不足可能需要调整损失函数的平滑项或者增大感受野。from scipy import ndimage import numpy as np pred_mask (torch.sigmoid(logits) 0.5).cpu().numpy().astype(np.int16) labeled, num_features ndimage.label(pred_mask) sizes ndimage.sum(pred_mask, labeled, range(1, num_features 1)) small_components (sizes 50).sum() print(f预测mask中的小孤立区域数量{small_components})这段代码对单个预测结果做连通域分析统计面积不足50像素的微小区域数量。参数可调分辨率是256×25650像素大约占全图的0.076%对于肺部感染分割来说更小的区域基本是噪声。若该数量超过3个我会回看对应原图确认是不是将血管误判成了感染区域——这是COVID分割里一个高频现象。最后说个习惯从那以后我每次拿到一份新数据集都会强制先跑一遍配对校验、看一遍mask可视化、再统计前景占比把这三步称为“数据三查”。别说光是第一步就救回过不少次训练。希望帮到你。本文还有配套的精品资源点击获取