简介:本资源面向医学图像分割方向的初学者与算法实践者,提供一套乳腺细胞癌症分割的二分类数据集,可用于训练与验证语义分割网络,帮助解决医学影像中病灶区域自动提取的入门实验需求。压缩包共103个文件,以png图像为主(101个),另含1个txt类别说明与1个py可视化脚本,整体约23.91MB,采用7z格式打包。数据分为训练集与测试集:训练集含40张原图及40张对应mask,测试集含10张原图及10张mask,图像分辨率统一为512×512,mask为0与255的阈值图像,0代表背景、255代表癌症细胞,具体类别可在classes文本中查看。配套脚本无需修改即可直接运行,随机抽取一张图片,将原始图像、GT图像以及GT在原图上的蒙板结果一并展示并保存至当前目录,便于直观核对标注质量。目前已有207人学习下载,适合作为分割模型训练、掩膜可视化与数据预处理的练手素材。
1. 乳腺细胞癌症分割数据集:50 张图里藏着医学图像分割的完整闭环
医学图像分割入门最卡脖子的从来不是模型结构,而是找不到一份干净、带标注、能直接跑通的小数据集。公开的息肉分割、皮肤病变数据集动辄几百上千张,下载慢、标注格式杂,光做数据清洗就能劝退一批人。这份乳腺细胞癌症分割数据集走的是另一条路:50 张 512×512 的 PNG 图像,训练集 40 张、测试集 10 张,每张图配一张同尺寸的 mask 标签,像素值只有 0 和 255 两种,0 是背景、255 是癌症细胞。它小到能在笔记本上几分钟跑完一轮,又完整到包含 images、masks、classes 文本和一份可视化脚本,适合刚接触医学图像分割、想先把「读数据—看标签—训模型—验结果」这条链路走通的人。下面按我实际拆包的顺序讲清楚它怎么用、参数怎么设、哪里容易翻车。
2. 拆开数据包:目录结构、标签语义与可视化脚本怎么跑
2.1 目录结构与文件命名规律
拿到压缩包解压后,常见的目录组织是这样:
breast_cancer_seg/ ├── train/ │ ├── images/ # 40 张原图,png │ └── masks/ # 40 张对应 mask,png ├── test/ │ ├── images/ # 10 张原图 │ └── masks/ # 10 张对应 mask ├── classes.txt # 标签类别说明 └── visualize.py # 可视化脚本文件名是07_1.png、04_8.png、10_1.png这种「编号_序号」的形式,images 和 masks 两个目录里同名文件一一对应。这一点很关键:做分割训练时,图像和标签必须靠文件名配对,任何一张图找不到同名 mask,训练时就会报FileNotFoundError或者更隐蔽地读到上一批缓存。我一般会先跑一遍配对检查,确认两边文件名集合完全相等再往下走。
import os img_dir = "train/images" mask_dir = "train/masks" imgs = {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(".png")} masks = {os.path.splitext(f)[0] for f in os.listdir(mask_dir) if f.endswith(".png")} print("仅图片有:", imgs - masks) print("仅标签有:", masks - imgs) print("配对成功:", len(imgs & masks))这段代码用集合差集找出单边存在的文件。os.path.splitext去掉扩展名后比较,避免.png大小写或多余后缀干扰。正常输出应该是两个空集合加一个 40。如果差集非空,先手动补齐或删掉孤立文件,别带着脏数据进训练。
2.2 mask 的像素语义与 classes.txt
这份数据集的 mask 是单通道阈值图,像素分布只有 0 和 255。0 对应背景,255 对应癌症细胞区域。classes.txt里记录了具体类别,常见是两行,一行背景一行前景。这里有个新手最容易踩的点:很多人拿到 mask 直接除以 255 当标签,结果发现模型输出全是 0。原因是 255/255=1,看起来没问题,但如果用某些框架的CrossEntropyLoss,它期望的是类别索引 0/1 的整型张量,而不是 0/1 的浮点。正确做法是先确认任务类型——二分类分割用BCEWithLogitsLoss配 0/1 浮点标签,多分类用CrossEntropyLoss配 0/1 整型标签。
import numpy as np from PIL import Image mask = np.array(Image.open("train/masks/07_1.png")) print("shape:", mask.shape) # (512, 512) 或 (512, 512, 3) print("unique:", np.unique(mask)) # 期望 [0, 255] print("前景像素占比:", (mask == 255).mean())np.unique是验证标签语义最快的手段。如果输出里出现 128、200 这类中间值,说明 mask 被做过抗锯齿或压缩,需要重新二值化。前景占比能帮你判断样本是否极度不平衡——乳腺细胞分割里前景往往只占几个百分点,后面选损失函数时要考虑这一点。
2.3 可视化脚本的运行与输出解读
数据集自带的visualize.py是随机抽一张图,把原图、GT 掩膜、GT 叠加在原图上的蒙版三张图并排展示,并保存到当前目录。脚本不需要改路径就能跑,前提是你解压后直接在数据集根目录执行。
cd breast_cancer_seg python visualize.py运行后当前目录会多出一张拼接图。三栏分别是:原始 RGB 图像、纯黑白 mask、红色半透明叠加图。叠加图是判断标注质量的关键——如果红色区域明显溢出到细胞外的空白区,或者漏掉了明显的细胞团,说明这份标注本身有噪声,训练时要么接受这个上限,要么手动修几张。我一般会连跑几次脚本,随机多看几张,对整体标注风格有个印象再开训。脚本里通常用matplotlib的subplot做拼接,alpha参数控制叠加透明度,想调得更清楚可以把它从 0.5 改到 0.3。
3. 把数据喂进模型:从 Dataset 到训练循环的落地写法
3.1 自定义 Dataset 的同步增强
50 张图属于极小样本,直接训必然过拟合。常见做法是在 Dataset 里做在线增强,但分割任务有个铁律:图像和 mask 必须用同一组随机参数做几何变换,否则标签就错位了。用albumentations可以一次性同步处理。
import cv2 import numpy as np import albumentations as A from torch.utils.data import Dataset class BreastSegDataset(Dataset): def __init__(self, img_dir, mask_dir, size=512): self.img_dir = img_dir self.mask_dir = mask_dir self.names = sorted(os.listdir(img_dir)) self.tf = A.Compose([ A.Resize(size, size), A.HorizontalFlip(p=0.5), A.VerticalFlip(p=0.5), A.RandomRotate90(p=0.5), A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=15, p=0.5), ]) def __len__(self): return len(self.names) def __getitem__(self, idx): name = self.names[idx] img = cv2.imread(os.path.join(self.img_dir, name)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask = cv2.imread(os.path.join(self.mask_dir, name), 0) mask = (mask > 127).astype(np.float32) # 二值化到 0/1 out = self.tf(image=img, mask=mask) img = out["image"].astype(np.float32) / 255.0 img = img.transpose(2, 0, 1) # HWC -> CHW return torch.from_numpy(img), torch.from_numpy(out["mask"]).unsqueeze(0)A.Compose里的几何变换会同时作用于 image 和 mask,这是分割增强和分类增强最大的区别。mask > 127把 255 压成 1,同时容忍轻微压缩噪声。transpose(2,0,1)是 PyTorch 要求的通道前置格式,忘了这步会报维度不匹配。unsqueeze(0)给 mask 补一个通道维,配合BCEWithLogitsLoss使用。
3.2 损失函数与评价指标的选择
前景占比低的时候,纯 BCE 容易被背景主导,模型学会全预测 0 就能拿到很高的像素准确率,但 IoU 惨不忍睹。常见做法是 BCE 加 Dice 的混合损失。
import torch import torch.nn as nn class BCEDiceLoss(nn.Module): def __init__(self, bce_weight=0.5): super().__init__() self.bce = nn.BCEWithLogitsLoss() self.bce_weight = bce_weight def forward(self, logits, targets): bce_loss = self.bce(logits, targets) probs = torch.sigmoid(logits) inter = (probs * targets).sum(dim=(2, 3)) union = probs.sum(dim=(2, 3)) + targets.sum(dim=(2, 3)) dice = (2 * inter + 1e-6) / (union + 1e-6) dice_loss = 1 - dice.mean() return self.bce_weight * bce_loss + (1 - self.bce_weight) * dice_lossbce_weight控制两项的配比,0.5 是常用起点。Dice 里的1e-6防止空 mask 导致除零。评价指标建议直接算 IoU 和 Dice,别只看 loss,因为 loss 下降不代表分割边界变好。验证时把torch.sigmoid(logits) > 0.5作为预测掩膜,和 GT 比。
3.3 训练循环与显存控制
512×512 的图,batch size 设 4 在 8G 显存上比较稳,设 8 可能就 OOM。训练轮数不用太多,50 张图 100 到 200 轮足够看到收敛趋势。
from torch.utils.data import DataLoader import torch.optim as optim train_ds = BreastSegDataset("train/images", "train/masks") train_loader = DataLoader(train_ds, batch_size=4, shuffle=True, num_workers=2) model = ... # 常见选 U-Net 或轻量 DeepLabV3+ model = model.cuda() criterion = BCEDiceLoss().cuda() optimizer = optim.Adam(model.parameters(), lr=1e-3) for epoch in range(150): model.train() for img, mask in train_loader: img, mask = img.cuda(), mask.cuda() optimizer.zero_grad() logits = model(img) loss = criterion(logits, mask) loss.backward() optimizer.step()num_workers=2在 Windows 上偶尔会卡死,遇到就改成 0。学习率 1e-3 配 Adam 是安全起点,如果 loss 震荡就降到 3e-4。每 10 轮存一次权重,小数据集上过拟合来得快,早停比硬训到底更划算。
4. 避坑与排查:这份数据集上最容易翻车的五件事
4.1 现象:训练 loss 正常下降,但预测全是黑图
原因通常是标签没二值化,mask 读进来还是 0/255,BCEWithLogitsLoss把 255 当成极大正样本,梯度爆炸后模型输出饱和。解决:在 Dataset 里强制(mask > 127).astype(np.float32),并在第一个 batch 打印mask.max()确认是 1.0。
4.2 现象:可视化脚本报FileNotFoundError
原因是没在数据集根目录执行,脚本里用的是相对路径。解决:cd到解压后的根目录再跑,或者把脚本里的路径改成绝对路径。别在别的目录用python /path/to/visualize.py,相对路径会以当前工作目录为基准。
4.3 现象:图像和 mask 尺寸对不上,报维度错误
512×512 是标准尺寸,但个别图可能被工具重新保存成 513 或 511。解决:在 Dataset 里统一A.Resize(512, 512),别依赖原始尺寸一致。同时检查 mask 是不是三通道,三通道 mask 要取单通道再二值化。
4.4 现象:验证集 IoU 远低于训练集
50 张图里训练 40、测试 10,测试集太小,单张图波动就能让指标跳十几个点。解决:别把测试集当唯一标准,从训练集里再切 8 张做验证,或者做 5 折交叉验证。小数据集上报告单一 IoU 数字意义有限,多看几张预测叠加图更实在。
4.5 现象:增强后 mask 出现灰色边缘
原因是用了带插值的几何变换,mask 被双线性插值出中间值。解决:几何变换对 mask 用最近邻插值,albumentations默认对 mask 就是最近邻,但如果你手动用cv2.resize就要显式指定interpolation=cv2.INTER_NEAREST。
5. 小数据集的进阶玩法:交叉验证、伪标签与边界后处理
50 张图想榨出更多信息,单次训练切分太浪费。我一般会做 5 折交叉验证:把 50 张图分成 5 份,每份 10 张轮流当验证集,其余 40 张训练。这样每个样本都参与过验证,指标更稳,还能顺带产出 5 个模型做集成。集成时把 5 个模型的 sigmoid 输出平均再阈值化,边界通常比单模型干净。
import numpy as np def ensemble_predict(models, img): probs = [torch.sigmoid(m(img)).cpu().numpy() for m in models] mean_prob = np.mean(probs, axis=0) return (mean_prob > 0.5).astype(np.uint8)阈值 0.5 不是金科玉律。医学分割里如果更怕漏检,把阈值降到 0.3 能提高召回,代价是误检增多。可以画一条不同阈值下的 Dice 曲线,挑拐点。
另一个技巧是伪标签:用训练好的模型在测试集 10 张图上预测,把置信度高的预测当额外标签,和原训练集合并再训一轮。小数据集上这招偶尔能涨一两个点,但前提是初始模型别太差,否则错误会被放大。我一般只在 Dice 已经过 0.7 之后才尝试。
边界后处理也值得一试。预测出的 mask 常有孤立小噪点,用形态学开运算去掉,再保留最大连通域,能明显改善视觉效果。
import cv2 def postprocess(mask): kernel = np.ones((3, 3), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) num, labels, stats, _ = cv2.connectedComponentsWithStats(mask) if num > 1: largest = 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA]) mask = (labels == largest).astype(np.uint8) return maskMORPH_OPEN先腐蚀后膨胀,去掉小噪点。connectedComponentsWithStats找出所有连通域,保留面积最大的那个。注意如果一张图里确实有多个独立细胞团,这步会误删,所以先看几张可视化结果再决定要不要加。
从那以后我每次拿到新的分割数据集,都强制先跑一遍配对检查、np.unique看标签、可视化脚本随机抽三张,这三步走完再写训练代码。这份乳腺细胞癌症分割数据集规模小、结构清晰,正好适合把这套流程固化成习惯。希望帮到你。
本文还有配套的精品资源,点击获取