ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UNet与UNet++细胞图像分割实战:从数据管线到切片推理

UNet与UNet++细胞图像分割实战:从数据管线到切片推理 简介基于UNet与UNet的细胞图像医学图像分割源码包面向计算机相关专业正在开展毕业设计、课程设计或期末大作业的学生也适合希望提升项目实战能力的Python学习者。压缩包共48个文件、约95KB内含44个Python代码文件、环境依赖清单、Dockerfile与项目说明文档覆盖数据预处理、网络模型构建、训练评估与预测可视化等完整环节。这套代码来自导师指导且获评99分的高分大作业结构清晰、运行简便已有181人学习。通过该资源可以掌握医学图像分割任务的工程实现思路包括数据加载与增强、Dice评分计算、切片推理以及SAHI检测工具集成等关键模块也能结合源码中的目录组织与实验流程快速复现细胞分割效果为复现论文、二次开发或完成同类课题提供可靠参考。1. 细胞图像分割的完整代码链路UNet/UNet与切片推理基于UNet和UNet的细胞图像医学图像分割Python源码最容易被低估的一点是它不止给了两个网络模型而是把训练、评估、切片推理串成了一条完整链路。绝大多数刚接触医学图像分割的人会把注意力先放在网络结构上但真正让细胞分割结果拉开差距的往往是训练前的数据管线和推理时的切片策略。这份代码以unet目录、sahi目录和几个顶层脚本组成unet_model.py负责网络定义train.py负责训练evaluate.py给离线指标predict.py处理普通尺寸图像slicePredict.py配合sahi目录处理超大病理切片。适合正在做图像分割方向毕业设计或课程设计的学生也适合想在一个项目里同时看UNet、UNet、数据增强、Dice评估和切片推理如何协作的Python开发者。2. UNet与UNet网络结构拆解跳跃连接、嵌套密集块与深度监督2.1 UNet为什么能成为医学图像分割基线细胞分割本质是逐像素二分类模型要对图中每个像素判断它属于细胞还是背景。普通分类网络最后输出一个类别概率分割网络则要输出一张与输入等尺寸的掩码。UNet的编码器-解码器结构通过下采样逐步提取高层语义再通过上采样恢复分辨率同时用跳跃连接把编码器各层细节拼到解码器对应阶段让边界信息不至于在下采样过程中完全丢失。对细胞这种目标小、边缘模糊、目标之间常有粘连的数据这种设计比单纯加深网络更稳。它也是很多人入门图像分割时接触的第一张unet网络结构图左边收缩路径右边扩张路径中间四条横向连接。这也是它和yolo这类检测式方案最大的区别不用先框出每个细胞再细化而是直接回归逐像素掩码在细胞粘连场景下更省事。2.2 unet_parts.py 的基本卷积块与通道设计unet_parts.py 里最核心的是DoubleConv、Down和Up三个模块。DoubleConv就是两次3x3卷积加BatchNorm加ReLUDown在下采样前先做DoubleConv再配MaxPoolUp负责把低分辨率特征上采样后与编码器特征拼接。常见写法如下class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.conv(x)参数上3x3卷积padding1保证特征图尺寸不变两次卷积让感受野逐步扩大而不引入过多参数。unet模型默认把第一层通道数定为64此后每下采样一次翻倍64、128、256、512、1024。底层不继续减半这是UNet原文就有的做法目的很简单最深层需要保持足够的高容量特征表达而不是一味扩大通道数。2.3 UNet的嵌套密集连接改了什么UNet原版跳跃连接是把编码器特征直接拼到解码器但两者语义层次差异大浅层细节噪声多深层语义位置偏差也大。UNet的改动是在每条跳跃路径上插入逐级卷积特征先经过一次、两次甚至三次融合再传给解码器。用公式表达就是x^{i,j}等于对上一级上采样结果和编码器特征做concat后再接一个卷积块。这样编码器和解码器特征不再是生硬拼接而是逐层对齐语义模型能学会在什么尺度上信任哪一路特征。代价是参数和计算量上升但细胞的边界通常更完整。这也是unet模型改进里风险最低的一类思路不换骨架只改跳跃连接就能观察到验证集Dice的变化。2.4 深度监督与unet_model.py的输出设计UNet的另一个特性是深度监督deep supervision。训练时每一层解码器都接一个1x1卷积输出预测多个输出分别与真值求loss再求和梯度能更直接地传到浅层梯度消失问题也得到缓解。常见实现里会有deep_supervision开关训练时打开推理时只保留最后一层输出或者把多个输出做sigmoid后取平均。unet_model.py里通常就是把模块拼起来并提供选择UNet还是UNet的入口。下面这张对比表可以直接用于写文档或论文对比项UNetUNet跳跃连接编码器特征直接拼接嵌套密集块逐级融合参数规模较小适合小数据集较大数据量少时容易过拟合梯度路径相对较长更短更密收敛通常更快深度监督一般不启用原生支持典型场景显存受限、数据量少数据较充足、追求边界精度选型建议数据量在几百张以内先用UNet跑出一个稳定基线有了可靠的数据增强并且确认数据加载无误后再开UNet的深监督看提升。我自己做细胞分割时习惯先把UNet基线跑通再切UNet做对比实验这样即使结果变差也能定位到是跳跃连接还是数据管线的差异。3. 细胞数据集构建与Dice评估数据管线里最容易拖后腿的环节3.1 dataprocess.py 的预处理逻辑细胞图像原始数据通常是显微镜或病理切片导出的染色图存在尺寸不统一、像素范围不固定、标注掩码格式杂乱的问题。dataprocess.py常见做法是先统一读图方式再resize到模型输入尺寸最后归一化。灰度输入时in_channels1如果是RGB染色图则保留3通道并把in_channels改成3。掩码处理比原图更讲究resize必须用最近邻插值否则边缘会出现介于0和1之间的假像素干扰Dice计算。def preprocess_image(img_path, size(512, 512)): img cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE) img cv2.resize(img, size, interpolationcv2.INTER_LINEAR) img img.astype(np.float32) / 255.0 # 归一化到 [0, 1] return torch.from_numpy(img).unsqueeze(0) # 单通道 def preprocess_mask(mask_path, size(512, 512)): mask cv2.imread(str(mask_path), cv2.IMREAD_GRAYSCALE) mask cv2.resize(mask, size, interpolationcv2.INTER_NEAREST) mask (mask 127).astype(np.float32) return torch.from_numpy(mask).unsqueeze(0)关键点有两处一是归一化放在resize之后避免float矩阵插值引入额外精度开销二是掩码阈值化的阈值最好按数据集统计常见标注文件背景为0、目标区域为255threshold取127比较通用。直接拿255做除法会把目标变成1.0但如果标注里存在介于两者之间的连续灰度阈值化这一步骤就更关键不能省略。3.2 data_loading.py 的Dataset与增强训练时数据加载走data_loading.py里的Dataset实现它负责把预处理后的图像和掩码按索引配对。真正的工程坑在文件名对齐图像目录和掩码目录文件名不一致时匹配应按相对路径而不是绝对路径并做好排序。class CellDataset(Dataset): def __init__(self, image_dir, mask_dir, transformNone): self.images sorted(glob.glob(str(image_dir / *))) self.masks [str(mask_dir / Path(p).name) for p in self.images] self.transform transform def __len__(self): return len(self.images) def __getitem__(self, idx): img preprocess_image(self.images[idx]) mask preprocess_mask(self.masks[idx]) if self.transform is not None: img, mask self.transform(img, mask) return img, mask数据增强方面随机水平翻转、垂直翻转、90度旋转是保底组合crop要和resize配合使用。细胞图像的方向性通常不明显几何增强基本不会破坏语义。相比自然图像医学图像里还会用弹性形变模拟组织变形albumentations可以直接做。容易错的是图像和掩码必须用同一组随机参数变换分开写Transform掩码会错位训练出来的模型学到一个错误的映射关系。我一般会先跑一个四五行的可视化脚本把Dataset返回的第一张图和第一张mask拼在一起检查提前暴露文件名错位、掩码阈值错误、增强不同步这三类最常见问题而不是等训练完再怀疑数据。3.3 dice_score.py 与评估指标的取舍细胞区域通常只占整张图的很小比例背景占九成以上。这时用accuracy会严重虚高模型全输出背景也有90%的正确率显然不能说明分割效果。医学图像分割里最常看的是Dice系数和IoU。Dice强调查全和查准的平衡对漏检小目标非常敏感所以dice_score.py在训练和评估阶段都会被重复调用。指标公式类别不平衡表现医学分割使用频率Dice2TP / (2TPFPFN)小目标漏检会被明显惩罚最常用IoUTP / (TPFPFN)与Dice单调相关变化更敏感次之Accuracy(TPTN) / (PN)背景占比高时容易虚高一般不作为唯一指标dice_score.py 通常会把每个样本摊平后计算核心代码大概长这样def dice_coeff(pred_mask, target_mask, smooth1e-7): intersection (pred_mask * target_mask).sum() return (2.0 * intersection smooth) / (pred_mask.sum() target_mask.sum() smooth)smooth参数防止分母为0也能让指标在小目标场景下更稳。评估时pred_mask必须先经过阈值化或argmax再和target计算直接传logits会让分子分母都失去语义。4. train.py 训练与 evaluate.py 评估参数设置与收敛判断4.1 环境与依赖先看项目里的requirements.txt和Dockerfile。典型的依赖是torch、torchvision、opencv-python、numpy、pillow、tqdm数据增强可能用到albumentations。直接安装依赖即可pip install -r requirements.txt如果本机cuda环境比较乱用Dockerfile构建镜像更省事torch对应的cuda版本会锁在镜像里不会污染本地环境docker build -t cell-seg . docker run --gpus all -it --rm -v $(pwd):/workspace cell-seg python train.py --net unetppDocker方式对毕设答辩演示尤其友好换一台机器只要装nvidia-docker就能复现训练结果不用重装一遍python环境。4.2 train.py 的核心参数训练入口通常是train.py接收网络类型、数据路径、epoch数、batch size、学习率等参数。一套常用启动命令python train.py \ --net unetpp \ --epochs 120 \ --batch-size 8 \ --lr 1e-4 \ --in-channels 1 \ --out-channels 1 \ --val-every 5参数含义和建议值如下表参数建议值说明--netunet 或 unetpp决定加载哪个网络结构--epochs100-150小数据集跑太久会过拟合配合早停--batch-size4-16512x512输入下8是稳妥起点--lr1e-4Adam一般1e-4SGD需要1e-2量级--in-channels1或3灰度图填1RGB染色图填3--val-every5每5个epoch在验证集上跑一次指标batch size不是越大越好一是显存放不下二是细胞数据集本身不大时过大的batch会让训练早期的梯度方向过于平滑反而不容易跳出局部最优。学习率直接决定Dice能不能涨起来损失不降时第一反应是把lr往下调一个量级而不是改网络结构。4.3 损失函数BCE与Dice的组合细胞分割是逐像素二分类任务单独用BCE在背景占优时会把边界学得模糊单独用Dice又容易出现初始loss剧烈波动。常见做法是两者加权相加训练阶段和dice_score.py里用的指标保持一致口径。class CombinedLoss(nn.Module): def __init__(self, bce_weight0.5, dice_weight0.5, smooth1.0): super().__init__() self.bce nn.BCEWithLogitsLoss() self.bce_weight bce_weight self.dice_weight dice_weight self.smooth smooth def forward(self, logits, target): probs torch.sigmoid(logits) bce self.bce(logits, target) intersection (probs * target).sum() dice (2.0 * intersection self.smooth) / ( probs.sum() target.sum() self.smooth ) return self.bce_weight * bce self.dice_weight * (1 - dice)dice的输入是sigmoid后的概率计算的是dice loss也就是1减去dice score。smooth取1.0时梯度更平滑取1e-7时指标更严格但梯度噪声更大。两个权重不一定要对半有的场景把dice_weight调到0.7边界会更锐利训练初期发现loss抖动明显就先调回0.5。4.4 用evaluate.py 验证收敛与常见问题训练结束后用evaluate.py加载最优权重在验证集上计算Dice、IoU并输出分割结果图python evaluate.py \ --checkpoint checkpoints/best_unetpp.pth \ --data data/val \ --save-dir results/eval评估时一般对每张图分别算Dice再取平均。整体Dice在0.8以上对细胞分割来说已经可用低于0.5先检查数据而不是网络。常见问题有几个loss不降通常因为学习率太高或mask没归一化导致梯度爆炸训练集Dice高而验证集低说明数据量不够先加翻转和旋转这类正则手段分割结果总是全背景检查数据增强是否对image和mask分别随机施加了参数或者mask路径是否指错。train.py里通常会配合早停连续若干个val_every周期Dice没有提升就降低学习率或直接停止。保存checkpoint时优先保存验证集Dice最高的权重而不是最后一个epoch因为小数据集上最后一个epoch往往已经过拟合。5. SAHI切片推理实战大尺寸病理图不爆显存也不漏目标的预测方案5.1 整图推理的两个致命问题病理切片导出的图像常常是几千乘几千像素。直接resize到512x512小细胞会缩成几个像素漏检不可避免直接全图推理显存又不够而且UNet对输入尺寸有下采样倍数限制任意尺寸会带来不可控的padding误差。slicePredict.py解决的正是这个问题。它依赖sahi目录下的切片推理逻辑slicing.py负责把大图切块并记录坐标prediction.py对每个块调用模型输出概率图postprocess.py把概率图按坐标贴回原图并处理重叠区域annotation.py负责把结果转成可视化标注格式。5.2 切片推理的运行命令用slicePredict.py进行推理的常用方式python slicePredict.py \ --checkpoint checkpoints/best_unetpp.pth \ --input data/test/large_slide.tif \ --output results/large_mask.tif \ --slice-width 512 \ --slice-height 512 \ --overlap-ratio 0.2 \ --mode mean切片尺寸设为512和训练输入分辨率一致避免resize带来的分布偏移。overlap-ratio 0.2表示相邻切片之间有20%像素重叠位于切片边界的细胞即使被切断也会在另一块切片内以完整形态出现。mode参数决定重叠区域如何合并mean是取平均概率二值分割也常用max各有优劣mean更能平滑边界max能保住强响应目标。5.3 边界参数与显存调节技巧切片宽高最好是网络下采样倍数的整数倍。UNet通常下采样16或32倍512已经是32的整数倍不用改。显存不够时优先减小切片尺寸而不是缩小模型输入因为模型输入一变训练和推理的分布就错位了。显存仍然紧张时把batch size调到1再不行换半精度推理。如果贴回后的结果图边缘出现黑色条纹通常是滑窗无法整除大图尺寸时padding补零造成贴回时只写真实图像区域不要把padding区回填。提示切片尺寸太小会让目标被切断的概率上升512是多数细胞分割场景下的折中值overlap提高到0.3可以进一步减少接缝伪影推理时间大约多40%。5.4 不改变网络还能降低误检的TTA技巧最后一个成本很低的稳定性技巧对每个切片做水平翻转和垂直翻转分别预测后把概率图翻回来再平均能明显压低单次预测的边界抖动。它不需要改网络也不需要重新训练代价是推理时间变为原来的4倍适合做最终结果提交或写论文指标时用。把切片尺寸固定在512的整数倍、重叠率不低于0.2并保留原始坐标回贴这套基于UNet和UNet的细胞图像分割流程在整张病理切片上就能做到显存可控且目标不漏。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进