☰
医学图像分割数据集实战:乳腺肿瘤细胞核mask处理与U-Net训练避坑
2026/9/28 1:33:56 网站建设 项目流程

简介:面向乳腺肿瘤细胞核分割任务的医学图像数据集,适用于医学影像分割、深度学习实训与算法验证。原始图像为 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的映射,或者用cmap='gray'加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_path="vis_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, cmap="gray", vmin=0, vmax=1) 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, dpi=150) 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, dtype=np.float32) / 255.0 mask_np = np.array(mask, dtype=np.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/masks;image_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, smooth=1.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(dim=1) dice = (2.0 * intersection + smooth) / (pred.sum(dim=1) + target.sum(dim=1) + 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 张 PNG,167 张原图里大部分没有对应标签。拿没有标签的图的预测结果去比对,要么比对了空气,要么程序自动匹配到了错误的 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, cmap='jet', alpha=0.3, vmin=0, vmax=1),让库帮你做色彩映射。

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), interpolation=transforms.InterpolationMode.BILINEAR), transforms.RandomHorizontalFlip(p=0.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-image

python -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_counts=True) total = mask.size ratio = {int(k): float(v / total) for k, v in zip(unique, counts)} # 统计目标区域连通域数量,辅助判断目标是否碎片化 labeled, num = ndimage.label(mask == 1) return ratio, num

audit_mask输入 mask 路径,输出两个值:ratio是各类别像素占比字典,num是 1 区域的连通域数量。逻辑说明:np.unique统计每个像素值的出现次数,除以总面积得到占比;ndimage.label对二值图做连通域标记,返回标记矩阵和连通域个数,这个数字能直观看出目标是大块连续还是碎成几十块。如果某张 mask 的 1 占比只有 0.5% 且连通域数量超过 50,说明标注目标高度碎片化,网络在这种图上很难学出稳定的梯度信号,需要重点做数据增强补偿。跑完整个数据集后,把统计结果按占比排序,能快速找出那些「脏样本」——比如 mask 全为 0 的漏标图,这类图必须剔除,否则就是纯噪声。

把自检脚本跟可视化脚本连起来用,效果更好。先批量跑audit_mask拿到每张图的指标,再对指标异常的图单独跑三视图可视化,把自动统计和人工判断结合起来。你有兴趣还可以在自检脚本里加一个尺寸检查,把宽高不是 256 或 1000 的图列出来,这些小坑单独看都不起眼,攒多了就变成数据预处理里的隐形时间黑洞。

这个习惯帮我在好几个项目里避免了大返工。从那以后,我每次拿到新的医学图像分割数据集,不管是谁发的、格式多规整,都强制先过一遍自检脚本,把像素占比、连通域数、尺寸分布这三张表打出来再决定怎么设计训练流水线。数据不透明时,模型表现得再玄学都找不到原因,而这份自检脚本就是让数据开口说话的第一步。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询