
简介这份数据集面向医学图像分割任务聚焦肺部感染COVID区域分割提供256×256分辨率的原始肺部图像与对应的二值掩码掩码中感染区域为白色可直接用于深度学习分割模型的训练与效果评测。资源共含2000个文件以1998张PNG图像为主覆盖原始图和标签图另附1个TXT说明文件与1个Python可视化脚本压缩包约80.6MB。数据集已划分训练集1864对、验证集466对、测试集583对并对DRIVE数据集做了扩充合计近三千张样本适用于医学影像分析和计算机视觉方向的学生或研究者。其中附带的可视化脚本能随机抽取一张样本同时展示原图、真实标签以及标签叠加在原图上的蒙版效果方便直观核对分割质量。目前已有403人浏览学习可作为肺部感染区域分割任务的基础数据资源。1. 从“约2500张带标签的肺部感染分割数据”说起解决的是训练断粮问题做医学图像分割的人最怕的不是模型写不出来而是手里只有三五张图正当你准备调U-Net时测试集不知道从哪里找。这个肺部感染COVID图像分割数据集价值就在于它一次性给了约2500张图像和对应的标签掩码图像是CT或X光片标签把感染区域逐像素画出来。对研究者来说它是训练基线的口粮对想从自然图像转医学影像的算法工程师来说它是熟悉“标注格式、数据分布、CT窗宽、感染尺度”这几件事的捷径。先别急着把网络跑起来。这2500张数据能不能用、训练出来的Dice是不是虚高都取决于你对数据本身的判断。本文会把从目录检查、掩码校验、划分策略到U-Net训练、翻车现场排查的全链路讲清楚每一段都有可直接复制的脚本和参数。2. 数据集的组成与标注格式拿到手先别急着训练先做三件事2.1 目录结构与文件命名图像、掩码、文档各自归位一个合格的分割数据集通常不会把图像和掩码混在一个文件夹里。常见做法是images/和masks/两个兄弟目录同一个样本的图名与掩码名保持一致比如covid_001.png对应covid_001.png。拿到压缩包后第一步不是解压完就开训而是先做一个完整盘点。数据元素常见格式拿到手建议做什么原始图像PNG / JPG / DICOM统计尺寸、通道数、灰度范围标签掩码PNG 二值图 / 整数图读取唯一像素值确认只有背景和前景划分清单CSV / TXT看是否已经划分好 train/val 文件名说明文档MD / PDF确认图像来源与标注协议我一般会先写一个检查脚本把这四类信息全打印出来避免后面训练到一半发现掩码全是黑的。from pathlib import Path from PIL import Image import numpy as np root Path(./covid_seg_dataset) images_dir root / images masks_dir root / masks img_paths sorted(images_dir.glob(*.png)) mask_paths sorted(masks_dir.glob(*.png)) print(f图像数量: {len(img_paths)}, 掩码数量: {len(mask_paths)}) for img_path in img_paths[:5]: img np.array(Image.open(img_path).convert(L)) mask np.array(Image.open(masks_dir / img_path.name).convert(L)) print(f{img_path.name}: 图像 {img.shape} 像素范围 {img.min()}-{img.max()}, f掩码唯一值 {np.unique(mask)})这段代码就做了三件事核对图像和掩码的数量是否一致检查图像是否单通道检查掩码的唯一像素值。如果掩码里出现0, 255以外的值比如128说明它是多类别标注或者被人为合成过后面要单独处理。还有一类坑有的数据集把掩码存成 16-bit PNGPIL 读出来确实有值但很多框架里的Image.open默认会把它转成 8-bit会造成标签整体偏移。建议先输出np.unique(mask)看一眼再决定。2.2 掩码标签的含义与二值化感染区域和背景怎么区分肺部感染分割的关注目标是“磨玻璃影”和“实变”区域。在标注协议里这类区域通常被当成前景并标成白色背景信息统一写成黑色。也就是说标签要做的就是找出图像里“感染区域”的像素坐标。如果掩码是纯二值的训练信号最简单模型输出一个通道做Sigmoid就行。但实际拿到手的数据集掩码有时是RGB三通道看起来是黑白但通道是3通道或者带有半透明边缘。这种掩码属于“伪二值图”如果不处理直接喂给CrossEntropyLoss背景类别会变成多类问题模型会产生迷惑。from PIL import Image import numpy as np mask np.array(Image.open(masks/covid_001.png)) # 不转灰度直接读 print(mask.shape, mask.dtype, np.unique(mask)) # 如果是 (H, W, 3)压缩成单选通 if mask.ndim 3: mask mask[:, :, 0] # 统一规范到 0 和 1 mask_bin (mask 127).astype(np.uint8) print(二值化后唯一值:, np.unique(mask_bin))逻辑说明先把数组压成二维再做一次阈值二值化。如果原始掩码是纯黑白的这一步不会改变标签如果掩码边缘有抗锯齿产生的灰色像素这一步会把灰色吞到前景或背景。对于感染分割这种病灶区域讨论的是“有没有”而不是“概率”所以这种严格二值化是合理的。建议把二值化后的掩码重新覆盖成png文件存到一个新目录避免每次都重复计算。同时验证一下感染区域的占比如果占比普遍低于 5%那训练时的正负样本失衡会相当严重。2.3 数据划分策略约2500张怎么切train/val/test医学分割数据集有一个和自然图像数据集非常不一样的地方同一个病人的若干张相邻切片往往长相高度相似。如果直接按文件名随机切分极易把同一个病人的片子同时扔进训练集和验证集模型在验证集上看到的图像“眼熟”Dice 虚高上线后立刻现原形。正确的做法是先给每个样本找到病人编号再按病人分组切分。文件名一般会包含编号信息比如patient7_slice3.png按前缀就可以得到病人ID。from sklearn.model_selection import GroupShuffleSplit import numpy as np import pandas as pd files sorted(images_dir.glob(*.png)) patient_ids [p.name.split(_)[0] for p infiles] df pd.DataFrame({file: [p.name for p in files], patient: patient_ids}) group_split GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(group_split.split(df, groupsdf[patient])) train_files df.iloc[train_idx] val_files df.iloc[val_idx] print(f训练集 {len(train_files)} 样本, 验证集 {len(val_files)} 样本) print(f验证集病人数: {val_files[patient].nunique()})逻辑说明GroupShuffleSplit按groups参数切分同一病人ID的所有切片只会落在一侧。约2500张数据建议训练集 70%80%验证集 10%15%留一部分做测试集。如果数据集本身没有测试集至少要保证验证集病人与训练集完全无交集。3. CT图像预处理与数据增强让约2500张数据顶上万张用3.1 从像素值到HU窗宽窗位医学分割的第一步不是归一化自然图像做分割上来就是减均值、除方差。但 CT 影像不是普通照片它本质是人体组织的密度分布像素值直接对应 HUHounsfield Unit。肺部感染显示为磨玻璃影或实变区在肺窗下看最清晰。肺窗的经典设置是窗宽 W1500窗位 L-450显示范围近似为 [L-W/2, LW/2]。但是你从压缩包里直接读到的 PNG/JPG 大概率已经是经过窗口变换的 8-bit 图像而不是原始 HU 值。这种情况下不能再按 HU 去算只能把它当普通灰度图做归一化。但有一点要注意直接 Min-Max 到 [0,1] 会让背景和病灶的对比度变弱。我一般会先做一个带对比度拉伸的预处理把低对比度的 CT 像素在合适范围内拉开import numpy as np def apply_window(img, window_width1500, window_level-450): lower window_level - window_width / 2 upper window_level window_width / 2 img np.clip(img, lower, upper) img (img - lower) / (upper - lower) return img def preprocess_ct(img_8bit): img apply_window(img_8bit.astype(np.float32)) img (img - 0.5) / 0.5 return img参数说明如果原始图像已经做过窗口变换直接再做一次apply_window等于二次裁剪反而损失细节。正确姿势是先看图像像素分布如果像素集中在 0255 但灰度偏灰直接用preprocess_ct里的归一化即可如果图像里存在大量接近黑底的高对比结构可以先轻度裁剪对比度再归一化。3.2 裁剪与缩放避免让模型学会识别黑边CT 图像另一个常见问题是边缘黑边和文字水印。直接把整张图送进网络模型会利用“黑边位置”来做判断而不是理解病灶形态。对约2500张数据来说裁剪掉的像素还能减少显存消耗。常见做法是去黑边后再中心缩放。我通常用一个简单策略把整张图像的像素值减去最小值统计每行每列的非零像素数量找到内容区域的外接矩形再按矩形中心扩边裁剪。医学图像内容不像自然图像有明确语义边界直接中心裁剪通常是安全的。3.3 数据增强配置离线增强与在线增强的选择2500张数据不算多增强是刚需。但医学分割增强有一个铁律图像和掩码必须做完全相同的几何变换否则标签错位。这里强烈建议直接用albumentations这类同步变换库而不是自己在 PyTorch 里手写 transform。常用配置我放在下面import albumentations as A train_transform A.Compose([ A.RandomResizedCrop(256, 256, scale(0.8, 1.0), p0.5), A.HorizontalFlip(p0.5), A.Rotate(limit15, border_mode0, value0), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), A.ElasticTransform(alpha1, sigma50, alpha_affine50, p0.3), ]) val_transform A.Compose([A.Resize(256, 256)])说明RandomResizedCrop的value0让填充区为黑色在医学图像里黑边是背景不影响判断ElasticTransform对感染区域形态做微小的局部形变能提升模型对组织形态变化的鲁棒性但强度不能过大否则病灶形状被破坏。在线增强在训练循环中实时生效推荐只对训练集使用离线增强适合先把数据扩到固定倍数再拿来训练但会占用大量磁盘空间一般项目不推荐。一个容易翻车的点是增强随机数种子必须共享给 image 和 mask。albumentations自动完成这一步但如果手写torchvisiontransform一旦忘了同步随机种子掩码就会被错位轻则准确率下降重则视觉上发现病灶区域不对。4. 用PyTorch训练一个分割基线从加载掩码到计算Dice4.1 自定义Dataset类图像与掩码必须走同一套变换约2500张数据要训练一个端到端分割模型首先要写一个干净的 Dataset。这里把图像读取、掩码二值化、预处理和同步变换放在一起方便后面在训练循环里直接复用。from torch.utils.data import Dataset import torch import cv2 class CovidSegDataset(Dataset): def __init__(self, image_paths, mask_paths, transformNone, is_trainTrue): self.image_paths image_paths self.mask_paths mask_paths self.transform transform self.is_train is_train def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img cv2.imread(str(self.image_paths[idx]), cv2.IMREAD_GRAYSCALE) mask cv2.imread(str(self.mask_paths[idx]), cv2.IMREAD_GRAYSCALE) mask (mask 127).astype(np.float32) if self.transform: augmented self.transform(imageimg, maskmask) img augmented[image] mask augmented[mask] img preprocess_ct(img) img torch.from_numpy(img).unsqueeze(0).float() mask torch.from_numpy(mask).unsqueeze(0).float() return img, mask这段代码的要点训练和推理共享同一个读取逻辑但 transform 不同掩码统一转成 float 二值张量图像在送入网络前完成归一化。这样任务数据流是从磁盘到 CUDA 张量的一整条线中间没有手写内存拷贝的隐患。4.2 损失函数与评估指标BCEDice组合是稳妥起点肺部感染区域通常只占整张 CT 图像的很小比例。用纯BCEWithLogitsLoss会被大面积背景淹没模型学到输出全0也能把 loss 压得很低。常见做法是BCE Dice组合损失让模型既按像素做概率回归也被整体区域重叠度约束。def dice_loss(pred, target, smooth1e-6): pred torch.sigmoid(pred) pred pred.contiguous().view(-1) target target.contiguous().view(-1) intersection (pred * target).sum() dice (2.0 * intersection smooth) / (pred.sum() target.sum() smooth) return 1 - dice def combined_loss(pred, target): bce torch.nn.functional.binary_cross_entropy_with_logits(pred, target) dice dice_loss(pred, target) return bce dice参数说明smooth防止除零也可以用1.0dice_loss没有使用阈值为 0.5 的硬划分而是直接用 sigmoid 概率计算软 Dice这样梯度更平滑。需要留意的是如果发现训练早期 Dice 一直很低可以在总损失里给 dice 部分乘以 0.5让训练初期更依赖 BCE 稳定方向后期再提升 Dice 权重。4.3 模型选型与微调从U-Net出发先不追求魔改很多人拿到分割数据就问“yolov8训练自己的数据集怎么配置”但医学语义分割和自然图像实例分割不太一样目标不是检测框而是像素级区域。主流首选还是 U-Net 或其变体 U-Net、DeepLabV3在约2500张数据规模下U-Net 加预训练编码器是性价比最高的组合。我一般用segmentation_models_pytorch库直接创建模型省去手写编码器import segmentation_models_pytorch as smp model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels1, classes1, )参数说明in_channels1是因为 CT 灰度图像只有一个通道当然也可以用in_channels3配合单通道图复制成三通道但那样卷积的第一层多了两次冗余计算encoder_weightsimagenet表示加载预训练权重这里不需要自己再单独下载 ImageNet-1K 数据集PyTorch 官方权重会自动下载到缓存目录。如果显卡显存有限可以换用timm-efficientnet-b0作为解码器参数量更小。4.4 训练脚本显存、batch、lr和早停分割网络的显存开销远大于分类网络输入尺寸大、中间层特征图多。对于 256×256 输入batch size 通常从 8 开始如果你的显卡不够可以降到 2但此时应该启用梯度累积。from torch.utils.data import DataLoader from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR device cuda if torch.cuda.is_available() else cpu train_loader DataLoader(train_ds, batch_size8, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size8, shuffleFalse, num_workers4) optimizer AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50) best_dice 0.0 for epoch in range(50): model.train() for img, mask in train_loader: img, mask img.to(device), mask.to(device) logits model(img) loss combined_loss(logits, mask) optimizer.zero_grad() loss.backward() optimizer.step() model.eval() val_dice 0.0 for img, mask in val_loader: img, mask img.to(device), mask.to(device) with torch.no_grad(): logits model(img) pred (torch.sigmoid(logits) 0.5).float() val_dice dice(pred, mask).item() val_dice / len(val_loader) scheduler.step() if val_dice best_dice: best_dice val_dice torch.save(model.state_dict(), best_unet.pth) print(fEpoch {epoch}: val_dice{val_dice:.4f})这里的关键参数lr1e-4适合 fine-tune 预训练编码器如果从零训练可以上调到1e-3T_max50与总 epoch 一致让学习率按余弦曲线降到接近 0。只保存验证集 Dice 最高的权重避免训练后期过拟合时覆盖了最优解。5. 常见问题与避坑记录训练医学分割模型的5个翻车现场5.1 掩码是彩色的“伪二值图”训练时模型永远学不到前景现象训练 loss 能下降但验证 Dice 一直贴着 0。 原因掩码读出来是三通道 RGB每个通道都包含灰度渐变模型把它当成多分类任务输出被“彩色边缘”误导。 解决读取掩码时强制convert(L)或cv2.IMREAD_GRAYSCALE然后统一做127的二值化。5.2 图像尺寸不统一导致collate报错现象训练第一个 batch 时报RuntimeError: stack expects each tensor to be equal size。 原因数据集里有几张大图或小图未经 Resize 直接进入 DataLoader。 解决在 transform 中统一控制尺寸验证集用Resize训练集用RandomResizedCrop保证输出张量都是(1, 256, 256)。5.3 感染区域太小Dice初值低到怀疑人生现象感染区域可能只占图像的 3%但 Intersection 永远是 0导致首轮 Dice 为 0。 原因Dice 按样本整体计算区域太小且初始预测全为 0分子为 0。 解决先观察掩码覆盖面积如果过低改用Focal Dice损失并让 Dice 权重在初期稍低一些另一个办法是给 Dice 的分子分母同时加smooth避免分母把结果压成 0。5.4 增强时过度处理掩码被插值成灰色现象训练后模型预测的病灶边界毛糙甚至输出整体模糊的不是 0/1。 原因用了torchvision的Resize插值导致二值掩码被插值成 0.3、0.7 之类的小数预测阶段模型学着输出“半激活”值。 解决使用albumentations或opencv的INTER_NEAREST插值处理掩码几何变换后强制mask (mask 127).astype(float)恢复二值。5.5 同一患者切片散落在训练集和验证集模型得分虚高现象验证 Dice 高到 0.95部署到新 CT 上直接掉到 0.6。 原因划分时按文件名随机切分没有按病人分组。 解决回归第 2 章的GroupShuffleSplit以病人ID为分组依据重新划分。这个坑在我自己项目里出现过一次重做划分后验证 Dice 掉了 0.2但实际临床测试反而更稳定。6. 进阶验证在约2500张数据上把模型成色看透6.1 按患者分组的K折交叉验证单次划分的验证集波动很大尤其约2500张规模不算大。建议做 5 折交叉验证仍然按患者ID分组统计每一折的 Dice 均值和标准差。如果某一道折的 Dice 明显低于其他折说明该折内影像数据特殊比如图像亮度差异大、感染面积偏小这对你的预处理方式是一个直接反馈。6.2 测试时增强与伪标签推理时对同一张图做水平翻转和多尺度缩放取多次预测的均值往往比单次预测稳定。伪标签则适合用来扩训练集先在验证集上拿到较高 Dice 后对未标注图像做预测只保留预测概率大于 0.9 的核心区域作为伪标签加入训练。但每次迭代只加一部分避免模型被自身噪声带偏。6.3 上线前检查清单检查项具体操作掩码是否严格二值读取所有掩码做np.unique检查图像与掩码是否对齐随机绘制五张叠加图肉眼验证病灶重合验证集是否无数据泄漏按患者ID分组复核推理时是否同步做了预处理训练和推理共用同一个preprocess_ct输出是否经过后处理去掉小于 50 像素的孤立连通域我做 CT 分割项目最深的体会是删掉那些看起来“差不多”的掩码检查最终损失的往往是模型上线后的口碑。约2500张数据足够让你验证思路但对每个像素负责靠的还是这些反复确认的习惯。希望这篇笔记帮你在同样的数据规模上少走几趟弯路。本文还有配套的精品资源点击获取