大脑肿瘤MRI分割数据集详解:从掩码处理到U-Net训练
发布时间:2026/9/10 2:56:10来源:尧图网络
简介面向医学图像分割与深度学习入门者提供一套大脑肿瘤MRI二维分割数据集类别设计简洁聚焦Tumor前景与背景的二分类任务适合图像分割模型的训练与效果验证。图像统一缩放至416×416分辨率训练集包含1632张原始图片及1632张对应掩膜测试集包含240张原始图片与240张掩膜标签以1和0区分前景与背景背景简单、前景区域丰富且标注良好可直接用于U-Net、DeepLab等常见分割网络的输入与精度评估。压缩包共2000个文件以png/jpg图像文件和一个Python可视化脚本为主体整体大小48.17MB目录划分清晰便于按训练集、测试集快速检索。可视化脚本会随机提取一张样本同时输出原始图片、GT掩膜以及GT叠加在原图上的蒙版效果方便直观检验标注质量和模型预测结果。目前已有1879人学习使用适合医学影像分析课程作业、算法对比试验或科研预研等场景。1. 大脑肿瘤分割数据集416×416 MRI切片与二分类掩码的对应关系拆开压缩包你会看到一批以 y431_jpg.rf.c7ebeffb6e184a65ac9d74065e76d6d8.jpg 命名的文件。这种命名不是随意起的rf 是 Roboflow 的导出标记后面的长十六进制串是资源内部的对象 ID前面的 y431 才是原始图像名。这是一份人脑 MRI 切片的大脑肿瘤分割数据集分辨率统一为 416×416前景类别只有 Tumor 一个背景不算类别所以说的“2 分割”是指标签设定里 0 是背景、1 是肿瘤。训练集 1632 对图片和掩码测试集 240 对同时附带了可视化脚本能把原图、GT 掩码、GT 蒙板一次性画出来。这份数据适合谁用刚开始做医学图像分割、需要一份干净基准数据来验证 U-Net 系列网络的人也适合想测试数据增强和损失函数在“背景简单、前景边界明显”场景下表现的人。2. 解剖大脑肿瘤数据集的目录结构与掩码存储规则2.1 Roboflow 导出命名为什么不能用顺序索引对齐图片和掩码train 和 test 目录内部结构一致分别挂 images 和 masks 两个子目录。images 下面是 jpg掩码文件可能以 png 或 jpg 形式出现取决于导出时选择的编码。这个数据集的关键约束是每一张 mask 与对应图片共享同一个文件主名stem只是扩展名不同。你的数据加载逻辑必须以主名作为关联键而不是用os.listdir的顺序盲配对因为文件系统的返回顺序不保证两张表对齐。brain-tumor-segmentation/ ├── train/ │ ├── images/ │ │ ├── y431_jpg.rf.c7ebeffb6e184a65ac9d74065e76d6d8.jpg │ │ ├── y492_jpg.rf.d1dc8cb01e14d08c955b0f2ebf441ba4.jpg │ │ └── ... (1632 files) │ └── masks/ │ ├── y431_jpg.rf.c7ebeffb6e184a65ac9d74065e76d6d8.png │ ├── y492_jpg.rf.d1dc8cb01e14d08c955b0f2ebf441ba4.png │ └── ... (1632 files) ├── test/ │ ├── images/ │ └── masks/ └── visualize.py上面目录树里掩码我默认写成了 .png实际如果导出为 jpg 也不影响逻辑。文件名里的 c7ebeffb6e184a65ac9d74065e76d6d8 是 Roboflow 生成的唯一 ID同一张图在 images 和 masks 里 ID 完全一致。所以最稳妥的匹配方式是读取图片的 stem到 mask 目录里去找同 stem 的文件而不是直接遍历两个目录再 zip。Roboflow 还可能在文件名里附加_jpg后缀这是告诉你原图是 jpg 格式。后面做正则匹配或写数据加载器时要注意这些下划线分隔的段避免把 stem 截断成只有一个 y431。2.2 掩码像素值语义0 是背景但前景可能是 1 也可能是 255摘要里说“标签的 mask 图像为 1 的阈值0 为背景”这句话在落地时要非常小心。Roboflow 导出二分割标签时有两种常见编码一种是单通道灰度图背景像素值 0、前景像素值 1另一种也是单通道灰度图但背景 0、前景 255。如果你直接对像素值做mask / 255.0第一种编码下前景 1 会被压成 0.0039损失函数直接崩溃如果直接mask 1第二种编码又什么都匹配不到。正确做法是用阈值比较mask 0把任何非零值统一映射成前景再转成 float 张量。import cv2 import numpy as np from pathlib import Path mask_path Path(train/masks/y431_jpg.rf.c7ebeffb6e184a65ac9d74065e76d6d8.png) mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) print(unique pixels:, np.unique(mask)) # 输出可能是 [0 1]也可能是 [0 255] mask_bin (mask 0).astype(np.uint8) print(foreground ratio:, round(float(mask_bin.mean()), 4))这段代码在拿到数据后应该第一个跑。np.unique(mask)直接告诉你掩码到底是 0/1 还是 0/255 编码mask 0则把两种编码统一成布尔标签。我一般在数据加载的__getitem__里直接做这一步而不是提前把所有 mask 暴力重写一遍因为 1872 个文件重写后还要再校验一次没必要。foreground ratio能帮你快速判断肿瘤区域占整张图的比例这个比例直接决定后面损失函数要不要加权重。2.3 用脚本核对图片与掩码的完备性下载之后先别急着写模型。Roboflow 导出的归档偶尔会出现某张图片缺掩码或者标签文件里混进多余 mask 的情况。我一般先跑一遍配对检查把缺失和多余的文件名列出来确认没有问题再进训练循环。from pathlib import Path SUPPORTED_MASK_EXT {.png, .jpg, .jpeg, .bmp} def verify_pair(img_dir: str, mask_dir: str): img_dir Path(img_dir) mask_dir Path(mask_dir) images {p.stem: p for p in img_dir.glob(*.jpg)} masks {p.stem: p for p in mask_dir.glob(*.*) if p.suffix.lower() in SUPPORTED_MASK_EXT} missing sorted(set(images) - set(masks)) extra sorted(set(masks) - set(images)) print(fimages: {len(images)}, masks: {len(masks)}) print(fmissing masks: {len(missing)}) for name in missing[:10]: print( MISSING, name) print(fextra masks: {len(extra)}) for name in extra[:10]: print( EXTRA, name) return missing, extra verify_pair(train/images, train/masks) verify_pair(test/images, test/masks)脚本做了两件事一是用集合差集找出缺失和多余的掩码二是打印统计量。注意missing只列了前 10 个因为如果数据真的出错往往是一整段切片序列连续缺列太多反而刷屏。extra存在的可能原因包括标注工具导出了额外的空掩码或备份文件这类文件训练时如果被加载器扫到会把一个纯背景样本混进训练集肿瘤区域占比被拉低训练倒不至于崩但验证集指标会带着噪声。这里我用集合的键是stem天然屏蔽了扩展名差异png 和 jpg 混存的情况也能对齐。3. 把可视化代码改成批量蒙版校验工具3.1 三张子图的绘制逻辑原图、GT、GT 蒙板的关系数据自带的 visualize.py 干的事是随机抽一张图把原图、GT 灰度掩码、GT 在原图上的蒙板三个图横向排开。这个思路对分割数据是够用的因为单张 GT 灰度图肉眼直接看并不直观尤其是肿瘤边界和脑组织灰度接近的时候。蒙板层的作用是把掩码覆盖到原图上让你一眼看出标注边界是否贴合解剖结构。原版脚本逻辑通常长这样我用 PyTorch 之外依赖更少的方式重写了一份等效版本。import cv2 import numpy as np import matplotlib.pyplot as plt from pathlib import Path def find_mask(img_path: Path, mask_dir: Path) - Path: candidates list(mask_dir.glob(img_path.stem .*)) assert len(candidates) 1, fmask 匹配异常: {img_path.stem}: {candidates} return candidates[0] img_dir Path(train/images) mask_dir Path(train/masks) img_paths list(img_dir.glob(*.jpg)) picked np.random.choice(len(img_paths), size4, replaceFalse) for idx in picked: img_path img_paths[idx] img cv2.imread(str(img_path)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(str(find_mask(img_path, mask_dir)), cv2.IMREAD_GRAYSCALE) mask_bin (mask 0).astype(np.uint8) overlay img.copy() overlay[mask_bin 0] (255, 0, 0) # 红色蒙版 blended cv2.addWeighted(overlay, 0.35, img, 0.65, 0) fig, axes plt.subplots(1, 3, figsize(14, 5)) axes[0].imshow(img) axes[0].set_title(MRI Image) axes[0].axis(off) axes[1].imshow(mask_bin, cmapgray) axes[1].set_title(GT Mask) axes[1].axis(off) axes[2].imshow(blended) axes[2].set_title(GT Overlay) axes[2].axis(off) plt.tight_layout() plt.savefig(fvis_{img_path.stem}.png, dpi150, bbox_inchestight) plt.close(fig)这段代码把随机抽 1 张改成抽 4 张每张单独保存成vis_原文件名.png。overlay[mask_bin 0] (255, 0, 0)这行是在原始图像的副本上把所有掩码覆盖的像素点直接染成红色然后用addWeighted按 0.35 的权重融合。为什么要先染红再融合而不是直接alpha混合因为直接混合需要把掩码区域外的 alpha 也做一次 mask 操作代码会多两行而先染再融最简单还能保留脑沟回处的灰度对比。3.2 服务器无显示环境时 matplotlib 的兜底配置你很可能是在云主机或远程容器里跑这份数据根本没有显示器。matplotlib 默认的plt.show()在无 GUI 环境会直接报错所以上面代码里我全部用plt.savefig并且plt.close(fig)关掉画布。如果你还是想先看一次交互窗口可以在脚本开头强制指定 Agg 后端import matplotlib matplotlib.use(Agg)Agg是不依赖窗口系统的非交互式后端渲染结果直接写文件几乎在所有 Linux 服务器上都可用。如果你本地有显示器并且想弹窗看把这行注释掉就行。3.3 批量可视化时怎么筛出异常掩码单张可视化看着没问题还不够我习惯把测试集 240 张全部跑一遍叠加图然后按掩码区域面积排序重点看面积最大和最小的几张。面积最大的可能是标注把脑室或水肿一起圈了进来面积最小的可能是误标注的孤立噪点这两种情况都会影响训练时对边界的约束。排序脚本只需要在上面循环里加一行mask_bin.sum()作为 key。另一个常见坑是如果find_mask匹配不到任何文件脚本会抛AssertionError这其实是在提醒你 mask 目录路径给错了。Roboflow 导出时掩码有时会保存到train/labels而不是train/masks下载后解压先看一眼目录名别想当然。4. 基于 2D U-Net 的大脑肿瘤分割训练配置与损失函数选择4.1 为什么从 2D U-Net 起步而不是 3D 或 Transformer这份数据集给的是 2D 切片每张 416×416 的 jpg 是一张独立的 MRI 横断面文件之间没有提供层厚、扫描序列、患者 ID 这类三维重建信息。也就是说你无法直接把图片堆成 3D volume 去训练 3D U-Net。BraTS 这类基准数据集用 3D U-Net 是因为它提供完整的 nii.gz 体数据而这里强行按文件名顺序堆 z 轴相邻切片可能来自完全不同的扫描模型会学到一堆假纹理。2D U-Net 在这个数据规模下是最合理的起点另一个原因是你只有 1632 张训练图而 U-Net 是出了名的在小数据集上也能收敛的架构。它的跳跃连接把编码器每层特征图直接拼到解码器保证 416 分辨率下的边界细节不丢失。相比之下 DeepLab V3 的 ASPP 模块在 16 倍下采样下工作对小肿瘤边界的敏感度差一些SegFormer 这类 Transformer 结构没有 ImageNet 预训练权重保护1632 张图很难训稳。4.2 数据加载与数据增强实现数据加载要解决的问题有两个文件路径匹配和掩码二值化。下面的 Dataset 类把 2.2 节的阈值逻辑直接合入此外把掩码转成(1, 416, 416)的 float 张量配合 PyTorch 的 BCELoss 或 DiceLoss 使用。import torch import cv2 import numpy as np import albumentations as A from torch.utils.data import Dataset from pathlib import Path class BrainTumorSegDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone): self.img_paths sorted(Path(img_dir).glob(*.jpg)) self.mask_dir Path(mask_dir) self.transform transform def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_path self.img_paths[idx] img cv2.imread(str(img_path)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask_matches list(self.mask_dir.glob(img_path.stem .*)) assert len(mask_matches) 1, fmask mismatch: {img_path.name} mask cv2.imread(str(mask_matches[0]), cv2.IMREAD_GRAYSCALE) mask (mask 0).astype(np.float32) # 统一为 0/1 标签 if self.transform is not None: aug self.transform(imageimg, maskmask) img, mask aug[image], aug[mask] img torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 mask torch.from_numpy(mask).unsqueeze(0) # (1, H, W) return img, mask train_transform A.Compose([ A.HorizontalFlip(p0.5), A.Rotate(limit10, border_modecv2.BORDER_CONSTANT, p0.3), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), ])assert len(mask_matches) 1这行是安全阀如果掩码缺失或重复命名会立刻暴露。permute(2, 0, 1)把 HWC 的 numpy 数组转成 CHW 的 PyTorch 张量。增强里我没有加弹性形变因为脑部解剖结构对空间形变的容忍度低过分形变会扭曲脑室相对位置水平翻转在医学上对应人体左右对称属于相对安全的增强旋转控制在 10 度以内避免把脑干这类位置敏感结构转得过分夸张。4.3 DiceLoss 与 BCE 的组合公式肿瘤区域在这类 MRI 切片里可能只占整图的 5% 到 15%纯二值交叉熵会被大面积背景主导模型很快收敛到“全预测背景”的局部最优。DiceLoss 天然不敏感类别不平衡它只关注预测掩码和 GT 掩码的交叠比例所以这里把两个损失加起来。import torch.nn.functional as F def dice_loss(pred_logits, target, smooth1.0): pred torch.sigmoid(pred_logits) num 2 * (pred * target).sum(dim(2, 3)) smooth den pred.sum(dim(2, 3)) target.sum(dim(2, 3)) smooth return 1 - (num / den).mean() def combined_loss(pred_logits, target): bce F.binary_cross_entropy_with_logits(pred_logits, target) dice dice_loss(pred_logits, target) return bce dicepred_logits是模型输出的未经过 sigmoid 的原始 logits所以 BCE 用binary_cross_entropy_with_logitsDice 分支内部自己再做sigmoid。两个损失的梯度量级不同BCE 大致在 0.1 到 1 之间DiceLoss 在 0 到 1 之间直接相加问题不大。如果你发现训练初期损失下降很快、后期震荡可以改成0.5 * bce dice让 Dice 主导边界优化。4.4 训练超参与显存参考下面这套配置在 416×416 输入、2D U-Net 基础通道数 32 的前提下12GB 显存的卡可以跑 batch size 824GB 显存可以跑到 16。配置项参考值说明输入尺寸416×416保持数据集原始分辨率不额外 resizebatch size8 / 1612GB 显存用 824GB 用 16优先降 batch 别降分辨率基础通道数32U-Net 第一层卷积通道逐层翻倍到 512优化器AdamW, lr1e-3, wd1e-4小数据集上比 SGD 收敛稳权重衰减防过拟合训练轮数80配合早停验证 Dice 连续 10 轮不涨就中断输入归一化除以 255MRI 不是自然图像不做 ImageNet 均值方差归一化关于输入归一化多说一句自然图像的 ImageNet 均值方差统计对 MRI 灰度图没意义直接img / 255.0让像素落在 0 到 1 区间即可。用 ImageNet 预训练权重时另说但这里 U-Net 是随机初始化不需要那套均值方差。5. 掩码阈值、类别不平衡与数据划分的三个边界问题5.1 阈值归一化放哪个位置决定了你后面改模型要动多少代码我在 2.2 节埋了个问题mask 可能是 0/1 也可能是 0/255。如果你在数据预处理的早期阶段就把所有掩码统一重写为 0/255 的 PNG丢给可视化脚本没问题但喂给神经网络时还要再除一次 255。更麻烦的是有些标注工具导出时会对多边形边缘做抗锯齿处理边缘像素值不是单纯的 0 或 255而是 128 一类的中间灰度。此时mask 0会把抗锯齿像素全归为前景边界比真实标注粗一圈mask 127会把抗锯齿像素归为背景边界往里缩一圈。我一般会先跑一个像素直方图看掩码有没有中间灰度带。如果只有两个峰阈值写mask 0最安全如果出现第三个峰说明边缘有插值就要想清楚你自己要“粗半像素”还是“细半像素”的边界。对脑肿瘤这种病理边界本身模糊的数据这个误差可以忽略但对血管分割这类极细结构抗锯齿像素可能比血管主干还宽处理方式直接决定你最终 IoU 上限。5.2 肿瘤区域占比先量化再定损失权重类别不平衡不能靠感觉先跑统计代码拿到训练集所有掩码的前景占比分布再决定要不要给交叉熵加权。下面的脚本扫一遍训练集掩码输出平均前景比例和分位数。import cv2 import numpy as np from pathlib import Path def foreground_stats(mask_dir: str): ratios [] for m in Path(mask_dir).glob(*.*): mask cv2.imread(str(m), cv2.IMREAD_GRAYSCALE) if mask is None: continue ratios.append(float((mask 0).mean())) ratios np.array(ratios) print(favg: {ratios.mean():.4f}, fp10: {np.percentile(ratios, 10):.4f}, fmedian: {np.median(ratios):.4f}, fp90: {np.percentile(ratios, 90):.4f}) foreground_stats(train/masks)输出结果如果中位数低于 0.10说明大部分切片里肿瘤只占十分之一不到这就是典型的极端不平衡。这种情况下单独的 BCE 会让模型倾向于输出全背景DiceLoss 的梯度能强制模型关注前景区域。如果中位数在 0.2 以上前景相对充裕可以调低 Dice 的权重比如用bce 0.5 * dice让 BCE 帮助稳定像素级分类。5.3 文件名前缀可能暗示同源切片划分时要注意泄漏这个数据集的划分是随机的train 1632 张、test 240 张已经是定死的。但从文件名结构看y431、y492、y374 这些前缀很可能来自原始扫描序列的不同帧同一个前缀的不同图片在病灶纹理、位置、亮度上高度相关。如果你要做严格的泛化评估随机划分会把同源切片同时放进训练集和测试集导致 Dice 虚高。检查办法是把测试集文件名前缀和训练集做交集。import re from pathlib import Path def extract_prefix(filename: str) - str: m re.match(r^(.*?)_(?:jpg|png|jpeg), filename) return m.group(1) if m else filename train_prefix {extract_prefix(p.name) for p in Path(train/images).glob(*.jpg)} test_prefix {extract_prefix(p.name) for p in Path(test/images).glob(*.jpg)} overlap train_prefix test_prefix print(overlap prefixes:, len(overlap)) for item in sorted(overlap)[:10]: print(item)如果 overlap 数量很大说明你有相当一部分同源切片跨集分布。要缓解你可以自己按前缀重新划分把同一前缀的所有切片全部放进训练集或测试集。重新划分后 train 和 test 的数量不再是 1632/240但泛化指标会更诚实。这里要注意的是Roboflow 导出时文件名里的 y431 并不保证一定是患者编号也可能是原始图片名所以这个检查的意义在于“怀疑并验证”而不是直接断定泄漏。5.4 想套 YOLOv8 或检测框架时的格式转换陷阱热词榜里“yolov8 训练自己的数据集”搜索量不低不少人是拿这份分割数据往 yolov8-seg 里灌的。YOLOv8 的分割标签要求是归一化的多边形顶点坐标文件每行一个目标类别加一组 xy 坐标点不是逐像素掩码。把这份数据的掩码转成 polygon 会遇到一个实际问题脑肿瘤边界经常带毛刺和细小凸起转多边形时顶点数量一旦设得太大训练时损失计算开销猛涨设得太小小肿瘤会被拟合成三角形或四边形形状信息直接丢失。我的建议是常用语义分割网络就用逐像素的掩码监督不要转检测框架。如果你确实要在同一套工作流里做目标检测和分割可以用 Label Studio 这类标注软件的导出插件重新导出 COCO 格式或者把掩码做轮廓提取后再根据轮廓面积筛选顶点数。这个过程里最容易出错的是多边形顶点顺序必须为顺时针否则后处理画 mask 时会得到空洞。6. 评估大脑肿瘤分割预测Dice 系数计算与预测蒙版输出6.1 在测试集上计算 Dice 与 IoU训练完的模型最后都要在 test 的 240 张图上跑一遍得到打分。分类任务看 accuracy 在这里没有意义因为全预测背景也能有 85% 以上的 accuracy。分割任务看 Dice 和 IoU下面的函数同时输出这两项。import torch import numpy as np torch.no_grad() def evaluate_segmentation(model, test_loader, device, thr0.5): model.eval() dice_list, iou_list [], [] for img, mask in test_loader: img img.to(device) mask mask.to(device) logits model(img) pred (torch.sigmoid(logits) thr).float() inter (pred * mask).sum(dim(2, 3)) union (pred mask).clamp(max1).sum(dim(2, 3)) dice (2 * inter / union.clamp(min1e-6)).cpu().numpy() iou (inter / union.clamp(min1e-6)).cpu().numpy() dice_list.append(dice) iou_list.append(iou) dice float(np.concatenate(dice_list).mean()) iou float(np.concatenate(iou_list).mean()) print(fDice: {dice:.4f}, IoU: {iou:.4f}) return dice, iouunion我用(pred mask).clamp(max1)计算这比pred | mask更适合张量运算clamp 把重叠区域的 2 压回 1等价于并集。预测阈值thr默认 0.5但要注意 0.5 不一定是最优肿瘤边界模糊的样本把阈值提到 0.6 能压掉部分假阳性降到 0.4 能提高召回具体取多少要先在自己的验证集上扫一遍。6.2 输出预测蒙板与 GT 的并排对比图评估指标只看数字不够你得亲眼看到预测边界和 GT 差在哪。我把 5.2 节的可视化逻辑复用一下改成调用模型输出预测掩码并把预测蒙板用青色画出和 GT 的红色蒙板在同一张原图上对比。def save_prediction_overlay(model, test_loader, device, save_dirpred_vis): import cv2, matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt from pathlib import Path Path(save_dir).mkdir(exist_okTrue) model.eval() count 0 for img, mask in test_loader: with torch.no_grad(): logits model(img.to(device)) pred (torch.sigmoid(logits) 0.5).float() batch img.cpu().numpy().transpose(0, 2, 3, 1) for i in range(batch.shape[0]): if count 16: return orig (batch[i] * 255).astype(np.uint8) gt mask[i, 0].cpu().numpy() pr pred[i, 0].cpu().numpy() overlay_gt orig.copy() overlay_gt[gt 0] (255, 0, 0) # GT 红色 overlay_pr orig.copy() overlay_pr[pr 0] (0, 255, 0) # 预测 绿色 fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(overlay_gt); axes[0].set_title(GT) axes[1].imshow(overlay_pr); axes[1].set_title(Prediction) axes[2].imshow(orig); axes[2].set_title(Origin) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(f{save_dir}/sample_{count:03d}.png, dpi120, bbox_inchestight) plt.close(fig) count 1保存对比图时固定随机种子比你想的更关键。如果不固定每次评估抽到的 16 张样本不同某一轮抽到一个边界模糊的样本Dice 下降 0.02你会误判模型变差了。在脚本开头加上random.seed(42)、np.random.seed(42)、torch.manual_seed(42)确保多次评估看到的是同一批样本这样模型对比才有意义。本文还有配套的精品资源点击获取
网站建设高端定制企业官网