去年有个项目要做肺结节风险分级前的病灶分割,某医院给了一批边界极不规则的亚实性结节影像。UNet系列、DeepLab系列我前后试过一圈,医生反馈永远是“肿瘤是圈出来了,但边界就像用蜡笔画的,很多细小的分叶和毛刺都抹平了”。后来我把SAM(Segment Anything Model)拉进来做候选掩码生成,边界质量直接上了一个台阶,医生肉眼评估的通过率从大概六成涨到九成。这篇就聊我在医疗影像上用SAM把分割边界做到更细的完整路线:为什么SAM在边界上比传统分割模型有优势、怎么配参数、怎么避坑、怎么验收,尽量写到能直接抄作业。
1. 医疗分割的边界问题,为什么值得单独拿出来说
1.1 医生说的“边界好”到底指什么
在医疗影像里,边界从来不是一个可以用“轮廓对没对上”这种二元口径覆盖的问题。放射科医生判断一个病灶要不要手术、放疗靶区怎么勾画、疗效评估里肿瘤是缩小了还是稳定了,都依赖对边界的精准判断。以肺结节为例,亚实性结节边缘的分叶、毛刺、胸膜凹陷,全是良恶性评估的重要线索;肝脏肿瘤分割时,边界贴近血管或胆管,稍一模糊就可能影响手术规划;脑部病灶分割更是边界决定预后判断。
一个很反直觉的事是:很多分割模型在常规指标上已经做到90%以上的Dice,但医生仍然不满意。原因在于Dice这种区域重叠类指标对“里面大差不差”很宽容,对“边界那两三毫米”很迟钝。而临床上恰恰就是那两三毫米决定性质判断。这就是我反复强调的:医疗影像的边界问题,本质是高频空间细节的保留问题,不是简单的整体重合度问题。
1.2 传统模型在边界上输在哪
传统医学分割模型大多走的是编码器-解码器结构,输入图像不断下采样,卷积层一层层把特征图缩小到原来的1/16甚至1/32,再逐步上采样恢复分辨率。这个过程天然是一个“信息压缩又恢复”的通道,病灶主体能留住,但边界上的高频细节——比如一两个像素宽的毛刺、棘突、膜结构——会在多次池化和降采样中失真。
再加上训练时的损失函数设计。绝大多数分割训练用的Dice Loss或交叉熵,都是在区域级别做梯度回传:边界附近像素错了,对损失的影响和内部大块像素错了相比,几乎可以忽略。于是模型的优化方向是“把大块区域保住”而不是“把边缘抠细”。当然也有auxiliary loss自带边界监督的方案,但这些方案对标注质量要求极高,而医学标注本身又恰恰是最不稳定的环节——不同医生对同一病灶边界的判断存在天然差异,模型很容易学到平均化的模糊边界。
我无意否定传统模型的价值,它在很多场景很能打。但边界精细这个维度,传统模型的架构目标其实不是它。后面SAM的表现,恰好补上了这一环。
2. SAM的分割逻辑,和普通医学分割模型有什么本质不同
2.1 它不是一个专用模型,而是一个“分割引擎”
SAM的思路不是“我这个模型针对某种器官做分割”,而是“你给我一个提示,不管是点、框、还是粗略掩码,我把对应的对象轮廓给你从图像里切出来”。这种思路在架构上表现为三件套:一个图像编码器(Image Encoder)负责把整张图变成特征表示;一个提示编码器(Prompt Encoder)把点、框、掩码等提示变成条件;一个掩码解码器(Mask Decoder)在特征图上解码出具体的前景掩码。
用大白话讲,传统模型像一台专门做肺叶切除的手术机器人,装上就能干活,但换个部位就抓瞎。SAM更像一套通用机械臂,你给它一个定位点,它能帮你从任意场景里精准抓出目标。这种通用性在医疗影像上的价值在于:同一个模型可以同时处理CT、MRI、超声、病理切片,提示方式也灵活。医生在某个边界模糊区域点一下,或者拉一个矩形框,SAM就能结合这个提示把边界重新理一遍。
2.2 边界为什么会在SAM里被单独照顾
很多人不知道,SAM的掩码解码器一次会输出三份候选掩码,语义上分别对应“整体”“细节”“更细”三个粒度。在实际使用中,我经常遇到第一份掩码主体很好但边界圆润,第二份掩码边界出现更多凹陷和尖角,第三份则可能过度破碎到需要人工判断。这个设计天然就是在让使用者选择边界细节的保留程度。
另一个更底层的优势在于SAM的图像编码器使用了大规模视觉Transformer结构,特征图本身保留着较强的全局上下文和局部细节信息,对高频边界特征的敏感度比传统纯卷积网络好。SAM训练时面对的提示也包含大量点提示和粗糙掩码提示,这种训练迫使模型学会在局部提示下仍然能够还原对象完整且精细的轮廓。这两个特点叠加,让我在测试中明显感觉到:同样一个肺结节,SAM输出的候选在边界的锐利度和不规则形态还原度上,确实比传统分割模型好一截。
2.3 医疗影像不是SAM的默认菜,但底子是好的
请别误会我是在无限吹捧SAM,它没有为医疗影像做过专门的“特调”。SAM的训练数据以自然图像为主,对CT的HU值范围、MRI的灰度分布、超声的噪声纹理并不天然熟悉。我刚开始直接拿CT影像喂给原始SAM,结果并不理想,边界甚至会出现一些纹理级伪影。
但这不代表SAM不适用于医疗影像。好消息是,SAM这种“提示+解码”的工作机制决定了它不需要像专用模型那样从头学每个器官的形状,它只需要在特征层面理解“什么是一个对象、什么是一个边界”。我们可以通过预处理、提示设计、后处理和轻量微调,把医学影像的数据分布拉回到SAM熟悉的范围。这就是后面几节要展开的内容。
3. 在医疗影像上跑通SAM的最小方案
3.1 环境、权重、加载代码
环境配置并不复杂。Python版本3.9以上,PyTorch跑得动就行,显存至少要有12GB,推荐24GB左右。安装官方开源的segment-anything包,再下载对应模型权重即可。常用的有vit_b、vit_l、vit_h三档,医疗影像上我一般推荐直接用vit_h,精度最好,显存不够再换vit_b。
加载模型的代码非常短:
import torch from segment_anything import sam_model_registry, SamPredictor sam = sam_model_registry["vit_h"](checkpoint="sam_vit_h_4b8939.pth").to("cuda") predictor = SamPredictor(sam)这里有个容易忽略的点:SamPredictor.set_image()接口内部会把图像resize到模型要求的尺寸,并计算图像嵌入。如果要做多次提示迭代(比如先框选、再点选修正),一定要在循环外面先调用predictor.set_image(),而不是每次预测都重新算一次图像嵌入。我见过不少人在这里犯懒,导致推理速度慢了十几倍。
3.2 DICOM预处理和窗宽窗位
这是医疗影像和自然图像最大的差异点。CT影像本质是HU值,范围可能从-1024到3000多,直接丢给SAM一定会出问题。我建议的预处理链路是:先用pydicom或SimpleITK读取DICOM,拿到像素数组和窗宽窗位参数,然后把HU值截断到目标窗宽范围内,再线性映射到0到255,最后复制成三通道交给SAM。
以肺结节分割为例,我常用窗宽1500、窗位-600,这个参数下磨玻璃区域和实性成分的对比度都保留得不错。做肝脏CT则用窗宽350、窗位40。MRI影像没有标准窗宽窗位,但绝大多数序列都是12位左右灰度,直接做分位数归一化也能用。核心原则是:让目标组织在灰度上有区分度,而不是原样把整个动态范围塞给模型。
3.3 两路推理:自动生成加提示修正
SAM的推理有两种方式:自动生成掩码和提示引导。自动生成掩码用的是SamAutomaticMaskGenerator,不需要任何提示,直接输出全图所有目标的候选掩码;提示引导则是用点、框等人工提示,让模型只分割你关心的目标。
我的建议是把两条路结合:先用SamAutomaticMaskGenerator跑一遍,拿到候选掩码;再把候选里明显靠近目标解剖位置的掩码挑出来,作为粗略提示喂给SamPredictor做二次修正。这么做的效果比我单纯用自动掩码或单纯用框提示都要稳定。自动掩码容易出现碎片化,提示引导如果提示位置不够准也容易被误导,两条路互补之后,稳定性和精细度都能提升。
4. 让边界做到精细:五条我实测有效的打法
4.1 多尺度输入:让边缘不随缩放“混过去”
SAM图像编码器内部会把输入图统一到固定分辨率。如果原始影像很大,整图缩放过程中,边界上的高频细节就会因为像素重采样而平滑掉。这在自然图像上问题不大,但在医学影像上很致命——很多毛刺和分叶本身就只有几个像素宽。
我的做法是对同一影像做多尺度缩放,分别推理,再把结果融合。具体来说,我会生成三个尺度的输入:原图直接缩放到模型输入尺寸、缩小到0.75倍、放大到1.25倍,分别得到掩码后,将三个掩码重采样回原图分辨率,逐个像素投票,以多数胜出为准。注意三个尺度之间不要差太多,0.75到1.25倍足够了,差距过大反而会导致融合出来的边界抖动。
实测下来,多尺度推理让亚实性结节的边界轮廓稳定了不少,尤其是一些细小的分叶和棘突,在单尺度推理时常常被“平均”掉,多尺度投票之后能保留下来。
4.2 滑窗重叠推理:切碎了拼接时别留接缝
医疗影像动辄512x512、1024x1024,甚至几百层的三维序列。整图缩放免不了边界失真,我倾向于做滑窗推理。把图像切成固定大小的块,比如512x512,相邻块之间留出20%左右的重叠区域,每一块独立通过SAM推理,最后在重叠区做加权融合,中心权重高、边缘权重低。
滑窗有两个好处:一是每个窗口内的目标相对较大,SAM的形态敏感度更高;二是重叠区的冗余推理能抑制单次推理的随机波动。坏处是计算量成倍增加,我的折中方案是只在包含目标的区域做滑窗,先用一个粗检测或者自动掩码确定目标bounding box,再在box周围扩展一圈做精细分割。
4.3 用医学先验框把提示引到正确位置
SAM对框提示的响应非常直接,框内就是目标,框外忽略。但框给得不好,后果也很明显:框太大,模型容易把周围粘连组织也切进来;框太小,模型可能只切出目标的一部分。我的经验是,在医疗场景里框应该比医生目测的bounding box略微外扩2%到5%。
这个外扩比例怎么定?如果你有粗定位模型,直接拿它的输出框再膨胀一点就行;如果纯人工标注,宁可稍微大一些再靠点提示修正,也别让边界卡死在框边缘。SAM的解码器对框提示的边界非常尊重,如果目标贴住框缘,分割结果会在框缘处产生切割痕迹,后面处理起来很麻烦。
4.4 边界级后处理:形态学和CRF的组合
后处理是SAM医疗应用里被低估的一环。我常用的组合拳有三步:第一步,连通域分析去掉面积过小的碎片;第二步,用形态学闭运算填补目标内部的细小空洞和裂缝;第三步,用条件随机场(CRF)在边界处做逐像素精修。
前两步很好理解,第三步值得展开。SAM输出的是离散掩码,在边界处经常存在锯齿。CRF的思路是:在低层像素特征上建立概率模型,让相邻像素点倾向于拥有相同的标签,颜色或灰度差异大的地方则给予更高的标签切换惩罚。用大白话说,就是让边界顺着图像中天然的灰度边缘走,而不是生硬地拐直角。这一步能显著提升边界的平滑度和解剖一致性。CRF的迭代次数不要太多,5到10次足够,太多会把边界重新磨圆。
4.5 冻结主干做轻量微调
如果你的数据分布和自然图像差异实在太大,比如超声或者病理切片,光靠零样本SAM和后处理可能还是不够。这时候可以走轻量微调路线:冻结图像编码器,只训练掩码解码器和提示编码器相关模块,或者用低秩适配(LoRA)的方式微调。
我一般用100到200张带精细标注的切片,学习率设1e-4量级,训几十个epoch,显存占用不大,效果却有明显提升。这里多说一句:不要一上来就全参数微调,一是医疗数据量通常不够,容易过拟合;二是全参数微调之后,SAM原有的通用提示能力和多粒度候选输出会退化,这是得不偿失的事。
5. 医疗影像给SAM埋下的暗坑:我踩过的几个典型问题
5.1 直接整图缩放,边界全糊
最早跑通SAM的时候,图方便直接把整张CT缩放到模型输入尺寸,出来的边界结果让我有点怀疑人生:一些明显边缘锐利的病灶,分割轮廓却是圆弧过渡,完全没有应该有的棱角。后来一查,问题就出在整图缩放上——尺寸跨度太大,边界上的高频信息在缩放过程中被重采样算法抹平了。
解决办法就是我前面说的滑窗加多尺度。如果你只是做实验验证,至少也要保证目标在输入图中的像素占比不要太小。一个简单参考:目标短径不要小于输入图尺寸的1/10,小于这个比例时SAM的边界精细度会明显下降。
5.2 灰度图简单复制成三通道的隐性问题
医疗影像大多数是灰度图,主流做法是复制三通道喂给SAM。这个做法本身在工程上没问题,很多官方示例也这么干。但在实际测试中我发现,直接复制三通道会让模型在灰度变化平缓的区域出现一种“纹理化”的错误分割:边界上出现大量细碎的伪影,像是把噪声也当成了纹理特征。
更稳的做法是先把灰度图做一次对比度增强再复制三通道,比如限制对比度自适应直方图均衡化(CLAHE)。这么一来,复制三通道造成的信息冗余被控制在更低水平,SAM的边界识别会更干净。对CT影像来说,调好窗宽窗位本身就够,CLAHE可以作为MRI和超声影像的默认选项。
5.3 “Everything”模式在医疗场景的碎片灾难
自动掩码生成器设计之初面向的是自然图像里的任意目标,这让它到了医学影像上必然水土不服。一条CT横断面图,里面可能包含体表轮廓、肋骨、肌肉、血管、病灶,自动模式能给你生成上百个碎片掩码,其中大部分完全不是你要的东西。我第一次跑everything模式,面对一堆碎片差点直接放弃SAM。
对策分两层:参数层,把pred_iou_thresh调到0.9以上,stability_score_thresh也调高,同时把min_mask_region_area设成一个合理值,比如按原图分辨率换算后的最小面积,这一步能过滤掉绝大多数噪声碎片;逻辑层,千万别指望everything模式直接输出最终结果,它的定位应该是候选目标发现器,发现之后再用提示引导做精细分割。
5.4 部分容积效应和小目标阈值误删
薄层CT里,病灶在不同层面上的表观差异很大。由于部分容积效应,很多真正的病灶边缘区域灰度介于病灶和正常组织之间,SAM容易在这一带犹豫,输出的边界会往里缩一圈。这点在后处理里要格外小心:形态学开运算、面积阈值过滤等操作如果参数过猛,会把真正的边界也一并削掉。
我的判断习惯是:后处理宁可保守,不要激进。面积阈值只删掉明显孤立的散点,不要删掉所有小连通域;形态学操作先看一遍每一个改动区域的overlay,确认没有伤到真边界再做批量处理。边界精细这件事,很多时候不是靠多调几个参数变好的,而是靠少做几个错误操作保住的。
6. 边界质量怎么量化:从指标定义到验收方案
6.1 Dice很好看,边界还是歪:为什么不能只用Dice
Dice相似系数衡量的是预测掩码和真值掩码的区域重叠比例,对整体命中率非常敏感,但对边界的小偏差并不敏感。假设一个病灶的直径是3厘米,边界平均往外偏1毫米,Dice可能只掉1个百分点左右,看起来依旧漂亮,但在医生眼里这1毫米可能已经改变了手术切除范围的判断。
这也是为什么我团队的验收清单里,Dice永远只作为辅助指标,真正的边界质量要看专门的边界类指标。有同行可能会说“医生肉眼把关就行了”,但项目流程里必须有可量化的客观指标,否则没法迭代,换一个人评审结果就不一样。边界类指标的价值就是让“边界好坏”这件事可以脱离个人主观感受被量化。
6.2 该关注的边界指标与计算逻辑
我实际用得最多的是三个:平均表面距离(ASSD)、95% Hausdorff距离(HD95)、边界交并比(Boundary IoU)。三个指标各看一个侧面,建议全部算出来再看。
| 指标 | 衡量内容 | 优缺与适用场景 |
|---|---|---|
| Dice | 区域重叠率 | 宏观评估,对边界偏移不敏感,适合快速筛查 |
| ASSD | 预测边界到真值边界的平均距离 | 对整体边界偏移敏感,单位是像素或毫米,直观 |
| HD95 | 95%分位的最大表面距离 | 重点捕捉最离谱的局部边界偏差,防“某一段完全跑偏” |
| Boundary IoU | 边界带的交并比 | 直接计算边界附近区域的匹配程度,精细度最敏感 |
以HD95为例,它计算的是预测边界上每个点到真值边界最近距离的分布,取95%分位。这个值如果大于1毫米,说明至少有5%的边界处存在超过1毫米的偏差,在亚厘米级病灶上已经是不可接受的了。我在项目里一般用HD95 < 1mm作为边界达标的硬门槛。
6.3 可复用的验收流程
验收不要拿着单个病例的展示图拍脑袋。我建议做成固定流程:先在测试集上跑完全部测试病例,分别计算Dice、ASSD、HD95、Boundary IoU,按病灶尺寸分三层统计(小于1cm、1到3cm、大于3cm),这样能看出模型在哪些尺寸段边界更容易崩。随后把每个病例的表面距离误差图导出成热力图,让医生直接看误差空间分布,而不是只看整体轮廓叠图。
我踩过的一个典型情况是:整体Dice在0.9以上,HD95也达标,但医生反馈某一段边界上出现了局部凸起。一查热力图,果然是某个层面的边界局部偏了3毫米,但因为这个病灶大,整体指标被摊平了。引入Boundary IoU之后这个问题立刻暴露出来。所以我的验收结论是:指标表格和误差热力图必须配套,只看任何一个都容易漏。
另外提醒一点,标注者的稳定性也很关键。验收阶段最好让同一位医生对同一批病例标注两次,间隔两周以上,用两次标注结果算一个“医生自身一致性”。如果医生两次标注的HD95本身就有1.5毫米的波动,那模型做到1毫米以内已经属于过拟合标注噪声了,这个信息能帮你避免在边界指标上无意义地内卷。
最后分享一点私房经验
踩过这么多坑,我最大的体会是:SAM在医疗影像上不是灵丹妙药,它更像一个极其聪明的胚子,需要你替它做医学场景的适配和约束。碰到边界模糊、形态不规则、低对比度这类传统模型容易“糊”的场景,SAM往往是惊喜来源;但碰到边界本身清楚、目标形态规则的小病灶,传统模型也未必差,没必要为了用SAM而强行上SAM。
再分享一个小技巧:正式投入大量标注之前,先拿几十张图跑一遍“自动掩码生成+人工提示修正”的流程,把每个病例最后留下的边界不稳定区域标出来。如果大部分不稳定区域都集中在外轮廓的某些固定解剖位置,那说明这个位置大概率存在部分容积效应或解剖粘连,靠调推理参数很难彻底解决,需要考虑采集参数或重建算法层面的配合。这个判断在项目立项阶段帮过我很多次,能省下大量的标注和复审成本。