乳腺肿瘤细胞核分割实战:从掩膜处理到模型训练

发布时间:2026/9/12 21:29:15
乳腺肿瘤细胞核分割实战:从掩膜处理到模型训练 简介医学图像分割数据集面向乳腺肿瘤细胞核分割任务提供训练集与测试集其中训练集包含66张原始图像与66张对应掩膜测试集包含167张原始图像与16张对应掩膜每张原始图像均为HE染色病理切片尺寸涵盖256×256与1000×1000两种分辨率统一使用PNG格式存储。掩膜为0/1阈值图像素值0表示背景1表示肿瘤细胞核区域类别定义清晰可在随附的classes文本中查看方便直接用于深度分割模型的训练与评估。压缩包内还配有Python可视化脚本无需修改参数即可运行随机抽取样本后同时显示原图、真实掩膜及掩膜叠加在原图上的效果便于快速检查数据和验证标注质量。整套资源共167个文件以PNG图像为主另含1个类别说明与1个可视化脚本压缩后大小约62MB已有192人学习下载。该数据集可直接服务于医学图像分割研究或课程实验适合初学者熟悉分割流程也适合科研人员用于算法验证与效果对比。1. 乳腺肿瘤细胞核分割2类数据集的真实定位拿到“乳腺肿瘤细胞核分割2类”这个数据集大部分人的第一反应是先问这和普通的目标检测数据集差在哪如果把细胞核当成一个个小方框去检测YOLO 也能开箱即用但病理图像的诊断级任务并不会这样处理。这里的“分割”要求的是像素级归属——每一个像素要么属于细胞核要么属于非核区域。2 类分割即前景核与背景非核这是病理图像分析里最常见也最容易被低估的二分类问题也是后续核分级、Ki-67 增殖指数估算、肿瘤微环境分析的前提。更实际的问题是这类数据集不像 COCO 或 ImageNet 那样容易找公开版本。因为医学数据的伦理和隐私限制很多论文里提到的乳腺病理数据集往往需要授权申请真正能直接下载的又常常是 TIFF 级别的超大图带标注但缺少现成的可视化脚本。所以你要做的核心工作不仅是“读取数据”而是把原始图、标签文件、可视化代码串成一条能快速迭代的流水线。这篇文章就从标签文件的组织方式讲起到可视化、到模型训练和评估给你一条可以照着走的路径。适合刚开始接触病理分割的研究生也适合做模型工程但第一次碰医学图像的工程师。2. 2 类标签的组织方式从掩膜到标签文件的转换逻辑2.1 细胞核分割为什么很少用四边形标注常见的目标检测数据集里标签文件大多是一串坐标表示矩形框或旋转框。但细胞核的形状是高度不规则的有的呈椭圆有的被挤压成细长条还有的紧挨着彼此边界模糊。矩形框做检测可以做分割就会把周围间质组织也框进正样本导致模型学到“核加一圈非核”的特征。所以细胞核分割的标签几乎都是语义掩膜mask即一张与原始图像同尺寸的单通道图像素值为 0 或 1多类则为 0、1、2...。这也是这个数据集命名为“2类”的原因非核为 0核为 1。如果拿到手的是坐标格式的标签文件比如 JSON 或 XML 里的多边形点集你需要先把它栅格化为掩膜。这里有一个常见误区直接用 OpenCV 的fillPoly填多边形通常没问题但要注意边界处理。病理原图分辨率可能达到 100000×100000 像素OpenCV 的坐标类型是 int32超出范围会溢出。所以第一步是切块patch在块的尺寸内做填充而不是对整张图操作。2.2 标签文件的标准样式与解析代码以目前医学图像标注工具比较通用的格式为例标签文件可以是 JSON 或 YOLO 分割格式。假设你拿到的一个 JSON 标签长这样{ image: TCGA-A2-A0YE-DX1.png, width: 512, height: 512, objects: [ {class: 1, polygon: [[120, 300], [130, 290], [145, 285], [160, 295], [155, 310]]}, {class: 1, polygon: [[300, 100], [315, 95], [330, 105], [325, 120], [300, 115]]} ] }解析这段 JSON 并生成掩膜的代码如下import cv2 import numpy as np import json def polygons_to_mask(json_path, height, width): with open(json_path, r) as f: data json.load(f) mask np.zeros((height, width), dtypenp.uint8) for obj in data[objects]: # 注意polygon 是闭式坐标fillPoly 会自动闭合 pts np.array(obj[polygon], dtypenp.int32) cv2.fillPoly(mask, [pts], color1) # 细胞核语义分割下0 是背景1 是核 mask (mask 0).astype(np.uint8) return mask这段代码的关键点有两处dtypenp.int32是 OpenCV 多边形填充的硬性要求用 float 会直接报错fillPoly的color1表示掩膜值设为 1最后再统一转成 0/1 是为了防止某些标注里出现重复叠加以外的干扰。实际做的时候我建议你不要在内存里保留整张完整掩膜直接对切块尺寸生成否则内存占用会被大图撑爆。2.3 YOLO 分割标签的转换坑有些工具导出的是 YOLO 分割格式每个目标一行class_index x1 y1 x2 y2 ...坐标被归一化到 0~1 之间。这种格式解析起来更简单但有一个容易踩的坑YOLO 分割的坐标必须乘回原始宽高再传给fillPoly否则所有核都会被画成一条斜线。另外YOLO 格式里如果多边形点数超过 100部分版本会自动抽稀导致边界损失。医学场景下宁可保留原始 JSON 也不要直接用抽稀过的格式。3. 数据可视化代码验证标签与图像对齐的实操手段3.1 一张覆盖全流程的可视化脚本有了图像和掩膜第一件事不是训练而是验证标签是否对齐。很多公开数据集提供的是二次处理的图像比如将原图从 RGB 转成了灰度或者做了颜色归一化这时如果直接叠加掩膜会出现明显的偏移。我通常写一个可视化脚本把原始图、掩膜、叠加三者并排输出到一张图上。import matplotlib.pyplot as plt import numpy as np import cv2 import os def visualize_patches(image_dir, mask_dir, output_dir, patch_size512, num_samples4): os.makedirs(output_dir, exist_okTrue) image_files sorted(os.listdir(image_dir)) # 随机筛选几个样本保证不是固定位置 np.random.seed(42) selected np.random.choice(len(image_files), num_samples, replaceFalse) for i in selected: img_name image_files[i] img cv2.imread(os.path.join(image_dir, img_name)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask_name img_name.replace(.png, _mask.png) mask_path os.path.join(mask_dir, mask_name) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 如果图像是 40x 扫描倍率推荐先下采样再显示 if img.shape[0] 2000: img cv2.resize(img, (patch_size, patch_size)) mask cv2.resize(mask, (patch_size, patch_size), interpolationcv2.INTER_NEAREST) # 叠加掩膜为 1 的区域染成红色 overlay img.copy() overlay[mask 0] (255, 0, 0) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img) axes[0].set_title(Original) axes[1].imshow(mask, cmapgray) axes[1].set_title(Mask) axes[2].imshow(overlay) axes[2].set_title(Overlay) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(os.path.join(output_dir, fvis_{img_name}), dpi150) plt.close() if __name__ __main__: visualize_patches(./images, ./masks, ./visual_check)注意代码里interpolationcv2.INTER_NEAREST是必须的。掩膜是离散值如果使用默认的双线性插值边缘会产生 0.4 这类非整数后续无论是计算 Dice 还是转成模型输入都会出问题。同步缩放到同一尺寸时原图可以用cv2.INTER_LINEAR掩膜必须用最近邻。3.2 可视化的三个观察重点生成可视化图后不要只看叠加效果漂不漂亮要看三件事第一掩膜是否覆盖细胞核的完整边界。核的边缘应该是紧贴细胞膜轮廓的如果出现某一块核边缘外面多了一圈红色说明标注者把胞浆也标进去了这会导致模型把胞浆误判为核。第二是否有粘连核被错误合并成一个连通域。很多标注软件会把紧挨的多个核合并成一个多边形这种标签在训练时会让模型学到“两个核连在一起是一个整体”。如果这种比例超过 10%建议先用分水岭算法进行后处理清洗。第三图像和掩膜的图像尺寸是否一致。偶尔会遇到 PNG 位深不同或 EXIF 旋转导致的分辨率错位可视化一眼就能看出来不用等训练完才发现评估指标不对劲。3.3 数据划分时的标签一致性切训练集、验证集、测试集时永远不要按图像文件随机切分。一个患者的切片可能包含多张图像如果同一个患者的图同时出现在训练和验证里模型会把背景纹理背下来表现会虚高。按患者 ID 分文件块是病理数据划分的基本纪律。实际操作中我习惯在数据文件名里带上患者编号然后按患者编号分组再对分组后的样本做切分这样能避免数据泄露。4. 基于 2 类掩膜训练乳腺肿瘤细胞核分割模型4.1 数据加载器与数据增强的最小实现在 PyTorch 环境下训练数据集的加载器必须处理两个问题一是图像与掩膜的同步增强二是样本类别不平衡。下面是一段可以直接用的加载器代码针对 2 类掩膜设计。import torch from torch.utils.data import Dataset import cv2 import numpy as np import albumentations as A class NucleusSegDataset(Dataset): def __init__(self, image_list, mask_list, transformNone): self.image_list image_list self.mask_list mask_list self.transform transform def __len__(self): return len(self.image_list) def __getitem__(self, idx): img cv2.imread(self.image_list[idx]) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(self.mask_list[idx], cv2.IMREAD_GRAYSCALE) # 掩膜二值化 mask (mask 0).astype(np.uint8) if self.transform: augmented self.transform(imageimg, maskmask) img augmented[image] mask augmented[mask] # 转成张量图像归一化到 [0,1]掩膜为 long 型 img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 mask torch.from_numpy(mask).long() return img, mask配合的增强管线里我会固定加入三项水平翻转、垂直翻转、随机旋转 90 度。病理图像具有旋转不变性但要注意旋转后掩膜的值必须保持最近邻采样。transform A.Compose([ A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomRotate90(p0.5), ])这里不使用随机裁剪或缩放因为细胞核的大小在同等放大倍率下是相对固定的缩放会改变核的物理尺寸导致模型学到错误的尺度特征。如果在测试集上发现精度偏低先检查训练时的放大倍率与测试图是否一致。4.2 损失函数Dice CE 的配比理由2 类分割中细胞核面积往往只占整张图的 5%20%直接用 CE Loss 会让模型倾向于把所有像素预测为背景因为背景占多数。常见做法是CE Loss 和 Dice Loss 相加。def dice_loss(pred, target, smooth1.0): # pred: logits, shape (B, C, H, W) # target: shape (B, H, W) pred_probs torch.softmax(pred, dim1) # 只针对前景类计算 Dice p pred_probs[:, 1, :, :] t (target 1).float() intersection (p * t).sum() dice (2.0 * intersection smooth) / (p.sum() t.sum() smooth) return 1.0 - dice组合使用时CE Loss 负责让每个像素的预测概率更“确定”Dice Loss 负责平衡前景与背景之间的梯度。比例上我建议CE Dice权重各为 1。也有团队把 Dice Loss 的权重放到 0.5但病理图像里核和核之间的边界对错分非常敏感权重太高会让训练早期震荡太低又学不到前景特征。保持 1:1 是一个省心的起点。4.3 评估指标Dice, IoU 与 Hausdorff 边界距离训练过程中打印单个 Dice 是不够的对于细胞核分割Dice 和 IoU 能反映面积重合度但无法反映边界质量。最好同时计算Hausdorff 距离HD95它衡量预测边界与真实边界之间的最大偏差。HD95 对医学图像分割的临床意义在于如果某条血管或某个核的边界预测偏差超过一个细胞直径即使 Dice 在 0.9 以上下游诊断任务也是不可用的。下表是模型收敛后我常拿来做判断的参考范围适用对象是 512×512 的乳腺肿瘤细胞核切片具体数值会随数据集不同变化但数量级可以作为对照。指标较好水平需排查水平Dice0.80 ~ 0.90 0.70IoU0.70 ~ 0.85 0.55HD95像素5 ~ 12 20预测核数 / 真实核数0.9 ~ 1.1 0.7 或 1.5如果预测核数明显多于真实核数多半是把单个核拆成了两半如果明显少于真实核数则是粘连核没被分开。这两种情况都无法靠调整损失权重解决需要回到数据后处理层专门做粘连核分离。5. 边界不齐与类别不平衡的实战排错5.1 验证集上手动检查预测掩膜的五个固定位置训练结束后不要只看 metric。把验证图像的预测结果和真实掩膜叠加成对比图取五张有代表性的 esample一张核密布的区域、一张核稀疏的区域、一张有脂肪空泡的区域、一张有炎症细胞浸润的区域、一张组织边缘区域。检查这五个位置比看一百张平均图更能发现问题。def predict_and_compare(model, img_tensor, true_mask_tensor, output_path): model.eval() with torch.no_grad(): logits model(img_tensor.unsqueeze(0)) pred torch.argmax(logits, dim1).squeeze(0).cpu().numpy() true true_mask_tensor.squeeze(0).numpy() fig, axes plt.subplots(1, 3, figsize(12, 4)) axes[0].imshow(true, cmapgray) axes[0].set_title(GT) axes[1].imshow(pred, cmapgray) axes[1].set_title(Pred) # 差异图红色为假阳性蓝色为假阴性 diff np.zeros((*true.shape, 3), dtypenp.uint8) diff[(true 1) (pred 0)] (255, 0, 0) diff[(true 0) (pred 1)] (0, 0, 255) axes[2].imshow(diff) axes[2].set_title(FP(red) / FN(blue)) plt.savefig(output_path, dpi150) plt.close()如果红色区域全部集中在核的边缘说明边界不够精确可以尝试在损失函数中加一个边界感知项如果蓝色区域大片出现在某个角落说明有染色不均导致模型没学到那个区域的纹理特征需要做颜色归一化。5.2 后处理用形态学操作分离粘连核模型输出的预测掩膜里粘连核是高频问题。经典的后处理方案是对预测概率图做分水岭分割前提是先做距离变换。实现如下def split_connected_nuclei(pred_mask, min_distance10): dist cv2.distanceTransform(pred_mask, cv2.DIST_L2, 5) _, local_max cv2.threshold(dist, 0.3 * dist.max(), 255, cv2.THRESH_BINARY) local_max cv2.dilate(local_max, np.ones((3, 3), np.uint8)) markers cv2.connectedComponents(local_max.astype(np.uint8))[1] markers markers 1 # 确保背景为 0 markers[pred_mask 0] 0 labels cv2.watershed(cv2.cvtColor(pred_mask * 255, cv2.COLOR_GRAY2BGR), markers) result np.zeros_like(pred_mask) result[labels 1] 1 return result这段代码的min_distance参数需要根据数据集的放大倍率调整。40 倍物镜下两个相邻核的中心距至少有 20~30 像素所以取 10 作为低阈值比较安全。如果你在推理时先对整张图切 patch那么后处理应该逐 patch 做然后再拼回全图否则全图做距离变换内存消耗太大。5.3 染色归一化一个容易被遗忘的预处理病理切片的染色强度和色相因医院、制片时间而异。同一个模型在不同中心的数据上表现差异很大往往不是模型问题而是染色分布不一致。做推理前先对每个 patch 计算其归一化统计量与训练集的中位统计量对齐。最简单的做法是Reinhard 颜色归一化——将各个通道的均值与标准差映射到目标图上。一位做病理图像的老工程师常用的话是“你省掉这步后面调三个月损失函数都救不回来。”当然如果训练和测试数据来自同一个采集源这步可以跳过。最后提一个我在调参时踩过的具体坑当你把 Dice 调到 0.87 之后发现线上表现反而比 0.84 差时先怀疑后处理而不是网络结构。很多时候预测概率分布变得自信了但粘连核没有分开Dice 虚高下游任务崩盘。把后处理的分水岭阈值从固定值改为按连通域面积自适应能让最终指标稳定许多。本文还有配套的精品资源点击获取