AMOS多器官切片数据集:CT/MR多模态分割实战与避坑指南
简介本资源面向医学图像分割方向的算法学习者与研究者提供大型CT、MR多器官切片数据集AMOS用于训练与验证2D分割模型。数据分别沿轴位面、冠状面、矢状面切分并剔除ROI区域不足1%的切片同时做了Windowing增强mask涵盖脾脏、左肾上腺、胃等16个类别0为背景。压缩包共约2000个文件以1998张png图像与对应mask为主另含1个txt说明和1个py可视化脚本整体约623MBx、y、z三轴分别提供1772、18726、21887组图像与标签规模梯度明显便于按需选用。配套show.py可快速叠加查看mask效果降低数据检查成本。已有153人学习适合希望复现多器官分割流程、扩充实验数据或搭建切片级训练管线的读者参考使用。1. 医学图像分割数据AMOS 多器官切片到底解决了谁的燃眉之急如果你做过腹部 CT 或 MR 的多器官分割大概率经历过这样的场景手里只有肝脏标注想加个脾脏、胰腺、肾脏就得自己从头勾一遍一个病例少说两小时。更麻烦的是 CT 和 MR 的标注体系不统一模型训完 CT 换到 MR 上直接崩。AMOS 这个数据集就是冲着这个痛点来的——它把 CT 和 MR 两种模态、15 个腹部器官的体素级标注放在同一套标签体系下还额外提供了三个切面轴状位、冠状位、矢状位的切片数据和可视化代码。换句话说你拿到的不是一堆原始 NIfTI 文件让你自己切而是已经切好、标签对齐、能直接喂给 2D 分割网络的多器官数据。适合谁做腹部多器官分割的研究生、需要快速验证 2D/3D 分割 pipeline 的算法工程师、以及想拿多模态数据做域适应实验的人。低剂量 CT 图像去噪后接分割、AI 对 CT 超分辨率重建后接分割这些热方向都能拿 AMOS 当评测底座。2. 拆开 AMOS 的数据包三个切面、标签体系和可视化代码怎么用2.1 数据组织逻辑与标签映射AMOS 的原始数据是 3D 体数据但标题里明确说了“包含 3 个切面的切片数据”。这意味着数据提供方已经沿着三个解剖轴做了切片提取。轴状位切片是横断面冠状位是前后方向的切面矢状位是左右方向的切面。每个切面方向下切片按顺序编号存储标签文件与图像文件一一对应。标签体系是这套数据的核心价值。15 个器官的整数标签从 1 开始编号0 是背景。常见的映射关系是1-脾脏、2-右肾、3-左肾、4-胆囊、5-食管、6-肝脏、7-胃、8-主动脉、9-下腔静脉、10-胰腺、11-右肾上腺、12-左肾上腺、13-十二指肠、14-膀胱、15-前列腺/子宫。不同版本可能有微调拿到数据后第一件事就是确认标签映射表。提示CT 和 MR 的标签编号通常一致但 MR 的 T2 序列对某些器官如胰腺的对比度不如 CT训练时要注意模态差异。可视化代码一般是一个 Python 脚本用 matplotlib 或 ITK-SNAP 的 Python 接口做叠加显示。它的作用不是“好看”而是让你在训练前快速检查标签是否错位、切片顺序是否颠倒、某些切面是否缺失标签。2.2 加载切片与标签的最小代码假设数据目录结构是amos/slice_axial/images/和amos/slice_axial/labels/图像是 PNG 或 JPG标签是同名的 PNG。下面这段代码能让你在 30 秒内确认数据可用性。import os import numpy as np from PIL import Image import matplotlib.pyplot as plt # 数据根目录按实际路径修改 root amos/slice_axial img_dir os.path.join(root, images) lbl_dir os.path.join(root, labels) # 取第一张切片做检查 img_files sorted(os.listdir(img_dir)) lbl_files sorted(os.listdir(lbl_dir)) # 确认图像和标签文件名一一对应 assert len(img_files) len(lbl_files), 图像和标签数量不一致 for i, (img_f, lbl_f) in enumerate(zip(img_files, lbl_files)): # 去掉扩展名后比对防止 001.png vs 001_label.png 这种命名差异 if os.path.splitext(img_f)[0] ! os.path.splitext(lbl_f)[0]: print(f命名不匹配: {img_f} vs {lbl_f}) break else: print(命名检查通过) # 加载第一张 img np.array(Image.open(os.path.join(img_dir, img_files[0])).convert(L)) lbl np.array(Image.open(os.path.join(lbl_dir, lbl_files[0])).convert(L)) print(f图像尺寸: {img.shape}, 标签尺寸: {lbl.shape}) print(f标签唯一值: {np.unique(lbl)}) # 叠加显示 fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img, cmapgray) axes[0].set_title(CT/MR 切片) axes[1].imshow(lbl, cmapnipy_spectral) axes[1].set_title(多器官标签) axes[2].imshow(img, cmapgray) axes[2].imshow(lbl, cmapnipy_spectral, alpha0.4) axes[2].set_title(叠加效果) for ax in axes: ax.axis(off) plt.show()逻辑说明先做文件名一致性断言这是血泪经验——很多公开数据集的图像和标签命名规则不统一直接按索引读取会错位。convert(L)把图像转成单通道灰度标签也转灰度避免 RGB 通道干扰。np.unique(lbl)打印出来的值应该只包含 0 到 15 的整数如果出现 255 或其他值说明标签编码方式不同需要做映射。参数说明root指向切面目录如果你要换冠状位或矢状位只改这一行。alpha0.4控制叠加透明度标签颜色太深会盖住解剖结构。2.3 三个切面的选择策略轴状位切片数量最多通常用于 2D 分割网络的主训练集。冠状位和矢状位切片数量少但能提供不同方向的空间上下文。常见做法是轴状位训练主模型冠状位和矢状位做测试时增强TTA或者三个切面分别训一个 2D 模型再做投票融合。如果你做的是 3D 分割这三个切面的切片数据反而更适合做 2.5D 方案——取相邻三张轴状位切片堆成三通道输入比单张切片多了一点层间信息又不像完整 3D 卷积那么吃显存。3. 用 AMOS 切片训练 2D 多器官分割模型从 DataLoader 到损失函数3.1 构建 PyTorch Dataset 与增强管道拿到切片数据后最稳的起点是 nnU-Net 的 2D 配置但如果你想自己控制增强策略下面这个 Dataset 类可以直接抄。import torch from torch.utils.data import Dataset, DataLoader import albumentations as A from albumentations.pytorch import ToTensorV2 class AMOSSliceDataset(Dataset): def __init__(self, img_dir, lbl_dir, transformNone): self.img_dir img_dir self.lbl_dir lbl_dir self.transform transform self.img_files sorted(os.listdir(img_dir)) self.lbl_files sorted(os.listdir(lbl_dir)) assert len(self.img_files) len(self.lbl_files) def __len__(self): return len(self.img_files) def __getitem__(self, idx): img np.array(Image.open( os.path.join(self.img_dir, self.img_files[idx])).convert(L)) lbl np.array(Image.open( os.path.join(self.lbl_dir, self.lbl_files[idx])).convert(L)) # 标签值减 1把 1-15 映射到 0-14背景保持 0 会冲突 # 更稳妥的做法背景设为 255 或单独处理这里用 0 背景 1-15 器官 # 训练时 CrossEntropyLoss 的 ignore_index 设为 0 即可忽略背景 if self.transform: augmented self.transform(imageimg, masklbl) img augmented[image] lbl augmented[mask].long() return img, lbl # 训练增强随机缩放、旋转、弹性变形、亮度对比度扰动 train_transform A.Compose([ A.Resize(512, 512), A.ShiftScaleRotate(shift_limit0.1, scale_limit0.15, rotate_limit20, p0.5), A.ElasticTransform(alpha1, sigma50, p0.3), A.RandomBrightnessContrast(p0.3), A.Normalize(mean[0.5], std[0.5]), ToTensorV2() ]) # 验证增强只做 resize 和归一化 val_transform A.Compose([ A.Resize(512, 512), A.Normalize(mean[0.5], std[0.5]), ToTensorV2() ]) train_ds AMOSSliceDataset(amos/slice_axial/images, amos/slice_axial/labels, transformtrain_transform) train_loader DataLoader(train_ds, batch_size16, shuffleTrue, num_workers4, pin_memoryTrue)逻辑说明ShiftScaleRotate和ElasticTransform是腹部器官分割里最有效的两种增强因为器官形状在不同个体间差异大弹性变形能模拟这种形变。Normalize的均值和标准差用 0.5 是经验值如果你发现图像整体偏暗或偏亮先统计训练集的像素均值再改。参数说明Resize(512, 512)是显存和精度的折中24G 显存跑 512×512 的 batch size 16 没问题。num_workers4根据 CPU 核数调整设太大反而拖慢。pin_memoryTrue在 GPU 训练时能加速数据传输。3.2 损失函数选择与类别不平衡处理腹部器官分割的类别不平衡非常严重——肝脏可能占图像 15% 像素肾上腺可能不到 0.1%。纯 CrossEntropyLoss 会让模型偏向大器官。常见做法是 CrossEntropy 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): # logits: (B, C, H, W), targets: (B, H, W) num_classes logits.shape[1] probs F.softmax(logits, dim1) targets_onehot F.one_hot(targets, num_classes).permute(0, 3, 1, 2).float() # 忽略背景类只算前景器官的 Dice probs probs[:, 1:, :, :] targets_onehot targets_onehot[:, 1:, :, :] intersection (probs * targets_onehot).sum(dim(2, 3)) union probs.sum(dim(2, 3)) targets_onehot.sum(dim(2, 3)) dice (2. * intersection self.smooth) / (union self.smooth) return 1 - dice.mean() class CombinedLoss(nn.Module): def __init__(self, ce_weight0.5, dice_weight0.5): super().__init__() self.ce nn.CrossEntropyLoss(ignore_index0) # 忽略背景 self.dice DiceLoss() self.ce_weight ce_weight self.dice_weight dice_weight def forward(self, logits, targets): return (self.ce_weight * self.ce(logits, targets) self.dice_weight * self.dice(logits, targets))逻辑说明ignore_index0让 CrossEntropy 不计算背景像素的损失把梯度集中在前景器官上。DiceLoss 里也去掉了背景通道避免背景 Dice 接近 1 拉高整体分数、掩盖小器官的差表现。smooth1e-6防止分母为零。参数说明ce_weight和dice_weight各 0.5 是起点。如果发现小器官肾上腺、胆囊Dice 一直上不去把 dice_weight 提到 0.7。如果边界很毛糙把 ce_weight 提到 0.6。3.3 训练循环与验证指标device torch.device(cuda if torch.cuda.is_available() else cpu) model torchvision.models.segmentation.deeplabv3_resnet50( pretrainedTrue, num_classes16).to(device) # 15 器官 背景 optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) criterion CombinedLoss() for epoch in range(50): model.train() for imgs, lbls in train_loader: imgs, lbls imgs.to(device), lbls.to(device) optimizer.zero_grad() outputs model(imgs)[out] loss criterion(outputs, lbls) loss.backward() optimizer.step() scheduler.step() # 每 5 个 epoch 打印一次验证 Dice if (epoch 1) % 5 0: model.eval() dice_scores [] with torch.no_grad(): for imgs, lbls in val_loader: imgs, lbls imgs.to(device), lbls.to(device) outputs model(imgs)[out] preds outputs.argmax(dim1) # 逐器官算 Dice这里省略具体实现 print(fEpoch {epoch1}, Val Dice: {np.mean(dice_scores):.4f})逻辑说明DeepLabV3 的num_classes16对应 15 个器官加 1 个背景。ignore_index0已经在损失里忽略背景但验证时背景 Dice 也不该计入平均。CosineAnnealingLR比 StepLR 更平滑适合分割任务。参数说明lr1e-4是 AdamW 的常用起点如果 loss 震荡就降到 5e-5。weight_decay1e-5防止过拟合数据量小的时候可以提到 1e-4。4. 多模态 CT/MR 混训与低剂量场景下的避坑清单4.1 避坑一CT 和 MR 混训时模型直接崩现象把 CT 和 MR 切片混在一个 DataLoader 里训练前几个 epoch loss 正常下降之后突然飙升验证 Dice 掉到 0.1 以下。原因CT 的 HU 值范围是 -1000 到 3000MR 的像素强度是任意单位两者分布差异巨大。如果共用同一套归一化参数模型会学到“模态”而不是“器官”。解决在 Dataset 里加一个模态标记对 CT 和 MR 分别做 z-score 归一化。CT 先截断到 [-200, 300] 再归一化MR 按每个病例的均值和标准差归一化。更彻底的做法是加一个模态嵌入层或者用两个独立的 BatchNorm 分支。4.2 避坑二矢状位切片标签错位现象轴状位训练正常换到矢状位后某些切片标签完全对不上器官轮廓偏移几十个像素。原因矢状位切片的提取方向与轴状位不同如果数据提供方在生成切片时没有统一坐标系原点图像和标签可能沿某个轴翻转了。解决用可视化代码逐张检查前 10 张和后 10 张切片。如果发现翻转对标签做np.fliplr或np.flipud。更稳妥的做法是直接读原始 3D 体数据自己用nibabel或SimpleITK重新切确保图像和标签用同一个变换矩阵。4.3 避坑三低剂量 CT 图像噪声导致小器官漏检现象在低剂量 CT 切片上肾上腺、胆囊这些小器官的 Dice 比常规剂量低 15 到 20 个百分点。原因低剂量 CT 的量子噪声在软组织区域特别明显小器官的边界被噪声淹没。解决训练时加高斯噪声增强模拟低剂量条件。推理时先做非局部均值去噪或 BM3D再送进分割网络。如果做的是端到端方案可以在分割网络前加一个轻量去噪头用感知损失约束。4.4 避坑四可视化代码直接跑报错现象拿到可视化代码后运行报ModuleNotFoundError: No module named skimage或AttributeError: module matplotlib has no attribute imshow。原因可视化代码依赖的库版本和你的环境不一致或者代码里用了旧版 matplotlib 的 API。解决先pip install scikit-image matplotlib opencv-python如果还报错检查代码里是否有plt.imshow被覆盖。最省事的办法是用 ITK-SNAP 手动加载图像和标签虽然土但不会翻车。4.5 避坑五切片顺序与病例编号混淆现象训练集和验证集划分后验证 Dice 异常高但测试集一塌糊涂。原因切片数据是按病例顺序排列的如果直接按文件名随机划分同一个病例的不同切片会同时出现在训练集和验证集里造成数据泄漏。解决从文件名或目录结构中提取病例 ID按病例划分训练/验证/测试。如果数据提供方没有保留病例 ID那就按切片编号的连续性做块划分——前 80% 连续切片做训练后 20% 做验证。5. 用三个切面做 TTA 与多模态域适应的进阶技巧5.1 三切面测试时增强的具体实现轴状位模型训练完后直接拿冠状位和矢状位切片做推理Dice 通常会掉 5 到 10 个点。但如果你把三个切面的预测结果在 3D 空间里对齐再投票能找回大部分损失。具体做法是对每个体素取三个切面模型输出的 softmax 概率平均值再 argmax。def fuse_three_views(axial_logits, coronal_logits, sagittal_logits): 三个切面的 logits 需要在 3D 空间对齐后融合 这里假设已经重采样到同一坐标系 # softmax 转概率 axial_prob F.softmax(axial_logits, dim1) coronal_prob F.softmax(coronal_logits, dim1) sagittal_prob F.softmax(sagittal_logits, dim1) # 平均融合 fused_prob (axial_prob coronal_prob sagittal_prob) / 3.0 return fused_prob.argmax(dim1)逻辑说明三个切面的模型可以是同一个模型在不同切面数据上微调也可以是同一个模型直接推理。关键是 logits 要在同一 3D 坐标系下对齐否则融合无意义。参数说明如果某个切面的模型明显更弱可以调权重比如轴状位 0.5、冠状位 0.25、矢状位 0.25。5.2 从 CT 到 MR 的域适应微调策略AMOS 同时提供 CT 和 MR这天然适合做无监督域适应实验。常见做法是在 CT 上预训练然后在 MR 上只微调 BatchNorm 层和最后一层分类头其余层冻结。这样只需要少量 MR 标注就能达到不错的效果。# 冻结除 BatchNorm 和分类头之外的所有层 for name, param in model.named_parameters(): if bn not in name and classifier not in name: param.requires_grad False # 只优化未冻结的参数 optimizer torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr1e-4)逻辑说明BatchNorm 层统计的是域相关的均值和方差微调它们能让模型快速适应新模态的强度分布。分类头负责最终的器官判别也需要更新。参数说明lr1e-4比从头训练可以稍大因为只更新少量参数。如果 MR 数据量极少少于 10 个病例把 lr 降到 5e-5 防止过拟合。5.3 验证分割质量的三个硬指标Dice 只是起点。真正判断一个多器官分割模型能不能用还要看指标含义合格线平均 Dice所有器官 Dice 的均值大器官 0.90小器官 0.75HD9595% 豪斯多夫距离大器官 5mm小器官 10mm体积相似度预测体积与真实体积比值0.85 到 1.15 之间HD95 对边界敏感能暴露 Dice 看不出的毛刺和离群点。体积相似度能发现模型是否系统性地高估或低估某个器官。我自己的习惯是每次训完一个新模型先跑一遍三个切面的 TTA然后挑 Dice 最低的三个器官单独看可视化。十次里有八次问题出在胆囊和肾上腺上这两个器官的边界在 CT 上本来就不清晰模型学不好是常态。遇到这种情况与其调网络结构不如回去检查标签质量——很多时候是标注本身就有歧义。希望帮到你。本文还有配套的精品资源点击获取