☰
TCGA乳腺肿瘤细胞核分割实战:从数据集处理到U-Net全流程
2026/10/7 13:39:11 网站建设 项目流程

简介:面向医学图像分割与病理图像分析场景,这份乳腺肿瘤细胞核分割数据集以二分类掩膜提供训练与验证样本,服务于肿瘤细胞核分割、病灶区域提取及医学影像预处理任务。图像为png格式,分辨率有256×256与1000×1000两种,mask为0/1阈值掩膜,0表示背景、1表示肿瘤细胞核区域,并附classes txt说明类别标签。资源共167个文件,主体为165张png图像(含原始图与对应掩膜),另有1个txt标签说明文件和1个Python可视化脚本,7z压缩包整体约62.07MB。数据集已划分训练集与测试集,训练集与测试集均含images和masks目录,便于直接训练与验证;可视化脚本运行后会自动抽取一张图像,将原图、GT掩膜及GT叠加效果保存到当前目录,可快速核对标签质量。目前已有194人学习,适合医学图像分割入门者、算法工程师及科研人员使用。

1. 乳腺肿瘤细胞核分割数据集:为什么它是第一个跑通的医学图像分割项目

医学图像分割里,乳腺肿瘤细胞核分割是一个“数据干净、跑起来立刻能看到东西”的入门到进阶场景。这份数据集从 TCGA 公开病理切片裁剪而来,统一输出 PNG 格式,分辨率覆盖 256×256 和 1000×1000 两档,分割目标是二分类:0 是背景,1 是肿瘤细胞核。训练集 66 张原图配 66 张同名掩码,测试集 167 张原图和对应掩码目录,标签文件全部是 0/1 阈值图,不用像 COCO 或 VOC 那样解析 JSON 和 XML。更省事的是,它还附带一个数据可视化脚本,随机抽一张图就能把原始图、GT、GT 叠加在原图上的结果一次性画出来,直接运行即可。适合谁?准备医学图像分割课设或入门实验的新手,以及想快速验证 U-Net 类模型在细胞核这种密集小目标上表现如何,却不想花时间整理数据的工程师。下面从目录结构讲到掩码格式,从可视化脚本讲到 PyTorch 基线模型,最后聊聊把模型推到全分辨率推理的路径。

2. 数据集解剖:TCGA切片命名、0/1标签文件与训练测试划分

2.1 文件名中的身份信息与两档分辨率取舍

初次打开数据集,目录里全是以 TCGA 开头的长文件名,比如 TCGA-HE-7129-01Z-00-DX1.png、TCGA-B0-5711-01Z-00-DX1.png、TCGA-E2-A1B5-01Z-00-DX1.png。TCGA 是 The Cancer Genome Atlas(癌症基因组图谱)的缩写,这是一套公开的肿瘤多组学与病理数据计划。文件名按它的固定规则编码:HE 代表 H&E 染色,中间段是病例或样本编号,DX1 代表诊断用途的切片。这类命名习惯和很多公开病理数据集一脉相承,理解了它,以后换到其他 TCGA 衍生数据集时,也能快速定位组织来源和染色类型。

分辨率两档:256×256 和 1000×1000。这个参数直接影响网络能看到的上下文范围。256×256 的图块放进 U-Net,采样后的感受野刚好覆盖一到两个细胞核,适合快速迭代超参数,显存占用也小;1000×1000 则保留腺体结构、间质纹理等信息,让模型分类前景时更容易结合周围环境,收敛出的边界往往更稳。两种尺度不要混在一个 epoch 里随机进网络,否则 BatchNorm 的统计量会被不同尺度的纹理特征拉偏,我一般先在 256×256 上跑通流程,再切到 1000×1000 做最终训练。

格式全部是 PNG。这一点对分割很重要:PNG 无损压缩能保住细胞核边缘的形态学细节,而 JPG 在高频纹理区域容易产生块状伪影,这些伪影在像素级监督下会被模型直接当成特征学走。用 PNG 等于先排除一个数据层面的干扰项。如果训练时需要缩小图像,建议用cv2.resize加INTER_AREA做下采样,直接用线性插值容易在密集核区域产生摩尔纹。

2.2 标签文件:0/1二值掩码、classes.txt与读取顺序

标签和原图完全同名,后缀也是.png,打开后是单通道灰度图,像素值只有 0 和 1:0 表示背景,1 表示肿瘤细胞核。classes.txt 文本里写明类别约定,项目摘要里也明确了 mask 模板是 0/1 的阈值图像。这种二分类分割最大的好处是省掉类别映射的麻烦,数据加载阶段只需要读图、配对、二值化。

读取时最容易翻车的点是用默认方式读掩码。cv2.imread不指定第二个参数时,会把 8 位灰度图读成三通道 BGR 图,后面做损失计算形状对不上。标准做法是显式指定灰度模式读取,读完顺手打印一下像素分布:

import cv2 import numpy as np image = cv2.imread("train/images/TCGA-HE-7129-01Z-00-DX1.png") image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) mask = cv2.imread("train/masks/TCGA-HE-7129-01Z-00-DX1.png", cv2.IMREAD_GRAYSCALE) print("mask shape:", mask.shape) print("mask unique:", np.unique(mask))

这里COLOR_BGR2RGB是给原图用的,因为 matplotlib 和 PyTorch 预处理都默认 RGB 顺序;掩码用IMREAD_GRAYSCALE读出来是单通道 H×W 矩阵,正好可以直接和模型输出的 logits 计算损失。打印unique有两个目的:一是确认掩码没有意外中间灰度,二是排查是否存在空标注(全零)样本。如果发现数值里混着 254、255,先不要急着阈值化,可以检查掩码是不是被某个环节重存成了彩色图;这份数据集本身是干净的 0/1 阈值图,但这种检查习惯能帮你排除很多第三方数据集的隐患。

像素值语义类别
0背景(间质、脂肪等)background
1肿瘤细胞核foreground

2.3 训练集/测试集目录组织与划分的意义

目录结构采用 images 加 masks 的经典布局,训练集和测试集各自独立:

dataset/ ├── train/ │ ├── images/ # 66张原图 │ └── masks/ # 66张同名掩码 └── test/ ├── images/ # 167张原图 └── masks/ # 同名掩码文件

原图与掩码通过文件名一一对应,用 pathlib 处理非常顺手。训练集 66 对,测试集 167 对,这是典型的医学分割小样本配置。但注意,数据量小不代表问题简单,一张 256×256 的病理图里常常包含几十到上百个细胞核,等于每张图自带几十个标注实例,模型的训练信号密度并不低。

划分工作作者已经完成,用户不需要二次 split。很多人会忽略 split 阶段的风险:如果自己随机划分,来自同一张全切片的不同图块可能同时被分进训练集和测试集,模型等于见过了测试内容,指标虚高但泛化能力差。这份数据集直接给出固定训练测试划分,既规避了数据泄漏,也方便不同模型在同一环境下公平对比。实验时不要自己重排目录,就按作者给的划分走。

3. 数据可视化脚本直接运行:原图、GT掩膜与叠加图检查

3.1 脚本为什么能做到“不用改直接跑”

数据包附带的可视化脚本,目标只有一个:随机抽取一张图片,将原始图、GT掩膜、GT在原图上的蒙版图像三张图绘制出来并存盘。作者写好的版本无需传参,进入目录后直接运行,这对刚接触分割数据的同学很友好。脚本核心逻辑我用代码还原一下:

import cv2 import random import numpy as np from pathlib import Path import matplotlib.pyplot as plt image_dir = Path("images") mask_dir = Path("masks") image_files = sorted(list(image_dir.glob("*.png"))) selected = random.choice(image_files) mask_path = mask_dir / selected.name image = cv2.imread(str(selected)) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) mask = cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) mask = (mask > 0).astype(np.uint8) * 255 overlay = image.copy() overlay[mask > 0] = [255, 0, 0] blended = cv2.addWeighted(overlay, 0.5, image, 0.5, 0) plt.figure(figsize=(15, 5)) plt.subplot(1, 3, 1) plt.imshow(image) plt.title("Original") plt.axis("off") plt.subplot(1, 3, 2) plt.imshow(mask, cmap="gray") plt.title("GT Mask") plt.axis("off") plt.subplot(1, 3, 3) plt.imshow(blended) plt.title("Mask Overlay") plt.axis("off") plt.tight_layout() plt.savefig("visualization_result.png", dpi=150) plt.show()

代码里的关键决策有三个。一是图像统一做 BGR 转 RGB,保证 matplotlib 显示的颜色正常;二是 mask 先转成二值再乘回 255,这样用灰度 colormap 显示时前景是纯白而不是灰色半透明;三是叠加用addWeighted做半透明混合,权重各 0.5,红色标注和底下原图纹理能同时看清。想让红色更明显,把第一权重调到 0.7、第二权重调到 0.3 即可,这个参数不影响数据本身。

3.2 运行环境与输出位置

脚本不依赖深度学习框架,只需要 Python 解释器加三个库。安装命令如下,数秒内就能完成:

pip install opencv-python numpy matplotlib

安装完成后,进入脚本所在目录执行python 可视化脚本文件名.py(具体文件名以数据包内为准)。脚本会自动扫描当前目录下的 images 和 masks 文件夹,随机选样本,画完图把结果写到当前目录,文件名通常是visualization_result.png。如果你的机器是无桌面环境的服务器,建议在脚本开头加一行matplotlib.use("Agg"),否则plt.show()会因为没有显示设备而报错。运行成功后,打开输出图片检查三样东西:原图与掩码是否来自同一个样本、掩码是否覆盖在原图正确位置、有没有出现全空白掩码。这些凭肉眼就能判断,也是数据体检的第一步。

3.3 随机显示背后的数据质量观察技巧

跑一次只能看一个样本,建议连续跑五六次,每次观察不同样本。重点看两类异常:一是掩码里出现与细胞核形态无关的大块连通区域,说明标注时把腺体腔或坏死区也选了进去;二是掩码边缘紧贴图像块边界,这种情况往往让模型很难学到边界处的真实分布。此外,如果两份分辨率的图都存在,分别在各自 images 目录下运行脚本,确认两档分辨率下 GT 生成规则一致。可视化不只是给人看的,也是给模型训练兜底的保险——先看见,再训练。

4. 用PyTorch搭细胞核分割基线:加载器、U-Net与BCE+Dice损失

4.1 自定义Dataset:图像与掩码同步读取

分割任务的数据加载器比分类多一步:返回的标签是 H×W 矩阵,不是标量。下面这个 Dataset 类是我在类似病理分割项目里的常用模板:

import torch import cv2 import numpy as np from pathlib import Path from torch.utils.data import Dataset class TumorNucleusDataset(Dataset): def __init__(self, image_dir, mask_dir, size=(256, 256)): self.image_files = sorted(list(Path(image_dir).glob("*.png"))) self.mask_dir = Path(mask_dir) self.size = size def __len__(self): return len(self.image_files) def __getitem__(self, idx): image_path = self.image_files[idx] mask_path = self.mask_dir / image_path.name image = cv2.imread(str(image_path)) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image = cv2.resize(image, self.size, interpolation=cv2.INTER_AREA) image = image.astype(np.float32) / 255.0 mask = cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) mask = cv2.resize(mask, self.size, interpolation=cv2.INTER_NEAREST) mask = (mask > 0).astype(np.float32) img_tensor = torch.from_numpy(image.transpose(2, 0, 1)).float() mask_tensor = torch.from_numpy(mask).unsqueeze(0) return img_tensor, mask_tensor

这里有两个容易被忽略的工程点。第一,掩码 resize 务必用INTER_NEAREST。如果像图像那样用线性或区域插值,掩码边缘会被插值成 0.3、0.7 这类浮点值,模型看到的不再是硬边界,学出来的轮廓会变糊;最近邻插值能严格保留 0/1 语义。第二,掩码不需要做归一化,它本身就是 0/1,归一化反而破坏类别含义。Dataset 返回的 mask_tensor 形状是 1×H×W,正好和模型输出的 1×H×W logits 对齐。如果你在研究环境里用 Windows,注意目录路径不要带中文,OpenCV 在读中文路径时会静默返回 None,这种报错最迷惑人。

4.2 模型选型:U-Net加预训练编码器的理由

对于二分类细胞核分割,U-Net 是久经验证的基线结构。它靠编码器提取多尺度特征,解码器逐层恢复分辨率,跳跃连接把低层细节传给高层,对细胞核这种直径几十像素的密集小目标特别友好。自己从零写一个 U-Net 至少要几百行,工程上直接用segmentation_models_pytorch库更快,它内置多个预训练主干和解码器:

import segmentation_models_pytorch as smp model = smp.Unet( encoder_name="resnet34", encoder_weights="imagenet", in_channels=3, classes=1, activation=None, )

encoder_weights="imagenet"表示加载 ImageNet 预训练的 ResNet34 权重。医学图像和自然图像分布差异大,有人觉得预训练权重没用,但在只有几十张训练图的小数据集上,预训练编码器能明显加快收敛并抑制过拟合。activation=None则是把 sigmoid 从网络里拿出来,让模型直接输出 logits,方便后面配合BCEWithLogitsLoss做数值稳定的计算。如果你的显存紧张,可以换encoder_name="resnet18",参数量更小,训练一个 epoch 能省不少时间。

4.3 损失函数:BCE与Dice的组合以及不平衡处理

从可视化脚本就能看出,肿瘤细胞核在整幅图里占的面积比例通常很低,可能不到 15%。这种情况下单独用 BCE,网络很容易学会输出全背景,因为全背景的 loss 已经足够低。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, probs, targets): intersection = (probs * targets).sum() union = probs.sum() + targets.sum() dice = (2.0 * intersection + self.smooth) / (union + self.smooth) return 1.0 - dice bce = nn.BCEWithLogitsLoss() dice_fn = DiceLoss() def combined_loss(logits, masks): probs = torch.sigmoid(logits) return bce(logits, masks) + dice_fn(probs, masks)

BCE 负责像素级梯度,Dice 负责区域重叠信号,二者相加是我在二分类分割里的默认组合。训练时用 Adam,初始学习率 1e-4,256×256 输入下 batch size 可以到 16。每个 epoch 结束把模型切到 eval 模式,在验证集上算一次 Dice 和 IoU,不要只看训练 loss 一路下降就闷头训练。IoU 和 Dice 的公式很简单:Dice 是 2 倍预测与真值交集除以两者像素数之和,IoU 是交集除以并集,数据量不大手写几行就能算清楚。

5. 医学图像分割常见避坑:掩码读取、数据对齐与类别不平衡

5.1 现象:掩码读出来是三通道,模型直接报错

刚搭好 DataLoader,跑到loss.backward()之前 shape 对不上,排查半天发现 mask 的 shape 是 3×H×W。原因很简单:cv2.imread默认按三通道方式读取灰度 PNG,读出来是 H×W×3,代码里直接转置就变成了 3×H×W,跟模型输出的 1×H×W 对不上。解决方式是在读掩码时显式加cv2.IMREAD_GRAYSCALE,并在 Dataset 里加一行断言:

assert mask.ndim == 2, "mask should be single channel"

这个坑在分割任务里出现频率最高,但它也最好排查,因为错误信息指向明确。从那以后我每写一个新的 Dataset 类,都会先在__getitem__里加形状断言,而不是等训练崩了再回去查。

5.2 现象:测试集指标远低于训练集

训练集 Dice 到了 0.85,测试集只有 0.61,落差很大。常见原因有两个:一是 test 里两档分辨率的分布和 train 不一致,模型在训练时适应了某一档尺度;二是训练时把图统一 resize 到 256×256,测试时却把 1000×1000 原图整张直接送进模型,特征尺度变了,批归一化的统计量对不上。解决方法是让 test pipeline 和 train 完全同构:要么也统一 resize,要么把 1000×1000 切成和训练一致的 patch 再推理,最终拼接评估。每次做实验前把 train 和 test 的预处理代码放在同一个函数里复用,能少踩一半坑。

5.3 现象:预测结果几乎全是背景

模型收敛后,输出的 mask 全零或者只有零星几个像素。原因就是典型的类别不平衡:前景占比太低,单一 BCE 被背景主导,网络学会了恒定输出 0,因为这样 loss 已经很低。解决方式是换成 BCE+Dice 组合,Dice 直接度量预测和真值的集合相似度,前景占比再小也能给出有效梯度。还可以检查一下训练样本里有没有全零 mask,如果有,单独剔除或复制到不同 epoch 中,避免模型被空标注样本带偏。

5.4 现象:数据增强把掩码弄错位了

训练时对原图做了 RandomHorizontalFlip,对掩码也做了同样的翻转,结果可视化发现错位。原因通常是用两个独立的np.random调用分别决定图和掩码的翻转,两次随机值不一致等于各翻各的。解决方式是把图和掩码放进同一个随机状态里操作,例如用同一个np.random.RandomState(seed)对象生成变换参数,或者更简单:先把图翻转,再用完全相同的索引操作对掩码做切片反转。我用后者,因为少一层随机状态管理,代码不容易出暗病。

6. 进阶技巧:把256×256模型用滑窗搬到1000×1000全图推理

模型在 256×256 上训练完之后,拿它处理 1000×1000 的大图,最直接的方式是整张 resize 再推理,但这样会丢失细胞核的细节尺度,模型在小 patch 上习得的特征和缩放后的块不再一致。更稳定的做法是滑窗推理:把大图切成 256×256 的 patch,每个 patch 独立过模型,再拼回全图。交叠区域取平均可以消掉 patch 边界处的接缝伪影,stride 越小接缝越不明显,但计算量也越大。我通常用 stride=128,也就是 50% 重叠,效果和速度比较平衡。

import numpy as np import torch def sliding_window_inference(model, image, patch_size=256, stride=128, device="cuda"): model.eval() h, w = image.shape[:2] output = np.zeros((h, w), dtype=np.float32) count = np.zeros((h, w), dtype=np.float32) for y in range(0, h - patch_size + 1, stride): for x in range(0, w - patch_size + 1, stride): patch = image[y:y+patch_size, x:x+patch_size] patch_tensor = torch.from_numpy(patch.transpose(2, 0, 1)).unsqueeze(0).float().to(device) with torch.no_grad(): logits = model(patch_tensor) prob = torch.sigmoid(logits).squeeze(0).squeeze(0).cpu().numpy() output[y:y+patch_size, x:x+patch_size] += prob count[y:y+patch_size, x:x+patch_size] += 1 output = output / np.maximum(count, 1) return (output > 0.5).astype(np.uint8)

这个函数里logits经过 sigmoid 后是每个像素属于肿瘤细胞核的概率,count记录每个位置被几个 patch 覆盖过,最后做归一化相当于重叠区域取平均。拼接完成后还可以加一步后处理:用scipy.ndimage.label找出所有连通域,把面积小于几十个像素的孤立点去掉,医学分割里这类小噪点通常不是真实核团。自从有一次我把 1000×1000 整图直接送进模型导致显存溢出,从那以后我每次做病理图推理都强制先跑一遍滑窗模板,把 patch_size、stride 和阈值三个参数写死在一个配置文件里再开工。这套方法不只适用于这份乳腺肿瘤细胞核数据,换到任何高分辨率病理切片分割都能直接套用。希望帮到你。

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

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

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

立即咨询