简介:面向医学图像分割任务,这份数据集聚焦乳腺肿瘤细胞核分割(2类),适用于深度学习分割模型的训练、验证与算法对比。图像与mask均为png格式,分辨率涵盖256×256与1000×1000,mask采用0/1阈值模板,分别对应背景与肿瘤细胞核;训练集含66对图像与掩膜,测试集单独划分,可直接支持U-Net、DeepLab等常见分割网络的训练与评估。样本文件名带TCGA病例标识,便于追溯来源与后续扩展。压缩包共167个文件,以png图片为主体,另含1个txt类别说明和1个py可视化脚本;脚本无需修改即可运行,随机抽取一张样本,同屏展示原始图像、GT掩膜及GT叠加效果,帮助快速核对标签质量。资源整体约62MB,已有194人学习,适合医学影像分析方向的研究者、竞赛选手及深度学习初学者,可作为模型训练与基准测试的现成数据基础。
1. 乳腺肿瘤细胞核分割(2类)数据集的定位:一个能直接上手的医学图像分割起手式
做乳腺病理图像的细胞核分割,第一步往往不是选网络,而是找一个标注靠谱、结构简单的数据集练手。这个标题给的就是这类东西:乳腺肿瘤细胞核的 2 类分割,两类指「背景」和「细胞核」,不是把上皮、淋巴细胞、间质等核类型细分。它自带图像、标签文件和数据可视化代码,意味着你能在几小时内跑通「读图—看掩码—训练—评估」的闭环,不用把时间耗在格式转换和手动标注上。适合刚进医学图像分割方向的研究生,也适合想把数字病理快速落地验证的工程团队。这类二分类核分割是病理 AI 里最值得先做透的基础任务,核密度、核形态、Ki67 阳性率这些下游指标全都建立在它上面。
2. 拆解数据集与标签文件:目录结构、掩码取值与格式转换
拿到数据集先别急着训练,用五分钟把目录和标签的真实格式看清楚,后面能省几小时排查。乳腺肿瘤细胞核分割数据集最常见的组织方式有两种:一种是images/与masks/两个平级目录,图像和掩码同名不同后缀;另一种是全部放在同一目录,靠文件名后缀区分。先用一条命令看全貌。
tree -L 2 dataset/# Windows 下没有 tree 时 dir /s dataset逻辑说明:tree -L 2只看两层,够判断目录结构,不会因为病理扫描图的大文件把终端刷爆。Windows 上dir /s是等效替代。看的时候重点确认三件事:图像和掩码是否一一对应、掩码是不是单通道灰度图、文件名是否带病例前缀。乳腺肿瘤数据集的命名通常类似case12_slide03_region07.png,这个前缀后面做数据划分有大用。
2.1 标签文件的真实格式:单通道掩码和 0/255 约定
很多第一次接触分割数据集的人会在掩码读取上翻车。乳腺肿瘤细胞核分割的标签文件一般是 PNG 格式的单通道灰度图,像素只有两种取值:0 表示背景,255 表示细胞核。有的版本存成 0 和 1,有的是三通道彩色图但三个通道内容完全一样。读取方式必须显式指定灰度模式:
import cv2 import numpy as np mask = cv2.imread("dataset/masks/mask_001.png", cv2.IMREAD_GRAYSCALE) print(np.unique(mask)) # 输出可能是 [0 255],也可能是 [0 1]逻辑说明:IMREAD_GRAYSCALE强制以单通道读入。很多教程里直接cv2.imread(path)默认按三通道 BGR 读,灰度 PNG 会被复制成三通道,np.unique出来的是[0 255]的三维组合,后面不管训练还是可视化都会踩坑。这里要养成的习惯是:所有用到掩码的地方,读入后先print(np.unique(mask))确认取值。
参数说明:0/255 是标注工具导出时的常见格式,但训练时多数框架要求类别索引从 0 开始,因此读入后要么除以 255,要么用astype(np.uint8)后重映射。我一般统一转成 0/1,少一层换算。
2.2 分割掩码与目标检测标注的本质差别
如果你之前用 YOLO 训练过自己的数据集,对.txt里的一串归一化框坐标很熟。分割不一样,标签是逐像素的,没有「框住目标」这回事。用 COCO 2017 数据集结构来类比会更清楚:COCO 的实例分割标签是 JSON 里存多边形点坐标,训练前要把点栅格化成 mask;而这个标题下的数据集已经帮你做完了栅格化,打开就是 PNG 掩码,省掉了 polygon-to-mask 这一步。这既是优点也是风险——你看不到原始标注过程,只能信任掩码本身。所以拿到数据后的第一件事是可视化抽查,而不是直接开训。
另外要留意「2类」的定义边界。有的公开数据(比如多类核标注版本)会把上皮核、淋巴细胞核、基质核分开标,标题里说 2 类,说明已经合并成「背景 + 细胞核」的语义。但如果原始素材是多类标,合并时不能简单mask > 0一把梭,见下一节。
2.3 把多类别掩码重映射成 2 类:一个通用转换脚本
从朋友那拷来的数据、或从公开项目(如 MoNuSeg 系列)拿到的原始标注,常常是多类的。要做 2 类分割就得先合并。一个足够通用的脚本如下:
import cv2 import glob import os import numpy as np src_masks = sorted(glob.glob("raw_masks/*.png")) os.makedirs("masks", exist_ok=True) for p in src_masks: m = cv2.imread(p, cv2.IMREAD_GRAYSCALE) m2 = (m > 0).astype(np.uint8) * 255 out = os.path.join("masks", os.path.basename(p)) cv2.imwrite(out, m2) print(f"{p} -> {out}, classes={np.unique(m2)}")逻辑说明:m > 0把所有非背景像素统一变成 True,astype(np.uint8)得到 0/1,* 255后存成 0/255 的 PNG。这一段最关键的假设是:所有非零像素都算「细胞核」。如果你的原始标注里 1 是上皮核、2 是纤维核、3 是炎性核,想保留全部,这个脚本成立;但如果原始标注里有 255 表示「待确认」或「背景遮罩」,直接合并会把噪声也当成前景。
参数说明:输出格式建议统一 0/255,方便cv2.imwrite直接落盘。如果之后用 PyTorch 的CrossEntropyLoss,训练时再除以 255 即可。合并前用np.unique(m)看一眼原始取值,确认没有中间灰阶,这是最容易忽略的一步。
2.4 遇到 HDF5 打包的数据集:先解包再动手
有些医学图像分割数据集为了减少海量小文件的 IO 开销,会打包成单个.h5文件。结构通常是images和masks两个 dataset,shape 是(样本数, 高, 宽, 通道数)。读取和拆包用h5py:
import h5py import cv2 import numpy as np import os os.makedirs("images", exist_ok=True) os.makedirs("masks", exist_ok=True) with h5py.File("dataset.h5", "r") as f: print("keys:", list(f.keys())) n = f["masks"].shape[0] for i in range(n): img = f["images"][i] mask = f["masks"][i] img = (img * 255).astype(np.uint8) if img.max() <= 1.01 else img.astype(np.uint8) mask = (mask * 255).astype(np.uint8) if mask.max() <= 1.01 else mask.astype(np.uint8) cv2.imwrite(f"images/img_{i:04d}.png", img) cv2.imwrite(f"masks/mask_{i:04d}.png", mask)逻辑说明:HDF5 的好处是单文件、支持按索引随机读取,但实际训练时 PyTorch 的DataLoader不太喜欢直接读 h5,频繁随机访问会卡。我一般先解包成 PNG,训练时用小文件随机读,速度更稳。代码里的归一化判断很关键:医学数据集的 HDF5 里图像经常存成 0~1 的 float,掩码也如此,直接astype(np.uint8)会得到全黑图。
参数说明:i:04d保证文件名按四位序号补零,排序不乱。解包后务必抽查几张,确认 mask 和 image 的内容对齐,不要只看 shape 就对上了。
3. 数据可视化代码落地:叠加显示、类别统计与超大图浏览
标题里明确带了「数据可视化代码」,说明这个数据集希望你在训练前先把数据看透。这个环节做扎实,后面训练中的很多反常现象一眼就能认出来。
3.1 图像与掩码叠加显示:最小可运行脚本
先给一个最直接的叠加可视化脚本。它能把细胞核区域用半透明红色盖在原图上,三张子图分别是原图、掩码、融合结果,一张图看清标签质量。
import cv2 import numpy as np from matplotlib import pyplot as plt img = cv2.imread("dataset/images/img_001.png") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask = cv2.imread("dataset/masks/mask_001.png", cv2.IMREAD_GRAYSCALE) overlay = img.copy() overlay[mask > 0] = (255, 0, 0) # 核区域染红 blend = cv2.addWeighted(img, 0.6, overlay, 0.4, 0) fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(img) axes[0].set_title("Image") axes[1].imshow(mask, cmap="gray") axes[1].set_title("Mask") axes[2].imshow(blend) axes[2].set_title("Overlay") plt.show()逻辑说明:overlay直接拷贝原图,把掩码非零像素位改成纯红,再用addWeighted做 alpha 融合。核心是addWeighted(img, alpha1, overlay, alpha2, 0),输出强度由两个权重加权,既能看清核的位置又不遮挡原图纹理。
参数说明:0.6 / 0.4是原图与红层的混合比例。细胞核排布密集时把红层权重调到 0.3 以下,否则红色连成片,看不出单个核的轮廓;核稀疏时用 0.5~0.6,视觉上更清楚。mask > 0兼容 0/1 和 0/255 两种掩码,不用特意改。另外注意cvtColor那行:OpenCV 读图是 BGR,plt.imshow按 RGB 解释,不转的话整张图会偏蓝偏红,这是叠加可视化最常见的翻车点。
3.2 类别占比统计:训练前先判断类别失衡程度
乳腺肿瘤细胞核分割里,背景占比通常远高于核。不量化这个值就选损失函数,大概率要走弯路。用几行脚本把前景占比算出来:
import cv2 import glob import numpy as np ratios = [] for p in sorted(glob.glob("dataset/masks/*.png")): m = cv2.imread(p, cv2.IMREAD_GRAYSCALE) fg = (m > 0).sum() total = m.size ratios.append(fg / total) ratios = np.array(ratios) print(f"样本数: {len(ratios)}, 前景占比均值: {ratios.mean():.4f}, 中位数: {np.median(ratios):.4f}") print(f"前景占比最低: {ratios.min():.4f}, 最高: {ratios.max():.4f}")逻辑说明:m > 0统计前景像素,除以总像素就是每张图的前景占比。均值和中位数同时看,能区分「整体都低」和「个别图高、多数图极低」两种情况。病理图中核密度差异很大,乳腺肿瘤切片里不同视野的核占比可以差出 5 倍以上。
参数说明:如果前景占比均值低于 0.1,整图喂进去训练大概率被背景主导。这个脚本的输出是你决定「整图训练还是 patch 采样」的直接依据,别跳过。
3.3 超大病理图的切片浏览:网格预览避免黑屏和 OOM
病理扫描图经常是几千乘几千像素,直接plt.imshow(img)要么内存爆掉,要么显示出来糊成一片。常见做法是把图像切成固定大小的 tile,网格显示前几块:
def grid_preview(img, mask, tile=512, max_cols=3): h, w = mask.shape[:2] rows = min(h // tile, 3) cols = min(w // tile, max_cols) fig, axes = plt.subplots(rows, cols, figsize=(4 * cols, 4 * rows)) for i in range(rows): for j in range(cols): p = img[i*tile:(i+1)*tile, j*tile:(j+1)*tile] m = mask[i*tile:(i+1)*tile, j*tile:(j+1)*tile] axes[i, j].imshow(p) axes[i, j].imshow(m, cmap="jet", alpha=0.3, interpolation="nearest") plt.show() grid_preview(img, mask, tile=512, max_cols=3)逻辑说明:按tile尺寸把图切成网格,只显示前三行前几列。用cmap="jet"叠加在图上,核区域被高亮成伪彩色,边界看得比纯红更清楚。interpolation="nearest"避免缩放时出现插值模糊,让缩略图保持锐利。
参数说明:tile=512适合显示中等细节;想看核形态用 256,想看组织排布用 512 或 1024。max_cols=3控制列数,网格总数控制在 9 个以内,显示速度才跟得上。这个函数后续每次训练迭代后输出预测结果时也能复用。
4. 把乳腺肿瘤细胞核分割(2类)数据集跑进 U-Net:数据划分、损失函数与评估指标
可视化确认过标签质量之后,再进训练。这一章给出能直接抄的数据划分、Dataset、损失函数和评估代码,按顺序组合就能跑通一个基础 U-Net。
4.1 数据划分:按病例分组而不是随机打散
乳腺病理图像有一个容易被忽略的问题:同一个病人的多张切片非常相似,随机打散会让同一病例出现在训练集和验证集,造成指标虚高。正确做法是按文件名前缀里的病例 ID 分组。
import glob import os from collections import defaultdict imgs = sorted(glob.glob("dataset/images/*.png")) groups = defaultdict(list) for p in imgs: base = os.path.basename(p) case = base.split("_")[0] # 按文件名前缀取病例 ID groups[case].append(p) cases = sorted(groups.keys()) print(f"病例数: {len(cases)}, 图像总数: {len(imgs)}") train_cases = cases[:int(len(cases) * 0.8)] val_cases = cases[int(len(cases) * 0.8):int(len(cases) * 0.9)] test_cases = cases[int(len(cases) * 0.9):] train_imgs = [p for c in train_cases for p in groups[c]] val_imgs = [p for c in val_cases for p in groups[c]] test_imgs = [p for c in test_cases for p in groups[c]]逻辑说明:base.split("_")[0]依赖文件命名带病例前缀。实际数据集命名如果不规则,先打印前 20 个文件名人工确认分隔符。按病例分组划分后,同病人的所有切片只进一个集合,验证结果才具备参考意义。
参数说明:8:1:1 是医学图像分割的常见比例,样本量小于几百张时可以把测试集并进验证集,留出更多训练数据。cases列表要先排序再切片,保证每次运行划分一致。
4.2 预处理与在线增强:翻转旋转安全,随机裁剪要谨慎
病理图像没有上下左右的方向性,翻转和 90 度旋转是绝对安全的增强。随机裁剪则要小心:整图里背景太多时,随机裁到的地方大概率不含核,相当于给模型喂了大量空白样本。
import random import numpy as np def train_aug(img, mask): if random.random() < 0.5: img = img[:, ::-1] mask = mask[:, ::-1] k = random.choice([0, 1, 2, 3]) img = np.rot90(img, k) mask = np.rot90(mask, k) return img.copy(), mask.copy()逻辑说明:水平翻转和 90 度旋转只改变像素位置、不改变像素值,对染色组织切片的语义完全友好。np.rot90的k取 0~3,相当于随机旋转 0/90/180/270 度。返回拷贝是防止后续 numpy 操作共享内存导致的隐式修改。
参数说明:这套增强只适合训练阶段。验证和测试时不要做任何随机操作,最多做固定的中心裁剪。医学图像分割里「翻转增强」几乎无成本,但不要加过强的颜色抖动——病理图的染色有一定标准,颜色扰动过大反而破坏核与背景的判别特征。
4.3 损失函数:单独用 BCE 为什么会在核分割上失效
细胞核分割的前景占比常低于 10%,直接BCEWithLogitsLoss会让模型倾向于把一切预测为背景,因为这样 loss 已经很低。Dice Loss 直接优化重合度,对类别失衡天然稳健。常用做法是 BCE 和 Dice 组合。
import torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth=1.0): super().__init__() self.smooth = smooth def forward(self, logits, target): prob = torch.sigmoid(logits).view(logits.size(0), -1) target = target.view(target.size(0), -1).float() intersection = (prob * target).sum(dim=1) dice = (2 * intersection + self.smooth) / ( prob.sum(dim=1) + target.sum(dim=1) + self.smooth ) return 1 - dice.mean() class BCEPlusDice(nn.Module): def __init__(self, bce_weight=0.5): super().__init__() self.bce = nn.BCEWithLogitsLoss() self.dice = DiceLoss() self.bce_weight = bce_weight def forward(self, logits, target): return self.bce_weight * self.bce(logits, target) + (1 - self.bce_weight) * self.dice(logits, target)逻辑说明:DiceLoss把 logits 经 sigmoid 转成概率,然后压平成(batch, -1)计算每张图的 Dice 系数,取平均后返回1 - dice作为损失。BCEPlusDice把两个损失按权重相加,BCE 提供像素级梯度,Dice 缓解类别失衡。
参数说明:smooth=1.0防止分子分母都为 0 时的除零错误。bce_weight常见取 0.5,如果你的数据集前景占比特别低(低于 5%),可以把bce_weight调到 0.3,让 Dice 主导;前景占比较高时回到 0.5~0.7。
4.4 评估指标:Dice、IoU 与像素精度,别只看准确率
细胞核分割这种极不平衡场景,准确率没有任何参考价值——全预测成背景也能到 90% 以上。评估至少看三个指标:
def compute_metrics(pred, target, threshold=0.5): pred_bin = (pred > threshold).astype(np.uint8) target_bin = (target > 0).astype(np.uint8) inter = np.logical_and(pred_bin, target_bin).sum() pred_sum = pred_bin.sum() target_sum = target_bin.sum() dice = 2 * inter / (pred_sum + target_sum + 1e-6) iou = inter / (pred_sum + target_sum - inter + 1e-6) acc = (pred_bin == target_bin).mean() return {"dice": dice, "iou": iou, "acc": acc}逻辑说明:pred是模型输出的概率图 0~1,target是掩码。比较前先把预测按阈值二值化,把 target 统一成 0/1。三个指标一起看,尤其是 IoU 和 Dice 同时偏低而 acc 很高时,说明模型在退化到背景预测。
参数说明:threshold=0.5不是唯一选择。核分割里提高阈值到 0.6~0.7 能显著减少假阳性,代价是召回下降。判断阈值是否合理的方法很简单:看验证集上预测概率的直方图,如果大部分预测值集中在 0.1 以下和 0.9 以上,说明模型很自信,阈值怎么选都不太影响;如果大量预测值在 0.4~0.6 徘徊,调阈值就是在碰运气,不如回去修损失函数。这一步很玄学,但先把数据看透就能少调参。
5. 避坑与排查:乳腺肿瘤细胞核分割数据集上的 5 个翻车点
5.1 图像和掩码错位:训练正常但可视化完全对不上
现象:训练 loss 在下降,但把原图和掩码叠加显示,发现掩码的核位置和图像里的核位置根本对不上,有时掩码里出现图像中没有的结构。
原因:两个常见来源。一是glob.glob和os.listdir的排序不一致,图像按文件名排序、掩码按目录读入顺序,拼接时错位;二是数据集本身在打包时 images 和 masks 不是按同一顺序导出的。
解决:用文件名显式匹配,而不是依赖目录读入顺序。统一走sorted(glob.glob()),并且训练脚本里加一个断言,检查图像与掩码的文件名一一对应后把路径配对。进阶做法是随机抽 20 组图叠加保存成一张大图,人眼扫一遍,比任何断言都可靠。
5.2 背景占比太高:准确率 98% 但 Dice 为 0
现象:训练完成后验证集准确率很高,Dice 却是 0 或接近 0。看起来不可思议,其实模型把整张图都预测成了背景。
原因:背景像素占比超过 90% 时,BCEWithLogitsLoss会走进「全预测为背景」的局部最优,因为这个策略已经把 loss 压得很低。准确率高只是类别失衡的假象。
解决:换用 4.3 节的 DiceLoss 或 BCE+Dice 组合损失;更彻底的办法是放弃整图训练,改为按核密度采样 patch。我一般让可视化统计脚本先输出前景占比,低于 10% 就直接走 patch 采样路线,不进整图训练。
5.3 掩码边界粗细不一致:Dice 卡在 0.7 左右上不去
现象:验证 Dice 从头到尾卡在 0.7 附近,换更强的网络也突破不了。可视化预测结果发现,预测的核边界普遍比真实掩码细或粗一圈。
原因:标注源的边界一致性差。显微镜图像里细胞核边界本身就模糊,标注工具导出的轮廓线粗细不一致,加上不同标注人员的标准不同,会导致 GT 边界出现系统性偏差。模型学到的「正确边界」和 GT 之间有固定偏移,Dice 被抬高不了。
解决:先做标注一致性检查——统计所有掩码里连通域的面积分布,如果同一类结构的面积标准差异常大,考虑对掩码统一做形态学腐蚀或膨胀校准。训练层面可以引入边界损失或在 Dice 基础上加边界感知项。最务实的做法是把 GT 统一腐蚀 1~2 个像素再做训练,牺牲一点边界精度换取训练稳定性。
5.4 同一病例被随机划分到训练和验证集:训练指标虚高
现象:训练时验证 Dice 接近 0.9,自认为模型已经收敛,拿到新数据上一测只有 0.6,落差巨大。
原因:文件命名带病例前缀时,随机划分会把同一病人的多张切片分到训练集和验证集。病理切片不同区域纹理高度相似,模型在训练时已经「见过」验证集的同源图像,评估结果虚高。
解决:按 4.1 节的病例分组划分,保证同一个case前缀的所有图只进一个集合。如果数据集本身没有病例 ID,按图像采集时间或批次近似分组也行——重点是不让模型「背题」。
5.5 推理输出全黑或全是噪点:归一化和阈值问题
现象:训练正常,推理时积压出的掩码要么全黑、要么全白、要么满是孤立噪点。同一个脚本在验证集上表现正常,换到新图上就失控。
原因:三个典型来源。一是推理时输入的预处理和训练不一致,比如训练用的 0~1 归一化,推理时忘了除以 255;二是模型输出的是 logits 而不是概率,直接用> 0.5判阈值,logits 都在 0 附近时全被判成背景;三是滑窗推理拼接时没有处理重叠区的边界。
解决:把预处理和推理封装成同一个函数,禁止在推理脚本里手动写一遍。模型输出先过sigmoid再判阈值。滑窗推理时重叠区域取平均值而不是覆盖,能显著减少拼接接缝处的噪点。如果二值化后仍有大量孤立小点,加一个基于连通域面积的后处理,去掉小于 10 像素的连通域。
6. 进阶验证技巧:用错误叠加图和滑窗推理反推数据集质量
6.1 错误叠加图:十秒定位难例和标注缺陷
只在下游用「预测对了几张」来评价数据集是不够的。我习惯把每个验证样本输出成一张错误叠加图:真阳性画绿、假阴性画红、假阳性画黄。看分布比看数字有信息量得多。
def error_map(gt, pred): h, w = gt.shape vis = np.zeros((h, w, 3), dtype=np.uint8) tp = np.logical_and(gt == 1, pred == 1) fn = np.logical_and(gt == 1, pred == 0) fp = np.logical_and(gt == 0, pred == 1) vis[tp] = (0, 255, 0) vis[fn] = (255, 0, 0) vis[fp] = (0, 255, 255) return vis逻辑说明:三通道分别标记三类区域。红色集中在某个固定角落,基本可以判断是染色不均或漏标;黄色连成片说明阈值偏低,模型在正常组织区域产生了系统性误报。
6.2 滑窗推理处理超大病理图:重叠融合去接缝
整张病理图直接推理显存不够,切块推理又看这种错位现象。常见做法是滑窗带重叠,输出时对重叠区域取平均值。tile 取 512、重叠 64,能平衡显存和拼接质量;重叠太小接缝明显,重叠太大推理时间翻倍。我一般保留最后一张实验记录卡,包含数据版本、病例划分方式、增强开关、损失函数、优化器、学习率和三次重复实验的 Dice 均值。
| 项 | 记录值 |
|---|---|
| 数据版本 | 解包后 PNG v1,前景占比 11.2% |
| 划分方式 | 按 case 前缀,8:1:1 |
| 损失 | BCE+Dice,bce_weight=0.4 |
| 优化器 | AdamW,lr=1e-4 |
| Dice / IoU | 0.842 / 0.731 |
这套验证习惯帮我避过很多次「指标好看但实际不可用」的假收尾。最后给一点个人经验:拿到这类数据集,先花一小时做可视化和统计,再决定网络和损失函数,这个顺序省下的返工时间比想象中多得多。希望帮到你。
本文还有配套的精品资源,点击获取