ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

乳腺癌细胞分割数据集处理全流程:从WSI到模型训练

乳腺癌细胞分割数据集处理全流程:从WSI到模型训练 简介这是一份面向医学图像处理、病理智能分析及深度学习入门研究者的乳腺癌细胞分割数据集。资源包含58张HE染色组织病理学图像并配套相应标注文件可用于训练细胞分割模型为后续良性与恶性细胞分类提供基础。压缩包共232个文件以tif图像和xml标注文件为主整体大小约93.7MB适合在常规深度学习框架中直接开展实验或课程项目。目前已吸引270人学习下载兼顾数据真实性与标注完整性。读者可借助该数据集完成数据加载、分割模型训练、评价指标计算等环节也可参考HE染色图像特点理解病理切片中细胞形态与颜色差异对分割任务带来的挑战为相关科研或竞赛提供可复现的实践基础。1. 乳腺癌细胞分割图片数据集真正能用来训练的分割数据长什么样“乳腺癌细胞分割图片数据集”这几个字在实操里指向的东西差别很大可能是几百张已经切好的显微细胞图也可能是以 GB 计的病理全切片WSI标注格式从二值 mask 到 JSON 多边形都有。很多人在这一步就开始踩坑——下载完直接 resize 进模型结果要么显存炸掉要么训练 loss 很好看但预测 mask 跟原图对不齐。这篇笔记要解决的就是“拿到这类数据集之后怎么做”先判断你手里是哪一种形态再统一分辨率与标注格式然后选语义分割还是实例分割方案最后用可复现的方式验证。适合正在做医学图像分割项目、以及想把细胞分割流程迁移到其他病理任务的一线工程师。2. 先分清手里是哪一种乳腺癌分割数据WSI、显微图还是公开基准库2.1 病理全切片、显微荧光图与公开基准库三种数据形态的标注逻辑乳腺癌细胞分割最常见的原始图像来源是病理全切片Whole Slide ImageWSI。一张 WSI 是组织切片在 20x 或 40x 物镜下逐视野扫描后拼接出来的大图常见格式是 SVS、NDPI、MRXS单张文件动辄几百 MB 到几个 GB像素尺寸可以到 10 万乘 10 万。病理医生标注时通常是在高倍缩放下沿着细胞团边界画多边形导出格式五花八门有的标注平台给 GeoJSON有的给 XML 坐标有的直接给一张和原图相同尺寸的 PNG mask。这个场景下的数据量“大”指的是面积大不是图片张数多。另一种常见形态是荧光显微图或细胞涂片。这类图像尺寸小得多一般在 1024×1024 到 2048×2048 之间图像里能看到清晰的细胞核或细胞膜荧光信号。标注粒度通常是单个细胞每张图可能有几十到几千个细胞实例。这种数据天然适合做实例分割或计数任务但缺点是样本数量通常不够——一个课题下来可能只有几十上百张有效图直接训练深度学习模型很快会过拟合。还有一类是公开基准库。做乳腺癌病理分割经常绕不开 CAMELYON 系列以淋巴结转移切片为主、BACH 2018组织学图像分类与弱标注、TCGA-BRCA大规模公开癌症基因组图谱项目里的 HE 切片以及一些以细胞级分割为目标的数据集。这些数据集的共同特点是“标注已经规范过”但规范格式各不相同。有的给 RLE 编码有的给 COCO JSON有的只给了组织区域级别的弱标注。你在检索“乳腺癌细胞分割图片数据集”时搜到的资源很大概率就是这几种形态的混合体。2.2 判断数据集能用的五个检查项与落地筛选表拿到任何一份乳腺癌细胞分割图片数据集我会先跑一遍五个检查项而不是直接写训练脚本。第一个是图像格式SVS/NDPI 这类金字塔格式需要用 OpenSlide 这类库读取普通 PNG/TIF 可以直接用 CV2 读第二个是物理分辨率数据集有没有提供 mpp微米每像素或放大倍率这决定了后续重采样和 patch 切图的标准第三个是标注格式是二值 mask、灰度 mask、多边形坐标还是 RLE不同格式转换成本差异很大第四个是标注粒度标注是组织区域级别的“癌区/非癌区”还是细胞核级别的“逐个细胞”这直接决定你能做什么任务第五个是背景占比随机抽几张图数一下前景像素比例如果一张 512×512 的图里只有十几个像素是目标训练时 loss 会被背景淹没。下面这张表是我实际筛选数据集时用的记录格式填完基本能判断一份数据值不值得花时间检查项要记的内容影响图像格式SVS/NDPI/TIF/PNG决定读取库与内存策略物理分辨率mpp 或物镜倍率决定重采样目标与 patch 大小标注格式mask/JSON/RLE/XML决定是否需要写转换脚本标注粒度组织区域/细胞核/细胞膜决定选语义分割还是实例分割前景占比随机估算 5 张图的均值决定 loss 设计和训练采样策略这里有一个容易被忽略的点很多数据集不直接写“mpp”而是写“40x”“20x”。常见换算关系是 40x 对应约 0.25 mpp20x 对应约 0.5 mpp但不同扫描仪有偏差所以如果能从图像属性里读到 mpp优先用 mpp 而不是倍率。这个问题在你混合多个来源的数据时尤其要命——不同扫描仪、不同倍率的数据直接混在一起训练模型会学到“尺寸偏差”而不是“细胞特征”。2.3 训练前的第一道工序让标注和原图在同一个坐标系里对起来很多人拿到数据后的第一件事是写模型我的习惯恰恰相反——先做可视化检查。具体做法是把标注以半透明方式叠加到原图上人工看三五张图确认标注没有错位、没有类别错乱。这个问题在乳腺癌细胞分割里非常普遍因为不少数据集是从公开项目里二次整理的中间经过缩放、裁剪、坐标变换某个环节坐标系不一致标注就会整体偏移几十个像素。肉眼看不出来但训练时模型会一直尝试学习一个“错位的边界”最终 loss 降不下去或者降下去了但预测边界是糊的。对 WSI 格式我一般用 OpenSlide 读低倍率缩略图来检查因为直接读全分辨率图大概率把内存打爆。下面的脚本把 WSI 的缩略图和对应 mask 叠加后保存成一张预览图import cv2 import numpy as np from openslide import OpenSlide slide OpenSlide(case01.svs) thumb np.array(slide.get_thumbnail((2048, 2048))) thumb cv2.cvtColor(thumb, cv2.COLOR_RGB2BGR) # label_png 是和 WSI 同尺寸的标注图这里也做同样缩放 mask_full cv2.imread(label.png, cv2.IMREAD_GRAYSCALE) scale 2048 / slide.dimensions[0] mask_thumb cv2.resize(mask_full, (2048, int(mask_full.shape[0] * scale))) # 把标注区域染成红色方便肉眼判断对齐情况 overlay thumb.copy() overlay[mask_thumb 0] (0, 0, 255) blend cv2.addWeighted(thumb, 0.7, overlay, 0.3, 0) cv2.imwrite(check_overlay.png, blend)逻辑说明这段代码先把 WSI 缩放到 2048 宽的缩略图再用同样的缩放系数处理标注图最后把标注区域染红叠加。这里的 scale 用的是宽度比例因为 get_thumbnail 是按等比缩放的所以高度也要按同一个比例计算。参数说明检查阶段不需要高分辨率2048 宽度足够看出标注是否对齐如果发现标注整体偏移可以打印叠加后的图像用几何变换平移或缩放修正后再进入下一步。这一步虽然简单但能挡掉后面至少一半的“训练失败”。3. 预处理三步走统一 mpp、转换标注格式、再按组划分数据集3.1 统一物理分辨率mpp 不一致会让模型学到错误尺度癌细胞核的直径在真实物理世界里是相对固定的但不同倍率下拍出来的像素尺寸完全不同。在 40x 下一个细胞核可能占 50 个像素在 5x 下同一个细胞核只占 6 个像素。如果不统一物理分辨率就直接切 patch模型看到的是“同一类目标在不同尺度下的样子”训练时它会把尺度差异当成类别特征的一部分导致泛化能力很差。统一的方法是把所有图像重采样到同一个目标 mpp。选目标 mpp 时要看任务如果只做组织区域级别的分割目标 mpp 设 1.0 到 2.0 就够如果要做细胞核级别的分割目标 mpp 建议设在 0.25 到 0.5。下面的脚本演示了如何读取 WSI 的 mpp 并计算重采样比例import cv2 import numpy as np from openslide import OpenSlide slide OpenSlide(case01.svs) # OpenSlide 属性里读 mpp单位是微米/像素 src_mpp float(slide.properties.get(openslide.mpp-x, 0.25)) target_mpp 0.5 # 重采样比例目标 mpp 越大图像越小 scale src_mpp / target_mpp new_w int(slide.dimensions[0] * scale) new_h int(slide.dimensions[1] * scale) # WSI 较小或内存足够时可以直接缩略图读取 thumbnail np.array(slide.get_thumbnail((new_w, new_h))) thumbnail cv2.cvtColor(thumbnail, cv2.COLOR_RGB2BGR) cv2.imwrite(case01_resampled.png, thumbnail)逻辑说明scale 的计算公式是源 mpp 除以目标 mpp。如果原图是 0.25 mpp目标是 0.5 mppscale 等于 0.5图缩小一半反过来如果原图是 0.5 mpp 想统一到 0.25 mppscale 等于 2图放大一倍。注意放大图像时用 INTER_CUBIC 效果更好缩小图像时用 INTER_AREA 更合适。参数说明get_thumbnail 返回的是 PIL Image需要转成 numpy 数组再交给 OpenCV 处理。这里用 slide.dimensions 拿的是 WSI 全分辨率尺寸所以计算出的 new_w、new_h 是重采样后的目标尺寸。如果 WSI 超大比如 10 万像素宽建议用 OpenSlide 的 ReadRegion 按块读取处理而不是直接缩略图否则内存占用依然很高。3.2 把 JSON 多边形与 RLE 统一成单通道 mask类别 ID 从 0 还是 1 开始分割模型的标准输入是“原图 和原图同尺寸的单通道 mask”mask 里每个像素的取值是该像素的类别 ID。背景固定为 0第一个目标类别从 1 开始。但现实是数据集给的不一定是你想要的格式。LabelMe 导出的是 JSON 多边形坐标CVAT 导出的是 COCO JSONImageJ 导出的是 RLE 或 ROI。这些格式之间没有通用转换器每次拿到新数据都要写一次脚本所以我一般会先写一个统一的 JSON 到 mask 的转换函数。下面这个脚本能处理大部分“多边形坐标列表”形式的标注import json import cv2 import numpy as np def polygons_to_mask(json_path, img_w, img_h): with open(json_path, r) as f: data json.load(f) # 标注数据里通常是形状列表兼容 labelme 和 cvat 两类结构 shapes data.get(shapes, data.get(annotations, [])) mask np.zeros((img_h, img_w), dtypenp.uint8) for shape in shapes: # labelme 的 points 是多边形顶点cvat 的 segmentation 也可能是顶点列表 points shape.get(points, shape.get(segmentation, [])) if isinstance(points, dict): # 有些导出格式里 segmentation 是 RLE 字典先跳过 continue category shape.get(label, shape.get(category_id, 1)) # 把 string 标签映射成数字 ID背景固定 0 category_id 1 if category in (tumor, cell, 癌) else 2 pts np.array(points, dtypenp.int32).reshape(-1, 2) cv2.fillPoly(mask, [pts], category_id) return mask逻辑说明这里的核心动作是 fillPoly——把多边形顶点坐标填充成指定类别 ID。不同平台的字段名不同所以脚本里同时兼容了 shapes 和 annotations 两种常见结构。RLE 格式的字典暂时跳过因为 RLE 转 mask 通常需要 pycocotools处理方式不同。参数说明category_id 的映射规则要根据数据集实际情况改。如果只有一个目标类别可以直接全部填 1如果区分癌区和间质就需要按标签名分别映射到 1 和 2。注意 mask 的 dtype 一定要是 uint8否则后面转成 PyTorch Tensor 时可能有类型问题。3.3 patch 切图的三个参数patch_size、overlap 与前景过滤阈值WSI 不能整图进网络必须切成 patch。切图的三个关键参数是 patch_size、overlap 和前景过滤阈值。patch_size 取决于目标大小做细胞核分割512×512 是常见起步值因为大多数细胞核在 0.25 mpp 下也就 40 到 80 像素512 的视野里能包含足够上下文做组织区域分割可以开到 1024。overlap 的用途是避免目标恰好被切在 patch 边界上细胞级任务我一般设 32 到 64组织级任务可以设 0因为目标区域大边界损失不明显。前景过滤阈值用来丢弃几乎全是背景的 patch。乳腺癌病理图里经常有大量空白区域这些 patch 送进网络只会让模型偏向预测背景。下面这个函数生成所有满足前景比例要求的 patch 坐标import numpy as np def generate_patch_coords(mask, patch_size512, overlap64, min_fg0.02): h, w mask.shape[:2] step patch_size - overlap coords [] for y in range(0, h - patch_size 1, step): for x in range(0, w - patch_size 1, step): crop mask[y:y patch_size, x:x patch_size] fg_ratio (crop 0).mean() if fg_ratio min_fg: coords.append((x, y)) return coords逻辑说明step 等于 patch_size 减 overlap决定两个 patch 之间的步长。overlap 越大同一张 WSI 产出的 patch 越多数据量越大但相邻 patch 高度相似可能加剧过拟合。前景比例用 mask 里非零像素的占比来算min_fg 设 0.02 表示一个 patch 里至少有 2% 的像素属于目标类别。参数说明min_fg 的取值很敏感。设太高会把稀疏标注的 patch 全滤掉导致训练数据不足设太低背景 patch 太多loss 被背景主导。我一般的做法是先统计全数据集的前景占比分布取 P10 作为 min_fg而不是拍脑袋定。如果数据集标注本身很稀疏比如一个 512×512 patch 里只有几个细胞核min_fg 可能要降到 0.005。3.4 训练验证划分必须按切片分组不能只按文件名随机切切完 patch 后一个 10 万像素宽的 WSI 能产生几千个 patch。如果直接用 train_test_split 按文件名随机划分同一个 WSI 的 patch 会同时出现在训练集和验证集里验证指标会虚高得离谱。这不是模型好而是模型见过同一片区域的“邻居”。正确的做法是按 slide_id 分组划分一个病人或一张切片的所有 patch 必须全部进训练集或全部进验证集。下面这段代码用 GroupShuffleSplit 实现按切片分组划分import pandas as pd from sklearn.model_selection import GroupShuffleSplit df pd.read_csv(patches.csv) # 列: patch_path, mask_path, slide_id groups df[slide_id] gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(df, groupsgroups)) train_df df.iloc[train_idx].reset_index(dropTrue) val_df df.iloc[val_idx].reset_index(dropTrue) print(训练 patch 数:, len(train_df), 验证 patch 数:, len(val_df)) print(训练切片数:, train_df[slide_id].nunique()) print(验证切片数:, val_df[slide_id].nunique())逻辑说明GroupShuffleSplit 的 groups 参数指定了“哪些样本属于同一个组”划分时保证同一组的数据不会同时落在训练和验证两侧。这样验证集里出现的都是模型没见过的切片区域指标才有参考意义。参数说明test_size 按切片数量比例来设而不是按 patch 数量比例。如果一个项目只有 30 张切片test_size 0.2 意味着验证集只有 6 张切片模型验证的稳定性会差一些这时可以考虑改成 K 折交叉验证。random_state 固定下来方便后续复现和对比实验。提示如果数据里有“同一病人的多次活检”这种情况分组依据应该用 patient_id 而不是 slide_id否则同一病人的不同切片还是可能泄漏。4. 模型选型与训练配置语义分割还是实例分割别等训练完才决定4.1 你的任务落到哪个输出区域占比、细胞计数还是二者都要乳腺癌细胞分割图片数据集的用途通常分成两类。一类是“区域分割”判断图像里哪些区域是癌组织、哪些是间质、哪些是坏死区最终产出是一个类别概率图下游任务可能是计算肿瘤间质比或肿瘤浸润淋巴细胞TIL密度。这类任务用语义分割算法就够输出的是一个逐像素类别图。另一类是“细胞计数”要数出图像里到底有多少个癌细胞核或者每个细胞的轮廓边界。这类需求如果只用语义分割细胞之间稍微粘连一点结果就连成一片根本数不清。所以任务类型必须在选模型之前定下来——语义分割和实例分割的分界线就是“目标之间是否需要区分个体”。如果二者都要比如既要区域的 TIL 密度又要知道每个细胞的形态那就得上一套实例分割方案或者语义分割加后处理分水岭。很多人在这一步翻车的原因是把“分割”当成一个统一的任务随便下个语义分割模型就开始训训完之后发现数不了细胞再回头换实例分割数据准备和训练全部重来。所以我的建议是先花半天时间明确输出需求再决定技术路线而不是先选模型。4.2 先用 U-Net DiceLoss 跑出一份 baseline一个能改的参数模板对中小规模的乳腺癌细胞分割数据集U-Net 依然是最稳的起步模型。它的结构简单、显存占用可控、在少量数据下也不容易过拟合到完全没法看。这里给出一份我常用的训练循环模板它可以直接套在 PyTorch 工程里import torch import torch.nn as nn # loss 用 Dice BCE 组合兼顾区域重叠和像素级精度 def dice_bce_loss(pred, target, smooth1.0): pred torch.sigmoid(pred) bce nn.functional.binary_cross_entropy(pred, target) inter (pred * target).sum() dice 1 - (2.0 * inter smooth) / (pred.sum() target.sum() smooth) return bce dice model UNet(in_channels3, out_channels1) # 二分类输出单通道 optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-5) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max200) for epoch in range(200): model.train() for img, mask in train_loader: img, mask img.cuda(), mask.cuda() pred model(img) loss dice_bce_loss(pred, mask) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step()逻辑说明输出层只有一个通道配合 sigmoid 做二分类背景和前景用阈值 0.5 切分。Dice loss 解决正负样本不平衡问题BCE 保留像素级梯度信号两者相加是医学分割里最常见的组合。参数说明学习率 1e-4 是 AdamW 在分割任务上的通用起点如果 loss 震荡太厉害可以降到 3e-5batch size 取决于显存细胞分割 patch 通常是 512×512batch 8 在 24G 显存下比较稳epoch 设 200 是因为医疗数据量小加上 CosineAnnealing 调度器后期学习率下降能帮助收敛到更平滑的局部最优。4.3 升级路线YOLOv8-seg 与分水岭后处理怎么选如果你的任务明确了要实例级输出——每个细胞单独一个轮廓——常见做法是上 YOLOv8-seg 这类实例分割模型。它的优势是训练命令简单、推理速度快、部署生态成熟。下面是用 YOLOv8-seg 训练自定义数据集的命令和配套的 data.yaml 配置yolo segment train datacells.yaml modelyolov8n-seg.pt epochs200 imgsz512 batch8 projectruns/segment namebreast_baseline# cells.yaml path: ./cells train: images/train val: images/val nc: 1 names: [cell]数据格式上 YOLOv8-seg 要的是“图像 同名 txt”txt 里每行是一个实例的轮廓归一化坐标。如果你的标注是语义分割的 mask需要先提取连通域再转轮廓。但这里有一个现实问题癌组织区域的细胞密度极高一张 512×512 的 patch 里可能有几百个细胞实例数量一旦超过几百YOLOv8-seg 的训练和后处理都不轻松检测头也会因为目标重叠严重而漏检。另一种常见做法是“语义分割 分水岭后处理”先用 U-Net 预测前景概率图再对概率图做距离变换最后用分水岭算法把粘连的细胞核分开。这种做法在病理细胞分割里非常经典而且不需要实例级标注因为它是从区域 mask 出发生成实例。分水岭的关键参数是最小峰高和最小区域面积这两个值要根据细胞核大小调没有通用值。两种路线怎么选取决于你的标注格式和下游需求。如果数据集自带实例级标注每个细胞一个多边形优先用 YOLOv8-seg如果只有区域 mask但又想数细胞分水岭是更省事的路径。4.4 一次选型参考表网络、标注成本、训练参数与适用场景方案需要的标注输出形式适用场景训练成本U-Net 语义分割区域 mask类别概率图癌区占比、TIL 密度低单卡可训DeepLabv3 语义分割区域 mask类别概率图大视野组织分割中速度略慢YOLOv8-seg 实例分割实例多边形每个目标的 mask细胞计数与形态分析中标注成本高U-Net 分水岭区域 mask实例 mask从语义标注转实例计数低调参略繁琐训练参数这块给一个起步参考表。它不保证最优但能让你第一次训练不翻车参数建议值说明patch_size512细胞核分割的通用选择batch_size824G 显存下的安全值learning_rate1e-4AdamW配合 CosineAnnealingepochs200小数据集建议配早停lossDice BCE解决前景稀疏问题数据增强翻转 旋转 弹性形变医疗小数据的刚需5. 高频翻车点排查5 个让细胞分割项目白忙一场的细节5.1 loss 降到很低但 mask 糊成一团先检查标注是否错位现象训练 loss 一路降到 0.1 以下但把预测结果叠到原图上一看mask 边缘全是糊的边界偏移明显甚至整片区域位置都不对。原因数据准备阶段标注和原图坐标系不一致。常见来源是数据集二次整理时先裁剪了原图、后缩放了标注或者 WSI 读取时选了不同 level 导致坐标未换算。解决回头运行 2.3 的叠加检查脚本随机挑 35 个 patch 人工核对。发现错位后用仿射变换把标注对齐到原图再重新切 patch。注意 mask 和原图用同样的插值方式mask 放大时要用最近邻插值避免类别边界被插出中间值。5.2 验证集 mIoU 虚高到 0.9同一张切片的 patch 泄漏进了训练集现象验证集 Dice 0.88mIoU 也不错换一批外部数据测试直接跌到 0.4。排查发现训练时验证集指标稳定得很好但模型泛化能力很差。原因数据划分用了普通随机切分同一个 WSI 的数千个 patch 被同时分到训练集和验证集。验证集里全是训练集的“邻居”模型记住局部纹理就能拿高分。解决严格按 slide_id 分组划分见 3.4 的 GroupShuffleSplit 代码。同一病人的多次活检尽量按病人分组。如果数据量太少改成按切片分组的 K 折交叉验证比单次划分稳定得多。5.3 语义分割结果里细胞连成一片需求是计数方案却选错了现象模型输出的前景区域大块连在一起细胞核之间没有边界数不出个数。训练时的 Dice 指标不低因为重叠区域确实被覆盖了但下游任务要的是“每张图里有多少个细胞”这结果没法用。原因把计数任务硬套在语义分割上。语义分割天然不区分个体相邻细胞的 mask 会在边界处融合。解决先回到 4.1 的需求判断。如果必须计数两条路一是准备实例级标注用 YOLOv8-seg 训练二是保留语义分割模型在后处理里对前景概率图做距离变换加分水岭把粘连区域切开。5.4 显存 OOM 或训练慢到无法接受把 WSI 整图 resize 进了模型现象训练脚本一启动就报 CUDA out of memory或者一个 epoch 要跑几小时。检查代码发现有人直接把 WSI 用 OpenCV 的 resize 压到 1024×1024 整图送进网络。原因WSI 的原始信息量远大于普通图片整图 resize 到小尺寸虽然能塞进显存但细胞级别的细节全部丢失模型学的不是分割是“缩略图抠图”。同时大图进入网络后特征图尺寸极大计算量和显存占用成倍增长。解决统一重采样到目标 mpp 后切 patchpatch 尺寸控制在 512 到 1024batch 大小按显存动态调整。如果显存还是不够用梯度累积模拟更大 batch而不是把 patch 调大。5.5 推理时背景噪声大、假阳性一片前景过滤阈值和增强策略不当现象训练时指标正常但推理整张 WSI 时边缘血管、空白区域被大量误报成阳性视觉上像蒙了一层噪点。原因训练 patch 里背景占比过高模型没学过“什么是坚决不要的前景”。另一部分原因是数据增强里只做了翻转没有覆盖染色差异和亮度变化推理时遇到不同批次的切片染色差异就翻车。解决切 patch 时按前景比例过滤min_fg 设到 0.02 以上。推理时对输出概率图做形态学开运算去掉孤立噪点。对于来自不同扫描仪的样本增加染色归一化预处理或者把弹性形变、颜色抖动加进增强策略。6. 验证分割结果把 patch 拼回 WSI再和人工计数对一下6.1 并行重建全分辨率 mask解决 overlap 重叠区域的拼接模型输出的是一个个 patch 的预测 mask最终要拼回整张 WSI 才有业务意义。拼接时最怕的是 overlap 区域出现接缝。处理方式不是直接覆盖而是用叠加计数取平均每个位置的预测值累加同时记录该位置被多少个 patch 覆盖最后除以覆盖次数。import numpy as np def reconstruct_mask(coords, patch_predictions, out_size): coords: [(x, y), ...] patch 左上角坐标列表 patch_predictions: [mask_patch, ...] 模型输出的逐 patch 预测 out_size: (h, w) 重建后 mask 尺寸 acc np.zeros(out_size, dtypenp.float32) count np.zeros(out_size, dtypenp.float32) for (x, y), pred in zip(coords, patch_predictions): ph, pw pred.shape[:2] acc[y:y ph, x:x pw] pred count[y:y ph, x:x pw] 1 # 防止除零未被覆盖的区域保持 0 acc np.divide(acc, count, outnp.zeros_like(acc), wherecount 0) # 二值化时给概率 0.5 一个阈值 return (acc 0.5).astype(np.uint8)逻辑说明count 数组记录每个像素被多少个 patch 覆盖最终 acc 是平均概率。overlap 越大count 的值越大平均后能有效平滑模型在 patch 边缘的不稳定预测。6.2 用视觉检查和计数一致性做双通道验证重建完 mask 后我会做两件事。第一是用 3DSlicer 或其他支持叠加显示的软件把原图和 mask 叠在一起按滑动切片的方式检查边界是否贴合、有没有整块区域被漏掉。这一步主要抓“结构性错误”比如某个象限整体没预测出来。第二是统计计数对重建后的 mask 做连通域标记数出细胞数量再和同一个区域的人工标注数量对比误差在 10% 以内的模型才有上线价值。from scipy import ndimage # mask_pred 是重建后的二值 mask label_map, num_cells ndimage.label(mask_pred) print(模型预测细胞数量:, num_cells)逻辑说明ndimage.label 会为每个连通域分配一个唯一 IDnum_cells 就是细胞数量。这个值如果和人工计数对不上通常是粘连没切开或小目标被过滤掉了。我现在的习惯是任何一份乳腺癌细胞分割图片数据集到手先花一天时间做完可视化、重采样、格式统一和分组划分模型训练反而是后面最简单的一步。这套流程挡住过太多次“训练指标好看、实际一用就翻车”的问题建议你也不要在模型选型上花太多时间纠结先把数据管线理顺。希望帮到你。本文还有配套的精品资源点击获取
返回列表