
简介面向医学图像分割与深度学习研究者提供超过2000张胸部X光气胸图像及对应语义分割标签覆盖Pneumothorax类别可直接用于训练和验证气胸自动分割模型。数据集已划分为训练集与验证集并对图像进行了对比度拉伸、resize等增广处理有助于提升模型在不同影像条件下的泛化能力。压缩包共2000个文件以1998个png图像及标签为主另含1个txt类别说明文件和1个Python脚本可用于读取类别与数据预处理整体包体约183.48MB。已有648人学习/下载png文件中包含原始影像与对应分割掩码txt文件明确类别名称py脚本提供基本的数据读取与增强操作示例方便快速接入U-Net、DeepLab等常见分割模型进行训练与评估。标签为二值掩码便于计算mIoU、Dice等指标适合临床辅助诊断、医学影像教学及算法研究等多种场景。整体内容经过整理目录结构清晰适合医学图像分割方向的初学者建立完整流程认知也便于有一定经验的研究者直接用于对比实验和算法改进。1. 气胸胸片语义分割数据集为什么 2000 张带掩码的片子比 2 万张裸图更值钱急诊科医生判断气胸靠的是胸片上那条“脏层胸膜线”——压缩肺边缘一道细到几乎看不见的弧形白线周围还有肋骨、锁骨、衣服褶皱和皮肤阴影干扰。人眼都会漏算法要做的是帮人在一堆结构里把它框出来。这个医学图像分割数据集给到的就是超过 2000 张 Chest X-Ray 原图和逐像素的气胸掩码标注形式和语义分割任务完全对齐拿来就能训 U-Net 这类模型。它解决的核心问题很直接让模型学会在胸片上“画”出气胸区域而不是只输出一个“有或没有”。适合做医学影像 AI 落地、语义分割入门复现、以及想验证分割 pipeline 是否可靠的工程师和学生。2. 拆解数据集的组成DICOM 胸片、RLE 掩码和 2000 张图的组织形态拿到这类数据我一般不会直接开训。先花半天把文件结构、像素范围和掩码编码方式摸清楚后面省下的可不是半天是几天。气胸分割数据集最常见的形态是两种文件混在一起图像要么是原始 DICOM要么是已经转好的 PNG掩码要么是逐像素的 PNG/NumPy要么是压缩在 CSV 里的 RLE 字符串。每种格式都有各自的坑逐个过一遍。2.1 图像侧DICOM 的位深、窗宽窗位与像素缩放胸片原始的 DICOM 文件不是 8 位灰度图。很多设备存的是 12 位或 16 位灰度数值范围从 0 到 4095甚至到 65535直接拿cv2.imread读出来的全黑或者全白基本都是位深处理出了问题。正确做法是先读 DICOM把原始像素值通过 Rescale Slope 和 Rescale Intercept 转成标准单位再做窗宽窗位裁剪最后归一化到 0-255。import pydicom import numpy as np ds pydicom.dcmread(sample.dcm) arr ds.pixel_array.astype(np.float32) # 部分设备存的是带符号/经过缩放的原始值需要还原成标准灰度 if hasattr(ds, RescaleSlope) and ds.RescaleSlope is not None: arr arr * float(ds.RescaleSlope) if hasattr(ds, RescaleIntercept) and ds.RescaleIntercept is not None: arr arr float(ds.RescaleIntercept) # 肺野窗窗位 -600窗宽 1500把肺纹理对比度拉开 window_center -600.0 window_width 1500.0 low window_center - window_width / 2.0 high window_center window_width / 2.0 arr np.clip(arr, low, high) arr (arr - low) / (high - low) * 255.0 image arr.astype(np.uint8)这段代码里pixel_array出来的是原始像素不乘不加减直接显示往往会发灰或者完全不可见。窗宽窗位的选择决定了胸片展示效果肺野窗适合看气胸骨窗适合看骨折这个任务里固定用肺野窗就够。如果数据集里已经直接给了 PNG意味着资源方在前面替你把窗宽窗位这一步做好了可以直接跳过。但我仍然建议抽几张贴出来看一眼确认灰度范围不是被错误压缩过的。2.2 掩码侧把 RLE 字符串解成逐像素 mask掩码最常见的打包方式是 RLE 编码写在 CSV 里形如“123 45 678 90”含义是“从第 123 个像素开始连续 45 个像素属于目标再从第 678 个像素开始连续 90 个像素属于目标”。注意起点从 1 开始不是 0。空掩码通常写作-1。这种编码把上万像素压成几百字节但对新人来说是最容易翻车的环节解码顺序、起点、reshape 方向错了掩码就整个错位。import numpy as np def rle_decode(rle_str, shape(1024, 1024)): mask np.zeros(shape[0] * shape[1], dtypenp.uint8) if rle_str -1 or not rle_str: return mask.reshape(shape) parts [int(x) for x in rle_str.split()] starts parts[0::2] lengths parts[1::2] for start, length in zip(starts, lengths): start start - 1 # RLE 起点是 1-based mask[start:start length] 1 return mask.reshape(shape) def rle_encode(mask): flat mask.ravel() diff np.diff(np.concatenate([[0], flat, [0]])) starts np.where(diff 1)[0] 1 ends np.where(diff -1)[0] lengths ends - starts 1 rle .join(f{s} {l} for s, l in zip(starts, lengths)) return rle解码逻辑里最关键的是mask.reshape(shape)默认按行优先展开对应的 RLE 也必须按行优先顺序编码。如果发现解出来的掩码在图像上显示为斜向条纹多半是编码顺序与 reshape 顺序不一致。编码函数留给导出预测结果用模型输出 mask 后回程成 RLE交给下游或用于提交时保持一致。建议在预处理阶段就把 RLE 全部解成 PNG 或者.npy缓存下来训练时直接读掩码文件避免每个 epoch 都做字符串解析IO 会拖慢整个流程。2.3 统计正负样本比与病灶大小分布这一步很多人跳过我不建议。把掩码全部解出来后要统计两类信息一是没有气胸的负样本占多少二是正样本里病灶区域占全图的比例分布。import pandas as pd df pd.read_csv(train_rle.csv) # 假设有 image_id 和 rle 两列 sizes [] for rle in df[rle]: mask rle_decode(rle) sizes.append(mask.sum() / mask.size) sizes np.array(sizes) print(总样本数:, len(sizes)) print(正样本数:, (sizes 0).sum()) print(负样本数:, (sizes 0).sum()) print(正样本像素占比 P25/P50/P75:, np.percentile(sizes[sizes 0], [25, 50, 75]))负样本占比通常在三四成甚至更高。如果你图省事把负样本全删了模型很快会变成“什么都画一点”的过度自信预测器因为训练时没见过“没有目标”的正确形态。我的做法是保留全部负样本但在采样器里适当提高正样本权重。像素占比的 P25 数字也很重要如果四分之一的病灶只占全图不到 1% 像素那这种小病灶就是评估时最该单独看的子集后面所有阈值和后处理参数都得迁就它。3. 数据装载与预处理把 1024×1024 胸片规整成 U-Net 的标准输入训练代码跑起来之前先把预处理做成一条固定管线。胸片不是自然图像归一化方式、尺寸策略、增强范围都和 ImageNet 那一套有区别。这里每一步改错最终都会体现在验证集 Dice 上而且是那种“说不清哪不对”的缓慢下滑。3.1 胸片归一化不要直接除以 255 完事很多从自然图像转过来的同学习惯把图像读成 uint8除以 255 变成 0-1 就喂进模型。胸片这样处理不是不行但效果差。胸片里有大片白色骨影、黑色肺野还有设备厂商打上去的文字标记直接做全局 min-max 归一化会把像素分布拉得很偏。常见做法是先在预处理阶段完成窗宽窗位裁剪把灰度压到 0-255再统一除以 255转成 float32 后输入网络。另一个细节是裁剪黑边。很多 DICOM 转出来的图四周有一圈纯黑边框如果不对齐处理每次 resize 后黑边占比不一样等于给模型注入了无意义的协变量。我会在预处理里做一个固定操作先把灰度大于 5 的像素区域的外接矩形找出来裁剪出有效肺野区域再 resize 到目标尺寸。这样所有样本的有效内容比例一致模型看到的肺野大小是稳定的。3.2 尺寸策略直接保留 1024 还是统一到 512气胸线本身极其细在 1024×1024 原图上可能只有几个像素宽一旦 resize 到 512线的宽度会被压到一两像素再经过 U-Net 的四次下采样边界信息基本就没了。但无脑上 1024 也不现实显存占用翻四倍训练速度大幅下降。我的建议是分两阶段走。先用 512×512 把模型结构和损失函数跑通验证训练管线没有低级错误然后直接用 1024 输入做正式训练或者用 512 训练得到的权重在 1024 分辨率上微调十来个 epoch。显存不够时用随机裁剪 512 代替全局缩放但要注意裁剪块不能太小否则一块肺野里全是背景相当于不停制造假负样本。512 起步、1024 微调是我在气胸分割上用过最稳妥的组合。3.3 数据增强的医学边界翻转可以上下颠倒不行语义分割常用的增强手段在胸片上有一半不能直接用。增强操作是否推荐参数建议水平翻转推荐概率 0.5左右肺对称垂直翻转不推荐肺尖在下不符合生理模型会学到错误的空间先验小角度旋转谨慎限制 ±10 度以内亮度对比度扰动谨慎幅度控制在 0.1 以内过强会让胸膜线淹没弹性形变不推荐气胸线是细线结构形变容易让掩码失真水平翻转可以用因为左右肺都有发生气胸的可能解剖结构镜像后仍然合理。垂直翻转我坚决不用胸片有明确的“上肺尖下肋膈角”空间语义翻转后模型会学着把肺尖特征和肋骨形态错配。旋转保持小角度超过 15 度时纵隔和肋骨的投影关系就变了。弹性形变这类在细胞分割里常用的增强在气胸细线上很容易把 1 像素宽的线拉断得不偿失。3.4 自定义 Dataset把图像、mask、正样本权重绑在一起数据预处理稳定后封装成一个Dataset类。这里假设你已经把 DICOM 和 RLE 都转成了 PNG 格式图像和掩码文件名一一对应这是训练前最舒服的组织方式。import cv2 import torch from torch.utils.data import Dataset class PneumoDataset(Dataset): def __init__(self, image_ids, root, size512, trainTrue): self.image_ids image_ids self.root root self.size size self.train train def __getitem__(self, idx): pid self.image_ids[idx] image cv2.imread(f{self.root}/images/{pid}.png, 0) mask cv2.imread(f{self.root}/masks/{pid}.png, 0) mask (mask 127).astype(np.uint8) if self.train: image, mask self.augment(image, mask) image image.astype(np.float32) / 255.0 image torch.from_numpy(image).unsqueeze(0) mask torch.from_numpy(mask).float().unsqueeze(0) return image, mask def __len__(self): return len(self.image_ids)这里mask 127的二值化很关键。很多掩码 PNG 在保存时为了看起来直观会把 0/1 的掩码乘以 255 存储如果你的掩码是 0/1 格式这一步也不会有副作用。图像转成 float 并除以 255 后管道里不再出现任何 uint8 运算增强库和模型输入都能兼容。unsqueeze(0)是把 H×W 变成 1×H×W保持单通道输入不要在这里顺手转成三通道 RGB胸片单通道完全够用三通道只会增加参数和显存消耗。提示用 Albumentations 做增强时必须同时传入 image 和 mask并保证二者的 transform 参数完全一致。4. 训练与评估损失函数怎么配Dice 为什么虚高模型架构、损失函数、评估策略是训练阶段三件套。很多人把精力全放在刷网络结构上气胸这种细线分割任务上损失函数和评估指标的坑比结构本身更多。4.1 模型选型U-Net 和它的预训练编码器气胸分割的默认起点就是 U-Net。原始 U-Net 结构简单编解码对称适合小批量实验。但全从头训练收敛慢而且胸片这种结构固定的图像预训练编码器提供的底层边缘纹理特征仍然有用。常见选型有三个U-Net 原始结构参数量少显存友好适合从零跑通流程。U-Net ResNet34 编码器ImageNet 预训练权重容易获取收敛快是我在这个任务上的首选。FPN 结构多尺度特征融合对大小病灶都有话要说但细节恢复不如 U-Net 的跳跃连接直接。我的实践感受是先把编码器固定成 ResNet34训练流程全部跑通再回头试更深的 ResNet50 或更重的 DeepLabv3。不要一开始就在模型结构上铺太多时间气胸分割的瓶颈通常不在模型表达能力而在数据分布和评估方式。4.2 损失函数BCE 与 Dice 的混合配方二值分割最常见的损失就是 BCE 和 Dice Loss。单用 BCE 在正样本像素极少的情况下模型会快速收敛到“全部预测为背景”因为背景占了绝大多数像素BCE 已经够低。单用 Dice Loss 对小病灶的梯度非常不稳一个样本里只有几十个正像素时Dice 的数值会剧烈跳动。import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): pred torch.sigmoid(pred) pred pred.view(pred.size(0), -1) target target.view(target.size(0), -1) intersection (pred * target).sum(dim1) union pred.sum(dim1) target.sum(dim1) dice (2 * intersection smooth) / (union smooth) return 1 - dice.mean() def bce_dice_loss(pred, target, bce_weight0.5, dice_weight0.5): bce F.binary_cross_entropy_with_logits(pred, target) dice dice_loss(pred, target) return bce_weight * bce dice_weight * dicesmooth1.0不算玄学它防止了目标区域为空时分母为 0 导致的除零错误。bce_weight和dice_weight建议从 0.5/0.5 起步如果发现训练集上预测倾向于全背景把 bce 权重降到 0.3让 Dice 主导优化方向。注意dice_loss里用的是经过 sigmoid 的概率不是 logits这是与 BCE 最大的区别写成公式时很容易在这里翻车。4.3 评估指标Dice、IoU 之外还要盯住小病灶 Recall验证时的指标如果只算一个整体 Dice会被大片病灶和负样本掩盖真实水平。一个病灶面积占全图 10% 的样本 Dice 可能到 0.9而占全图 0.5% 的样本 Dice 只有 0.4两者平均下来还挺好看。我要求自己至少分三组看负样本单独统计、小病灶面积占比小于 1%单独统计、剩余正样本一组。def dice_coef(pred, mask, threshold0.5): pred (pred threshold).astype(np.uint8) intersection (pred * mask).sum() return 2 * intersection / (pred.sum() mask.sum() 1e-6)1e-6是防止两个空数组碰撞时除零。对负样本的预测如果全是 0Dice 会因为加了平滑项而显得不错这就是整体指标的欺骗性。小病灶的 Recall 比 Dice 更接近临床价值漏掉一个气胸比多画一块正常组织的代价大得多。我在验证时会把阈值从 0.3 到 0.7 扫一遍看小病灶 Dice 的峰值出现在哪个阈值然后据此调整推理阶段的后处理参数。4.4 训练配置与能跑通的循环结构超参我给一个自己常用的起点输入 512×512batch size 16优化器 AdamW初始学习率 1e-4weight decay 1e-4用 CosineAnnealingLR 把学习率在一个周期内降下来训练 40 到 60 epoch。显存只有 12G 左右时batch size 调到 8学习率对应降到 5e-5效果差别不大。for epoch in range(epochs): model.train() for images, masks in train_loader: images, masks images.cuda(), masks.cuda() preds model(images) loss bce_dice_loss(preds, masks) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()我还开了混合精度训练显存省下不少速度也能提升约 30%。训练中要盯两条曲线训练集的 Dice 是否持续上升验证集的 Dice 是否跟得上。如果训练集 Dice 接近 0.95 而验证集只有 0.6多半不是模型容量问题回第 5 章的避坑清单找原因。5. 避坑与排查气胸分割最容易翻车的 5 个现场这章写的是我实际踩过的坑。每一条都按“现象、原因、解决”来写你在复现时遇到类似问题可以直接对照排查。5.1 现象训练 Loss 一直降验证 Dice 纹丝不动训练集上 Loss 掉得很稳但验证集 Dice 始终在一个低位徘徊。最常见的原因是数据集划分时按图像随机切分同一个病人的多张胸片同时进了训练集和验证集模型相当于在“背题”验证集失去意义但表现出来的不是过拟合而是指标停滞。解决方法是强制按病人 ID 划分数据。拿到数据集后先看有没有病人维度信息如果有优先按病人分组再切分。没有病人信息时至少要做相似性去重同一患者在不同体位的片子特征高度相似混在两边会让验证结果虚高。如果划分没问题再看负样本是不是被过度删减导致验证集与训练分布不一致。5.2 现象预测 mask 断成碎线边界发飘模型输出的气胸区域像一堆碎点子或者本该连续的胸膜线断成好几截。原因是细线结构经过 U-Net 多次下采样后边缘细节丢失加上训练时增强的旋转角度过大让模型对线型结构的连续性感知变弱。解决分两步一是在后处理里做形态学闭运算把临近的断线连接起来二是过滤掉面积过小的连通域。闭运算的核不要太大3×3 或 5×5 就够核太大会把周围正常组织也并进来。我的经验是先统计训练集掩码的最小连通域面积以这个值的 1/10 作为过滤阈值。5.3 现象模型把锁骨和骨影当成气胸预测结果里锁骨下方或肋骨边缘出现团块状假阳性。这往往不是模型的问题而是标签本身不够干净。胸片这个模态里锁骨、肩胛骨和胸膜线的灰度层次接近如果标注人员在勾掩码时把胸膜线附近的骨影一并圈了进去模型自然会学成“这些高亮纹理都是气胸”。解决方法是手工审查训练掩码挑出假阳性最多的 30 到 50 张把明显标错的部分用标注工具修正。这个过程虽然耗时但对 Dice 的提升比换任何模型结构都直接。另外也可以在增强时把亮度对比度扰动的幅度调低避免模型过度依赖局部高亮特征。5.4 现象验证集 Dice 0.85实际临床没法用这是最坑的现象。整体 Dice 看着不错拆开看才发现大片气胸的样本 Dice 普遍在 0.9 以上小病灶样本只有 0.3 左右负样本预测全空被当成完美命中把平均分拉上去了。解决方法是把评估指标按病灶大小分层并且只看正样本的平均 Dice。我会在验证脚本里输出一个分层的指标表像素占比小于 1%、1% 到 5%、大于 5% 三组分别算 Dice 和 Recall。如果发现小病灶组 Recall 低于 0.5说明模型在临床意义上不可用这时再回头调输入分辨率、阈值和后处理而不是靠整体指标骗自己。5.5 现象DICOM 读出来全黑或者全白图像预处理阶段有人读 DICOM 后直接astype(np.uint8)得到一张全白的图要么是一张全黑的图。原因在于 DICOM 的原始像素范围是 12 位或 16 位直接截断到 8 位时所有超过 255 的值都变成了 255。解决办法就是回到第 2.1 节的完整管线先乘 Rescale Slope再加 Rescale Intercept把像素值还原到真实灰度范围然后再做窗宽窗位裁剪。调试时可以打印arr.min()和arr.max()正常胸片在窗宽裁剪前应该是几百到几千的范围而不是 0 到 255。看到这个现象第一反应不是去调模型而是去查预处理。6. 交付前的验证技巧五张片子就能看出模型底细模型训完我给自己定了一条死规矩不直接看日志里的平均指标先挑五张片子人工核验。挑法有讲究一张大范围气胸、一张小病灶早期气胸、一张无气胸的正常胸片、一张带有胸腔引流管的胸片、一张肺纹理杂乱的老年胸片。这五张覆盖了模型最常见的过拟合和误判模式。把预测和掩码叠加显示规则是绿色是真实掩码红色是模型预测黄色是两者重叠。我的观察顺序是先看黄色区域是否连续再看红色区域是否出现在骨骼边缘最后看正常胸片上有没有成片的假阳性。如果五张里有三张都需要“仔细解释才能说过去”那这个模型还不能交付指标再高也只是数字。import cv2 import numpy as np def overlay_with_mask(image, mask, pred): show cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) show[mask 0] (0, 255, 0) show[pred 0] (255, 0, 0) show[(mask 0) (pred 0)] (0, 255, 255) return show两个进一步能提点的技巧。第一个是测试时增强推理时对原图和水平翻转图各预测一次把两个概率图取平均再阈值化能稳定把小病灶的预测从“时有时无”变得连续。第二个是后处理里加上面积过滤把训练集里最大连通域面积的下界统计出来低于这个值的连通域直接丢弃做的是“宁可漏掉疑似小点也不让噪声冒充病灶”的精修。这两个技巧叠加通常能在保持 Recall 不掉的前提下把假阳性减少一两个数量级。我早期做过一个失败版本整体 Dice 刷到 0.87拿去给医生看对方一眼指出模型在锁骨阴影上画了块假气胸。从那以后我就明白了医学分割模型的终点不是一个分数而是能被人在关键病例上信任。现在每训一版我都保留这五张片子作为固定验收集指标和人工核验双轨并行。希望这套流程对你有用也帮你少走点我走过的弯路。本文还有配套的精品资源点击获取