
简介本资源面向医学图像分割方向的初学者与算法实践者提供一套416×416分辨率的人脑MRI肿瘤二分割数据集及配套可视化脚本可用于训练与验证U-Net等分割模型帮助解决医学影像中前景标注获取困难的问题。压缩包共2000个文件以1759个png图像与掩膜、240个jpg样本及1个Python可视化脚本为主整体约48.17MB采用7z格式打包。数据集划分为训练集与测试集训练集含1632张原始图像及1632张对应mask测试集含240张图像及240张maskmask中像素值1表示肿瘤前景、0为背景标注质量良好、背景简洁。配套脚本可随机抽取一张图片将原图、GT图像及GT在原图上的蒙板叠加效果一并展示并保存至当前目录便于快速核验标注与直观理解分割任务。目前已有1880人学习下载适合作为医学图像分割入门与实验对比的实用素材。1. 大脑肿瘤分割数据集从 MRI 切片到二分类掩码这套东西到底怎么用手里有一批脑部 MRI 的 NIfTI 文件想训一个能自动勾出肿瘤区域的模型但卡在第一步——数据怎么组织、标签怎么对齐、可视化怎么验证这是很多做医学图像分割的工程师遇到的真实场景。大脑肿瘤分割2 分割数据集本质上就是解决这个问题的输入是 T1 或 FLAIR 模态的脑部 MRI 体数据输出是一张二值掩码前景是肿瘤区域背景是正常脑组织类别只有两个——肿瘤和非肿瘤。它适合三类人刚入门医学图像分割、想找一个结构清晰的数据集跑通全流程的新手需要快速验证 U-Net 类模型在脑肿瘤任务上表现的算法工程师以及做辅助诊断原型、需要可视化工具来核对分割结果的产品开发者。这个数据集的核心价值不在于数据量多大而在于它把「体数据切片、标签对齐、掩码可视化」这条链路完整地摆在你面前让你能把精力花在模型和调参上而不是在数据清洗上反复翻车。2. 数据集的目录结构与标签格式拿到手先别急着训2.1 常见的目录组织方式与文件命名规律大脑肿瘤分割数据集通常以体数据为单位组织每个病例一个文件夹里面包含影像文件和对应的标签文件。常见的做法是影像存为.nii或.nii.gz标签存为同名的_seg.nii.gz或放在labels/子目录下。以 BraTS 风格的二分类数据集为例目录结构一般长这样dataset/ ├── images/ │ ├── patient_001.nii.gz │ ├── patient_002.nii.gz │ └── ... ├── labels/ │ ├── patient_001_seg.nii.gz │ ├── patient_002_seg.nii.gz │ └── ... └── splits/ ├── train.txt ├── val.txt └── test.txt影像文件和标签文件必须一一对应文件名中的患者 ID 是唯一的关联键。splits/目录下的 txt 文件每行写一个患者 ID用来划分训练集、验证集和测试集。如果你拿到的数据集没有 splits 目录需要自己按 7:1:2 或 8:1:1 的比例划分注意同一个患者的所有切片只能出现在同一个集合里否则会出现数据泄漏——这是血泪经验别问我是怎么知道的。2.2 标签的编码方式0 和 1 还是 0 和 255二分类分割任务的标签编码方式直接影响损失函数的选择和评估指标的计算。常见的有两种一种是标签值直接是 0 和 1前景为 1背景为 0另一种是 0 和 255前景为 255背景为 0。前者可以直接用于BCEWithLogitsLoss或DiceLoss后者需要先做归一化或阈值处理。用 Python 读取标签并检查唯一值可以快速确认编码方式import nibabel as nib import numpy as np label_path dataset/labels/patient_001_seg.nii.gz label nib.load(label_path).get_fdata() unique_values np.unique(label) print(f标签唯一值: {unique_values}) print(f前景像素占比: {(label 0).sum() / label.size:.4f})这段代码做了两件事读取 NIfTI 标签文件打印所有唯一值来确认编码方式计算前景像素占比帮你判断类别是否极度不平衡。如果前景占比低于 1%说明肿瘤区域非常小训练时需要用 Dice Loss 或 Focal Loss 来缓解正负样本失衡。参数说明nib.load()返回的是 NIfTI 对象get_fdata()将其转为 numpy 数组默认是 float64如果数据量大可以指定dtypenp.float32节省内存。2.3 影像与标签的空间对齐检查影像和标签必须共享同一个仿射矩阵affine matrix否则切片时会出现错位。用下面的代码检查两者的 shape 和 affine 是否一致import nibabel as nib import numpy as np img nib.load(dataset/images/patient_001.nii.gz) label nib.load(dataset/labels/patient_001_seg.nii.gz) print(f影像 shape: {img.shape}, 标签 shape: {label.shape}) print(f影像 affine:\n{img.affine}) print(f标签 affine:\n{label.affine}) print(faffine 是否一致: {np.allclose(img.affine, label.affine)})如果 shape 不一致说明标签和影像的裁剪范围不同需要重新配准或裁剪。如果 affine 不一致但 shape 相同切片时可能仍然对齐但体素间距不同会导致物理尺寸偏差。常见做法是统一重采样到 1mm 各向同性分辨率用nibabel.processing.resample_from_to或SimpleITK完成。这一步不做后面可视化时你会发现掩码飘在肿瘤旁边那就是玄学现场了。3. 从 NIfTI 到训练张量切片、归一化与数据增强3.1 沿轴向切片并过滤空切片3D 体数据直接送入 3D 模型对显存要求高很多工程师选择沿轴向切片成 2D 图像用 U-Net 或 DeepLab 做逐片分割。切片时要注意过滤掉没有肿瘤的空白切片否则正负样本比例会进一步失衡。import nibabel as nib import numpy as np import os def extract_slices(image_path, label_path, output_dir, min_tumor_pixels10): img nib.load(image_path).get_fdata(dtypenp.float32) label nib.load(label_path).get_fdata(dtypenp.float32) patient_id os.path.basename(image_path).replace(.nii.gz, ) os.makedirs(os.path.join(output_dir, images), exist_okTrue) os.makedirs(os.path.join(output_dir, labels), exist_okTrue) saved 0 for z in range(img.shape[2]): img_slice img[:, :, z] label_slice label[:, :, z] if (label_slice 0).sum() min_tumor_pixels: continue img_slice (img_slice - img_slice.min()) / (img_slice.max() - img_slice.min() 1e-8) label_slice (label_slice 0).astype(np.uint8) np.save(os.path.join(output_dir, images, f{patient_id}_z{z:03d}.npy), img_slice) np.save(os.path.join(output_dir, labels, f{patient_id}_z{z:03d}.npy), label_slice) saved 1 return saved total 0 for f in os.listdir(dataset/images): if f.endswith(.nii.gz): pid f.replace(.nii.gz, ) total extract_slices( fdataset/images/{f}, fdataset/labels/{pid}_seg.nii.gz, slices_2d ) print(f共提取 {total} 个有效切片)这段代码的逻辑是逐层读取 3D 体数据跳过肿瘤像素少于min_tumor_pixels的切片对影像做最小-最大归一化到 [0,1]标签二值化为 0/1最后保存为.npy文件。参数min_tumor_pixels控制过滤阈值设太小会引入大量噪声切片设太大会丢失小肿瘤样本一般 10 到 50 之间比较合理。归一化用最小-最大而不是 Z-Score是因为 MRI 的强度范围因扫描仪和序列而异最小-最大能把不同来源的数据拉到同一尺度。3.2 训练时的在线数据增强策略2D 切片训练时数据增强是提升泛化能力的关键。医学图像常用的增强包括随机旋转、随机缩放、弹性变形、亮度对比度扰动。用albumentations库可以方便地组合这些操作import albumentations as A import cv2 import numpy as np train_transform A.Compose([ A.RandomRotate90(p0.5), A.ShiftScaleRotate(shift_limit0.0625, scale_limit0.1, rotate_limit15, p0.5), A.ElasticTransform(alpha1, sigma50, p0.3), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.3), A.GaussNoise(var_limit(10.0, 50.0), p0.2), ]) def apply_transform(image, mask): augmented train_transform(imageimage, maskmask) return augmented[image], augmented[mask]参数说明RandomRotate90做 90 度整数倍旋转适合脑部切片的对称性ShiftScaleRotate做平移、缩放和 ±15 度旋转模拟不同扫描角度ElasticTransform做弹性变形模拟组织形变alpha控制变形强度sigma控制平滑程度RandomBrightnessContrast模拟不同扫描仪的亮度差异GaussNoise模拟噪声。注意增强只对训练集做验证集和测试集保持原始分布否则评估指标会虚高。3.3 用 DataLoader 组装训练批次把切片数据封装成 PyTorch Dataset配合 DataLoader 实现批量加载import torch from torch.utils.data import Dataset, DataLoader import numpy as np import os class BrainTumorDataset(Dataset): def __init__(self, image_dir, label_dir, transformNone): self.image_dir image_dir self.label_dir label_dir self.transform transform self.files sorted(os.listdir(image_dir)) def __len__(self): return len(self.files) def __getitem__(self, idx): fname self.files[idx] image np.load(os.path.join(self.image_dir, fname)) label np.load(os.path.join(self.label_dir, fname)) if self.transform: image, label self.transform(image, label) image torch.from_numpy(image).unsqueeze(0).float() label torch.from_numpy(label).unsqueeze(0).float() return image, label train_dataset BrainTumorDataset(slices_2d/images, slices_2d/labels, apply_transform) train_loader DataLoader(train_dataset, batch_size16, shuffleTrue, num_workers4, pin_memoryTrue)__getitem__返回的是(1, H, W)的单通道张量因为 MRI 切片是灰度图。batch_size16适合 256×256 的切片和 8GB 显存的显卡如果显存不够降到 8 或 4。num_workers4根据 CPU 核心数调整pin_memoryTrue在 GPU 训练时能加速数据传输。4. 可视化代码把分割结果叠回 MRI 上看4.1 单张切片的预测结果叠加显示训练完模型后最直观的验证方式是把预测掩码叠在原始 MRI 上。下面的代码用 matplotlib 实现三栏对比原图、真实标签、预测结果。import matplotlib.pyplot as plt import numpy as np def visualize_prediction(image, gt_mask, pred_mask, save_pathNone): fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(image, cmapgray) axes[0].set_title(MRI 原图) axes[0].axis(off) axes[1].imshow(image, cmapgray) axes[1].imshow(gt_mask, cmapReds, alpha0.5) axes[1].set_title(真实标签) axes[1].axis(off) axes[2].imshow(image, cmapgray) axes[2].imshow(pred_mask, cmapBlues, alpha0.5) axes[2].set_title(模型预测) axes[2].axis(off) plt.tight_layout() if save_path: plt.savefig(save_path, dpi150, bbox_inchestight) plt.show()alpha0.5控制掩码的透明度太大会遮住 MRI 的纹理细节太小看不清边界。真实标签用红色预测结果用蓝色两者重叠的区域会呈现紫色方便快速判断漏检和误检。如果预测掩码是 logits需要先过 sigmoid 再二值化pred_mask (torch.sigmoid(logits) 0.5).cpu().numpy()。4.2 3D 体数据的多切片网格展示单张切片只能看局部要评估整个肿瘤的形态需要把多个连续切片拼成网格import matplotlib.pyplot as plt import numpy as np def visualize_volume_slices(volume, mask, num_slices16, cols4, save_pathNone): z_indices np.linspace(0, volume.shape[2] - 1, num_slices, dtypeint) rows (num_slices cols - 1) // cols fig, axes plt.subplots(rows, cols, figsize(cols * 3, rows * 3)) for i, z in enumerate(z_indices): ax axes[i // cols, i % cols] ax.imshow(volume[:, :, z], cmapgray) ax.imshow(mask[:, :, z], cmapReds, alpha0.4) ax.set_title(fSlice {z}) ax.axis(off) plt.tight_layout() if save_path: plt.savefig(save_path, dpi150, bbox_inchestight) plt.show()num_slices控制展示的切片数量cols控制每行显示几列。np.linspace在 0 到最大层数之间均匀取点保证覆盖整个肿瘤区域。这种网格图适合放在论文或报告里一眼就能看出肿瘤在三维空间中的分布范围。4.3 评估指标的计算与可视化分割任务常用的评估指标是 Dice 系数和 IoU。下面的代码计算这两个指标并画出柱状图import numpy as np import matplotlib.pyplot as plt def dice_coefficient(pred, target, smooth1e-6): pred pred.flatten() target target.flatten() intersection (pred * target).sum() return (2. * intersection smooth) / (pred.sum() target.sum() smooth) def iou_score(pred, target, smooth1e-6): pred pred.flatten() target target.flatten() intersection (pred * target).sum() union pred.sum() target.sum() - intersection return (intersection smooth) / (union smooth) dice_scores [] iou_scores [] for image, gt, pred in test_samples: dice_scores.append(dice_coefficient(pred, gt)) iou_scores.append(iou_score(pred, gt)) fig, ax plt.subplots(figsize(8, 5)) x np.arange(len(dice_scores)) ax.bar(x - 0.2, dice_scores, width0.4, labelDice) ax.bar(x 0.2, iou_scores, width0.4, labelIoU) ax.set_xlabel(测试样本) ax.set_ylabel(分数) ax.legend() plt.show()smooth1e-6防止分母为零。Dice 系数对类别不平衡不敏感适合肿瘤分割这种前景占比小的任务。如果 Dice 低于 0.6说明模型欠拟合或数据增强太激进如果 Dice 高于 0.9 但视觉效果很差可能是过拟合或标签泄漏。5. 避坑与排查脑肿瘤分割里最容易翻车的 5 个地方5.1 现象训练 loss 正常下降但 Dice 一直上不去原因标签编码方式和损失函数不匹配。比如标签是 0/255但损失函数按 0/1 计算导致前景像素的梯度被放大 255 倍模型实际上在优化一个错误的 objective。解决在 Dataset 的__getitem__里统一做label (label 0).astype(np.float32)确保标签是 0/1。如果已经用了 0/255 的标签训练了一段时间检查损失函数是否用了BCEWithLogitsLoss的pos_weight参数这个参数需要按前景和背景的比例设置而不是按像素值大小。5.2 现象可视化时掩码和肿瘤位置对不上偏移几毫米原因影像和标签的 affine 矩阵不一致或者切片时用了不同的方向比如一个沿轴位切一个沿冠状位切。解决用nib.aff2axcodes(img.affine)检查两者的方向编码是否一致。如果不一致用nib.as_closest_canonical()统一到 RAS 方向。切片时确保 image 和 label 用同一个z索引不要一个用img.shape[2]一个用label.shape[2]。5.3 现象验证集 Dice 很高但测试集一塌糊涂原因数据泄漏。同一个患者的不同切片被分到了训练集和验证集模型记住了患者的特征而不是肿瘤的特征。解决按患者 ID 划分数据集而不是按切片。在splits/里写患者 ID加载时先根据患者 ID 过滤切片。如果数据集没有提供患者 ID从文件名里提取比如patient_001_z005.npy里的patient_001。5.4 现象训练时显存溢出batch size 降到 1 还是 OOM原因切片分辨率太高或者模型用了 3D 卷积。256×256 的切片用 2D U-Net 一般 8GB 显存能跑 batch size 8如果 OOM 说明分辨率可能是 512×512 或者模型参数量太大。解决先把切片 resize 到 256×256 或 128×128用cv2.resize或torch.nn.functional.interpolate。如果必须用高分辨率改用梯度累积loss loss / accumulation_steps每accumulation_steps个 batch 才optimizer.step()。5.5 现象预测结果全是背景或全是前景原因类别极度不平衡导致模型退化为预测多数类。如果前景占比只有 0.5%模型把所有像素预测为背景就能达到 99.5% 的准确率但 Dice 为 0。解决换损失函数。BCEWithLogitsLoss加pos_weight参数或者直接用DiceLoss。Dice Loss 的梯度只关注预测和真实的重叠区域对类别不平衡不敏感。如果用了 Dice Loss 还是全背景检查学习率是不是太大模型在初期就陷入了局部最优。6. 进阶技巧用滑窗推理处理大体积数据与模型集成6.1 滑窗推理解决显存不够又想用高分辨率的问题训练时可以把切片 resize 到小尺寸但推理时如果直接 resize 回去小肿瘤的边界会糊掉。滑窗推理的思路是在原始分辨率的切片上开一个固定大小的窗口逐窗口预测再把结果拼回完整切片。import torch import numpy as np def sliding_window_inference(model, image, window_size256, stride128, devicecuda): model.eval() h, w image.shape output np.zeros((h, w), dtypenp.float32) count np.zeros((h, w), dtypenp.float32) for y in range(0, h - window_size 1, stride): for x in range(0, w - window_size 1, stride): patch image[y:ywindow_size, x:xwindow_size] patch_tensor torch.from_numpy(patch).unsqueeze(0).unsqueeze(0).float().to(device) with torch.no_grad(): pred torch.sigmoid(model(patch_tensor)).cpu().numpy()[0, 0] output[y:ywindow_size, x:xwindow_size] pred count[y:ywindow_size, x:xwindow_size] 1 output output / (count 1e-8) return (output 0.5).astype(np.uint8)window_size是滑窗大小stride是步长。stride小于window_size时窗口之间有重叠重叠区域的预测结果取平均能减少边界效应。stride一般设为window_size的一半比如 256 的窗口用 128 的步长。如果图像尺寸不能被步长整除最后一行和最后一列需要单独处理或者先 padding 到整除。6.2 模型集成用多个模型的投票提升 Dice单个模型的预测可能有随机性把 3 到 5 个不同初始化或不同架构的模型预测结果做平均通常能提升 1 到 3 个 Dice 点。def ensemble_predict(models, image, devicecuda): preds [] for model in models: model.eval() with torch.no_grad(): tensor torch.from_numpy(image).unsqueeze(0).unsqueeze(0).float().to(device) pred torch.sigmoid(model(tensor)).cpu().numpy()[0, 0] preds.append(pred) avg_pred np.mean(preds, axis0) return (avg_pred 0.5).astype(np.uint8)集成时要注意模型之间的多样性可以用不同的随机种子训练同一个架构也可以用 U-Net、DeepLab、SegFormer 等不同架构。如果所有模型都在同一个训练集上过拟合集成效果有限。常见做法是每个模型用不同的数据增强组合或者用交叉验证的方式训练 5 折模型。6.3 后处理去掉小连通域和填充空洞模型预测的掩码经常有一些孤立的噪点或内部空洞用形态学操作可以清理import cv2 import numpy as np def postprocess_mask(mask, min_area50): mask mask.astype(np.uint8) num_labels, labels, stats, _ cv2.connectedComponentsWithStats(mask, connectivity8) clean_mask np.zeros_like(mask) for i in range(1, num_labels): if stats[i, cv2.CC_STAT_AREA] min_area: clean_mask[labels i] 1 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) clean_mask cv2.morphologyEx(clean_mask, cv2.MORPH_CLOSE, kernel) return clean_maskmin_area控制最小连通域面积小于这个面积的区域被认为是噪声。cv2.MORPH_CLOSE做闭运算填充掩码内部的小空洞。kernel大小根据肿瘤的典型尺寸调整5×5 适合 256×256 的切片如果切片更大可以用 7×7 或 9×9。后处理不是必须的但如果你的 Dice 卡在 0.85 上不去试试加这一步有时候能白捡 1 到 2 个点。我自己做脑肿瘤分割的习惯是先把数据集的标签编码和 affine 对齐检查一遍这一步花 10 分钟能省掉后面 10 小时的排查。然后切片、归一化、训一个 baseline U-NetDice 到 0.8 左右再考虑加增强和集成。别一上来就堆模型数据没理顺再好的架构也是白搭。希望帮到你。本文还有配套的精品资源点击获取