
简介《基于深度学习的医学图像分割技术研究进展》是一篇面向医学影像分析领域研究者的综述论文系统梳理了深度学习技术在医学图像分割中的应用进展。内容先阐述医学图像分割在临床诊断中的基础作用介绍CT、MRI、超声等常见影像的特点再重点对比分析CNN、FCN、U-Net等代表性算法在脑组织、肺部、血管等分割任务中的应用效果同时指出算法目前面临标注数据集规模有限、可解释性不足和泛化能力较弱等挑战并探讨了数据集扩展、算法改进与多模态数据融合等发展方向。资源以单个PDF文件形式提供大小仅981KB目前已有472人学习。对从事深度学习、医学图像处理或数据分析的研究者而言这份文献既可作为快速建立技术认知的入门材料也可作为课题调研与论文写作中的专业参考。1. 基于深度学习的医学图像分割从手动勾画到自动标注这条路走到哪了如果你在任何一个医学影像科室待过一天大概都见过这样的场景医生对着几百层的CT序列一层一层手动勾画肝脏边界画完一个病例要花上四五十分钟画完还得让另一位医生复核一遍。基于深度学习的医学图像分割就是冲着这个场景去的——把逐层勾画变成输入图像、输出掩码的自动推理从分钟级压到秒级。这篇笔记想说的不是哪个网络能刷多高的Dice而是把「基于深度学习的医学图像分割」这个方向从模型演进、数据管线、训练参数到落地避坑完整拆一遍。读者如果是刚入行的算法工程师可以照着步骤把第一个分割模型跑通如果已经做过一阵子也能在损失函数、数据增强和评估指标这些细节里找到自己踩过的坑。至于标题里挂着的那份PDF——它本质上是这个领域过去几年演进路线的快照而我们要聊的是这条路线里真正值得投入的部分。2. 主线演进从U-Net到nnU-Net再到Transformer医学图像分割模型是怎么走过来的2.1 U-Net为什么能成为baseline编码器-解码器与跳跃连接的取舍逻辑U-Net之所以在这个领域统治了这么多年不是因为结构复杂恰恰是因为它的结构对医学图像的特性太对味了。医学图像通常数据量小一个数据集几百例就算不错图像内容相对固定器官的解剖位置和形态虽然有变异但远没有自然场景那么多样。这种前提下一个端到端的全卷积网络配合跳跃连接就能在很小的数据集上学出不错的分割结果。我一般拿到一个新任务第一件事永远是训一个U-Net baseline。不是因为它最好而是因为它的表现稳定、可预期而且训练速度快。U-Net的编码器部分通过逐层下采样提取语义特征回答「这是什么结构」解码器部分通过上采样恢复分辨率回答「这个结构在哪里」跳跃连接做的事是把编码器各层的高分辨率细节直接拼到解码器对应层让网络在恢复空间位置的时候不丢失边缘信息。这三点配合起来就是一个适合医学图像的默认骨架。参数层面常见的配置是初始通道数64每下采样一次通道翻倍最深到512卷积核统一用3x3激活用ReLU下采样用2x2 maxpool上采样用转置卷积。唯一值得调整的是网络深度——如果图像分辨率很低或者器官很小把深度从4层减到3层往往比硬套深层更有效。从学术演进的角度看后来的很多改进比如残差连接、注意力机制、Dense连接都是在这个骨架上做加法。建议新人在动手改结构之前先把这个baseline跑熟把训练流程中的变量固定住再谈改进。否则你很难判断Dice的上涨到底是结构带来的还是学习率、数据增强这些因素带来的。2.2 nnU-Net不靠网络创新靠自动化配置把分割训练变成稳定流程nnU-Net这个名字容易让人误以为它是一个更强的U-Net变体实际上它是一套「自动配置的数据集适配流程」。它做的事情是拿到一个新数据集之后自动分析图像的模态、尺寸、spacing、标注稀疏程度然后替你决定归一化怎么做、重采样到什么分辨率、patch size设多大、用2D还是3D还是级联、训练多少轮、学习率怎么调。这套流程的价值在工程上怎么强调都不过分。我自己经历过最典型的场景是一个数据集只有几十例标注很稀疏前景占比不到1%。如果手工去调U-Net光是patch size和归一化方式就能试两周。而nnU-Net会在几分钟内根据数据统计量给出一个合理配置跑出来的结果通常比人手调参的版本高好几个点Dice。另一个核心设计在于它把预处理和训练策略固化成规则而不是玄学。比如CT图像它自动做全局统计的裁剪和z-score归一化MRI这种没有标准单位的模态它自动改用逐病例的z-score重采样目标则根据所有训练样本的spacing中位数来确定。这些规则本身就是这个领域长时间积累的共识被工程化成了代码。我的建议是任何新数据集都先跑一遍nnU-Net得到baseline再去尝试花哨的结构。你会发现在很多公开医学分割任务上nnU-Net本身就超过了大量专门设计的网络。那些号称比U-Net高几个点的论文有不少是没把baseline调好。2.3 Transformer进医学分割全局上下文是补药还是毒药Transformer进入医学图像分割是最近几年最热的方向之一。它带来的核心能力是长距离依赖建模CNN受限于感受野要看到跨区域的上下文得靠层层堆叠而Transformer的注意力机制可以直接建立任意两个位置之间的联系。在器官边界模糊、需要结合远处解剖结构来判断的场景里这种全局视野理论上很有价值。实际落地的做法一般分两类一类是混合架构用Transformer做编码器提取全局特征再用CNN解码器恢复分辨率代表思路如UNETR和Swin UNETR另一类是纯Transformer分割模型把分割当作逐像素的分类或掩码预测来做。从实际效果看混合架构明显更实用纯Transformer在小数据集上很难训稳。但有一个现实要认清医学分割数据集普遍很小Transformer是参数大户很容易过拟合。我踩过的坑是在一个只有百来例的数据集上把U-Net编码器换成Transformer编码器训练loss降得漂亮验证Dice反而比U-Net低三四个点。后来把训练数据扩到上千例Transformer的优势才开始显现。所以我的判断是Transformer在当前医学分割里更像是一个「可选的编码器升级」而不是替代品。如果你手上的数据量够大、器官边界问题复杂值得试如果数据量小老老实实跑U-Net系反而更稳。下表是两者的直观对比架构核心机制优势局限适用场景U-Net系卷积跳跃连接小数据友好、训练稳定、显存适中局部感受野受限于卷积中小数据集、快速落地Transformer混合架构注意力编码器CNN解码器全局上下文、复杂边界更强数据需求大、训练慢、易过拟合大规模数据集、难例分割纯Transformer全注意力分割头统一框架、结构简洁医疗小数据极难训稳预训练权重充分时尝试3. 数据管线实操DICOM读取、重采样、增强的三个必调环节3.1 用SimpleITK读DICOM序列为什么不能只读pixel_array医学图像分割项目里最常见的第一个翻车点不是模型而是数据读取。很多新人上手会用某个图像库直接把DICOM文件读成二维numpy数组然后当作普通图像去训练。这样做的直接后果是体素之间的物理间距信息全部丢失不同病例的图像分辨率完全不一致模型学到的分割边界在物理空间上根本对不齐。正确做法是先把整个DICOM序列读成一个三维体数据并且保留origin、spacing、direction这些空间元数据。我一般用SimpleITK来完成这一步它可以自动按z轴顺序排好切片同时保留空间信息之后做重采样、插值都方便。下面是读取序列的核心代码import SimpleITK as sitk def load_dicom_series(dicom_dir): reader sitk.ImageSeriesReader() dicom_names reader.GetGDCMSeriesFileNames(dicom_dir) reader.SetFileNames(dicom_names) reader.MetaDataDictionaryArrayUpdateOn() reader.LoadPrivateTagsOn() image reader.Execute() return image # 返回的是sitk.Image包含origin/spacing/direction这段代码的关键在于GetGDCMSeriesFileNames会自动识别同一个序列的所有切片MetaDataDictionaryArrayUpdateOn打开后可以读取每个切片的私有标签后面做方向矫正时需要用到。返回值是sitk.Image而不是numpy数组目的是守住空间元数据。只要还没确定重采样方案就不要急着转numpy否则就是主动扔掉了最关键的定位信息。转换成numpy的时机应该是预处理流程的最后一步。转换时用sitk.GetArrayFromImage(image)注意SimpleITK的数组顺序是z、y、x也就是先切片序号再行再列。这个顺序和很多自然图像处理库的惯例相反是后续写训练代码时最容易错的地方。3.2 重采样到各向同性体素CT与MRI的归一化参数怎么设医学图像不同病例之间的spacing差异很大有的CT扫描层厚1mm有的5mm如果直接用一个固定形状的网络去处理模型会同时面对不同物理分辨率下的器官形态很难学稳定。所以预处理必须做一个统一操作把所有的体数据重采样到同一个目标spacing。重采样时图像用线性插值标签用最近邻插值这是铁律。线性插值用在标签上会产生不存在的中间值比如标签原本只有0和1插出来一个0.5训练时会直接干扰损失计算。下面是一个通用的重采样函数import SimpleITK as sitk def resample_to_spacing(sitk_img, target_spacing, is_labelFalse): orig_spacing sitk_img.GetSpacing() orig_size sitk_img.GetSize() target_size [ int(round(orig_size[i] * orig_spacing[i] / target_spacing[i])) for i in range(3) ] resampler sitk.ResampleImageFilter() resampler.SetOutputSpacing(target_spacing) resampler.SetSize(target_size) resampler.SetOutputOrigin(sitk_img.GetOrigin()) resampler.SetOutputDirection(sitk_img.GetDirection()) if is_label: resampler.SetInterpolator(sitk.sitkNearestNeighbor) else: resampler.SetInterpolator(sitk.sitkLinear) return resampler.Execute(sitk_img)target_spacing一般选各向同性比如1.0x1.0x1.0mm。选各向同性的理由是让网络在三个方向上的感受野一致避免某一维被过度拉伸或压缩。如果器官本身在某一方向上有明显的解剖特点比如血管沿长轴分布可以考虑非各向同性但默认从各向同性开始最安全。归一化方面CT和MRI是两套完全不同的逻辑。CT有物理单位亨氏单位可以直接做窗宽窗位裁剪再用全局均值和标准差做z-score。常用的裁剪范围是-1000到1000覆盖绝大多数软组织如果目标器官是肝脏、肾脏这类实质性脏器裁剪到-200到400的窄窗往往效果更好。MRI没有标准单位不同扫描设备得到的信号强度不具备可比性所以必须逐病例做z-score也就是每个体数据单独减均值除标准差。3.3 数据增强同步变换弹性形变能用颜色扰动要慎用医学图像分割的数据增强第一原则是「图像和标签必须做完全相同的变换」。这个要求听起来显而易见但实际操作中经常出问题尤其是随机翻转和随机旋转如果图像转了而标签没转模型会学到完全错误的对应关系。常见做法是用同一个随机种子让图像和标签调用同一套几何变换参数。代码层面往往是定义一个transform对象先对图像调用再对同一个对象调用标签数据而不是分别创建两个随机变换。弹性形变在医学分割里尤其常用因为它模拟的是器官在人体内的自然形态变化比如肠道蠕动、呼吸造成的肝脏位移。这类形变对提升小数据集的泛化能力帮助很大代价只是计算量稍高。颜色类增强需要非常克制。医学图像的灰度值在很多模态里具有物理意义CT的灰度对应组织密度MRI的信号强度虽无统一单位但在同一序列内也有诊断意义。大幅度改变亮度对比度或者做颜色抖动很容易破坏这些语义。我见过有项目在CT分割里用了强烈的对比度拉伸训练时Dice不错一到新数据上直接崩就是因为增强制造的灰度分布在真实数据里根本不存在。另一个容易被忽视的点是随机裁剪产生的patch会破坏解剖结构上下文。如果patch尺寸太小模型可能只看到器官的一小部分无法判断它在体内的位置。所以patch size的下限不是显存决定的而是「至少包含足够的解剖上下文」决定的。这一点在下一章的2D/3D选型里还会继续展开。4. 训练与评估损失函数、2D/3D选型与评价指标4.1 Dice损失成为默认的数学直觉与代码实现医学图像分割里背景像素占比极高器官区域经常只占整张图像的百分之一甚至千分之一。如果用交叉熵损失模型只要把全部像素预测为背景loss就已经很低了但分割结果毫无意义。Dice损失之所以成为默认选择是因为它直接在「预测区域和真实区域的overlap」上做优化天然对类别不平衡不敏感。Dice系数的公式是2乘以交集除以两个集合的并集取值在0到1之间。作为损失函数时把它变成1减去Dice。为了让这个操作可微实际实现时不会直接数像素而是用逐像素相乘再求和来替代交集用逐像素相加来替代并集。下面是一段典型的Dice损失实现import torch def dice_loss(pred_logits, target, smooth1.0): pred torch.sigmoid(pred_logits) intersection (pred * target).sum() union pred.sum() target.sum() dice (2.0 * intersection smooth) / (union smooth) return 1.0 - dice这个实现里target是0/1的标签张量pred_logits是网络输出的原始logits。函数内部先做sigmoid转换成概率再计算Dice。smooth参数的作用是避免目标区域为空时除零一般设1.0足够了。要注意的是这个写法默认是二分类分割多类别分割时会先对每个类别单独算Dice再取平均。实际项目中我不建议只用Dice Loss。它虽然解决了不平衡问题但在小目标上梯度不稳定容易震荡。常见的做法是Dice和交叉熵按权重相加比如各取0.5。交叉熵提供一个稳定的逐像素梯度Dice提供对不平衡的纠偏两者互补。如果发现模型假阳性特别多可以把Dice换成Tversky损失把惩罚权重偏向预测区域但这属于精细化调参等基础流程稳定后再考虑。4.2 2D、2.5D、3D怎么选一张对比表说清选2D还是3D是医学分割项目里绕不开的决策而且这个决策越早做越好因为后续所有数据管线、显存规划、训练时间都依赖它。我的经验是不要因为「3D听起来更高级」就选3D也不要用「2D更简单」来做决定而是根据数据形态和标注情况来选。先说2D。它把体数据切成单张切片独立训练优点是显存友好、训练速度快、标注要求低很多公开数据集直接提供的就是切片级标注。缺点是每张切片之间没有上下文关联模型无法利用器官在相邻层之间的连续性对于形状复杂或边界模糊的器官容易出现层间不连续。3D则是把整个体数据或一个patch输入网络直接学习三维空间特征。对于CT、MRI这类天然三维的数据3D模型能感知器官的空间连续性在小目标器官如胰腺、血管上通常明显优于2D。代价是显存消耗成倍增长、训练时间长、需要完整的体数据标注。2.5D是折中方案常见做法有几种把相邻几层切片拼成多通道输入或者从三个正交平面分别分割再融合。它能在不引入完整3D模型复杂度的情况下给模型提供一定的空间上下文。下面用一张表把三个选项的适用范围说清楚方案显存压力上下文能力标注要求适用场景2D低无层间信息切片级标注即可超声、内镜、X光等单帧图像2.5D中少量相邻层信息切片级标注即可数据集小、想兼顾上下文3D高完整空间上下文需要体数据标注CT/MRI体数据、小目标器官实际决策时我一般看三点标注是切片还是体数据显存够不够跑3D器官在三维方向上是否有明显的连续性。如果答案全部指向3D就用3D如果有任何一项不满足就选2D或2.5D起步。4.3 Dice、IoU、HD95怎么算评估代码与判读标准模型训练完了评估不是一句「看Dice」就结束的事。Dice和IoU都只衡量区域重叠程度在小器官上极其严格——一个直径几个毫米的病灶预测位置偏了一点Dice可能直接掉到0.5以下。而HD9595%豪斯多夫距离衡量的是预测边界和真实边界之间的距离以毫米为单位能更直观地反映临床关心的「边界准不准」。我习惯把三个指标一起算分别看区域重叠和边界精度。下面是一份参考实现import numpy as np from scipy.ndimage import distance_transform_edt def dice_score(pred, gt): pred (pred 0.5).astype(np.uint8) gt (gt 0.5).astype(np.uint8) inter np.sum(pred * gt) return 2.0 * inter / (np.sum(pred) np.sum(gt) 1e-8) def hd95(pred, gt, voxel_spacing(1.0, 1.0, 1.0)): pred (pred 0.5).astype(np.uint8) gt (gt 0.5).astype(np.uint8) if np.sum(pred) 0 or np.sum(gt) 0: return np.inf dt_gt distance_transform_edt(1 - gt, samplingvoxel_spacing) dt_pred distance_transform_edt(1 - pred, samplingvoxel_spacing) d1 dt_gt[pred 0] d2 dt_pred[gt 0] return np.percentile(np.concatenate([d1, d2]), 95)参数上voxel_spacing要对应你重采样之后的目标spacing这样HD95算出来才是真实物理距离否则只是一堆没有单位的像素距离。两个函数都在阈值0.5处做二值化如果模型输出概率图这个阈值可以按验证集微调不一定死用0.5。判读这些指标时有一个常见误解Dice高就是好。Dice对物体大小敏感同样是漏掉边界1mm的误差在大器官上Dice可能只从0.98降到0.97在小病灶上会从0.9直接掉到0.7。所以当读者看到一篇论文报告了很高的Dice时先看看它的目标器官是肝脏这种大器官还是胰腺这种小器官再判断这个数字的含金量。这也是我在评估时坚持同时看HD95的原因——它不会被器官大小稀释太多。5. 不翻车清单医学图像分割最常见的五个踩坑点5.1 训练loss不降Dice一直为0现象模型训练了十几个epochloss纹丝不动Dice始终是0所有预测都是背景。原因最常见的是前景像素占比实在太低模型直接学会了全预测背景。这种情况在肿瘤分割里特别常见病灶可能只占整张图的0.1%。第二个常见原因是学习率设置过高导致模型在损失面上震荡第三个原因比较隐蔽——标签和图像的分辨率或方向不一致模型根本学不到有效对应关系。解决先把损失函数改成Dice Loss或DiceCE组合解决前景占比失衡问题。学习率从1e-3往下降到1e-4看loss是否有反应。如果还不行检查预处理时标签的重采样插值方式确认标签没有出现非整数。最后可视化几张训练样本把图像和标签叠加起来看这一步能排查掉绝大多数对齐问题。5.2 验证集Dice接近90%临床却说不合格现象模型在验证集上Dice高达90%拿给临床医生评审对方直接说「这个不能用」边界粗糙、小病灶漏掉了一大半。原因Dice是一个区域重叠指标对大面积背景占优的分割任务天然宽松。验证集Dice高只能说明模型在「整体轮廓」上接近标注并不代表边界精细或小目标检出率高。尤其当标注本身来自单个人手工勾画时模型学到的是这个人勾画的平均风格而不是解剖真实边界。解决评估指标里加入HD95并展示预测边界与真实标注的叠加图给医生看。训练策略上可以给损失函数增加边界权重让网络对边缘区域给的惩罚更高也可以在后处理阶段不做过度的形态学开闭运算那些操作虽然能提升Dice但经常把真实的小病灶一并抹掉。不要只盯着Dice调参临床认可才是最终标准。5.3 同一患者数据进到训练集和测试集指标虚高而不自知现象实验报告显示模型Dice很高但部署到新病例上效果显著下滑比报告低十来个点。原因CT和MRI一个患者往往有多个序列、多个期相的数据如果按「文件」而不是按「患者」划分训练集和验证集同一个人的不同期相会同时出现在两边。模型等于提前见过这个人的解剖结构验证指标自然虚高。这个错误在医学分割里太常见了而且一旦发生整个实验结论都要作废。解决划分数据集时严格按患者ID分组。用GroupShuffleSplit替代普通的随机划分from sklearn.model_selection import GroupShuffleSplit splitter GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, valid_idx next( splitter.split(np.zeros(len(patient_ids)), groupspatient_ids) )参数上groups必须传患者唯一标识n_splits设1test_size按数据量设seed固定便于复现。这个错误属于「没有后悔药」的类型只能重跑实验所以一开始就要养成按患者分组划分的习惯。5.4 3D patch太大导致显存溢出batch size只能设为1现象3D模型训练时GPU显存溢出把batch size调到1还是不够程序直接崩溃。原因3D输入比2D多一个维度显存消耗是立方级增长的。一个256x256x128的输入spacing可能已经接近2mm的重采样显存量直接顶到边界。很多人遇到显存溢出第一反应是调小batch size但batch size调到1仍然不够时就得换思路了。解决最直接的办法是把patch size调小比如从128x128x96改成96x96x80显存占用立刻下降。其次可以选择混合精度训练显存基本能省一半。还有一个实用的技巧是梯度累积显存不够跑大batch就把一个batch拆成n个小batch分别算梯度累积n步后再更新参数。如果这些都做了还不够说明该退回到2.5D或2D方案了而不是继续压榨显存。5.5 换一台扫描仪性能暴跌域差异是沉默的杀手现象模型在训练数据的验证集上表现良好部署到另一家医院、另一台设备采集的数据上Dice直接掉15个百分点以上。原因不同厂商的扫描设备、采集协议、重建算法会让同一解剖结构的灰度分布和空间分辨率产生明显差异。模型在训练数据上学习的是这台特定的设备特征一旦遇到没见过的设备分布就会表现失灵。这个问题的隐蔽之处在于它的发生完全不受你的模型选型影响是所有医学分割落地中绕不开的坎。解决最有效的办法是训练数据里混入尽可能多种设备来源的数据哪怕每种少一点也能让模型见过更广的分布。其次归一化策略要严格统一CT按窗宽窗位裁剪加全局z-scoreMRI按逐病例z-score这能在一定程度上抹平设备差异。另一个常用思路是做在线数据增强时模拟不同设备的灰度扰动相当于给模型打了域变化疫苗。如果目标场景明确可以考虑在部署前用少量目标域标注数据做微调通常几十张就有效果。6. 下一步怎么卷半监督伪标注、基础大模型与落地取舍6.1 半监督伪标注让未标注数据参与训练的正确姿势医学图像分割的瓶颈始终是标注而不是算力。标注一个三维体数据的成本高到很多团队只标了几十例就不得不开始训练。半监督方案是我现在优先考虑的方向其中有代表性的是伪标注思路先用已标注数据训一个teacher模型让它预测未标注数据挑出高置信度的像素当作伪标签加入训练。伪标注落地时有几个关键参数要拎清。置信度阈值决定伪标签的质量我一般从0.9起步训练过程中逐步提高这样避免低质量伪标签污染模型。对于三维体数据建议按体素维度计算置信度只保留高置信区域参与损失计算。核心代码如下model.eval() with torch.no_grad(): logits model(unlabeled_volume) prob torch.softmax(logits, dim1) max_prob, pseudo_cls prob.max(dim1) trust_mask max_prob 0.9 # 高置信度像素才参与训练 loss criterion(logits, pseudo_labelpseudo_cls, weighttrust_mask)这段代码里最要紧的是最后的weight参数它让低置信度像素在损失中直接置零而不是强行纠正模型预测。半监督真正见效的前提是未标注数据和已标注数据来自同一分布所以做之前一定要确认数据来源一致否则伪标签会把错误分布学进去。6.2 基础大模型能不能直接用趋势判断与场景取舍自然图像领域的分割大模型和视觉基础模型最近也开始被尝试迁移到医学图像上。现有的经验是直接在自然图像上预训练的模型拿来做医学分割效果通常打折因为医学图像的单通道灰度、解剖结构语义和自然图像差异太大除非做针对性微调。但对于常见的大器官分割任务在足够大的医学数据上预训练过的模型确实能减少下游任务的标注需求。落到实际项目决策上我的取舍标准可以总结成一张表场景推荐做法理由标注几十例、常见器官U-Net或nnU-Net参数少小数据不容易过拟合标注几百例、目标小且边界模糊3D混合架构、组合损失空间上下文对小目标更友好大量未标注数据半监督伪标注激活未标注数据的利用价值边缘设备部署2D模型蒸馏3D推理慢、显存要求高这张表背后的逻辑是模型复杂度和数据规模必须匹配不要因为某篇论文刷了新SOTA就照搬到自己的项目里。小数据上老老实实的U-Net往往就是最优解数据规模大了Transformer和前注意力机制才能体现出价值。医学图像分割这个领域最后拼的还是数据理解、预处理质量和评估的严谨性。这些年我自己最大的教训是一上来就追新模型把baseline丢在一边结果新模型训了三个星期效果还不如第一天跑出来的nnU-Net。从那之后我养成了一个习惯——不管论文里吹得多猛先在自己的数据上把baseline结果钉死在墙上再决定要不要换模型。如果哪一天你在这个领域也遇到选择困难不妨先跑通一个最朴素的U-Net再回头来判断那些花哨的结构到底值不值得投入。希望帮到你。本文还有配套的精品资源点击获取