
简介这是一份面向医学影像分析与深度学习实践者的肺部图像分割数据集专注解决肺实质、左肺与右肺等结构的分割建模问题可直接用于模型训练、验证与测试。数据均为256×256分辨率标签掩码为前景像素值255的二值图像便于直接观察和计算Dice等分割指标。资源共2000个文件压缩包大小约253.01MB其中1999张PNG图像覆盖训练集与测试集的原始影像和对应掩码另有1个Python可视化脚本可随机抽取样本并生成原图、GT、GT蒙板对比图辅助快速检查数据质量和评估分割效果。训练集包含6849对图像与掩码测试集包含1712对划分清晰适合入门到进阶的医学图像分割项目实战。目前已有1387人学习下载。1. 医学图像分割数据集肺分割数据训练集测试集一次配齐做医学图像分割的人最怕的不是模型调不通而是数据不到位。这份肺分割数据集把训练集和测试集一次配齐省掉最耗时的标注环节拿到手就能开跑 U-Net直接对照 Dice、IoU 验证思路。它解决的是肺实质分割这个经典任务输入一张胸部影像输出对应的肺部区域掩码。适合刚入门医学图像分割的研究生、医疗 AI 算法工程师以及想快速验证分割想法但不想从零标数据的从业者。下面按「结构 → 训练 → 避坑 → 评估」的顺序把这份资源从拿到手到出结果完整过一遍。2. 数据集结构拆解先看清标注格式和目录规则再动手拿到数据集先别急着训练把目录结构和标注格式摸清楚能避免后面一半的报错。这一章只做两件事搞清楚文件怎么组织、掩码长什么样。2.1 目录结构与文件组织这份资源最常见的组织方式是 images 和 masks 两个平级目录各自内部再按 train、test 分好原图与掩码分开存放靠文件名一一对应。我拿到手的第一件事不是看 README而是先跑一段命令确认文件数量和命名规律。find . -type f | grep -E \.(png|jpg|jpeg|nii|dcm)$ | head -50 find . -type f | grep -E \.(png|jpg|jpeg|nii|dcm)$ | wc -l第一行查看前 50 个文件的完整路径确认命名是否成对存在第二行统计总数用来和 README 里声称的数量对比。如果 images 和 masks 的文件数不一致多半是漏标注或下载断档先把数据补齐再谈训练否则训练时索引会直接越界。除了解压后的原生目录我还会顺手检查有没有隐藏文件混进来比如 macOS 的 .DS_Store、Windows 的 Thumbs.db。这些文件被 os.listdir 当样本读进来后DataLoader 一跑就报错。常见做法是在遍历时加扩展名过滤3.1 的数据加载器里会给出完整写法。2.2 标注格式与关键参数肺分割的掩码通常是单通道二值图背景像素值为 0前景肺区像素值为 255用 PNG 或 JPG 存储。但有个细节容易翻车——部分资源的掩码被存成三通道 RGB 图三个通道内容完全一样直接读进来形状是 (H, W, 3)和模型的单通道输出对不上loss 计算立刻报错。拿到手后我一般用一行代码确认掩码的形状和取值from PIL import Image import numpy as np mask np.array(Image.open(masks/train/0001.png)) print(mask.shape, mask.dtype, np.unique(mask)) # 期望输出类似 ((512, 512), uint8, array([ 0, 255])) # 如果 shape 是 (512, 512, 3)说明是三通道掩码需转灰度提示判断掩码是否三通道最快的方法就是看 shape 的第二维和第三维是否一致。输出里 shape 如果是三维加载时就统一用 convert(L) 转成单通道unique 的结果应该只有 0 和 255 两个值如果出现 128 之类的中间值说明原始标注被压缩过或做过平滑训练前要重新二值化。还有一类资源掩码是 0/1 而不是 0/255这不影响训练加载器里统一处理即可。图像尺寸这项常见资源有 256×256、512×512 或原始大小不一。我的建议是训练前统一 resize 到 512×512U-Net 在这个分辨率下能兼顾显存占用和分割细节肺这种大器官目标256 会明显损失边缘精度如果原图本身比 512 还小保持原尺寸做 padding 即可不要强行放大引入插值伪影。参数常见取值说明图像尺寸512×512 / 256×256训练前统一 resize掩码通道单通道三通道需 convert(L)前景像素值255 或 1归一化后统一转 0/1文件格式PNG / JPGNIfTI 需换专用加载器掩码在 resize 时有一条硬性要求必须用最近邻插值NEAREST不能用双线性或三次插值。否则掩码边缘会出现 0 到 255 之间的过渡像素训练时模型把过渡值当软标签去学最终预测边界模糊。这一点在 3.1 会再次强调。2.3 训练集与测试集的划分逻辑这份资源已经把 train 和 test 分好了省掉自己切分的麻烦。但拿到划分后还要确认测试集是否真的独立有些数据集的不同切片来自同一批病例测试集和训练集在病人维度上重叠评估出来的指标会虚高。验证方法很直接import os train_names set(os.listdir(images/train)) test_names set(os.listdir(images/test)) overlap train_names test_names print(重叠文件数:, len(overlap))如果重叠文件不少报告指标时就要说明这一点或者手动把重复文件从训练集剔除。我的习惯是优先信任资源自带划分但重叠超过 5%就按文件名前缀比如病例 ID重新分组切分保证同一病人的所有切片只出现在一侧。这个检查花不了两分钟却能避免后续报告里的数据泄漏硬伤。3. 用 U-Net 跑通肺分割数据加载、训练参数与评估脚本U-Net 是医学图像分割的事实标准基线编码器逐层下采样提取语义特征解码器逐层上采样恢复空间分辨率配合 skip connection 把浅层细节传给深层。肺这种边界相对清晰、结构稳定的器官第一版 U-Net 就能拿到不错的 Dice不需要一上来就换 Transformer 分割模型先跑通基线再谈改进。3.1 数据加载器注意掩码插值方式数据加载器是整个流程里最容易藏 bug 的地方。下面这份基础版本直接对应这份资源的目录结构。import os import numpy as np import torch from torch.utils.data import Dataset, DataLoader from PIL import Image IMG_SIZE 512 class LungSegDataset(Dataset): def __init__(self, img_dir, mask_dir, sizeIMG_SIZE, augmentFalse): self.img_dir img_dir self.mask_dir mask_dir self.size size self.augment augment # 只保留图片文件过滤 .DS_Store 等隐藏文件 self.names sorted([ f for f in os.listdir(img_dir) if f.lower().endswith((.png, .jpg, .jpeg)) ]) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img Image.open(os.path.join(self.img_dir, name)).convert(L) mask Image.open(os.path.join(self.mask_dir, name)).convert(L) # 原图用 BILINEAR 缩放到统一尺寸 img img.resize((self.size, self.size), Image.BILINEAR) # 掩码必须用 NEAREST避免产生过渡像素 mask mask.resize((self.size, self.size), Image.NEAREST) img np.array(img, dtypenp.float32) / 255.0 mask np.array(mask, dtypenp.float32) / 255.0 mask (mask 0.5).astype(np.float32) # 重新二值化 img torch.from_numpy(img).unsqueeze(0) # (1, H, W) mask torch.from_numpy(mask).unsqueeze(0) # (1, H, W) return img, mask这段代码有三个关键点。第一convert(L) 统一转灰度顺便解决三通道掩码问题第二原图和掩码使用不同的插值方式这是分割任务的铁律augment 参数预留给你后续扩展增强逻辑mask 仍然要保持最近邻第三除以 255 并重新二值化保证输入和标签的取值范围一致。依赖就四个torch、Pillow、numpy、matplotlib评估边界指标时再加 scipy。实例化时按资源实际目录传路径即可测试集记得把 augment 关掉train_ds LungSegDataset(images/train, masks/train, augmentTrue) test_ds LungSegDataset(images/test, masks/test, augmentFalse) train_loader DataLoader(train_ds, batch_size8, shuffleTrue, num_workers4) test_loader DataLoader(test_ds, batch_size8, shuffleFalse, num_workers4)batch_size 设为 8 是 512×512 分辨率下的稳妥值显卡显存只有 8G 左右的话建议降到 4 并配合梯度累积具体见 4.5。num_workers 在 Windows 上如果报错改成 0 即可。3.2 训练参数与 loss 选型肺分割里背景像素远多于肺区像素普通 BCE loss 会倾向把一切预测为背景。最常见的做法是 Dice loss或者在 BCE 基础上叠加 Dice两个思路我都试过结论是 BCE Dice 组合最稳。def dice_loss(pred, target, smooth1e-6): pred torch.sigmoid(pred) inter (pred * target).sum() union pred.sum() target.sum() return 1 - (2 * inter smooth) / (union smooth) def bce_dice_loss(pred, target, bce_weight0.5): bce torch.nn.functional.binary_cross_entropy_with_logits(pred, target) return bce_weight * bce dice_loss(pred, target)Dice loss 对类不平衡天然鲁棒分母同时包含预测和标签的前景像素总和不会因为背景占多数就把梯度带偏叠加 BCE 是给每个像素一个独立的梯度信号防止 Dice 在训练初期对局部误差不敏感。bce_weight 一般取 0.5如果训练初期 loss 震荡厉害可以调到 0.3。优化器用 Adam初始学习率 1e-3训练 50 轮后降到 1e-4 微调。肺分割任务收敛比一般自然图像分割快50 轮以内 Dice 基本不再明显上涨不用像大模型那样动辄几百轮。如果 20 轮后 Dice 还在 0.6 以下挣扎问题大概率不在训练参数而在数据加载或前处理回头看第 4 章。3.3 测试集评估流程训练完成后评估脚本要固定下来不能每次手写。下面这段输出每个测试样本的 Dice 和 IoU并打印平均值。def evaluate(model, loader): model.eval() dice_list, iou_list [], [] with torch.no_grad(): for img, mask in loader: pred torch.sigmoid(model(img)) pred_bin (pred 0.5).float() inter (pred_bin * mask).sum(dim(1, 2, 3)) union pred_bin.sum(dim(1, 2, 3)) mask.sum(dim(1, 2, 3)) iou inter / (union - inter 1e-6) dice 2 * inter / (union 1e-6) dice_list.append(dice.cpu().numpy()) iou_list.append(iou.cpu().numpy()) dice_all np.concatenate(dice_list) iou_all np.concatenate(iou_list) print(fMean Dice: {dice_all.mean():.4f}, Mean IoU: {iou_all.mean():.4f}) return dice_all, iou_all注意这里逐样本计算指标再取平均而不是把所有样本的混淆矩阵累计起来一次性算——后者会放大目标大样本的权重对目标小的样本不公平。肺分割里不同病例的肺区大小差异不小逐样本平均更符合临床评估习惯。评估时阈值固定用 0.5。不要为了刷指标去测试集上调阈值那等于把测试集变成了验证集。想用 Otsu 自适应阈值就只允许在验证集上考察测试集的结果必须是流程跑完之后一次性得到。4. 肺分割训练避坑指南新手最容易翻车的五个问题这一章的五条坑我按「训练日志能看到的 → 输出图像能看到的 → 数据本身的问题」来排序。指标不正常时先别急着换模型结构按这个顺序排查通常十分钟内能定位到根因。每一条都是现象、原因、解决三步走。4.1 现象loss 卡住不降预测输出全黑现象训练了十几轮loss 几乎不动拿一张测试图推理输出掩码全是背景。原因最普遍的是掩码被当成三通道 RGB 读入标签形状变成 (3, H, W)而模型输出是 (1, H, W)广播后梯度信号被稀释成噪声另一类是掩码像素值 0/255归一化时忘了除以 255标签几乎全是 1loss 卡在错误的高位平台。解决加载器里统一 convert(L) 转灰度并除以 255然后打印 np.unique 确认标签只有 0 和 1。我每换一个数据集都会把这两行检查放到训练脚本最前面跑一遍确认标签分布正常再开训。4.2 现象Dice 分数不错但分割边界锯齿严重现象Dice 到了 0.94感觉不错把输出图放大看边界全是锯齿和零散小点。原因掩码在 resize 时用了双线性插值产生 0 到 1 之间的过渡像素模型学的是模糊边界而不是硬边界阈值一卡就切成锯齿另一个隐蔽来源是数据增强里的旋转缩放很多增强库默认对 mask 做平滑插值。解决掩码统一用 Image.NEAREST增强配置里显式指定 mask 用最近邻。验证方法是对 resize 后的掩码跑 np.unique出现接近 0.5 的小数就是插值出了问题。4.3 现象验证集指标好测试集突然崩掉现象验证集 Dice 0.93测试集只有 0.71差距大得不合理。原因最常见的是把测试集当验证集反复看模型和超参都对测试集产生了隐式拟合其次是训练时对测试数据也做了随机增强或者测试集和训练集存在同一病人的重叠切片。解决从第一天起就分成 train / val / test 三份只有 train 和 test 两份的话从 train 里再切 10% 当验证集测试集完全锁起来不看。测试集预处理固定为 resize 加归一化不做随机增强。如果确认资源划分里文件名或病例 ID 重复就按病例重新切分。4.4 现象肺内小结节或血管区域被模型吞掉现象大块肺区分割正常但边缘的小结节、血管末端被模型直接预测成背景。原因肺分割里背景像素占比高前景里大块肺区又占主导Dice loss 对小目标区域的梯度贡献极小模型选择牺牲细节换整体分数。解决换 Tversky loss把假阴性权重调高beta 取 0.7。Tversky 和 Dice 的区别只在 alpha、beta 两个权重Dice 等价于 alphabeta0.5调 beta 本质上是在 Dice 基础上给漏检加罚。def tversky_loss(pred, target, alpha0.3, beta0.7, smooth1e-6): pred torch.sigmoid(pred) tp (pred * target).sum() fp (pred * (1 - target)).sum() fn ((1 - pred) * target).sum() return 1 - (tp smooth) / (tp alpha * fp beta * fn smooth)alpha 控制假阳性惩罚beta 控制假阴性惩罚beta 大于 alpha 就是让模型更不愿意漏前景。如果发现模型反而把背景大片预测成肺就把 beta 调回 0.5。这是肺分割里处理小目标最常用的手段之一。4.5 现象显存报错 OOM现象512×512 分辨率batch 设 16跑两步直接 CUDA out of memory。原因U-Net 四层编码器的中间特征图显存占用不小batch 太大是新手最常踩的显存坑。解决先把 batch 降到 4还爆就用梯度累积模拟大 batch或者把输入降到 384×384。梯度累积是攒几个 batch 再 optimizer.step()但它对 BatchNorm 不友好——统计量仍按小 batch 更新介意这个就用 GroupNorm 或直接降分辨率。我的经验是 512 分辨率配 batch 48是大多数 8G 显存卡的舒适区。5. 评估指标实战Dice、IoU 与分割边界可视化指标算得准结果才可信。这一章讲两件事指标怎么算不踩坑以及怎么把分割结果可视化后人工复核。经常有人训练完只看一个 Dice 数字其实输出图里藏着大量指标反映不出来的问题而指标算法的口径不同数字也完全不可比。5.1 Dice 与 IoU 的正确计算方式3.3 里用的是逐样本平均这里再解释一次为什么不用全局混淆矩阵全局方式把全部样本的预测像素和标签像素累计到一起再算大目标样本在累计里占的权重大小目标样本的误差被稀释。肺分割里不同病例的肺面积差异可以接近一倍逐样本平均更公平。如果两份结果的评估方式不一致数字不能直接对比。补充一个容易算错的细节空目标样本的处理。测试集里如果存在某张图完全没有肺区mask 全黑Dice 分子分母都趋近于 0直接算会得到 0 或 NaN。def safe_metrics(pred_bin, mask): inter (pred_bin * mask).sum() union pred_bin.sum() mask.sum() - inter if union 0: # 标签为空且预测也为空视为完全正确 return 1.0, 1.0 dice 2 * inter / (pred_bin.sum() mask.sum() 1e-6) iou inter / (union 1e-6) return dice.item(), iou.item()空目标样本在报告时要单独说明数量不能静默剔除。如果数据里有正常肺区很小的样本IoU 天然偏低这是尺度效应不代表模型变差。比较不同模型时测试集、预处理、指标口径三者必须完全一致否则对比没意义。5.2 逐样本可视化与失败案例筛选指标之外最重要的是把预测结果画出来人眼过一遍。我习惯在评估时输出一个图片网格每个测试样本放三张图原图、标签掩码、预测掩码并标注 Dice。import matplotlib.pyplot as plt def save_overlay(img, mask, pred, dice, path): fig, axes plt.subplots(1, 3, figsize(12, 4)) axes[0].imshow(img.squeeze(), cmapgray) axes[1].imshow(mask.squeeze(), cmapgray) axes[2].imshow(pred.squeeze(), cmapgray) axes[0].set_title(input) axes[1].set_title(label) axes[2].set_title(fpred dice{dice:.3f}) for ax in axes: ax.axis(off) plt.savefig(path, bbox_inchestight, dpi100) plt.close()把这套函数跑在全部测试集上然后按 Dice 升序排列重点看排在最后 10% 的样本单独复制到一个 fail 目录里逐个打开。失败案例分几类要心里有数整体偏移说明模型空间定位有问题边缘大量锯齿说明插值或增强配置不对胸腔积液区域被误判成肺说明训练数据里负样本不够需要补充。这类定性结论是单一指标永远给不了的。5.3 再看一个边界指标HD95Dice 和 IoU 都是面积重叠类指标对边界质量不敏感。医学图像分割里最常用的边界指标是 95% Hausdorff 距离HD95衡量两组轮廓之间的距离单位是像素。HD95 越低预测边界越贴近标签边界很多高 Dice 的预测在 HD95 上会现出原形。from scipy.ndimage import distance_transform_edt def hd95(pred_bin, mask_bin): pred_edt distance_transform_edt(1 - pred_bin) mask_edt distance_transform_edt(1 - mask_bin) d1 pred_edt[mask_bin 0] d2 mask_edt[pred_bin 0] hd np.percentile(np.concatenate([d1, d2]), 95) return hd实现思路是计算两个方向的距离变换取 95 分位数剔除离群点干扰。HD95 对像素级抖动很敏感两个模型 Dice 差 0.01、HD95 可能差 2 个像素在需要清晰边界的场景下是重要区分信号。注意 HD95 对孤立噪点非常敏感一个远离主体的假阳性点就会让距离值跳升算之前最好先做连通域后处理把小于 50 像素的连通域去掉。指标衡量内容边界敏感度单位Dice前景面积重叠低无量纲IoU前景面积重叠低无量纲HD95轮廓点距离高像素报告指标时我习惯同时给 Dice、IoU、HD95 三个数单提任何一个都说明不了全貌。6. 进阶用法从分割掩码到肺面积统计分割出掩码之后下一个自然诉求是把掩码转成有临床意义的量肺面积占比是最常见的落地指标。思路是统计掩码前景像素数占整图的比例再按物理分辨率换算成真实面积。def lung_area_stats(mask, pixel_spacing_mmNone): mask_bin (mask 0.5).astype(np.uint8) area_pixel mask_bin.sum() total_pixel mask_bin.size ratio area_pixel / total_pixel if pixel_spacing_mm is not None: area_mm2 area_pixel * (pixel_spacing_mm ** 2) return ratio, area_mm2 return ratiopixel_spacing_mm 是单个像素对应的物理尺寸来自 DICOM 头信息如果资源给的是普通 PNG 没有物理信息就只报占比。除了面积我还会顺手做一步连通域分析把预测掩码里面积小于阈值的孤立噪点去掉避免把伪影当成肺区。这份肺分割数据集的训练集和测试集是打包好的下载后按前面几章的步骤一步步来能直接复现从数据加载到指标输出的完整流程。从那以后我每次拿到新的分割数据集都会先跑一遍「结构检查 → 标签分布 → 基线训练 → 可视化 → 面积统计」五步形成固定流程不再临时手写脚本。希望这份拆解和踩坑记录能帮到你少走几步弯路。本文还有配套的精品资源点击获取