简介本资源面向医学图像分割方向的研究者与算法学习者提供一套已预处理好的肝脏及肝脏癌症2D分割数据集可直接用于训练与评估分割模型。原始数据为Liver3d的nii.gz文件沿x轴切分并剔除前景区域不足0.05的切片共提取8千余张图像mask像素分为0背景、1肝脏、2癌症三类标签语义清晰。压缩包共约2000个文件以1998张png图像为主另含1个py可视化脚本与1个json配置文件整体约937.6MB数据已划分为训练集6227张图像及对应mask、测试集2668张图像及对应mask目录结构规整。配套脚本可随机抽取一张图片展示原始图像、GT图像及GT在原图上的蒙板效果并保存至当前目录便于快速核验标注质量。目前已有877人学习下载适合需要现成肝脏分割数据、希望省去繁琐预处理步骤的读者直接上手实验。1. 肝脏 2D 分割数据集从拿到手到跑通第一个 U-Net 要多久肝脏肿瘤分割是医学图像分割里最经典的入门场景之一但真正卡住大多数人的不是模型结构而是数据。你拿到的往往是一堆 NIfTI 格式的 3D 体数据动辄几百 MB 一个文件标签还是稀疏的病灶掩膜直接喂给网络要么显存爆炸要么正负样本极度失衡。这个标题说的「Liver 肝脏癌症数据、划分了 2D 数据、并且划分了训练集和测试集」本质上解决的就是这个断层把 3D 体数据沿轴向切片成 2D 图像按病例级别切分训练/测试让你能在一张 8GB 显存的卡上跑通完整的训练-验证-推理链路。适合刚接触医学图像分割的算法工程师、做肝脏病灶辅助诊断方向的研究生以及需要快速验证分割方案可行性的从业者。下面按「数据长什么样 → 怎么读进来 → 怎么切分才不泄漏 → 怎么训 → 坑在哪」的顺序讲透。2. 肝脏 2D 分割数据的结构拆解与读取验证2.1 一份可用的肝脏 2D 数据集应该包含什么先明确一个判断标准拿到一个号称「划分好 2D 训练测试」的肝脏数据集你要检查的不是文件数量而是三件事——图像与掩膜是否严格一一对应、切分是否按病例隔离、标签的类别定义是否清晰。典型的目录结构长这样liver_2d/ ├── train/ │ ├── images/ │ │ ├── case_001_slice_032.png │ │ ├── case_001_slice_033.png │ │ └── ... │ └── masks/ │ ├── case_001_slice_032.png │ ├── case_001_slice_033.png │ └── ... ├── test/ │ ├── images/ │ └── masks/ └── dataset_info.json命名里带case_xxx和slice_xxx是关键它让你能追溯每张 2D 切片来自哪个病例。如果文件名只是一串无意义的数字你无法验证切分是否按病例隔离这是后面数据泄漏排查的前提。标签方面肝脏分割常见两种任务定义一是只分「肝脏 vs 背景」的二分类掩膜里前景像素值为 1 或 255二是「肝脏 肿瘤」的多分类前景像素值可能是 1肝脏和 2肿瘤。拿到数据第一件事就是确认像素值分布别默认它是 0/1。2.2 用 Python 快速验证图像与掩膜的对应关系不要急着写 DataLoader先花五分钟做一次完整性校验。下面这段脚本检查图像和掩膜是否一一对应、尺寸是否一致、标签值是否在预期范围内import os import numpy as np from PIL import Image from collections import Counter def validate_liver_2d(root_dir): 校验肝脏2D数据集的图像-掩膜对应关系与标签分布 for split in [train, test]: img_dir os.path.join(root_dir, split, images) mask_dir os.path.join(root_dir, split, masks) img_files sorted(os.listdir(img_dir)) mask_files sorted(os.listdir(mask_dir)) # 检查文件名集合是否完全一致 if set(img_files) ! set(mask_files): missing_mask set(img_files) - set(mask_files) missing_img set(mask_files) - set(img_files) print(f[{split}] 图像缺掩膜: {len(missing_mask)}, 掩膜缺图像: {len(missing_img)}) continue label_counter Counter() size_mismatch 0 for fname in img_files: img np.array(Image.open(os.path.join(img_dir, fname))) mask np.array(Image.open(os.path.join(mask_dir, fname))) if img.shape[:2] ! mask.shape[:2]: size_mismatch 1 continue # 统计掩膜中出现的唯一像素值 label_counter.update(np.unique(mask).tolist()) print(f[{split}] 样本数: {len(img_files)}, 尺寸不匹配: {size_mismatch}) print(f[{split}] 标签像素值分布: {dict(label_counter)}) validate_liver_2d(./liver_2d)逻辑说明先比对文件名集合任何一边多出文件都说明数据组织有问题再逐张读取检查图像和掩膜的空间尺寸是否一致尺寸不一致的样本直接跳过统计最后用Counter汇总所有掩膜里出现过的像素值。参数说明root_dir指向数据集根目录脚本假设目录结构是split/images和split/masks。如果你拿到的数据是split/img和split/label改一下路径拼接即可。标签分布输出里如果只看到{0: ..., 255: ...}说明是二分类如果出现{0, 1, 2}说明是多分类后续损失函数和输出通道数要对应调整。提示如果标签分布里出现了 0、1、2 之外的异常值比如 3、128先别急着归一化去查数据集说明文档很可能是标注工具导出的调色板索引需要做一次映射转换。2.3 按病例切分为什么比按切片随机切分重要这是医学图像分割里最容易被忽视、后果最严重的一个点。假设一个病例有 80 张切片你把这 80 张全部随机打散其中 60 张进训练、20 张进测试。由于同一病例相邻切片之间的解剖结构高度相似测试集里的切片在训练集里几乎能找到「孪生兄弟」。模型不需要学到肿瘤的形态特征只要记住这个病例的肝脏轮廓就能拿到很高的 Dice。这就是数据泄漏它会让你的测试指标虚高十几个点上线后直接翻车。正确的做法是按病例隔离同一个病例的所有切片只能出现在训练集或测试集其中之一。如果你拿到的数据集已经划分好了验证方法很简单——从训练集和测试集的文件名里提取case_id检查两个集合的case_id有没有交集import os import re def extract_case_ids(split_dir): 从文件名中提取病例ID假设命名格式为 case_XXX_slice_YYY.png case_ids set() for fname in os.listdir(split_dir): match re.match(r(case_\d)_slice_\d, fname) if match: case_ids.add(match.group(1)) return case_ids train_cases extract_case_ids(./liver_2d/train/images) test_cases extract_case_ids(./liver_2d/test/images) overlap train_cases test_cases print(f训练集病例数: {len(train_cases)}) print(f测试集病例数: {len(test_cases)}) print(f重叠病例: {overlap if overlap else 无切分正确})如果输出显示有重叠这个数据集就不能直接用你需要自己按病例重新划分。常见做法是先把所有病例 ID 收集起来按 8:2 或 7:3 的比例随机分配再根据分配结果把对应切片移动到新的目录。别偷懒用train_test_split直接对文件列表切那样切的是切片不是病例。3. 从 2D 切片到可训练张量预处理与 DataLoader 落地3.1 肝脏 CT 切片的窗宽窗位与归一化处理肝脏 CT 的原始像素值是 HUHounsfield Unit范围大约在 -1024 到 3000 之间。直接把这玩意喂给网络梯度要么爆炸要么消失。医学图像的标准做法是先用窗宽窗位把感兴趣区域的对比度拉出来再做归一化。腹部 CT 看肝脏常用窗宽 400、窗位 40 左右这个设置能把肝脏实质和周围组织的灰度差异拉开。计算公式是import numpy as np def apply_window(image, window_center40, window_width400): 对CT图像应用窗宽窗位输出范围归一化到[0, 1] lower window_center - window_width // 2 upper window_center window_width // 2 image np.clip(image, lower, upper) image (image - lower) / (upper - lower) return image.astype(np.float32)逻辑说明np.clip把超出窗范围的像素截断低于下界的变成 0高于上界的变成 1然后线性映射到 [0, 1]。这一步做完肝脏区域的灰度对比度会明显增强背景和无关组织被压缩。参数说明window_center和window_width不是固定的。如果你发现分割结果里肝脏边缘总是糊的可以试试把窗宽收窄到 300、窗位调到 50让肝脏实质的灰度范围更集中。如果数据已经是 PNG 格式且做过归一化这一步可以跳过但要在dataset_info.json里确认清楚。注意如果你的数据集里图像已经是 8 位 PNG像素值 0-255说明上游已经做过窗变换和归一化不要再套一遍窗宽窗位否则会把有效信息压没。判断方法很简单读一张图看最大值是不是 255。3.2 构建一个带增强的肝脏分割 Dataset下面是一个可以直接用的 PyTorch Dataset包含基础的几何增强和归一化import os import numpy as np import torch from torch.utils.data import Dataset from PIL import Image import random class Liver2DDataset(Dataset): def __init__(self, root_dir, splittrain, augmentTrue): self.img_dir os.path.join(root_dir, split, images) self.mask_dir os.path.join(root_dir, split, masks) self.filenames sorted(os.listdir(self.img_dir)) self.augment augment and split train def __len__(self): return len(self.filenames) def __getitem__(self, idx): fname self.filenames[idx] img np.array(Image.open(os.path.join(self.img_dir, fname)).convert(L)) mask np.array(Image.open(os.path.join(self.mask_dir, fname)).convert(L)) # 归一化到[0,1] img img.astype(np.float32) / 255.0 # 掩膜二值化前景为1背景为0 mask (mask 127).astype(np.float32) # 训练时做随机水平翻转和随机裁剪 if self.augment: if random.random() 0.5: img np.fliplr(img).copy() mask np.fliplr(mask).copy() # 转为Tensor增加通道维度 [1, H, W] img torch.from_numpy(img).unsqueeze(0) mask torch.from_numpy(mask).unsqueeze(0) return img, mask逻辑说明convert(L)确保读进来是单通道灰度图避免某些 PNG 带 alpha 通道导致维度对不上。掩膜用 127做二值化兼容 0/255 和 0/1 两种标签格式。增强只做水平翻转因为垂直翻转会破坏肝脏的解剖位置先验——肝脏永远在腹腔右侧翻转后模型学到的位置信息就乱了。参数说明augment参数在测试集上自动关闭。如果你要做多分类肝脏肿瘤把mask 127改成保留原始标签值并在损失函数里用CrossEntropyLoss替代BCEWithLogitsLoss。unsqueeze(0)增加的通道维度是必须的PyTorch 的卷积层要求输入是[N, C, H, W]。3.3 训练集和测试集的加载器配置差异训练集和测试集的 DataLoader 配置有三个关键差异很多人在这里踩坑配置项训练集测试集原因shuffleTrueFalse测试集不需要打乱保持顺序便于结果复现augment开启关闭测试时增强会引入随机性导致指标不可复现batch_size8-161-4测试集可以用小 batch 甚至单张推理节省显存num_workers4-82-4训练集需要更多 worker 预取数据from torch.utils.data import DataLoader train_dataset Liver2DDataset(./liver_2d, splittrain, augmentTrue) test_dataset Liver2DDataset(./liver_2d, splittest, augmentFalse) train_loader DataLoader(train_dataset, batch_size8, shuffleTrue, num_workers4, pin_memoryTrue) test_loader DataLoader(test_dataset, batch_size1, shuffleFalse, num_workers2)pin_memoryTrue在训练时能把数据提前锁到页锁定内存加速 GPU 传输测试时数据量小可以不开。num_workers不是越大越好设成 CPU 核心数的 1/4 到 1/2 比较稳设太大反而会因为进程调度开销拖慢速度。4. 肝脏分割模型训练损失函数、指标与显存控制4.1 肝脏分割为什么不能只用交叉熵肝脏在腹部 CT 切片里的面积占比通常只有 5% 到 15%肿瘤区域更小可能不到 1%。这种极端的前景-背景不平衡下交叉熵损失会被背景像素主导模型只要全预测背景就能拿到 85% 以上的准确率但 Dice 接近 0。标准做法是 Dice Loss 和 BCE Loss 组合import torch import torch.nn as nn class DiceBCELoss(nn.Module): def __init__(self, bce_weight0.5): super().__init__() self.bce_weight bce_weight self.bce nn.BCEWithLogitsLoss() def forward(self, pred, target): # pred: [N, 1, H, W] logits, target: [N, 1, H, W] 0/1 bce_loss self.bce(pred, target) pred_sigmoid torch.sigmoid(pred) intersection (pred_sigmoid * target).sum(dim(2, 3)) union pred_sigmoid.sum(dim(2, 3)) target.sum(dim(2, 3)) dice (2.0 * intersection 1e-6) / (union 1e-6) dice_loss 1 - dice.mean() return self.bce_weight * bce_loss (1 - self.bce_weight) * dice_loss逻辑说明BCE 提供逐像素的稳定梯度Dice 直接优化分割重叠度。1e-6是平滑项防止前景为空时除零。bce_weight0.5是常用的起点如果训练初期 loss 震荡厉害可以调到 0.7 让 BCE 主导如果 Dice 涨得太慢调到 0.3 让 Dice 主导。参数说明pred是模型输出的 logits不要提前做 sigmoid因为BCEWithLogitsLoss内部会做数值稳定的 sigmoid。target必须是 float 类型如果从 DataLoader 出来是 long加一句.float()。4.2 用 Dice 和 IoU 验证测试集上的真实表现训练 loss 下降不代表分割效果好必须用 Dice 和 IoU 在测试集上验证。这两个指标的计算方式不同Dice 对前景更敏感IoU 更严格def compute_metrics(pred_logits, target, threshold0.5): 计算Dice和IoU输入为logits和0/1标签 pred (torch.sigmoid(pred_logits) threshold).float() target target.float() intersection (pred * target).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target.sum(dim(2, 3)) dice (2.0 * intersection 1e-6) / (union 1e-6) iou (intersection 1e-6) / (union - intersection 1e-6) return dice.mean().item(), iou.mean().item()逻辑说明threshold0.5是默认的二值化阈值如果发现模型对肝脏边缘预测偏保守漏检多可以降到 0.4如果误检多升到 0.6。union - intersection就是并集减去交集得到的是假阳性加假阴性的像素数。参数说明这个函数返回的是 batch 内的平均值。如果你要报告每个病例的指标需要先按病例聚合切片结果再计算病例级别的 Dice。切片级别 Dice 通常比病例级别高 3 到 8 个点论文里报告哪个要写清楚。4.3 8GB 显存跑肝脏 2D 分割的参数配置显存不够是新手最常遇到的问题。下面是一组在 8GB 卡上验证过的配置输入尺寸 256×256U-Net 结构参数值说明输入尺寸256×256肝脏分割够用512 会翻倍显存batch_size88GB 卡的安全值16 会 OOM优化器Adamlr1e-3比 SGD 收敛快学习率调度CosineAnnealingT_max50从 1e-3 降到 1e-6混合精度amp开启后显存降 30%速度提 20%训练轮数50-80肝脏 2D 数据通常 50 轮收敛混合精度训练的关键代码from torch.cuda.amp import autocast, GradScaler scaler GradScaler() for epoch in range(50): model.train() for img, mask in train_loader: img, mask img.cuda(), mask.cuda() optimizer.zero_grad() with autocast(): pred model(img) loss criterion(pred, mask) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()autocast自动把部分运算转成 float16GradScaler防止梯度下溢。注意 loss 计算要在autocast上下文里但scaler.scale(loss).backward()要在外面。如果训练中出现 loss 变成 NaN先把混合精度关掉排查确认模型结构没问题再开。5. 肝脏 2D 分割避坑五个让指标虚高或翻车的真实问题5.1 测试集 Dice 0.95 但推理结果全是噪点现象训练日志里测试集 Dice 稳定在 0.93 以上但拿单张图推理可视化掩膜边缘全是散点肝脏内部还有空洞。原因数据泄漏。训练集和测试集里存在同一病例的相邻切片模型记住了这个病例的纹理特征换一个新病例就崩。另一个可能是掩膜在保存时用了 JPEG 有损压缩边缘像素值被插值污染二值化后产生噪点。解决用 2.3 节的脚本检查病例重叠。如果确认泄漏按病例重新划分。如果是掩膜压缩问题检查文件格式PNG 必须是无损的掩膜不要存成 JPG。已经存成 JPG 的重新从原始标注导出。5.2 训练 loss 震荡剧烈Dice 不升反降现象前 10 个 epoch loss 从 0.8 降到 0.3然后突然跳到 0.9Dice 从 0.7 掉到 0.4反复震荡。原因学习率太大或者 batch_size 太小导致梯度估计方差大。肝脏 2D 数据如果只有几百张切片batch_size8 时每个 batch 的样本差异很大梯度方向不稳定。解决先把学习率从 1e-3 降到 3e-4观察 5 个 epoch。如果还震荡把 batch_size 提到 16配合梯度累积或者换用 AdamW 加 weight_decay1e-4。另外检查数据增强是不是太激进水平翻转对肝脏是安全的但如果你加了随机旋转 30 度肝脏位置先验被破坏模型会学得很挣扎。5.3 肝脏边缘分割总是糊的Dice 卡在 0.85 上不去现象肝脏内部区域分割很准但靠近腹壁和膈肌的边缘总是多出一圈或少一块Dice 在 0.85 附近徘徊。原因CT 图像里肝脏与周围肌肉、膈肌的灰度差异很小窗宽窗位设置不合适时边缘对比度更低。另外U-Net 的下采样会丢失边缘细节如果只用最后一层输出边缘精度天然受限。解决把窗宽从 400 收窄到 300窗位从 40 调到 50增强肝脏实质与周围组织的对比。模型侧加一个深监督在 U-Net 的每个解码层都接一个 1×1 卷积输出辅助预测训练时把辅助 loss 加权 0.3 加进总 loss。这个改动通常能把边缘 Dice 提 2 到 4 个点。5.4 测试集指标比训练集低 15 个点现象训练集 Dice 0.92测试集只有 0.77差距远超正常过拟合范围。原因训练集和测试集的病例来源不同。比如训练集来自一台 CT 设备测试集来自另一台层厚、重建核、噪声水平都不一样。或者训练集里肝脏肿瘤病例多测试集里正常肝脏多类别分布偏移。解决先做分布对比统计两个集合的切片数量、前景像素占比、图像均值方差。如果差异大考虑在训练时加入更强的颜色抖动和噪声增强提升模型对设备差异的鲁棒性。如果测试集里正常肝脏多检查一下测试集里是不是有大量没有肿瘤的切片这些切片的前景只有肝脏Dice 计算方式要确认是否合理。5.5 推理时单张图显存够但批量推理 OOM现象训练时 batch_size8 没问题推理时想一次跑 32 张直接显存溢出。原因推理时没有torch.no_grad()PyTorch 默认会构建计算图显存占用和训练时一样甚至更高。另外如果模型里用了 BatchNorm推理时 batch 太大会累积统计量虽然不报错但结果会偏。解决推理代码必须包在with torch.no_grad():里并且调用model.eval()把 BatchNorm 和 Dropout 切到推理模式。批量推理的 batch_size 可以设成训练时的 2 到 4 倍因为不需要存梯度。如果还 OOM用torch.cuda.empty_cache()在每个 batch 后清理缓存。6. 把肝脏 2D 分割推到可用从 Dice 到临床可解释的最后一公里模型跑通、Dice 到 0.9 之后真正决定这个方案能不能落地的是你能不能解释模型为什么这么分割以及遇到失败案例时知道往哪调。我一般会做两件事一是把测试集里 Dice 最低的 10 个病例挑出来逐张可视化原图、金标准掩膜和预测掩膜的三联图二是统计这些失败案例的共性——是肿瘤靠近肝脏边缘导致漏检还是增强扫描期相不同导致灰度分布偏移。一个很实用的技巧是给预测结果叠加概率热力图。不要只输出二值掩膜把 sigmoid 后的概率图用伪彩色叠在原图上你能直观看到模型在边缘区域的置信度分布。如果边缘概率在 0.4 到 0.6 之间大面积徘徊说明模型对边界不确定这时候调阈值没用得回去检查训练数据里边缘标注是否一致。import matplotlib.pyplot as plt def visualize_prediction(img, mask, pred_prob, save_path): 三联图原图、金标准、预测概率热力图 fig, axes plt.subplots(1, 3, figsize(12, 4)) axes[0].imshow(img, cmapgray) axes[0].set_title(CT Slice) axes[1].imshow(mask, cmapgray) axes[1].set_title(Ground Truth) axes[2].imshow(img, cmapgray) axes[2].imshow(pred_prob, cmapjet, alpha0.5) axes[2].set_title(Prediction Overlay) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(save_path, dpi150) plt.close()这个可视化脚本我每次跑完新模型都会用它帮我发现过一个很隐蔽的问题模型在肝脏顶部靠近膈肌的位置总是过分割原因是训练集里这个区域的标注包含了部分膈肌组织模型把膈肌的低密度纹理当成了肝脏的一部分。后来把标注规范里膈肌区域重新界定Dice 没涨多少但临床医生看结果时不再挑刺了。另一个习惯是固定随机种子。医学图像分割的对比实验如果种子不固定同一份代码跑两次 Dice 能差 1 到 2 个点你根本分不清是改动有效还是随机波动。我一般在训练脚本开头写死torch.manual_seed(42)、np.random.seed(42)、random.seed(42)并且在 DataLoader 的worker_init_fn里也设种子确保多进程加载的增强序列可复现。最后说一个判断数据集值不值得投入的标准看它有没有提供病例级别的划分说明。如果只有切片级别的 train/test 目录但没有任何文档说明切分依据你花在验证数据泄漏上的时间可能比训练模型还多。遇到这种数据集先跑 2.3 节的病例重叠检查确认干净再投入。肝脏 2D 分割本身不难难的是数据这一层的坑把数据验证做扎实后面的事都是顺的。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?