尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FCN语义分割实战:腹部多脏器5分割从数据到模型全解析

FCN语义分割实战:腹部多脏器5分割从数据到模型全解析 简介这份资源面向需要进行医学图像分割实验的研究者或学习者以FCN网络为基础提供了完整的腹部多脏器五分割实战方案。包内包含数据集、训练代码、推理脚本以及训练好的权重文件代码经测试可直接使用。项目在仅训练20个epoch时全局像素准确度达0.99miou达0.80若增加训练轮数性能还有进一步提升空间。训练脚本会自动读取标签灰度值并对应FCN输出通道支持resnet50与resnet101作为特征提取骨干同时采用余弦退火学习率、交叉熵损失和Adam优化器并自动保存损失曲线、iou曲线、训练日志及最优权重。推理部分只需将图像放入inference目录后运行predict.py即可无需额外参数。资源共1025个文件主要包含png图像样本、Python源码、模型权重pth文件及说明文档压缩包大小约456MB目录结构清晰适合入门到进阶的医学图像分割实践者参考使用。已有219人学习下载。1. 为什么是 FCN腹部多脏器 5 分割的第一块敲门砖拿到一批腹部 CT标注里包含肝脏、脾脏、左肾、右肾、胰腺五个脏器要做的是一次性把它们全部切出来这就落到了 FCN 网络对腹部多脏器数据集的 5 分割任务上。FCN全卷积网络是语义分割真正意义上的第一个端到端方案它把 VGG 这类分类网络改造成能输入任意尺寸图像、输出逐像素类别标签的模型。在很多公开腹部多脏器数据集上FCN-8s 依然是跑 baseline 的首选也是后续理解 U-Net、DeepLab、Mask R-CNN 的基础。这套流程尤其适合刚接触医学影像分割的人能把数据清洗、模型训练、指标评估、结果后处理完整串起来一次打通。2. 把 FCN 讲透全卷积、上采样和跳跃结构为什么能用于多脏器分割2.1 从分类网络到全卷积丢掉全连接层到底丢掉了什么经典的 VGG16 分类网络最后三层是Linear(25088, 4096)、Linear(4096, 4096)、Linear(4096, 1000)输入 512×7×7 的特征图会被拍成一个长向量再映射成 1000 类的概率。问题在于全连接层把空间位置关系彻底抹平了——图像里“肝脏在右上、脾脏在左上”这种空间对应关系在向量化之后不复存在。对 5 分割任务来说我们要的不是“这张图里有没有胰腺”而是“第 300 行第 200 列那个像素到底是不是胰腺”。FCN 的做法是把全连接层替换成 1×1 卷积。等效地看卷积和全连接在数学上都能做线性映射但 1×1 卷积不要求输入特征图被拍平输出仍然是 H×W×C 的热力图空间位置一一保留。这一步替换带来的代价是感受野的变化全连接层天然能看到整张图的全局信息而 1×1 卷积只看单个位置深层特征里的全局语义是靠前面无数层卷积逐渐累加出来的。这也是为什么 FCN 需要后面的跳跃结构来补救浅层细节而不是像分类网络那样一次投票得出结果。2.2 转置卷积上采样FCN-32s、FCN-16s、FCN-8s 的区别VGG16 经过 5 次池化后特征图尺寸缩到输入的 1/32。要把这么小的热力图恢复到原图大小做逐像素预测依赖的是转置卷积也叫反卷积。转置卷积和普通卷积的前向/反向方向互换普通卷积把大图变小转置卷积把小图变大变大的倍数由卷积核大小、步长、padding 共同决定。比如 kernel4、stride2、padding1 的转置卷积输出尺寸恰好是输入的两倍这是 FCN 最常用的一档配置。FCN 原文给出了三个变体差别在于融合了哪些层的特征网络变体融合的特征层最终上采样倍数分割细节水平FCN-32s只用 pool5直接 ×32边缘粗糙小器官基本靠猜FCN-16spool5 pool4融合后再 ×16中等器官的边缘明显改善FCN-8spool5 pool4 pool3融合后再 ×8小器官边界最清晰胰腺更稳在腹部多脏器 5 分割场景里我一般直接选 FCN-8s。原因很直接胰腺是所有目标里体积最小、边界最模糊的它和周围脂肪、肠管的灰度非常接近如果只用 pool5 的深层语义上采样 32 倍胰腺很容易被拉成一个模糊的小团块。pool3 对应原图 1/8 分辨率保留了更多血管纹理和器官边缘信息融合进来之后对小目标的恢复能力明显更强。2.3 为什么拿 VGG-16 做骨干预训练与感受野FCN 的骨架选择 VGG-16不只是因为它结构简单。VGG-16 有 5 个 stage、13 个卷积层加 3 个全连接层每个 stage 内部用 3×3 卷积堆叠感受野逐层扩张浅层负责边缘、纹理深层负责语义。更重要的是 VGG-16 在 ImageNet 上有现成的预训练权重加载后可以把分类头换掉、卷积部分直接作为特征提取器初始化。医学影像数据量通常不大一个腹部多脏器数据集的训练切片往往只有几百到几千张完全随机初始化 VGG 层很难收敛到像样的结果而加载预训练权重后模型相当于只学“怎么在 CT 图像里找器官边界”而不是从零学“怎么看图”。2.4 多脏器分割选型对比FCN 与 U-Net、DeepLab 的取舍针对腹部多脏器 5 分割很多人一上来就想上 U-Net但我的建议是先把 FCN-8s 跑通再决定要不要换。这里有个容易忽略的差别FCN 是前馈结构特征从骨架上走一遍中间只做特征相加U-Net 是编码器-解码器结构每一层都要做跨层拼接显存占用和计算量都更大。DeepLab 系列引入了空洞卷积和各种后处理精度有优势但实现复杂度高新手调参容易失控。对比维度FCN-8sU-NetDeepLab v3模型参数体量较大较小较大显存占用中等偏高偏高小器官分割效果一般较好较好训练调参难度低中等较高代码成熟度高高中第一次跑多脏器分割我建议用 FCN-8s 当基线。它的收敛行为比较直白loss 不降、Dice 不动时问题大概率出在数据而非模型结构。等基线数据、指标、后处理都打通了再换成 U-Net 或 3D 模型去冲精度心态会稳很多。3. 处理腹部多脏器数据集从 DICOM 到能喂给 FCN 的 Tensor3.1 认识数据nii.gz 还是 png标签在哪里腹部多脏器数据集的形态大致分两种一种是完整的 3D 医学影像文件常见的是 nii.gz 格式里面同时包含 CT 体积数据和体素间距信息另一种是已经抽取好的 2D 切片通常是 png 或者 jpg配一个同尺寸的 mask 图。无论是哪种第一步都要确认标签通道里到底有哪些值。常见约定是背景为 0肝脏为 1脾脏为 2左肾为 3右肾为 4胰腺为 5但不排除不同数据集的标注顺序不一样。读入之后必须用np.unique扫一遍 mask确认里面实际出现的类别值再决定要不要做重映射。import numpy as np import SimpleITK as sitk def load_nii_mask(path): # 读取 nii.gz 格式的 mask 文件 itk_img sitk.ReadImage(path) mask sitk.GetArrayFromImage(itk_img) # 输出形状为 (z, h, w) print(mask shape:, mask.shape, dtype:, mask.dtype) print(unique labels:, np.unique(mask).tolist()) return mask # 使用示例确认标签值范围别急着归一化 # mask load_nii_mask(label.nii.gz)这段代码里sitk.GetArrayFromImage输出的轴顺序是 z、y、x和医学影像展示习惯一致但和很多深度学习代码里常见的 (C, H, W) 顺序不同一定要先确认轴顺序再往下走。另一个关键点是看 label 的 dtype很多标注工具会把它存成 uint8 或 int16如果出现负值或 255 这种异常值说明原始标注里有未定义区域或漏标后面统计 Dice 时要把这些位置排除掉。3.2 窗宽窗位不同器官的灰度范围差异为什么直接影响分割结果CT 图像的像素值单位是 HU亨氏单位但人体组织的 HU 范围跨度很大空气约 −1000脂肪约 −100 到 −50软组织约 20 到 80骨皮质能到 1000 以上。如果把原始值直接归一化到 0-1软组织之间的差异会被压缩得特别小肝脏、脾脏、胰腺在归一化图里几乎是一个灰度FCN 很难学到有用的边缘特征。腹部扫描一般用固定窗宽窗位把 HU 范围截断后再映射到 0-255一个常用的经验窗口是窗宽 400 HU、窗位 40 HU对应显示范围是 −160 HU 到 240 HU。def apply_hu_window(image_hu, ww400, wl40): # 输入为原始 HU 值的 CT 数据输出为 0-255 的 uint8 lower wl - ww / 2.0 # 计算窗口下限 upper wl ww / 2.0 # 计算窗口上限 clipped np.clip(image_hu, lower, upper) normalized (clipped - lower) / (upper - lower) return (normalized * 255).astype(np.uint8)ww是窗宽决定显示范围的大小wl是窗位决定范围的中心。参数调低窗位暗部组织会更亮调高窗位图像整体变暗。胰腺和肠管在 −160 到 240 HU 这个区间里依然容易混淆不要指望单窗口解决所有分割难点它只是把输入分布拉到一个 FCN 更容易学习的区间。对 5 分割任务我建议先固定这一套窗口跑通全流程之后想提精度再尝试多窗口输入或融合。3.3 重采样与裁剪解决层间距不一致和显存限制同一个数据集里不同患者的 CT 层间距可能从 0.5 mm 到 5 mm 不等直接按原始切片训练的话模型会看到“同一个肝脏不同厚度”的混乱输入。5 分割这类 2D 分割任务惯用做法是先重采样到固定各向同性体素间距比如 1×1×1 mm 或 2×2×2 mm再沿 z 轴抽取切片。重采样时图像用线性插值mask 必须用最近邻插值否则标签边缘会被插出一些并不存在的中间值。import SimpleITK as sitk def resample_to_spacing(itk_image, new_spacing(1.0, 1.0, 1.0)): # 重采样到各向同性体素间距 orig_spacing itk_image.GetSpacing() orig_size itk_image.GetSize() new_size [ int(round(orig_size[i] * orig_spacing[i] / new_spacing[i])) for i in range(3) ] resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing(new_spacing) resampler.SetSize(new_size) resampler.SetOutputDirection(itk_image.GetDirection()) resampler.SetOutputOrigin(itk_image.GetOrigin()) return resampler.Execute(itk_image)new_spacing的数值决定了重采样后体素的大小越小图像越精细、切片数越多、显存压力越大。1 mm 各向同性对于肝脏、脾脏这种大器官来说足够用重采样时间也可控。重采样完有些患者的腹腔只占整张图的 60% 以下这时候我会按训练集的平均形态做一个中心裁剪比如把 512×512 裁到 384×384 或 320×320既保留所有器官又显著减少背景面积让 FCN 的正样本比例更健康。3.4 数据划分与增强五分类里最容易出错的索引二维切片级别的分割任务一个最容易犯的错是按切片随机划分训练集和验证集。同一个患者的相邻 CT 切片非常相似切片级随机划分会把“同一个人的同一个肝脏”同时送进训练和验证Dice 虚高得离谱一换到新患者就现原形。正确做法是按患者划分把患者 ID 作为划分单位比如 80% 患者训练、10% 验证、10% 测试。# 数据增强用 albumentations对 image 和 mask 做相同变换 import albumentations as A train_transform A.Compose([ A.RandomCrop(256, 256), A.HorizontalFlip(p0.5), A.Rotate(limit10, p0.5), A.RandomBrightnessContrast(p0.2), ])增强时同样的变换必须同时作用在 image 和 mask 上所以用albumentations这类封装。RandomCrop的尺寸需要和网络输入对齐Rotate角度控制在 15 度以内角度过大会把 CT 里的解剖结构扭到不合理的形态。这里还有一个小索引陷阱如果 mask 的类别值是 1-5、背景为 0那么网络的num_classes要设成 6然后让 FCN 输出 6 通道的 logits类别索引直接对应。不要自作聪明把背景去掉改成 5 类否则所有背景像素都会参与梯度计算模型迟早整出“肝不肝、肾不肾”的错误分割。4. 用 PyTorch 复现 FCN-8s模型搭建、损失函数与训练参数4.1 搭建 FCN-8s 的分割头1×1 卷积、Dropout 和跳跃融合用 PyTorch 复现 FCN-8s 最稳妥的方式是直接加载torchvision.models.vgg16的预训练权重然后把它的 features 拆成五个 stage分别拿到 pool3、pool4、pool5 的特征。FCN 原文里把 VGG 的全连接层改造成了两层卷积加 Dropout再接一层 1×1 卷积输出类别分数整个过程不依赖任何第三方分割库代码量很小。import torch import torch.nn as nn from torchvision import models class FCN8s(nn.Module): def __init__(self, num_classes6): super().__init__() vgg models.vgg16(pretrainedTrue) features vgg.features # 按 VGG16 的池化位置拆出 5 个 stage self.stage1 features[0:5] # 输出 1/2 self.stage2 features[5:10] # 输出 1/4 self.stage3 features[10:17] # 输出 1/8 pool3 self.stage4 features[17:24] # 输出 1/16 pool4 self.stage5 features[24:31] # 输出 1/32 pool5 # 把 VGG 的全连接层替换成卷积层 self.classifier nn.Sequential( nn.Conv2d(512, 4096, kernel_size7, padding3), nn.ReLU(inplaceTrue), nn.Dropout2d(p0.5), nn.Conv2d(4096, 4096, kernel_size1), nn.ReLU(inplaceTrue), nn.Dropout2d(p0.5), ) # 各层输出的类别分数 self.score_fr nn.Conv2d(4096, num_classes, kernel_size1) self.score_pool4 nn.Conv2d(512, num_classes, kernel_size1) self.score_pool3 nn.Conv2d(256, num_classes, kernel_size1) # 转置卷积上采样分别对应 2x、2x、8x self.upscore2 nn.ConvTranspose2d(num_classes, num_classes, kernel_size4, stride2, padding1) self.upscore_pool4 nn.ConvTranspose2d(num_classes, num_classes, kernel_size4, stride2, padding1) self.upscore8 nn.ConvTranspose2d(num_classes, num_classes, kernel_size16, stride8, padding4) def forward(self, x): h self.stage1(x) h self.stage2(h) pool3 self.stage3(h) pool4 self.stage4(pool3) pool5 self.stage5(pool4) h self.classifier(pool5) h self.score_fr(h) # 1/32 h self.upscore2(h) # 1/16 h h self.score_pool4(pool4) h self.upscore_pool4(h) # 1/8 h h self.score_pool3(pool3) h self.upscore8(h) # 恢复到原图分辨率 return h几个参数值得单独说明。features[0:5]取到第一个 MaxPoolfeatures[10:17]以池化层结尾所以 stage 输出的空间尺寸按 2 的倍数缩小padding3的 7×7 卷积保证特征图尺寸不变kernel_size16, stride8, padding4的转置卷积恰好把 1/8 特征图放大 8 倍。如果你输入图片的尺寸不是 8 的倍数输出边缘会多出几个像素训练阶段建议统一裁剪到 256×256 或 512×512避免这种尴尬。pretrainedTrue首次运行会下载权重网络环境不好时先手动下载放到缓存目录。4.2 损失函数怎么选多脏器分割中的类别不平衡问题5 分割任务最典型的不平衡是胰腺和脾脏占比极小。一个 512×512 切片里肝脏可能占了 3 万个像素胰腺往往只有 1000 到 3000 个像素如果直接用CrossEntropyLoss胰腺的梯度贡献会被肝脏淹没网络学到的是“全部预测成背景就已经够低 loss”的偷懒路径。两种常用解法给类别加权重或者改用 Dice Loss。def compute_class_weights(labels, num_classes6, smooth1e-6): # 统计每个类别的像素占比反比作为权重 counts np.bincount(labels.flatten(), minlengthnum_classes).astype(np.float32) weights 1.0 / (counts smooth) weights weights / weights.sum() * num_classes # 归一化到均值为 1 return torch.tensor(weights, dtypetorch.float32) class DiceLoss(nn.Module): def __init__(self, num_classes6, smooth1e-6): super().__init__() self.num_classes num_classes self.smooth smooth def forward(self, logits, target): probs torch.softmax(logits, dim1) target_onehot torch.nn.functional.one_hot( target.long(), num_classesself.num_classes ).permute(0, 3, 1, 2).float() intersect (probs * target_onehot).sum(dim(2, 3)) numerator 2 * intersect self.smooth denominator probs.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) self.smooth dice numerator / denominator return 1 - dice.mean()compute_class_weights里smooth的作用是避免某个类完全没有像素时除零归一化让背景权重小于 1让五个器官类别的权重整体偏大。DiceLoss对类别不平衡有天然抗性因为它按类计算交集占比不依赖全局像素数量。我一般把加权 CrossEntropy 和 Dice Loss 按 0.5 : 0.5 组合起来用前者的梯度更平稳后者能把小器官的边界拉回来。4.3 训练参数学习率、batch size、优化器和 epoch 的经验值FCN 用预训练 VGG 初始化后骨干网络已经处于一个不错的局部最优附近学习率太大会把预训练特征冲坏。腹部 5 分割的一套稳妥配置如下参数推荐值说明优化器Adam默认 betas(0.9, 0.999)初始学习率1e-4骨干层可设 1e-5分割头 1e-3权重衰减1e-4防止过拟合batch size4 到 8输入 256×256 时显存 6G 可跑学习率调度ReduceLROnPlateau验证 Dice 停滞时降 0.1训练轮数60 到 100医学数据少早停机制建议开启ReduceLROnPlateau跟随指标选择modemax监控验证集的平均 Dice 而不是 loss。学习率初始化可以先跑 5 个 epoch 做一次 warmup 观察曲线如果第一轮 Dice 从 0 爬到 0.4 以上1e-4 这个量级就是合适的如果一直在 0.2 附近震荡大概率是数据预处理出了问题调学习率没有意义。4.4 显存不够怎么训练混合精度、梯度累积和 patch 训练很多人一上来就把 512×512 的切片直接塞进模型batch size 设成 8结果 11G 显存直接溢出。常见做法是降分辨率、降 batch或者用混合精度训练。PyTorch 的torch.cuda.amp可以在几乎不影响精度的情况下把显存占用砍掉一半而且现代显卡对自动混合精度有硬件加速训练速度还更快。scaler torch.cuda.amp.GradScaler() for images, masks in dataloader: images, masks images.cuda(), masks.cuda() with torch.autocast(device_typecuda): logits model(images) loss criterion(logits, masks) # 先反向传播再手动更新 scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad()如果混合精度后还是不够可以叠加梯度累积每accum_steps4个 batch 更新一次参数等效于把 batch size 扩大 4 倍显存占用不变。这两种手段之外还有一个更符合医疗场景的办法把 512×512 的大切片切成四个 256×256 的 patch 训练原图推理时再用滑窗拼接。腹部多脏器数据集的器官一般不会横跨整图patch 训练还能顺便增加样本量是目前兼顾显存和精度的常用路线。5. 5 分割避坑指南训练和预测阶段最常见的几个翻车点5.1 训练 Loss 不乱、Dice 却不动小器官被大器官吃掉了现象训练 loss 稳步下降看到 65 个 epoch 了肝脏、脾脏的 Dice 从 0.4 涨到 0.9但胰腺的 Dice 始终在 0.1 附近徘徊甚至偶尔掉到 0。原因这是典型的类别不平衡问题。加权 CrossEntropy 的权重虽然给了胰腺但权重只在 loss 计算时生效FCN 输出的特征图里胰腺区域本身占比太小梯度在大器官面前被稀释了。再叠加窗宽窗位不合适时胰腺和肠管对比度不够模型根本没学到胰腺的边缘特征。解决先用 3.2 的窗宽窗位确认输入图像里胰腺和周围组织的灰度差是否肉眼可辨再切换成 Dice Loss 或组合损失把 loss 的重心从小器官上捞回来。如果还是不动检查训练数据里是否有大量几乎看不见胰腺的切片适当过滤掉空器官占主导的样本。5.2 验证集表现突然暴涨后又暴跌按切片划分造成的泄露现象训练到一半验证集 Dice 突然跳到 0.97继续训练又跌回 0.5 附近整条曲线抖得像锯齿。你以为模型过拟合了但训练集 Dice 也不高。原因数据划分时把同一患者的相邻切片分别放进了训练集和验证集。相邻两张切片的器官形状几乎一模一样模型在训练时已经“见过”验证图像的近似版本验证指标虚高。一旦某个 epoch 的增强方式变化虚高现象又消失曲线自然会暴跌。解决严格按患者 ID 划分数据集保证任意患者的全部切片只出现在一个集合里。划分后输出一份患者 ID 对照表自查宁可训练数据少一点也不要在验证阶段得到一份自欺欺人的成绩。5.3 胰腺区域整片预测为背景窗宽窗位窗口没拉对现象其他四个器官都分割得挺好唯独胰腺区域几乎全被预测成背景即使把 Dice Loss 权重调到最大也救不回来。原因胰腺和周围腹膜后脂肪、肠系膜在原始 HU 值上非常接近默认的窗宽窗位窗口如果偏窄或偏宽胰腺区域在输入图像里就是一片模糊的灰色没有可区分的边缘。这类问题不属于模型能力问题属于输入分布问题。解决逐层检查预处理后的图像把胰腺所在切片单独存出来看。如果肉眼都分不清边界那就调整窗宽窗位范围比如扩大窗宽到 450 HU、微调窗位到 30 HU也可以尝试对输入图像做增强对比度或边缘锐化而不是盲目加大模型复杂度。5.4 输出全是背景标签索引和类别数对不上现象训练结束模型对所有测试图片都输出全 0 掩码预测结果里连一个非背景像素都没有。原因最常见的是num_classes设成了 5但 mask 里背景是 0、实际类别是 1-5于是模型输出 5 通道 logits类别 5 的胰腺永远没有对应的输出通道网络只能把背景学好。另一个可能是训练数据里有些 mask 是uint8存成 0-5但某些切片漏标导致标签里只有 0 和 1模型没有见过真正的五类样本。解决第一步在训练前用np.unique统计全量训练集 mask 的类别值第二步确认num_classes 背景 器官类别数第三步做一次可视化把模型预测的 argmax 结果叠加到原图上看是不是背景通道一直在霸榜。5.5 GPU 显存溢出但模型明明很小输入尺寸没裁剪现象FCN-8s 参数量不算大一跑训练就报CUDA out of memory把 batch size 降到 2 还是溢出。原因显存占用不光看参数量还看特征图的面积。VGG-16 在 512×512 输入下第一层卷积就会产出 512×512×64 的特征图一张图就是 16M 个 float多层叠加后 batch 8 直接吃掉大量显存。FCN 是“参数小、特征图大”的典型真正吃掉显存的是中间激活值。解决先按 4.4 用混合精度压掉一半显存再降输入尺寸到 256×256。医学分割任务里 256×256 对腹部大器官来说分辨率完全够用胰腺的分割精度不会因此降低太多。如果还想保留 512×512 的高分辨率就改成 patch 训练一次只送一个器官区域进网络推理时滑窗重叠 32 像素再平均融合。6. 从训练结果到 5 分割输出评估指标、后处理与最快优化路径6.1 指标计算Dice 和 mIoU 哪个更能反映多脏器分割好坏先跑一段评估代码按类别分别计算 Dice再求五个器官的均值。不要只看全局像素准确率背景像素占绝对多数会把所有错误都稀释掉。def compute_dice_per_class(pred, target, num_classes6, eps1e-6): dice_list [] for cls in range(1, num_classes): # 跳过背景 p (pred cls) t (target cls) inter (p t).sum() dice (2 * inter eps) / (p.sum() t.sum() eps) dice_list.append(dice) return dice_listmIoU 和 Dice 的数值趋势基本一致但 Dice 对小器官更敏感胰腺从 0.1 提到 0.3 时 Dice 的变化比 mIoU 更明显。5 分割的结果汇报里我会以“平均 Dice 为主胰腺 Dice 和肝脏 Dice 分别单列”因为平均指标很容易被肝脏拉高而掩盖胰腺的失败。6.2 预测结果后处理连通域过滤与孔洞填充FCN 输出的预测 mask 经常有零星的小噪点或者肝脏内部出现一个背景状的孔洞。腹部五类器官在解剖上都是独立的连通域所以最常见的后处理是按连通域过滤对每个类别保留面积最大的连通域再把类别内部的孔洞填充掉。from scipy import ndimage def postprocess_mask(pred, min_volume_ratio0.02): out pred.copy() num_classes pred.max() h, w pred.shape for cls in range(1, num_classes 1): mask (pred cls).astype(np.uint8) labeled, num_features ndimage.label(mask) if num_features 0: continue # 计算每个连通域的面积保留最大区域 sizes ndimage.sum(mask, labeled, range(1, num_features 1)) largest np.argmax(sizes) 1 keep (labeled largest) # 孔洞填充 keep ndimage.binary_fill_holes(keep) out[keep] cls out[(pred cls) (~keep)] 0 return outmin_volume_ratio是按全图面积比例过滤过小的连通域如果数据集里脾脏或肾脏出现了先天缺如、只存在于少数切片这部分器官面积本身就很小过滤阈值要相应调低。后处理只能修掉形状层面的错误如果胰腺连续好几层切片都预测成背景连通域过滤也救不回来还是要回到损失函数和数据侧找原因。6.3 当 FCN 卡在 0.85 平均 Dice最快的几条优化路径与我的教训如果你跑通了流程但平均 Dice 卡在 0.8 到 0.85先别急着换模型。我的优化顺序一般是第一检查是不是窗宽窗位只适配了一个数据集换另一个来源的数据时是否需要重新标定第二把单窗口输入改成双窗口一个看软组织一个看脂肪叠加成两通道输入不换网络结构也能提升对比度第三再考虑把 FCN-8s 的跳跃融合换成 U-Net 的跨层拼接。要注意的是FCN 的局限性在于上采样倍率固定为 8遇到极小器官时信息已经丢得差不多这时候换 U-Net 或直接上 3D 分割收益会比继续调 FCN 大得多。我自己第一次跑腹部 5 分割时最大的教训就是没先看 mask 的类别值分布把一组背景为 255 的标注当成背景为 0 处理前三天训练出来的模型在验证集上表现奇差最后发现是数据加载的 dtype 转换问题。现在每次换新数据集我都会先打印三样东西图像 HU 范围、mask 唯一值、按患者划分的样本统计确认无误后才进训练。把这些习惯固化下来FCN 这套 pipeline 复用的速度会快很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表