ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

腹部多脏器MRI分割数据集实战:从数据检查到训练避坑

腹部多脏器MRI分割数据集实战:从数据检查到训练避坑 简介这份资源面向医学影像分割方向的深度学习学习者与研究者提供MRI背景下的腹部多器官分割数据集可用于细粒度分割任务的训练与验证。数据图像分辨率为512×512jpg格式原图搭配png格式mask标签覆盖主动脉、胆囊、脾、左肾、右肾、肝、胰腺、胃等8类器官具体类别可在classes文本中核对。压缩包共约2000个文件以974个jpg图像、1024个png掩膜为主另含1个txt标签说明与1个py可视化脚本整体约37.54MB运行show脚本即可查看gt在images上的掩膜叠加效果图像经过对比度变换后更清晰。数据集划分为约1024张训练集与256张验证集目录结构清晰便于直接接入分割网络训练流程。目前已有183人学习下载适合希望快速开展腹部多脏器分割实验、验证模型效果或进行算法对比的读者参考使用。1. 从一张 512×512 的 MRI 切片说起这套腹部多脏器分割数据集到底能干什么如果你正在找一份能直接跑通、不用自己从 DICOM 开始清洗的腹部器官分割数据这套 MRI 数据集值得先看一眼。它把 1024 张左右的训练图像和 256 张左右的验证图像整理成了 jpg png 的配对形式图像统一 512×512mask 是单通道 png配套一个 classes 文本说明 8 类标签。场景很明确做腹部多脏器语义分割尤其是肝、脾、肾、胰腺、胆囊、主动脉、胃这些结构。适合两类人——一类是刚接触医学图像分割、想找一个规模适中、格式友好的数据集练手另一类是已经在做 MRI 器官分割、需要一个能快速验证网络结构或损失函数的基准集。它不解决数据标注质量问题也不承诺跨中心泛化但作为入门到中阶的实战素材省掉了最耗时的格式转换和掩膜对齐环节。2. 数据组织与标签体系先搞清楚 8 类标签和文件配对逻辑2.1 目录结构与文件命名规律拿到压缩包后第一件事不是急着写 DataLoader而是把目录结构看清楚。这套数据典型组织方式是训练集和验证集分开图像和 mask 各自成目录文件名一一对应。从项目正文给出的样例文件名能看出命名规律case0033_slice068.jpg这种形式case编号代表不同病例slice编号代表该病例下的不同切片。mask 文件同名但扩展名为 png放在对应 mask 目录下。常见做法是保持这种扁平结构不要按病例再分子目录否则写 Dataset 时路径拼接会多一层。我一般会先跑一段脚本统计文件数量和配对情况确认没有孤儿文件。import os from pathlib import Path img_dir Path(data/train/images) mask_dir Path(data/train/masks) imgs sorted([f.stem for f in img_dir.glob(*.jpg)]) masks sorted([f.stem for f in mask_dir.glob(*.png)]) print(f图像数量: {len(imgs)}) print(f掩膜数量: {len(masks)}) # 检查配对 img_set, mask_set set(imgs), set(masks) only_img img_set - mask_set only_mask mask_set - img_set print(f只有图像没有掩膜: {len(only_img)}) print(f只有掩膜没有图像: {len(only_mask)}) # 抽查前5个文件名 for name in imgs[:5]: print(name, -, (mask_dir / f{name}.png).exists())这段脚本做三件事统计图像和掩膜总数、找出不配对的文件、抽查前几个文件确认命名一致。参数上注意glob的扩展名要和实际一致有些数据集图像是 jpg 但 mask 是 png这里已经按实际情况区分。如果输出里only_img或only_mask不为零说明数据整理有问题需要先解决再往下走。2.2 classes 文件与 8 类标签的对应关系项目摘要里列了 8 类主动脉、胆囊、脾、左肾、右肾、肝、胰腺、胃。注意摘要里“脾”出现了两次这大概率是笔误实际以 classes 文本为准。常见做法是打开 classes.txt 逐行确认行号从 0 或 1 开始决定了你在 mask 里读到的像素值对应哪个器官。这里有个血泪经验很多医学分割数据集的 mask 像素值不是连续的 0-7而是跳着的比如 0 是背景1 是肝2 是脾5 是肾。如果你直接按mask class_id去取很容易漏掉或错位。我一般会先统计 mask 里出现的唯一像素值。import numpy as np from PIL import Image from pathlib import Path mask_dir Path(data/train/masks) sample_masks list(mask_dir.glob(*.png))[:20] unique_vals set() for mp in sample_masks: arr np.array(Image.open(mp)) unique_vals.update(np.unique(arr).tolist()) print(mask 中出现的像素值:, sorted(unique_vals))如果输出是[0, 1, 2, 3, 4, 5, 6, 7, 8]这种说明有 8 个前景类加背景如果缺了某个值就要去 classes 文件里核对是不是该类在采样中没出现还是标签本身就不连续。参数上sample_masks取前 20 张足够看出分布不用全量跑。这一步做完你才能确定后续损失函数里num_classes到底设 9 还是别的数。2.3 训练集与验证集的划分逻辑摘要写的是训练集 1024 张左右、验证集 256 张左右比例大致 4:1。这个划分如果是按切片随机分的要注意同一个病例的不同切片可能同时出现在训练和验证里导致验证指标虚高。常见做法是尽量按病例划分但这份数据已经分好了目录你只能接受现有划分。如果要做更严格的评估可以自己按 case 编号重新分。我一般会先统计训练集和验证集里各有多少个不同的 case判断是否存在病例重叠。import re from pathlib import Path from collections import Counter def count_cases(img_dir): cases [] for f in Path(img_dir).glob(*.jpg): m re.match(r(case\d), f.stem) if m: cases.append(m.group(1)) return Counter(cases) train_cases count_cases(data/train/images) val_cases count_cases(data/val/images) print(训练集病例数:, len(train_cases)) print(验证集病例数:, len(val_cases)) print(训练集切片数 top5:, train_cases.most_common(5)) print(验证集切片数 top5:, val_cases.most_common(5)) overlap set(train_cases) set(val_cases) print(训练验证重叠病例:, overlap)这段代码用正则从文件名提取 case 编号统计每个病例的切片数并检查训练验证是否有重叠。如果overlap非空说明验证集里有些病例的切片在训练集里也出现过评估时心里要有数。参数上正则(case\d)假设文件名以 case 开头如果实际命名不同需要调整。3. 从 jpg/png 到训练张量Dataset 与增强的落地写法3.1 自定义 Dataset 的完整实现PyTorch 里写一个适配这套数据的 Dataset 并不复杂关键是把图像和 mask 的路径配对、同步做增强、把 mask 的像素值映射到 0 到 num_classes-1。下面是一个可以直接抄的版本。import torch from torch.utils.data import Dataset import numpy as np from PIL import Image from pathlib import Path class AbdominalMRIDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone, num_classes9): self.img_dir Path(img_dir) self.mask_dir Path(mask_dir) self.transform transform self.num_classes num_classes self.names sorted([f.stem for f in self.img_dir.glob(*.jpg)]) def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img_path self.img_dir / f{name}.jpg mask_path self.mask_dir / f{name}.png image np.array(Image.open(img_path).convert(RGB)) mask np.array(Image.open(mask_path)) # 如果 mask 像素值不连续这里需要做映射 # 假设 classes 文件里定义的 id 就是像素值且连续 mask mask.astype(np.int64) if self.transform: augmented self.transform(imageimage, maskmask) image augmented[image] mask augmented[mask] # 图像归一化到 [0,1] 并转 CHW if isinstance(image, np.ndarray): image torch.from_numpy(image).permute(2, 0, 1).float() / 255.0 if isinstance(mask, np.ndarray): mask torch.from_numpy(mask).long() return image, mask逻辑说明__init__里用sorted保证图像和 mask 顺序一致num_classes默认 9 是背景加 8 类。__getitem__里先读图像和 mask如果用了 albumentations 做增强注意图像和 mask 要同步变换。最后图像转成 float 并归一化mask 保持 long 类型供交叉熵使用。参数上num_classes要根据 classes 文件实际类别数调整如果 mask 像素值不连续需要在mask mask.astype(np.int64)之前加映射表。3.2 增强策略对比度变换与几何变换的取舍摘要提到“数据经过对比度变换图像更清晰”说明原始数据已经做过一定预处理。你在训练时再加增强要注意别把对比度拉过头否则 MRI 的灰度层次会丢失。常见做法是几何变换为主颜色变换为辅。import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.Resize(512, 512), A.HorizontalFlip(p0.5), A.RandomRotate90(p0.5), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit15, p0.5), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), A.Normalize(mean(0.5, 0.5, 0.5), std(0.5, 0.5, 0.5)), ToTensorV2(), ]) val_transform A.Compose([ A.Resize(512, 512), A.Normalize(mean(0.5, 0.5, 0.5), std(0.5, 0.5, 0.5)), ToTensorV2(), ])参数说明HorizontalFlip对腹部器官分割通常安全因为左右翻转后器官位置虽然变了但解剖结构合理RandomRotate90要谨慎MRI 切片方向固定旋转 90 度可能产生不合理的解剖方向我一般只在小角度旋转里用ShiftScaleRotate。RandomBrightnessContrast的幅度控制在 0.1 以内避免破坏已经做过的对比度变换。Normalize的均值和标准差用 0.5 是常见做法如果数据分布偏暗可以改成实际统计值。3.3 损失函数与类别不平衡处理腹部器官分割里肝、脾、肾体积大胆囊、胰腺、主动脉体积小类别不平衡很明显。常见做法是交叉熵加 Dice 损失组合或者用带权重的交叉熵。import torch.nn as nn import torch.nn.functional as F class DiceLoss(nn.Module): def __init__(self, smooth1e-6): super().__init__() self.smooth smooth def forward(self, logits, targets): probs F.softmax(logits, dim1) targets_onehot F.one_hot(targets, num_classeslogits.shape[1]) targets_onehot targets_onehot.permute(0, 3, 1, 2).float() intersection (probs * targets_onehot).sum(dim(0, 2, 3)) union probs.sum(dim(0, 2, 3)) targets_onehot.sum(dim(0, 2, 3)) dice (2 * intersection self.smooth) / (union self.smooth) return 1 - dice.mean() class CombinedLoss(nn.Module): def __init__(self, weight_ce0.5, weight_dice0.5): super().__init__() self.ce nn.CrossEntropyLoss() self.dice DiceLoss() self.weight_ce weight_ce self.weight_dice weight_dice def forward(self, logits, targets): return self.weight_ce * self.ce(logits, targets) self.weight_dice * self.dice(logits, targets)逻辑说明DiceLoss 对每个类别单独算 Dice 系数再平均能缓解小器官被大器官淹没的问题。CombinedLoss 把交叉熵和 Dice 按权重相加weight_ce和weight_dice默认各 0.5如果小器官分割效果差可以把 Dice 权重提到 0.6 到 0.7。参数上smooth防止除零num_classes要和模型输出通道一致。4. 训练与验证从 show 脚本到指标监控的完整链路4.1 用 show 脚本先做可视化验证摘要提到“运行 show 脚本即可查看 gt 在 images 上的掩膜结果”这一步千万别跳过。在写训练代码之前先用 show 脚本把图像和 mask 叠在一起看几例确认标签对齐、类别颜色正确、没有整体偏移。常见做法是随机抽 5 到 10 张把 mask 以半透明红色叠在灰度图上。import numpy as np import matplotlib.pyplot as plt from PIL import Image from pathlib import Path def show_overlay(img_path, mask_path, alpha0.4): img np.array(Image.open(img_path).convert(RGB)) mask np.array(Image.open(mask_path)) # 把 mask 中非零区域涂红 overlay img.copy() overlay[mask 0] [255, 0, 0] blended (img * (1 - alpha) overlay * alpha).astype(np.uint8) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img) axes[0].set_title(原图) axes[1].imshow(mask, cmapjet) axes[1].set_title(mask) axes[2].imshow(blended) axes[2].set_title(叠加) for ax in axes: ax.axis(off) plt.show() img_dir Path(data/train/images) mask_dir Path(data/train/masks) for name in sorted([f.stem for f in img_dir.glob(*.jpg)])[:3]: show_overlay(img_dir / f{name}.jpg, mask_dir / f{name}.png)这段代码把原图、mask 和叠加结果并排显示。如果叠加后发现 mask 整体偏移或翻转说明图像和 mask 在预处理时没有同步需要检查 show 脚本或自己的读取逻辑。参数上alpha控制叠加透明度0.4 左右比较清楚。4.2 训练循环与验证指标训练循环本身是标准写法但验证指标要选对。腹部器官分割常用 Dice 系数和 IoU按类别算再平均。下面是一个简化的训练加验证框架。import torch from torch.utils.data import DataLoader from tqdm import tqdm def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for images, masks in tqdm(loader, desctrain): images, masks images.to(device), masks.to(device) optimizer.zero_grad() logits model(images) loss criterion(logits, masks) loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) torch.no_grad() def validate(model, loader, criterion, device, num_classes9): model.eval() total_loss 0 dice_sum torch.zeros(num_classes).to(device) count torch.zeros(num_classes).to(device) for images, masks in tqdm(loader, descval): images, masks images.to(device), masks.to(device) logits model(images) loss criterion(logits, masks) total_loss loss.item() preds logits.argmax(dim1) for c in range(num_classes): pred_c (preds c) mask_c (masks c) inter (pred_c mask_c).sum().float() union pred_c.sum().float() mask_c.sum().float() if union 0: dice_sum[c] 2 * inter / (union 1e-6) count[c] 1 dice_per_class dice_sum / count.clamp(min1) return total_loss / len(loader), dice_per_class逻辑说明训练循环里标准的前向、反向、优化三步。验证时按类别算 Dicecount记录每个类别在验证集中出现的批次数避免某些类别没出现时除零。参数上num_classes要和模型输出一致device根据是否有 GPU 设置。如果某些类别 Dice 一直很低回去检查该类的 mask 像素值是否被正确映射。4.3 学习率与优化器的选择医学图像分割里 Adam 和 SGD 都有人用Adam 收敛快但最终精度可能略低SGD 加动量调好了泛化更好。我一般先用 Adam学习率 1e-4配合余弦退火。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR model model.to(device) optimizer optim.Adam(model.parameters(), lr1e-4, weight_decay1e-5) scheduler CosineAnnealingLR(optimizer, T_max50, eta_min1e-6) criterion CombinedLoss(weight_ce0.5, weight_dice0.5).to(device) for epoch in range(50): train_loss train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, dice_per_class validate(model, val_loader, criterion, device) scheduler.step() print(fepoch {epoch}, train_loss {train_loss:.4f}, val_loss {val_loss:.4f}) print(dice per class:, dice_per_class.cpu().numpy().round(3))参数说明lr1e-4是 Adam 的常用起点如果 loss 震荡可以降到 5e-5weight_decay1e-5轻微正则化T_max50和总 epoch 数一致eta_min1e-6是学习率下限。每轮打印各类 Dice观察小器官是否在提升。5. 避坑与排查这套数据最容易翻车的五个地方5.1 现象mask 像素值不连续类别对不上原因classes 文件里定义的类别 id 和 mask 实际像素值不一致或者 mask 经过了调色板压缩。解决先跑 2.2 节的唯一值统计脚本把实际像素值列出来再和 classes 文件逐行对照。如果发现 mask 是调色板模式用Image.open(mask_path).convert(L)转成灰度再读。5.2 现象训练 loss 正常但验证 Dice 极低原因训练集和验证集的图像分布差异大或者验证集 mask 读取时用了错误的插值方式。解决检查验证集 transform 里有没有误加随机增强mask 的 resize 必须用最近邻插值。albumentations 里A.Resize对 mask 默认就是最近邻但如果自己写 PIL resize要指定Image.NEAREST。5.3 现象某些器官 Dice 始终为 0原因该器官在验证集中出现次数极少或者 mask 中该类的像素值被映射到了背景。解决统计验证集里每个类别的像素占比如果某类占比低于 0.1%考虑在损失里给它更高权重或者接受该指标波动大。同时确认 mask 映射表没有把该类归到 0。5.4 现象显存溢出batch size 上不去原因512×512 的图像加 9 类输出如果模型解码器通道数大显存占用高。解决先把 batch size 降到 2 或 4用梯度累积模拟大 batch或者把图像随机裁剪到 384×384 训练验证时再恢复 512。混合精度训练也能省不少显存。5.5 现象show 脚本显示的 mask 和训练时读到的 mask 不一致原因show 脚本可能用了convert(P)或直接读调色板而训练时用了convert(L)两者像素值不同。解决统一读取方式训练和可视化都用同一种模式。我一般统一用np.array(Image.open(path))不加 convert先看原始像素值再决定。6. 进阶技巧用病例分组交叉验证压榨这份数据的泛化信息这份数据已经分好了训练和验证但如果你想更严谨地评估模型可以自己按 case 做分组交叉验证。具体做法是把所有 case 编号收集起来随机分成 5 折每折里用 4 折的 case 做训练、1 折做验证。这样能避免同一病例的切片同时出现在训练和验证里指标更接近真实泛化能力。import re import numpy as np from pathlib import Path from sklearn.model_selection import GroupKFold all_imgs sorted(Path(data/train/images).glob(*.jpg)) \ sorted(Path(data/val/images).glob(*.jpg)) names [f.stem for f in all_imgs] groups [re.match(r(case\d), n).group(1) for n in names] groups np.array(groups) gkf GroupKFold(n_splits5) for fold, (train_idx, val_idx) in enumerate(gkf.split(names, groupsgroups)): train_names [names[i] for i in train_idx] val_names [names[i] for i in val_idx] print(ffold {fold}: train {len(train_names)}, val {len(val_names)}, ftrain cases {len(set(groups[train_idx]))}, val cases {len(set(groups[val_idx]))})这段代码用GroupKFold按 case 分组保证同一病例的切片不会跨折。参数上n_splits5可以改成 3 或 10取决于数据量和计算资源。跑完五折后把每折的 Dice 平均得到的指标比单次划分更可信。另一个技巧是测试时增强TTA。对验证集图像做水平翻转、小角度旋转分别预测后再把概率图平均通常能涨 1 到 2 个 Dice 点。实现上就是在 validate 函数里对每张图跑多次前向把 softmax 后的概率累加再取 argmax。注意 TTA 只适合几何变换对比度变换不要加否则概率分布会偏。torch.no_grad() def validate_tta(model, loader, device, num_classes9): model.eval() dice_sum torch.zeros(num_classes).to(device) count torch.zeros(num_classes).to(device) for images, masks in loader: images, masks images.to(device), masks.to(device) probs torch.zeros_like(model(images)) # 原始 probs torch.softmax(model(images), dim1) # 水平翻转 probs torch.softmax(model(torch.flip(images, dims[3])), dim1).flip(dims[3]) # 小角度旋转可以用 kornia 或手动实现这里省略 preds probs.argmax(dim1) for c in range(num_classes): pred_c (preds c) mask_c (masks c) inter (pred_c mask_c).sum().float() union pred_c.sum().float() mask_c.sum().float() if union 0: dice_sum[c] 2 * inter / (union 1e-6) count[c] 1 return dice_sum / count.clamp(min1)逻辑说明TTA 把原始预测和翻转后的预测概率相加翻转后的预测要再翻回来才能对齐。参数上只加了水平翻转旋转和缩放可以按需加但每加一种 TTA 推理时间就翻倍。我一般只在最终评估时用 TTA训练过程中不用。从那以后我每次拿到新的医学分割数据都强制先跑一遍文件配对检查和 mask 唯一值统计再动手写模型。这两个脚本花不了五分钟但能省掉后面几小时的排查。希望帮到你。本文还有配套的精品资源点击获取
返回列表