简介:本资源面向医学图像分割方向的初学者与算法实践者,提供一套416×416分辨率的人脑MRI肿瘤二分割数据集,前景为Tumor区域,mask以1表示肿瘤、0表示背景,标注质量良好、背景简洁,适合直接用于U-Net等分割模型的训练与验证。压缩包共2000个文件,以1759个png图像与掩膜、240个jpg样本及1个Python可视化脚本为主,整体约48.17MB,采用7z格式打包。数据按训练集与测试集划分:训练集含1632张原图及1632张对应mask,测试集含240张原图及240张mask,目录结构清晰,便于按images与masks配对读取。配套可视化脚本可随机抽取一张图片,同时展示原始图像、GT图像以及GT在原图上的蒙板叠加效果,并自动保存到当前目录,方便快速核验标注质量与分割结果。目前已有1880人学习下载,适合需要快速搭建脑肿瘤分割实验基线、验证模型效果的读者参考使用。
1. 大脑肿瘤分割数据集:从「2分割」标签到可视化,一套能直接跑通的方案
拿到一个医学图像分割数据集,最怕的不是模型跑不动,而是标签对不上、可视化出来一片黑、类别编号和掩码像素值错位。大脑肿瘤分割(2分割)这个方向,核心就三件事:图像与掩码严格配对、前景背景二分类的标签约定、以及把分割结果叠回原图做可视化验证。它适合两类人:一类是想入门医学图像分割但被 BraTS 那套多模态、多区域标签劝退的工程师;另一类是要快速验证某个分割网络(U-Net、SegFormer、nnU-Net 都行)在脑肿瘤二分类任务上到底能不能收敛的从业者。这篇不讲空泛的「医学 AI 前景」,只讲这个数据集怎么读、标签怎么定、可视化代码怎么写、训练时哪些参数必须调,以及我踩过的那些坑。
2. 先搞清楚「2分割」到底分的是什么:标签约定与数据组织
2.1 二分类掩码的像素值约定,别想当然
医学图像分割里,「2分割」通常指前景(肿瘤)和背景两类,但落到掩码文件上,不同来源的约定完全不一样。常见的有三种:掩码是 0/1 的 uint8、0/255 的 uint8、或者 0/1 的 float。如果你直接拿mask.max()当类别数,遇到 0/255 的掩码就会以为有 256 类,训练直接崩。
我一般拿到数据先做一次「体检」,把每张掩码的唯一值和占比打出来,这一步能省掉后面几小时的 debug:
import numpy as np import os from pathlib import Path def inspect_masks(mask_dir, sample_n=20): """抽查掩码文件的像素值分布,确认标签约定""" mask_paths = sorted(Path(mask_dir).glob("*.png"))[:sample_n] for p in mask_paths: m = np.array(__import__("PIL.Image").Image.open(p)) uniq, counts = np.unique(m, return_counts=True) ratio = counts / m.size # 打印唯一值和前景占比,前景占比过小要警惕 print(p.name, dict(zip(uniq.tolist(), np.round(ratio, 4).tolist()))) inspect_masks("./data/masks")逻辑说明:np.unique拿到掩码里所有出现过的像素值,return_counts给出每个值的像素数,除以总像素数就是占比。参数上,sample_n抽 20 张足够判断约定,不用全量跑。如果输出里出现 255 且占比和 1 差不多,说明是 0/255 约定,训练前必须除以 255 或做阈值化;如果只有 0 和 1,那可以直接当类别索引用。这一步不做,后面损失函数算出来的值会莫名其妙偏大或梯度爆炸。
2.2 图像与掩码的配对规则:文件名、尺寸、模态三对齐
配对出问题是最隐蔽的翻车点。常见做法是图像和掩码同名不同目录,比如images/case_001.png对masks/case_001.png。但有些数据集图像是.png、掩码是.tif,或者图像带_img后缀、掩码带_mask后缀。我一般写一个配对检查函数,把「有图无掩码」「有掩码无图」「尺寸不一致」三类问题一次性列出来:
from PIL import Image def check_pairing(img_dir, mask_dir): img_names = {p.stem for p in Path(img_dir).glob("*")} mask_names = {p.stem for p in Path(mask_dir).glob("*")} only_img = img_names - mask_names only_mask = mask_names - img_names print("仅有图像:", len(only_img), list(only_img)[:5]) print("仅有掩码:", len(only_mask), list(only_mask)[:5]) # 尺寸对齐检查 for name in list(img_names & mask_names)[:10]: img = Image.open(next(Path(img_dir).glob(f"{name}.*"))) msk = Image.open(next(Path(mask_dir).glob(f"{name}.*"))) if img.size != msk.size: print("尺寸不一致:", name, img.size, msk.size) check_pairing("./data/images", "./data/masks")逻辑说明:用stem(去掉扩展名)做集合运算,能同时兼容不同扩展名。尺寸检查只抽前 10 对,因为尺寸问题通常是全局性的,抽几对就能发现。参数上,如果你的数据是 3D 切片导出的 2D 图,还要额外确认切片顺序一致,否则会出现「图像是第 50 层、掩码是第 51 层」这种错位,模型学出来的边界永远是糊的。
2.3 数据集划分:别用随机划分骗自己
医学图像分割的划分有个血泪经验:同一个病人的不同切片必须落在同一个集合里。如果你按切片随机划分,训练集和验证集会共享同一个病人的相邻切片,验证指标虚高得离谱,上线就翻车。常见做法是按病人 ID 分组划分,7:1:2 或 8:1:1。如果数据集没给病人 ID,至少按文件名前缀分组。我一般会写一个分组划分脚本,把分组键提取出来再做GroupShuffleSplit或手写划分,确保验证集是「没见过的人」而不是「没见过的切片」。
3. 把数据喂进模型:Dataset 写法与三个必调参数
3.1 一个能直接用的 PyTorch Dataset
下面这个 Dataset 覆盖了读取、归一化、掩码二值化、增强接口,可以直接抄:
import torch from torch.utils.data import Dataset import numpy as np from PIL import Image import albumentations as A from albumentations.pytorch import ToTensorV2 class BrainTumorDataset(Dataset): def __init__(self, img_dir, mask_dir, img_size=256, train=True): self.img_paths = sorted(Path(img_dir).glob("*")) self.mask_dir = Path(mask_dir) self.img_size = img_size # 训练用增强,验证只做 resize 和归一化 if train: self.tf = A.Compose([ A.Resize(img_size, img_size), A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.3), A.Normalize(mean=(0.5,), std=(0.5,)), ToTensorV2(), ]) else: self.tf = A.Compose([ A.Resize(img_size, img_size), A.Normalize(mean=(0.5,), std=(0.5,)), ToTensorV2(), ]) def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img_path = self.img_paths[idx] img = np.array(Image.open(img_path).convert("L")) # 灰度,按需改 RGB mask_path = self.mask_dir / img_path.name mask = np.array(Image.open(mask_path).convert("L")) mask = (mask > 127).astype(np.float32) # 统一成 0/1,阈值 127 兼容 0/255 out = self.tf(image=img, mask=mask) return out["image"], out["mask"].unsqueeze(0) # mask 加通道维逻辑说明:convert("L")把图像转灰度,医学图像多数是单通道,如果你的数据是 RGB 就改掉。掩码用> 127阈值化,这一步同时兼容 0/1 和 0/255 两种约定,是省心的写法。unsqueeze(0)给掩码加通道维,因为后面 BCE 或 Dice 损失通常要求[B, 1, H, W]。参数上,img_size我一般设 256 或 512,脑肿瘤区域相对整图偏小,256 起步够用,显存紧张就降到 224。
3.2 归一化参数:别照搬 ImageNet 的 mean/std
医学图像和自然图像分布差很远,照搬 ImageNet 的mean=(0.485,0.456,0.406)是常见误用。灰度医学图我一般用mean=0.5, std=0.5做简单归一化,或者先统计训练集的全局均值和方差再填进去。如果你做的是 CT,还要注意窗宽窗位,HU 值范围可能到 -1000 到 3000,直接归一化会把软组织压成一团。常见做法是先做窗宽窗位截断(比如脑窗 0~80 HU),再归一化。
3.3 损失函数与类别不平衡:Dice 比 BCE 更稳
脑肿瘤在整张图里占比往往很小,前景可能只占 1%~5%,纯 BCE 会被背景主导,模型学会「全预测背景」就能拿到 95% 以上的准确率,但 Dice 接近 0。我一般用BCE + Dice组合,或者直接上DiceLoss。下面是一个能用的 Dice 实现:
import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth=1e-6): super().__init__() self.smooth = smooth def forward(self, logits, targets): probs = torch.sigmoid(logits) probs = probs.view(probs.size(0), -1) targets = targets.view(targets.size(0), -1) inter = (probs * targets).sum(dim=1) union = probs.sum(dim=1) + targets.sum(dim=1) dice = (2 * inter + self.smooth) / (union + self.smooth) return 1 - dice.mean()逻辑说明:smooth防止分母为 0,view把空间维拉平做逐样本计算,最后取 batch 平均。参数上,smooth设 1e-6 足够,设太大会让损失对小的前景不敏感。组合损失一般0.5 * BCE + 0.5 * Dice,如果前景特别小,把 Dice 权重提到 0.7。
4. 可视化代码:把分割结果叠回原图才算数
4.1 三通道叠加可视化,一眼看出边界对不对
只看 Dice 数字不够,必须把预测掩码叠回原图。下面这段代码把原图、真值、预测做成红绿叠加,边界错位一眼可见:
import matplotlib.pyplot as plt import numpy as np def visualize(img, gt_mask, pred_mask, save_path=None): """img: [H,W] 或 [H,W,3]; gt/pred: [H,W] 0/1""" img = img.squeeze() if img.ndim == 2: img = np.stack([img] * 3, axis=-1) img = (img - img.min()) / (img.max() - img.min() + 1e-8) overlay = img.copy() # 真值用绿色,预测用红色,重叠区域偏黄 overlay[..., 1] = np.where(gt_mask > 0, 1.0, overlay[..., 1]) overlay[..., 0] = np.where(pred_mask > 0, 1.0, overlay[..., 0]) fig, axes = plt.subplots(1, 3, figsize=(12, 4)) axes[0].imshow(img); axes[0].set_title("image") axes[1].imshow(gt_mask, cmap="gray"); axes[1].set_title("gt") axes[2].imshow(overlay); axes[2].set_title("overlay") for ax in axes: ax.axis("off") if save_path: plt.savefig(save_path, bbox_inches="tight", dpi=150) plt.close()逻辑说明:先把图像归一化到 0~1,再在绿色通道写真值、红色通道写预测,重叠处自然偏黄。参数上,dpi=150够看细节,批量可视化时记得plt.close()防止内存泄漏。这一步做完,你会立刻发现两类问题:预测掩码整体偏移(配准或 resize 问题)、边界系统性外扩(损失函数或阈值问题)。
4.2 批量可视化与指标联动
单张看效率低,我一般写一个批量函数,按 Dice 从低到高排序,优先看最差的 10 张。这样能快速定位是「某些病例特别难」还是「整体都差」。指标计算和可视化联动,是排查问题的标准动作。
5. 避坑与排查:五个真实踩过的坑
5.1 掩码全黑,Dice 恒为 0
现象:训练几个 epoch,损失不降,可视化出来预测全黑。原因:掩码读取时用了convert("L")但原掩码是调色板模式(P 模式),转灰度后前景值被映射成 0。解决:先np.array(Image.open(p))看原始值,如果是 P 模式,用convert("L")前先确认调色板,或者直接读原始数组做阈值化。
5.2 验证指标虚高,上线崩盘
现象:验证集 Dice 0.9+,换一批数据掉到 0.5。原因:按切片随机划分,同一病人的相邻切片泄漏到验证集。解决:按病人 ID 分组划分,确保验证集病人不出现在训练集。这个坑我踩过两次,第二次是因为文件名前缀没提取对,分组键写错了。
5.3 显存爆了,batch size 只能设 2
现象:256×256 的图,batch size 开到 8 就 OOM。原因:模型用了高分辨率特征图 + 大通道数,或者没开混合精度。解决:开torch.cuda.amp混合精度,显存能省 30%~50%;或者把img_size降到 224,再不行用梯度累积模拟大 batch。
5.4 增强把肿瘤「翻」没了
现象:训练时损失震荡,可视化发现有些样本掩码和图像对不上。原因:用了A.HorizontalFlip但图像和掩码的增强参数没同步,或者用了随机旋转后掩码插值用了双线性导致标签被平滑。解决:albumentations 的Compose会同步 image 和 mask,但掩码插值必须用最近邻,A.Resize默认对 mask 用最近邻,自定义增强时要显式指定interpolation=cv2.INTER_NEAREST。
5.5 阈值 0.5 不是万能药
现象:Dice 卡在 0.7 上不去,调阈值到 0.3 后涨到 0.78。原因:模型输出的概率分布偏保守,前景概率普遍偏低。解决:在验证集上扫一遍阈值(0.1~0.9),选 Dice 最高的那个,再固定到测试集用。这个操作不复杂,但很多人忘了做。
6. 进阶技巧:用 TTA 和阈值搜索把 Dice 再抬一截
训练跑通、可视化正常之后,想再榨一点指标,我一般做两件事:测试时增强(TTA)和阈值搜索。TTA 的做法是对同一张图做水平翻转、多尺度缩放,分别推理后把概率图平均,再阈值化。多尺度我一般用 0.75、1.0、1.25 三档,水平翻转加上就是 6 次推理,推理时间涨 6 倍但 Dice 通常能涨 1~3 个点,对小目标分割尤其明显。
阈值搜索和 TTA 要联动做:先对验证集做 TTA 得到平均概率图,再在 0.1 到 0.9 之间以 0.05 为步长扫阈值,记录每个阈值下的 Dice,选最高的。注意阈值要在验证集上选,不能拿测试集调,否则指标不可信。下面是一个简化的 TTA + 阈值搜索骨架:
def tta_predict(model, img_tensor): """img_tensor: [1,C,H,W],返回平均概率图""" probs = [] for scale in [0.75, 1.0, 1.25]: for flip in [False, True]: x = torch.nn.functional.interpolate( img_tensor, scale_factor=scale, mode="bilinear", align_corners=False) if flip: x = torch.flip(x, dims=[3]) with torch.no_grad(): p = torch.sigmoid(model(x)) if flip: p = torch.flip(p, dims=[3]) p = torch.nn.functional.interpolate( p, size=img_tensor.shape[-2:], mode="bilinear", align_corners=False) probs.append(p) return torch.stack(probs).mean(dim=0) def search_threshold(probs, gts, step=0.05): best_t, best_d = 0.5, 0.0 for t in np.arange(0.1, 0.95, step): d = dice_score((probs > t).float(), gts) if d > best_d: best_t, best_d = t, d return best_t, best_d逻辑说明:tta_predict对每个尺度和翻转组合推理,翻转后再翻回来保证空间对齐,最后插值回原尺寸取平均。search_threshold遍历阈值找最优。参数上,尺度档位别设太多,3 档够用,太多收益递减还拖慢推理。这套组合我在几个二分类分割任务上试过,稳定涨点,但前提是基础模型已经收敛,否则 TTA 只是把噪声平均了一下。
最后说个习惯:每次拿到新数据集,我第一件事不是写模型,而是花半小时做数据体检——掩码唯一值、配对情况、前景占比、尺寸分布。这半小时能省掉后面一整天。医学图像分割没有玄学,坑都在数据里。希望帮到你。
本文还有配套的精品资源,点击获取