乳腺癌细胞分割数据集实战:从病理切片到可训练掩码的完整指南
发布时间:2026/10/1 3:05:01来源:尧图网络
简介这份乳腺癌细胞分割图片数据集面向医学图像处理、病理分析与深度学习方向的研究者与学习者用于解决HE染色组织病理图像中细胞分割及良性、恶性细胞分类的典型难题。数据集包含58张真实HE染色组织病理学图像配套标注信息可支撑细胞分割、分类模型训练与算法验证等任务。压缩包共232个文件由116个tif图像文件与116个xml标注文件组成tif用于承载染色组织切片图像xml则记录对应的细胞标注与类别信息整体约93.7MB结构规整便于直接读取与预处理。目前已有270人学习下载适合作为医学图像分割入门与进阶的实战素材帮助读者快速搭建训练与评估流程理解病理图像中细胞边界模糊、染色差异等挑战并在此基础上开展良性细胞与恶性细胞的后续分类研究。1. 乳腺癌细胞分割图片数据集从病理切片到可训练掩码的第一道坎拿到一批乳腺癌病理切片想训练一个能逐像素区分肿瘤细胞和间质的模型最先卡住你的往往不是网络结构而是数据。乳腺癌细胞分割图片数据集本质是一批已经配好“原图 像素级掩码”的病理图像对掩码里每个像素标着它属于哪类组织。它解决的是把医生眼里的形态学判断翻译成模型能学的监督信号。适合谁做医学图像分割的算法工程师、想复现论文的硕博生、以及手里有切片但不知道怎么变成训练集的病理科技术人员。我见过太多人一上来就调 U-Net结果 Dice 卡在 0.6 上不去回头一看掩码边界糊成一片染色深浅不一训练集和验证集还来自同一张切片——这不是模型不行是数据这道坎没迈过去。这一章先把“数据集长什么样、为什么难、选哪条路”讲清楚后面再动手。2. 拆开一份乳腺癌分割数据集图像、掩码与元信息2.1 一张合格的原图该满足什么病理切片原图通常是全切片图像WSI动辄几万乘几万像素直接喂给网络不现实。常见做法是先切 patch比如 512×512 或 1024×1024。判断原图能不能用看三点染色是否一致HE 染色批次不同颜色分布能差出天际、是否有明显模糊或折叠伪影、分辨率是否够看清细胞核边界。我一般会先抽 20 张图肉眼过一遍再用颜色统计做量化筛查。import cv2 import numpy as np def stain_stats(img_path): img cv2.imread(img_path) # BGR img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 转灰度看整体亮度分布 gray cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) # 计算 RGB 三通道均值和标准差判断染色偏色 means img.reshape(-1, 3).mean(axis0) stds img.reshape(-1, 3).std(axis0) return means, stds, gray.mean(), gray.std() means, stds, g_mean, g_std stain_stats(patch_001.png) print(RGB均值:, means, RGB标准差:, stds) print(灰度均值:, g_mean, 灰度标准差:, g_std)这段代码做的是最粗的染色一致性检查。RGB 均值反映整体色调标准差反映对比度。如果一批图里某张的 R 通道均值比其它高出 30 以上大概率是染色偏红要么剔除要么做颜色归一化。灰度标准差低于 15 的图往往对比度不足细胞核和间质分不开训练时容易变成“全预测背景”的退化解。2.2 掩码的三种常见格式与转换掩码格式不统一是复现失败的头号原因。常见有三种单通道灰度图0 背景1 肿瘤2 间质……、RGB 彩色掩码每种颜色代表一类、以及 COCO 风格的 polygon JSON。你拿到的数据集是哪种决定了后面怎么读。格式存储方式优点坑单通道灰度PNG像素值即类别读取快省空间类别值不统一有的从 0 开始有的从 1RGB 彩色PNG颜色映射类别肉眼直观颜色容差导致边界像素误判Polygon JSON文本坐标点可无损缩放解析慢重叠区域归属需规则我一般统一转成单通道灰度类别从 0 开始连续编号。转换时最容易被忽略的是 RGB 掩码的颜色容差——你以为 (255,0,0) 是肿瘤但压缩后变成 (254,3,2)直接等值判断就漏了。import numpy as np from PIL import Image def rgb_mask_to_label(mask_path, color_map, tol10): # color_map: {(r,g,b): label} mask np.array(Image.open(mask_path).convert(RGB)) label np.zeros(mask.shape[:2], dtypenp.uint8) for color, cls in color_map.items(): diff np.abs(mask.astype(int) - np.array(color)) match np.all(diff tol, axis-1) # 容差匹配 label[match] cls return label color_map {(255, 0, 0): 1, (0, 255, 0): 2, (0, 0, 255): 3} label rgb_mask_to_label(mask_001.png, color_map) print(类别分布:, np.bincount(label.flatten()))容差tol设 10 是经验值太小会漏像素太大会把相邻类别吞掉。转换完必须用np.bincount看类别分布如果某一类占比超过 90%说明颜色映射写错了或者原图本身极不平衡。2.3 元信息被忽视的泄漏源头元信息包括切片编号、患者 ID、染色批次、扫描倍率。很多公开数据集只给图像和掩码不给患者 ID结果同一患者的切片被随机分到训练和验证集指标虚高。我一般会强制按患者或切片编号做分组划分而不是按 patch 随机分。如果元信息里没有患者 ID至少按切片文件名前缀分组。这一步不做后面所有调参都是自欺欺人。3. 从原始切片到可训练集切 patch、分层与增强3.1 切 patch 的尺寸与重叠策略WSI 切 patch 不是随便切。尺寸选 512 还是 1024取决于你的显存和目标细胞尺度。乳腺癌细胞核直径通常在 5–10 微米20 倍扫描下约 100–200 像素。512 的 patch 能包含几十个细胞适合分类和分割1024 视野更大但显存吃紧。重叠stride 小于 patch 尺寸能减少边界截断但会增加冗余。我一般用 stride patch_size // 2即 50% 重叠。import openslide # 常见 WSI 读取库 import numpy as np def extract_patches(wsi_path, patch_size512, stride256, level0): slide openslide.OpenSlide(wsi_path) w, h slide.level_dimensions[level] patches [] coords [] for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): patch slide.read_region((x, y), level, (patch_size, patch_size)) patch np.array(patch.convert(RGB)) # 过滤掉几乎全白的背景 patch if patch.mean() 220: patches.append(patch) coords.append((x, y)) return patches, coords patches, coords extract_patches(slide_001.svs) print(有效patch数:, len(patches))level0是最高分辨率显存和时间不够时可以降 level。patch.mean() 220是过滤空白区域的粗筛阈值根据染色深浅调一般 200–230 之间。注意read_region的坐标是 level 0 的绝对坐标别搞混。3.2 分层采样别让背景淹没肿瘤乳腺癌切片里肿瘤区域往往只占一小部分直接切 patch 会导致 80% 以上是背景或间质。训练时模型学会“全预测背景”就能拿高准确率但 Dice 惨不忍睹。常见做法是分层采样先按掩码统计每个 patch 的肿瘤占比分成高、中、低三档按比例采样。我一般让肿瘤占比大于 10% 的 patch 占训练集的 60% 以上。def stratified_sample(patches, masks, ratios(0.6, 0.3, 0.1)): high, mid, low [], [], [] for p, m in zip(patches, masks): tumor_ratio (m 0).mean() if tumor_ratio 0.1: high.append((p, m)) elif tumor_ratio 0.01: mid.append((p, m)) else: low.append((p, m)) n min(len(high), len(mid), len(low)) # 按比例采样不足的用低档补 selected high[:int(n*ratios[0]*3)] mid[:int(n*ratios[1]*3)] low[:int(n*ratios[2]*3)] return selected这段逻辑是简化版实际要按总数反推每档取多少。关键是别让低肿瘤占比的 patch 主导训练。如果高档 patch 不够宁可减少总训练量也不要硬凑。3.3 增强病理图像不能乱翻转通用增强里随机旋转、翻转、颜色抖动都常用但病理图像有方向性——组织纹理有固定走向过度旋转可能引入不真实形态。我一般只用 90 度整数倍旋转、水平垂直翻转颜色抖动幅度控制在 ±10%。另外弹性形变对细胞分割有帮助但参数要保守否则细胞核被拉变形掩码对不上。import albumentations as A transform A.Compose([ A.RandomRotate90(p0.5), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.ColorJitter(brightness0.1, contrast0.1, saturation0.1, hue0.05, p0.5), A.ElasticTransform(alpha1, sigma50, p0.3), # 保守弹性形变 ]) augmented transform(imagepatch, maskmask)ElasticTransform的alpha控制形变强度sigma控制平滑度。病理图像我一般 alpha 不超过 1sigma 不小于 50再大就容易把细胞核揉成一团。颜色抖动的 hue 要小因为 HE 染色的色调变化本身就不大抖多了反而失真。4. 训练前的数据校验与基线跑通4.1 用可视化做最后一道检查数据进网络前我一定会做一次叠加可视化原图、掩码、叠加图三张并排。看掩码边界是否贴合细胞核、有没有整体偏移、有没有类别标错。这一步能抓出 90% 的标注或转换错误。import matplotlib.pyplot as plt def visualize(image, mask, alpha0.5): fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(image) axes[0].set_title(原图) axes[1].imshow(mask, cmapjet) axes[1].set_title(掩码) axes[2].imshow(image) axes[2].imshow(mask, cmapjet, alphaalpha) axes[2].set_title(叠加) for ax in axes: ax.axis(off) plt.show() visualize(patches[0], masks[0])叠加时alpha设 0.5 左右既能看清掩码轮廓又不遮住原图纹理。如果发现掩码整体偏移几个像素多半是切 patch 时坐标没对齐回去查read_region的坐标和掩码提取的坐标是否一致。4.2 跑一个最小基线别一上来就上大模型数据校验完先跑一个最小基线。我一般用轻量 U-Net输入 256×256batch size 8训练 20 个 epoch看 Dice 能不能到 0.7 以上。如果连 0.6 都到不了别急着换模型先回去查数据和掩码。基线跑通的意义是确认“数据到模型的管道”是通的。import torch import torch.nn as nn class SimpleUNet(nn.Module): def __init__(self, in_ch3, out_ch4): super().__init__() self.enc nn.Sequential( nn.Conv2d(in_ch, 32, 3, padding1), nn.ReLU(), nn.Conv2d(32, 32, 3, padding1), nn.ReLU(), ) self.pool nn.MaxPool2d(2) self.dec nn.Sequential( nn.ConvTranspose2d(32, 32, 2, stride2), nn.Conv2d(32, out_ch, 1), ) def forward(self, x): x self.enc(x) x self.pool(x) x self.dec(x) return x model SimpleUNet() criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3)这个网络极简只为验证管道。out_ch4对应背景加三类组织实际类别数按你的掩码来。损失用交叉熵如果类别极不平衡换成 Dice CE 组合。学习率 1e-3 是 Adam 的常用起点跑不通再降。4.3 指标怎么看Dice 之外的三个数Dice 是分割主指标但只看 Dice 会漏掉很多问题。我还会看每类的 IoU看小类是否被吞、混淆矩阵看哪两类在互相误判、以及边界区域的 Hausdorff 距离看边界贴合度。如果 Dice 高但 Hausdorff 大说明内部预测对但边界糊这时候要查掩码边界质量和损失函数是否对边界加权。5. 避坑乳腺癌分割数据集里最容易翻车的五件事5.1 训练集和验证集来自同一张切片现象验证 Dice 比训练 Dice 还高或者两者都高得离谱。原因按 patch 随机划分同一张切片的相邻 patch 同时进了训练和验证模型记住了切片特征。解决按切片编号或患者 ID 分组划分用GroupShuffleSplit或手动按文件名前缀分。5.2 掩码类别值不连续现象训练时 loss 正常下降但预测结果只有背景和某一类。原因掩码里类别值是 0、1、3、7 这种不连续编号CrossEntropyLoss的num_classes设小了或者类别映射错位。解决转换时强制重映射为 0 到 N-1 连续值用np.unique检查。5.3 染色差异导致验证集分布偏移现象训练集 Dice 0.85验证集 0.55。原因训练和验证切片来自不同染色批次颜色分布差异大。解决做颜色归一化如 Reinhard 或 Macenko或者在增强里加大颜色抖动幅度让模型对染色变化鲁棒。5.4 patch 边界截断细胞核现象预测结果在 patch 边缘出现大量半截细胞Dice 在边界区域骤降。原因切 patch 时没有重叠或者重叠太小细胞核被切成两半掩码也只标了一半。解决增大重叠到 50%训练时对边界区域加权或者用滑窗推理后拼接。5.5 背景 patch 过多导致退化解现象模型把所有像素预测为背景准确率 90% 但 Dice 接近 0。原因训练集里背景 patch 占绝对多数模型学到“全背景”就能最小化 loss。解决分层采样保证肿瘤区域 patch 占比损失函数用 Dice 或 Focal Loss降低背景权重。6. 进阶用滑窗推理和 TTA 把 Dice 再抬两个点数据管道跑通、基线稳定后想再往上抬点最稳的两个技巧是滑窗推理和测试时增强TTA。滑窗推理解决的是大图拼接问题训练时用 512 的 patch推理时对整张 WSI 按 stride 256 滑窗每个位置预测后取重叠区域的平均或投票。这样边界更平滑不会出现 patch 拼接缝。def sliding_window_inference(model, wsi, patch_size512, stride256, num_classes4): h, w wsi.shape[:2] prob_map np.zeros((num_classes, h, w), dtypenp.float32) count_map np.zeros((h, w), dtypenp.float32) model.eval() with torch.no_grad(): for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): patch wsi[y:ypatch_size, x:xpatch_size] tensor torch.from_numpy(patch).permute(2,0,1).float().unsqueeze(0) / 255.0 out torch.softmax(model(tensor), dim1).squeeze(0).numpy() prob_map[:, y:ypatch_size, x:xpatch_size] out count_map[y:ypatch_size, x:xpatch_size] 1 prob_map / np.maximum(count_map, 1) return prob_map.argmax(axis0)stride越小重叠越多结果越平滑但越慢。我一般用 patch_size 的一半。count_map防止边界区域被重复累加导致概率偏高。推理完再和掩码算 Dice通常比直接整图推理高 1–3 个点。TTA 更简单对同一张图做水平翻转、垂直翻转、90 度旋转分别推理后把概率图平均。代价是推理时间翻几倍但稳定涨点。我一般只在最终提交或论文对比时开 TTA日常调参不开。def tta_inference(model, image): probs [] for k in range(4): # 0,90,180,270度 rotated np.rot90(image, k) tensor torch.from_numpy(rotated.copy()).permute(2,0,1).float().unsqueeze(0) / 255.0 out torch.softmax(model(tensor), dim1).squeeze(0).numpy() probs.append(np.rot90(out, -k, axes(1,2))) return np.mean(probs, axis0)np.rot90的k是旋转次数逆变换时用-k。注意rotated.copy()否则负 stride 的数组转 tensor 会报错。TTA 的概率平均比投票更稳因为保留了置信度信息。最后说个我自己的习惯每次拿到新数据集先花半天只做数据校验和可视化不碰模型。这半天省下来的时间比后面调参一周都值。乳腺癌细胞分割这方向数据质量决定上限模型只是逼近上限。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网