
简介面向乳腺肿瘤细胞核分割任务的医学图像数据集适用于医学影像分割、深度学习实训与算法验证。原始图像为 256×256 或 1000×1000 的 PNG掩膜为 0/1 阈值图0 为背景、1 为肿瘤细胞核类别说明写在 classes.txt 中便于解析标签图像文件沿用 TCGA 病理数据命名规则贴近真实组织切片场景。整包共 167 个文件165 张 PNG、1 个类别文本、1 个数据可视化 Python 脚本压缩后约 62.07MB。数据已按训练集与测试集划分均提供 images 原图与 masks 掩膜目录可直接用于 UNet、DeepLab 等语义分割模型的训练与评估。可视化脚本免配置运行自动抽取原图、GT 掩膜并保存叠加效果便于快速校验标注质量目前已有 194 人学习适合医学图像方向学生、研究人员开展细胞核分割、病灶提取等模型复现与算法对比也可作为课程设计、毕业设计或科研预实验的数据支撑。1. 医学图像分割数据集上手乳腺肿瘤细胞核 2 类分割的三件套做医学图像分割的人拿到一份新数据集第一反应通常是先看 mask 和原图对不对得齐而不是急着搭网络。这个乳腺肿瘤细胞核分割数据集正好把最磨人的环节提前处理好了images 和 masks 目录一一对应mask 是 0/1 的阈值图背景为 0、肿瘤细胞核区域为 1类别定义写进了 classes.txt还附带一个不用改参数就能跑的可视化脚本随机抽图把原始图像、GT 图像、GT 蒙板叠加图画在一起并保存。对正在做细胞核分割、医学图像分割入门复现的从业者来说它解决的是「有数据但标注不透明」的痛点下载下来就能看清标签长什么样、训练集和测试集怎么划分、标注边界是否贴合。用来跑通 U-Net 这类分割基线或者拿来做数据质量评估样本都很合适。2. 数据集结构与标签约定TCGA 命名、PNG 目录与 0/1 mask 的定义医学图像分割数据集最常见的坑不是网络搭不出来而是拿到手之后发现文件名对不上、mask 是多通道的被当单通道读了、类别索引不是从 0 开始。这个数据集在结构上把这些问题规避得比较干净但前提是你得先读懂它的组织方式。下面从命名规则、目录构成、训练测试划分三个角度拆开看。2.1 TCGA 命名规则怎么影响你的预处理文件列表里的名字形如TCGA-HE-7129-01Z-00-DX1.png、TCGA-B0-5711-01Z-00-DX1.png前缀 TCGA 在公开病理切片数据里非常常见代表这批切片的来源遵循统一的编号体系。对实际使用来说你不需要去逐个人工解读每个字段但要知道一个关键后果文件名前缀一样、中间 ID 不同意味着样本来源分散染色风格和切片质量会有差异。批量处理时不要硬编码文件名而应该用 glob 或后缀匹配。import glob train_images sorted(glob.glob(train/images/*.png)) train_masks sorted(glob.glob(train/masks/*.png)) # 通过文件名前缀匹配确保 image 和 mask 一一对应 paired [(img, mask) for img, mask in zip(train_images, train_masks) if img.split(/)[-1] mask.split(/)[-1]] print(f匹配成功 {len(paired)} 对)这段代码的逻辑是先把目录下所有 PNG 按文件名排序再以「同名」为条件配对。参数说明train/images/*.png是原图路径train/masks/*.png是标签路径如果你把数据解压后改了目录名这里要同步改。配对成功后paired列表里的每个元素都是(原图路径, mask路径)后续 Dataset 就直接从它取数据。注意这里假设原图和 mask 的文件名完全一致只是目录不同实际使用时先用os.listdir单独确认一下再动手。2.2 images/masks 目录与 classes.txt 的类别定义数据集的结构很直接训练集下images放原始病理图masks放同名标签图测试集同样两个目录。所有文件都是 PNG 格式图像分辨率包含 256×256 和 1000×1000 两种尺度。mask 是单通道的 0/1 阈值图像素值只有两个0 表示背景1 表示肿瘤细胞核区域。类别明细以数据包里的 classes.txt 文本为准训练前先 cat 一下这个文件确认索引和类别名对应关系。目录内容数量train/images原始病理切片图66 张train/masks对应 0/1 标签图66 张test/images原始病理切片图167 张test/masks对应 0/1 标签图16 张这里有个数字要特别留意测试集 images 有 167 张但 masks 只有 16 张。也就是说测试阶段只有 16 张图有 ground truth 可用于量化评估其余 151 张只能做推理输出、不能算指标。很多人拿到数据后直接把 167 张全跑评估出来的分数异常低就是因为拿没有标签的图跟空白 mask 比了这个放到第 5 章避坑部分详细说。2.3 训练/测试划分66 张与 167 张的规模意味着什么从规模上看训练集 66 对、测试集 167 张原图这是典型的小样本医学图像分割场景。小到你不能直接从头训练一个深网络大到足够跑通一个 U-Net 的完整训练流程。我一般会建议把训练集再切出 10% 当验证集比如 66 张里留 6 张做验证剩下 60 张训练。这么做的好处是 early stopping 有地方挂否则你只能拿那 16 张测试 mask 当验证来回调参容易过拟合到测试集上。另一个值得注意的点是分辨率不统一。256×256 的图和 1000×1000 的图混在同一个数据集里如果你不做任何处理直接进网络Batch 内尺寸不一致会直接报错。常见做法是在 Dataset 里统一 resize 到 256×256这块我在第 4 章给出完整代码。预处理阶段就统一好尺度省得训练时再手忙脚乱。3. 数据可视化脚本复盘原图、GT 与蒙板三视图的验证逻辑数据可视化脚本是这个资源里最容易被忽略但最实用的部分。它做的一件事随机从训练集抽一张图把原始图像、GT 图像、GT 在原图上的蒙板叠加图展示出来并保存到当前目录。你别小看这三张图它直接回答了一个问题——这份标注到底标得干不干净、能不能作为训练目标。脚本不需要改参数就能直接运行但理解它的内部逻辑能帮你把它改造成批量审计工具。3.1 脚本的取图逻辑与三视图排版脚本的核心思路是「随机抽样 三图对比」。它先从训练集图片列表里随机挑一张然后打开对应的原图和 mask用 Matplotlib 排成一行三个子图左边是原始病理图中间是 GT 灰度图右边是原图上叠加半透明 mask 的效果。保存的文件默认放在当前运行目录下文件名一般带时间戳避免覆盖。这段逻辑看起来简单但有一个细节值得注意mask 的像素最大值是 1如果直接用 Matplotlib 的默认 colormap 显示整张图会黑得几乎看不见标注区域。脚本里通常会做一次mask * 255的映射或者用cmapgray加vmin/vmax控制显示范围。你如果自己写可视化代码这个坑最容易踩——原图和 mask 叠加时mask 必须先乘 255 再转成 uint8否则叠加结果是一片黑。常见做法是固定随机种子再跑一次确保每次复现能看到同一张图的对比结果方便讨论和记录。3.2 可视化结果怎么读边缘、空洞与类别失衡三视图拼出来的意义不在于好看而在于快速回答四个问题。第一标注边缘是否贴合细胞核轮廓如果 GT 里出现大量方块状锯齿说明标注是低分辨率重采样出来的训练时你的网络输出也会跟着学出锯齿。第二mask 里有没有空洞细胞核内部如果有小黑点说明原图里核质区域某些像素没被标进去这会影响 Dice 的上限。第三正负样本比例如果整张 mask 里 1 的占比不到 1%训练时模型会倾向把所有像素预测成背景因为这样 loss 已经很低了后面损失函数选型要针对这一点做补偿。第四有没有粘连区域细胞核密集时相邻核的标注边界是否清晰这决定了你的模型需不需要额外做实例分离的后处理。看可视化结果时我会习惯性把窗口放大到 200% 检查边界细节。病理切片本身染色纹理复杂如果 mask 边界跟细胞核膜对不上大概率是标注时用了粗粒度画笔这种数据直接训练会让网络在边界上产生模糊预测后续要做 CRF 后处理才能救回来。3.3 不满足脚本默认输出时怎么改默认脚本是「随机抽一张 保存到当前目录」但实际使用场景往往需要指定某张图。改动很小核心是把随机选择改成按文件名索引import matplotlib.pyplot as plt from PIL import Image import numpy as np import os def visualize_pair(image_path, mask_path, save_pathvis_result.png): img np.array(Image.open(image_path).convert(RGB)) mask np.array(Image.open(mask_path).convert(L)) # mask 取值范围是 0/1叠到原图上时先映射到 0-255 overlay img.copy() mask_uint8 (mask * 255).astype(np.uint8) overlay[:, :, 0] np.where(mask_uint8 0, 255, overlay[:, :, 0]) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img) axes[0].set_title(Original) axes[1].imshow(mask, cmapgray, vmin0, vmax1) axes[1].set_title(GT Mask) axes[2].imshow(overlay) axes[2].set_title(Overlay) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(save_path, dpi150) plt.close()这段代码把脚本的核心逻辑抽成了一个可复用的函数。image_path和mask_path是单张图的路径save_path指定输出位置。参数说明convert(RGB)和convert(L)是保证通道数一致病理图可能有灰度图统一成 RGB 和灰度后避免维度报错overlay的红色通道直接置 255 是最简单的标注可视化方式实际中你也可以换成绿色或黄色。替换掉原来的随机逻辑后你可以对任意一张图生成三视图这比每次重新随机要可控得多。4. 把数据集接进分割训练管线Dataset 改造、损失函数与增强尺度可视化确认没问题之后下一个动作就是把这份数据接进你的训练流程。这里给出三个必做的改造Dataset 类的标准写法、2 类分割的损失函数选型、以及针对混合分辨率的数据增强策略。三件事都做完训练管线才算真正跑通。4.1 通用 Dataset 改造读图、mask 与统一分辨率不管你是用 PyTorch 还是 TensorFlow都绕不开「把磁盘上的 PNG 变成网络输入」。PyTorch 场景下我习惯写一个继承torch.utils.data.Dataset的类核心逻辑是__getitem__里同时读原图和 mask并统一做预处理。有一点强调一下原图可以用双线性插值缩放但 mask 必须用最近邻插值否则缩放过程中 0/1 边界会被平滑出中间值变成 0.4、0.8 这种垃圾标签。import os import numpy as np import torch from torch.utils.data import Dataset from PIL import Image class NucleiSegDataset(Dataset): def __init__(self, image_dir, mask_dir, image_size(256, 256)): self.image_paths sorted([ os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith(.png) ]) self.mask_paths sorted([ os.path.join(mask_dir, f) for f in os.listdir(mask_dir) if f.endswith(.png) ]) self.image_size image_size def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img_path self.image_paths[idx] mask_path self.mask_paths[idx] img Image.open(img_path).convert(RGB) mask Image.open(mask_path).convert(L) # 原图用 BILINEAR 缩放mask 用 NEAREST避免边界值被插值污染 img img.resize(self.image_size, Image.BILINEAR) mask mask.resize(self.image_size, Image.NEAREST) img_np np.array(img, dtypenp.float32) / 255.0 mask_np np.array(mask, dtypenp.int64) img_tensor torch.from_numpy(img_np).permute(2, 0, 1) mask_tensor torch.from_numpy(mask_np).unsqueeze(0) return img_tensor, mask_tensor逻辑说明image_dir和mask_dir分别指向train/images和train/masksimage_size统一缩放到 256×256这样不管原始是 256 还是 1000进网络的尺寸都一致。mask读取后做了两件事——转成int64、加一个通道维这是因为 PyTorch 的交叉熵损失要求标签是B×1×H×W的长整型张量。参数说明里最需要注意的是Image.NEAREST如果你在这里图省事也用了BILINEAR训练时你会发现 loss 降不下去或者 mask 边缘全是灰色过渡值。4.2 2 类分割的损失函数选型BCE 还是 Dice 系2 类分割的损失函数第一反应都是BCEWithLogitsLoss但在这个数据集上我会建议配合 Dice Loss 一起用。原因在于乳腺肿瘤细胞核在整张病理图中占比往往很小正负样本比例可能到 1:50 甚至更悬殊。BCE 在这种场景下会偏向预测全背景因为把每个像素预测成背景都能把 loss 压得很低。Dice Loss 直接优化「预测区域和真实区域的交并比」天然对类别不平衡不敏感两个都试一下再定。import torch def dice_loss(pred, target, smooth1.0): pred torch.sigmoid(pred) # 概率化到 [0,1] pred pred.reshape(pred.size(0), -1) target target.reshape(target.size(0), -1) intersection (pred * target).sum(dim1) dice (2.0 * intersection smooth) / (pred.sum(dim1) target.sum(dim1) smooth) return 1.0 - dice.mean()上面是一个标准 Dice Loss 实现。pred是模型输出 logits先过sigmoid变成概率reshape把每个样本的二维特征图拉成一维向量方便做逐样本的统计smooth是平滑项防止分母为 0一般取 1.0。实际训练中常见做法是BCEWithLogitsLoss DiceLoss加权组合比如bce_loss 0.5 * dice_loss。先单独跑一次纯 BCE 看指标再切到组合 loss 对比哪个验证集 Dice 高就用哪个。4.3 数据增强的边界翻转、旋转与 256/1000 尺度统一医学图像分割的数据增强要谨慎尤其是细胞核这种对形态敏感的标注。水平翻转和垂直翻转对细胞核任务基本无害因为核的朝向没有语义但旋转要谨慎——任意角度旋转会引入插值噪声破坏 mask 的二值性。我一般只用 90 度倍数的旋转保证 mask 的 0/1 结构不被破坏。随机裁剪是另一个有效手段但裁剪后要重新检查 mask 是否全为 0如果裁到背景区域这一对样本就废了训练时最好丢弃。尺度统一的问题在第 2.3 节提过数据集混着 256 和 1000 两种分辨率如果训练阶段全部 resize 到 256那 1000 的图相当于丢了大量细节如果全部喂原尺寸显存又扛不住。折中方案是训练时用随机缩放把图缩放到 256~512 之间的随机尺度再裁剪到 256相当于在有限显存下做多尺度学习。脚本里加一个torchvision.transforms.RandomResizedCrop((256, 256), scale(0.5, 1.0))就能实现注意scale参数别设太小否则裁出来的区域可能全是背景或者单个细胞核碎片。5. 避坑记录乳腺肿瘤细胞核分割数据集的五个翻车现场这个数据集结构算规整但我在实际拆解和复现过程中还是踩了几个坑。下面按「现象 → 原因 → 解决」的格式记录下来都是血泪经验你照着跑能省不少排查时间。5.1 现象mask 读出来是花屏用 OpenCV 的cv2.imread读 mask 后plt.imshow显示出来是灰蒙蒙的花屏边缘全是灰色过渡完全不是预想中的黑白二值图。原因OpenCV 默认把图像读成 BGR 三通道mask 虽然是单通道 PNG也被扩充成了三通道且像素值 0/1 映射到 0-255 后几乎全黑显示时灰度范围不对。解决读取时统一用cv2.imread(path, cv2.IMREAD_GRAYSCALE)或者直接用 PIL 的Image.open(path).convert(L)然后打印np.unique(mask)确认取值只有[0, 1]。如果发现最大值不是 1说明 mask 被某些工具转成了 0 和 255训练前要手动除以 255。5.2 现象测试集评估指标异常偏低用测试集的 167 张原图全部跑预测再跟测试集 mask 比对计算 Dice结果只有 0.3 出头怎么看都不合理。原因测试集 masks 目录下只有 16 张 PNG167 张原图里大部分没有对应标签。拿没有标签的图的预测结果去比对要么比对了空气要么程序自动匹配到了错误的 mask。解决评估阶段先用文件名交集过滤只保留test/images和test/masks中都存在的文件对。我一般写一个 set 取交集import os imgs set(os.listdir(test/images)) masks set(os.listdir(test/masks)) valid sorted(imgs masks) print(f可用于评估的图片对: {len(valid)})这段代码先用set求两个目录共有的文件名再排序输出数量。它的意义在于不信任目录结构的表面完整而是用名字交集确认真实标签覆盖度。你还可以把valid再划分到 train/val但就这个数据集而言我的建议是训练阶段完全不碰测试集的 16 张留到最后统一评估否则训练过程里你反复看这几张图等于手动过拟合。5.3 现象叠加图全黑看不到标注区域可视化脚本跑出来第三张 overlay 图只有原图mask 叠加部分完全看不见或者只有微弱的暗红色。原因mask 像素最大值是 1在叠加时如果直接用 mask 数组去改 RGB 通道相当于叠加了一个亮度为 1/255 的几乎透明图层肉眼当然看不见。解决叠加前强制做一次(mask * 255).astype(np.uint8)让标注像素变成 255再给原图通道赋值。如果你用 Matplotlib 画叠加更简单的办法是用ax.imshow(img)后再ax.imshow(mask, cmapjet, alpha0.3, vmin0, vmax1)让库帮你做色彩映射。5.4 现象训练 loss 震荡验证集 Dice 忽高忽低训练过程里 loss 曲线像锯齿一样上下跳验证集 Dice 第一轮 0.7、第二轮 0.3、第三轮又回 0.7完全没规律。原因训练集只有 66 对Batch 内图片来自不同 TCGA 样本染色风格差异大加上没有做归一化。有些病理图整体偏蓝、有些偏粉模型被不同风格的分布拉扯。解决至少做一次数据级归一化最常用的是 z-score 归一化按通道统计均值和标准差。如果你的流水线里已经有了ToTensor再额外增加from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256), interpolationtransforms.InterpolationMode.BILINEAR), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize用的 ImageNet 统计值是通用参数如果你有精力建议自己跑一遍训练集的均值和标准差替换进去。参数说明mean和std各三个数对应 RGB 三个通道顺序不能反。这一步加上之后loss 曲线会明显平滑验证集指标也变得可解释。5.5 现象脚本直接报ModuleNotFoundError: No module named cv2可视化脚本在本地跑不起来一执行就提示缺少 OpenCV 或 sklearn。原因你本机环境没装依赖或者装了但版本不兼容。Python 环境隔离没做好conda 和 pip 混用导致脚本跑在了一个缺包的解释器上。解决先建独立虚拟环境再装依赖别把项目包全塞进 base 环境。数据说明里提到的可视化脚本如果依赖 cv2标准做法是python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install opencv-python matplotlib numpy scikit-imagepython -m venv .venv创建虚拟环境source .venv/bin/activate激活它最后一条命令安装脚本运行时需要的库。参数说明scikit-image不是每个脚本都需要但做 mask 可视化时经常用到skimage.io或label相关函数装上能少踩一个坑。如果 python 启动时还是看不到虚拟环境检查是否当前 shell 激活的是正确的.venv路径Windows PowerShell 下激活命令是.venv\Scripts\Activate.ps1。6. 进阶写一个分割训练前的自检脚本把数据集变成「可审计」的输入可视化脚本只是抽看单张图但训练前更值得做的是对整个数据集跑一遍批量自检把每张 mask 的类别占比、目标连通域数量、异常尺寸一次性统计出来。这样你能在训练前知道这份数据到底值得投入多少时间而不是等到训练完才发现某个类别占了 99.5% 导致模型直接学废。我常用的自检脚本核心逻辑是这样import numpy as np from PIL import Image from scipy import ndimage def audit_mask(mask_path): mask np.array(Image.open(mask_path).convert(L)) unique, counts np.unique(mask, return_countsTrue) total mask.size ratio {int(k): float(v / total) for k, v in zip(unique, counts)} # 统计目标区域连通域数量辅助判断目标是否碎片化 labeled, num ndimage.label(mask 1) return ratio, numaudit_mask输入 mask 路径输出两个值ratio是各类别像素占比字典num是 1 区域的连通域数量。逻辑说明np.unique统计每个像素值的出现次数除以总面积得到占比ndimage.label对二值图做连通域标记返回标记矩阵和连通域个数这个数字能直观看出目标是大块连续还是碎成几十块。如果某张 mask 的 1 占比只有 0.5% 且连通域数量超过 50说明标注目标高度碎片化网络在这种图上很难学出稳定的梯度信号需要重点做数据增强补偿。跑完整个数据集后把统计结果按占比排序能快速找出那些「脏样本」——比如 mask 全为 0 的漏标图这类图必须剔除否则就是纯噪声。把自检脚本跟可视化脚本连起来用效果更好。先批量跑audit_mask拿到每张图的指标再对指标异常的图单独跑三视图可视化把自动统计和人工判断结合起来。你有兴趣还可以在自检脚本里加一个尺寸检查把宽高不是 256 或 1000 的图列出来这些小坑单独看都不起眼攒多了就变成数据预处理里的隐形时间黑洞。这个习惯帮我在好几个项目里避免了大返工。从那以后我每次拿到新的医学图像分割数据集不管是谁发的、格式多规整都强制先过一遍自检脚本把像素占比、连通域数、尺寸分布这三张表打出来再决定怎么设计训练流水线。数据不透明时模型表现得再玄学都找不到原因而这份自检脚本就是让数据开口说话的第一步。希望帮到你。本文还有配套的精品资源点击获取