
简介面向医学图像分割任务的海马体切片数据集适用于深度学习的语义分割模型训练与算法验证。数据源自左右海马体3D nii.gz文件已分别沿横截面(x)、冠状面(y)、矢状面(z)切分为2D图像并自动剔除前景区域占比不足3%的样本mask标注中1为前部海马体、2为后部海马体、0为背景。包内共2000个文件含1998张png格式的原图与对应标签图以及1个可视化脚本show.py和1个json配置文件整体压缩包约26.18MBx轴切片为35×51分辨率共4958对y轴为35×35分辨率共9465对z轴为51×35分辨率共4603对可直接划分训练集。show.py可随机抽取图片展示原始图像、GT标签及叠加效果方便快速检查数据质量。当前已有353人学习适合医学图像入门及分割方向研究者、开发者使用。1. 为什么是左右海马体切片分割从 3D nii.gz 到 2D PNG 的降维设计拿到一批左右海马体的 nii.gz 三维标注数据很多人的第一反应是直接上 3D U-Net。但实际操作中显存占用、预处理成本和标注校验会先卡住你单张 MRI 体数据动辄几百层直接把三维 patch 喂进网络batch size 稍微大一点就会爆显存。这套资源做了一个很实用的降维处理将 3D 海马体标注按横断面x、冠状面y、矢状面z切成 2D 切片并自动丢掉前景面积不足 3% 的纯背景帧最后导出成图像和标签均为 PNG 的医学图像分割数据集。它解决的是从 nii.gz 到可训练样本之间的脏活累活适合做 2D 分割模型验证、多切面对比实验或者在上 3D 模型之前先用 2D 网络快速跑通基线。2. 三个切面的目录结构与标签编码先看懂数据再写加载器这个数据集的目录不是常见的单张 image/mask 同级目录而是按切面轴各自组织。拿到压缩包后先看根目录通常会看到dataset.json、x.png、z.png以及hippocampus_164_18.png这类文件其中dataset.json是元数据入口。无论你是准备用 PyTorch 写 DataLoader还是直接丢给 nnU-Net 做训练第一步都要确认三件事图像和 mask 是否同名、每个切面目录下有多少样本、mask 里到底有几个类别。下面按这三件事来拆。2.1 文件命名、目录结构与 dataset.json摘要里写得很清楚x 轴 35×51 分辨率images 图片目录加 masks 模板目录4958 张图片和 4958 个对应的 masky 轴 35×359465 张z 轴 51×354603 张。也就是说每个轴都有独立的 images 和 masks 目录且图像文件名和 mask 文件名一一对应例如hippocampus_164_18.png同时出现在 images 和 masks 里。这种命名在医学影像切片中很常见前面是受试者或原始 3D 卷的 ID后面是切片序号hippocampus_164_18.png可以理解为编号 164 的 3D 卷第 18 层切片。先用一小段代码确认目录结构和 mask 形状import json from pathlib import Path from PIL import Image import numpy as np root Path(hippocampus_slices) for axis in [x, y, z]: images list((root / f{axis}_images).glob(*.png)) masks list((root / f{axis}_masks).glob(*.png)) print(axis, len(images), len(masks)) sample sorted(images)[0] mask_sample sorted(masks)[0] print(image, np.array(Image.open(sample)).shape, mask, np.array(Image.open(mask_sample)).shape)这段代码用glob匹配 PNG 文件sorted保证每次抽样顺序一致。代码输出的 shape 是(height, width)也就是 Pillow 读进来后 numpy 数组的行列顺序而摘要里说的 35×51 是宽×高所以 x 轴的 numpy 数组实际 shape 是(51, 35)。这个维度顺序在第 4 章写模型输入时一定要统一否则会出现张量维度对不上或结果整体转置的问题。如果dataset.json里放了文件列表或数据划分直接用json.load读出来看结构with open(root / dataset.json, r, encodingutf-8) as f: meta json.load(f) print(meta.keys())我一般会先打印keys再看里面是 train/val/test 划分还是 images/masks 文件列表。这个数据集的作者没有在摘要里说明 JSON 的具体字段所以不要假设字段名先打印再决定怎么往下写。2.2 mask 类别0 为背景1 为前部海马体2 为后部海马体标签文件不是二值图而是三类别语义分割。其中 mask 中 1 为前部海马体、2 为后部海马体、0 为背景。这意味着如果你的网络输出通道数为 3通道 0、1、2 正好对应背景、前部、后部。很多新手在读取 PNG 标签时会直接按 RGB 读成三通道导致标签维度变成(3, H, W)训练时和单通道图像对不上正确做法是用convert(L)读成单通道灰度图或者用cv2.imread(path, cv2.IMREAD_GRAYSCALE)。检查类别分布是否正常的代码unique, counts np.unique(mask, return_countsTrue) print(dict(zip(unique.tolist(), counts.tolist())))正常情况下unique里应该同时出现 0、1、2。如果某些切片只有 0 和 1说明该层刚好不包含后部海马体这是可以接受的但如果某个 axis 的所有 mask 都只有 0那就要检查数据解压是否完整或者原始的 nii.gz 标签是否在转换时丢失了前景。另外要注意标题里的“左右海马体”和 mask 中的“前部/后部”不是同一个维度的划分。原始 3D 标注可能同时包含左右海马体而导出的 2D mask 把分割目标定义成前部和后部海马体左右海马体不再用标签区分。如果你确实需要左/右海马体独立 mask必须回到原始 nii.gz 重新切或者用连通域分析把预测结果按左右脑拆开。这一点写论文时要说明否则评审会认为标签定义不一致。2.3 前景过滤为什么要丢掉前景不足 3% 的切片摘要里提到“自动去除了前景区域不足 3% 的数据”。处理逻辑很简单计算 mask 中类别 1 和类别 2 的像素总数除以整张 mask 像素数如果比例小于 0.03 就删除该切片。好处是训练时不会出现大量纯背景帧模型收敛更快也能缓解 Dice Loss 里背景占比过高带来的类别不平衡。不过这步过滤是有代价的。用这类数据集训练出来的模型在真实 3D 体积上推理时遇到海马体很小的切片会倾向于预测为背景因为训练分布里漏掉了那些低前景比例样本。我的经验是如果后续要拼回 3D 做完整分割最好自己在原始 nii.gz 上重新切一遍保留所有切片或者至少把前景过滤阈值从 0.03 放宽到 0.01让模型见过更多边界帧。数据集给的是“已经清洗过的版本”不是原始切片全集这一点要牢记。提示写论文或做实验记录时数据集章节需要写明过滤比例以及过滤前后的样本数量差异否则别人在同一个数据集上复现实验会对不上指标。3. 从标签文件到可视化show.py 拆解与两个增强改法作者在项目里提供了show.py作用是从 images 里随机挑一张图把原始图像、GT mask 和 GT 叠加图生成到当前目录。对于医学图像分割数据集这种可视化脚本的价值不只是“看一眼”它同时是数据质量检查工具。mask 边界是否贴合图像、类别颜色是否可区分、PNG 是否在解压时损坏都能一眼看出来。3.1 读取标签文件时的通道与数值检查先说读取。图像是灰度切片直接用convert(L)mask 虽然是 PNG但本质是索引标签后续要转成long喂给网络不能做归一化。读取和合法性检查from PIL import Image import numpy as np def load_slice(image_path, mask_path): img np.array(Image.open(image_path).convert(L)) mask np.array(Image.open(mask_path)) assert img.shape mask.shape, fshape mismatch: {img.shape} vs {mask.shape} assert mask.dtype np.uint8, fmask dtype is {mask.dtype}, expected uint8 unique np.unique(mask) assert set(unique.tolist()).issubset({0, 1, 2}), funexpected label: {unique} return img, mask这里的断言值得保留。下载类数据集经常出现尺寸不一致、mask 被误存成 RGB、标签数值漂移等问题用assert在加载阶段就把问题暴露出来能省下大量训练结束后才发现指标异常的排查时间。mask.dtype检查也很重要因为有些 PNG 会以 uint16 保存后面做one_hot或long()转换时可能出现隐式类型问题。3.2 三栏可视化与半透明叠加show.py的核心是三栏图左边是原始图像中间是 GT mask右边是 GT 叠加在原始图上的效果。右侧叠加需要把 mask 转成彩色三通道后再做 alpha 融合。用 matplotlib 实现import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt import numpy as np def show_slice(image_path, mask_path, save_pathvis.png): img, mask load_slice(image_path, mask_path) color_mask np.zeros((*mask.shape, 3), dtypenp.uint8) color_mask[mask 1] [255, 0, 0] # 前部海马体红色 color_mask[mask 2] [0, 255, 0] # 后部海马体绿色 fig, axes plt.subplots(1, 3, figsize(12, 4)) axes[0].imshow(img, cmapgray) axes[0].set_title(image) axes[1].imshow(mask, cmapgray) axes[1].set_title(GT) axes[2].imshow(img, cmapgray) axes[2].imshow(color_mask, alpha0.5) axes[2].set_title(overlay) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(save_path, dpi150) plt.close()由于 Pillow 读取的 mask 是(H, W)color_mask直接沿用同一个 shape 再扩展到 3 通道所以坐标是对齐的。alpha0.5是叠加透明度如果想更清楚地看 mask 边界可以降到 0.3如果想突出类别分布可以升到 0.7。这里有两个容易踩的坑一是color_mask的 dtype 必须是uint8不能是 float 类型否则叠加时 matplotlib 可能显示成空白二是figsize中宽度要足够三栏图如果宽度太窄细节会被压缩。3.3 批量生成对比图快速发现标注错位只看一张不够我一般会把一个 axis 下前几十张图全部生成缩略图拼成网格。这个操作对检查海马体是否总出现在图像中央、是否有切片发生左右翻转、mask 是否有整体偏移很有帮助。import os from math import ceil image_dir x_images mask_dir x_masks files sorted(os.listdir(image_dir))[:30] cols 6 rows ceil(len(files) / cols) fig, axes plt.subplots(rows, cols, figsize(cols * 3, rows * 3)) for ax, name in zip(axes.ravel(), files): img, mask load_slice( os.path.join(image_dir, name), os.path.join(mask_dir, name) ) overlay img.astype(float) overlay[mask 1] 255 ax.imshow(overlay, cmapgray) ax.axis(off) plt.tight_layout() plt.savefig(grid_check.png, dpi120)这里把 mask 为 1 的位置直接置成白色适合快速浏览整批数据的空间分布。如果 mask 为 2 的位置也需要突出可以再用红色通道画一层做法和show_slice里的color_mask一样。批量生成图还有一个好处发现个别样本的亮度范围与其他切片差异特别大时说明该样本可能来自不同采集序列训练时需要加入归一化或直方图匹配。4. 训练自己的海马体分割模型2D U-Net 数据管线与 Dice Loss三套切面数据准备好后下一步就是接入分割网络。虽然原始数据是 3D nii.gz但作者已经把三个轴切成了 2D PNG所以可以先训练 2D U-Net把基线跑出来再决定要不要上 3D U-Net 做最终版本。这一章给出一个可复现的 PyTorch 训练管线并且会对不同切面的分辨率差异做说明。4.1 为什么先用 2D 模型而不是直接上 3D U-Net3D U-Net 在医学图像分割里依然是金标准尤其是海马体这种体积小、解剖结构固定的小器官。但 3D 模型对显存、patch 大小和重采样策略非常敏感一个标准的 3D patch 通常要 96×96×96 或 128×128×128batch size 稍微大一点显存就爆了。这份数据集已经把每个 3D 卷拆成三个正交方向的 2D 切片天然适合先用 2D 模型验证分割思路。常见做法是三个切面分别训练三个独立的 2D U-Net推理时把三个方向的概率图平均也可以只选一个切面训练比如冠状面 y 轴样本数最多达到 9465 张单个模型就能训练得比较充分。选型上2D U-Net 的输入通道是 1灰度 MRI输出通道是 3。如果只把海马体当成前景可以输出 2 通道但本数据集的标签为前部海马体和后部海马体所以 3 通道更合适。网络编码器部分可以用 ResNet 预训练权重但第一层卷积要改成输入 1 通道或者在读取数据时把单通道复制成三通道以适配 ImageNet 预训练权重。4.2 自定义 Dataset同时消费三个切面写一个通用的 PyTorch Dataset让它既能加载x_images也能加载y_images只要传入不同目录即可import os import torch from torch.utils.data import Dataset from PIL import Image import numpy as np class SliceDataset(Dataset): def __init__(self, image_dir, mask_dir, transformNone): self.names sorted(os.listdir(image_dir)) self.image_dir image_dir self.mask_dir mask_dir self.transform transform def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img np.array(Image.open(os.path.join(self.image_dir, name)).convert(L)) mask np.array(Image.open(os.path.join(self.mask_dir, name))) if self.transform is not None: aug self.transform(imageimg, maskmask) img, mask aug[image], aug[mask] img torch.from_numpy(img).float().unsqueeze(0) / 255.0 mask torch.from_numpy(mask).long() return img, mask参数说明image_dir和mask_dir分别指向某个切面的 images、masks 目录transform使用 albumentations 时会返回字典image是 H×W 数组mask是 H×W 的标签数组。最后把图像除以 255 归一化到 [0,1]mask 保持原始数值用long作为交叉熵的 target。注意不要对 mask 做归一化也不要将 mask 转成 one-hot 再返回直接在损失函数里用类索引更省内存。4.3 Dice Loss 和评估指标的计算细节海马体在切片里通常只占很小一块直接用交叉熵会出现严重的类别不平衡。标准做法是把交叉熵和 Dice Loss 混合。Dice Loss 每次计算一个类别的 Dice然后把类别 1 和类别 2 的 loss 平均背景类别不参与import torch import torch.nn.functional as F def dice_loss(pred, target, num_classes3, smooth1.0): probs torch.softmax(pred, dim1) # (B, C, H, W) target_onehot F.one_hot(target, num_classes).permute(0, 3, 1, 2).float() loss 0.0 for c in range(1, num_classes): inter (probs[:, c] * target_onehot[:, c]).sum() union probs[:, c].sum() target_onehot[:, c].sum() smooth loss 1.0 - (2.0 * inter smooth) / union return loss / (num_classes - 1)这里的smooth用来防止分子分母同时为 0。当某个切片只有背景时类别 1 和类别 2 的inter和union都为 0加上smooth之后 loss 接近 1不会出现 NaN。评估时同样应该报告类别 1 和类别 2 的平均 Dice不报告背景 Dice因为背景占比过高会虚高整体指标。如果你在验证集上看到整体 Dice 很高但前部/后部海马体单独看很差多半就是背景类别参与了平均。训练循环核心device torch.device(cuda if torch.cuda.is_available() else cpu) model UNet2D(in_channels1, out_channels3).to(device) opt torch.optim.AdamW(model.parameters(), lr1e-4) for epoch in range(epochs): model.train() train_loss 0.0 for img, mask in loader: img, mask img.to(device), mask.to(device) pred model(img) loss dice_loss(pred, mask) 0.5 * F.cross_entropy(pred, mask) opt.zero_grad() loss.backward() opt.step() train_loss loss.item() print(epoch, train_loss / len(loader))这里把dice_loss和cross_entropy相加交叉熵权重是 0.5让 Dice Loss 主导。如果训练初期 loss 波动太大可以把交叉熵的权重改成 1.0让模型先学会大致类别分布再逐步提高 Dice Loss 占比。AdamW的lr1e-4在 2D 分割任务里通常比较稳不需要一开始就用余弦退火等验证集 Dice 不再上升时再降学习率。4.4 超参数与不同切面尺寸的匹配三个切面的图像尺寸不一样不能直接共用同一个 padding。整理成表切面宽×高样本数推荐输入尺寸x 轴横断面35×51495864×64pady 轴冠状面35×35946564×64pad 或 resizez 轴矢状面51×35460364×64padz 轴宽 51、高 35接近 64 的一半pad 之后补零信息不多。y 轴本身就是方形最省事。输入尺寸统一到 64×64 可以保证三个切面的模型结构完全一致只在数据加载时做 resize 或 padding。我不会为了省事直接 resize因为海马体本身尺寸小resize 会破坏解剖比例用 pad 到 64×64 更安全。做 online 增强时再随机 crop、旋转或水平翻转。epoch 可以按 100 设置batch size 在 32 附近。三个切面分别训练三个模型保存三套权重。推理时对同一张切片做 test time augmentation例如水平翻转后取两次预测的平均概率Dice 通常能涨 1 到 2 个百分点。对于海马体这种左右对称结构翻转增强基本不会引入错误。5. 跨切面集成与三维重建把 2D 预测拼回海马体体积只在一个切面上训练2D U-Net 很容易在切片方向产生不连续预测。真正可靠的评估不是只看单个切面的 2D Dice而是把三个切面的结果拼回三维体再计算三维指标。做法是每个切面模型输出该方向上的 2D 概率图然后根据切片序号映射到三维 volume 的对应索引。5.1 使用切片序号恢复三维索引hippocampus_164_18.png中的 18 表示原始 3D 卷的第 18 层切片。如果 x、y、z 三个方向的切片序号来自同一个 volume那么可以直接用三维数组承接import numpy as np # 假设原始 3D 数组 shape 为 (nx, ny, nz) vol_pred np.zeros((nx, ny, nz), dtypenp.float32) # x 方向每张切片对应 volume[slice_idx, :, :] for slice_idx, pred_2d in enumerate(pred_x_list): vol_pred[slice_idx] pred_2d # y 方向volume[:, slice_idx, :] # z 方向volume[:, :, slice_idx]这里的pred_2d要保证是概率图或 argmax 之后的类别图尺寸与原始切面分辨率一致。由于三个方向的宽高不同需要先把每个方向的预测 resize 回原始 2D 切片的宽高再做拼接。实际操作时要从dataset.json里取原始 3D shape否则只能按各轴已知尺寸组合。不同切面读入的 numpy 数组 shape 顺序不同这是最容易出错的地方。5.2 三维 Dice 与 Hausdorff 距离验证拼接结束后如果原始 nii.gz 中的 mask 还在可以用 SimpleITK 计算三维指标import SimpleITK as sitk pred_np vol_pred.astype(np.uint8) true_sitk sitk.ReadImage(label.nii.gz) pred_sitk sitk.GetImageFromArray(pred_np) pred_sitk.CopyInformation(true_sitk) dice_filter sitk.LabelOverlapMeasuresImageFilter() dice_filter.Execute(pred_sitk, true_sitk) print(Dice label1:, dice_filter.GetDiceCoefficient(1)) print(Dice label2:, dice_filter.GetDiceCoefficient(2))用 SimpleITK 的好处是可以同时拿到表面距离信息也可以直接算 Hausdorff 距离不需要自己写距离变换。必须注意重建后的pred_sitk要和真实标签的体素间距、方向保持一致所以pred_sitk要调用CopyInformation(true_sitk)。如果两个图像的 origin 或 spacing 不一致Dice 可能仍然偏高但 Hausdorff 距离会明显异常。做三个方向模型融合时建议把每个方向的概率图先对齐到同一个体素网格再按平均概率做 argmax。直接在 2D 层面拼接等到转 3D 时很容易因为切片索引错位导致边界区域出现层间噪声。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。