多模态图像分割统一方案:SEE-AO原理、数据引擎与部署实践
做图像标注这行的都懂一个别扭的地方:同一张图,语义分割刷一遍像素标签,实例分割拉一遍框,全景分割再把两者拼起来,交互式抠图工具又要重新点一遍。一个项目下来四套工具、四份标注、四个模型,最后还得写胶水代码把结果对齐。前段时间一位做工业质检的开发者给我看他们的标注后台,一张电路板照片,五个同事用五种工具各标了一遍,人工校验耗掉两天,最后发现实例掩码和语义标签的边界还打架。
这篇要聊的Segment-Everything-Everywhere-All-At-Once,为了叙述方便我把它缩写成 SEE-AO,它想干的事其实很朴素:一次前向、任意提示、任意粒度。Everything 指覆盖画面里所有目标而非只认训练好的那几类,Everywhere 指跨场景跨模态都能接,All-At-Once 指不用先分类再分割、不用先检测再抠图,一个请求出去,掩码直接回来。这是一套多模态图像分割系统,输入可以是一个点、一个框、一张粗糙掩码,也可以是一句自然语言描述,输出是像素级掩码加置信度。
适合谁看:如果你在做数据标注平台、工业质检、遥感解译、医学影像预处理、内容审核,或者单纯想把"分割一切"这件事塞进自己的产品链路,这篇的经验都能直接拿去用。全文按我自己的踩坑顺序组织,先讲这套统一方案到底解决了什么结构性问题,再钻进模型内部看三个舱室怎么分工,然后聊数据引擎这个容易被低估的一半,最后是推理部署和落地时那些文档里不会写的坑。基础一般的读者可以只看第 1 节和第 4、5 节,想自己训一版的从第 2、3 节入手。
1. 四套工具、四份标注:分割任务为什么非拆不可,又为什么必须合
1.1 语义分割、实例分割、全景分割与交互式抠图各自的算盘
先把这个领域的"分家史"说清楚,不然理解不了统一模型的价值。语义分割只管像素属于哪一类,同一类的两头牛在输出图里是同一种颜色,它解决的是"这片区域是什么";实例分割反着来,先找目标框再把框里的像素抠出来,同一类要区分个体,它回答"有几个、分别在哪";全景分割是前两者的缝合,把"不可数的背景"归为语义、"可数的前景"归为实例,好处是一次输出完整场景,坏处是标注成本翻倍;交互式分割又是另一条路,用户点一下、画个框,模型给出精细边缘,它不关心类别,只关心"你指的那个东西的轮廓"。
这四件事在工业界被拆成四个模型、四条数据管线,背后有很实在的历史原因。语义分割的经典结构是编码器-解码器全卷积网络,损失用逐像素交叉熵,训练目标是把每个像素分对;实例分割主流是"检测框 + 框内掩码"的两阶段路线,或者"中心点 + 嵌入向量"的单阶段路线,训练目标是让同一实例的像素在嵌入空间靠近;全景分割则要在后处理阶段做启发式合并,把语义结果和实例结果在重叠区域仲裁;交互式分割的输入里有人工提示,网络结构必须支持提示编码,训练数据是"图 + 提示 + 掩码"的三元组。
拆开的好处是每个任务都能把结构调到最贴合自己的形态,坏处列举起来一长串:标注成本重复,同一张图的像素标签被标了三四遍;误差累积,检测框偏一点,框内掩码就跟着偏;维护成本,四个模型的输入格式、预处理、后处理、部署方式都不一样;粒度断裂,语义分割给不出个体,实例分割又常常忽略"背景"这类无个体概念的区域。我在一个遥感项目里见过最夸张的情况,地块语义分割用一套管线,建筑物实例分割用另一套,最后要做"某地块内建筑物占地面积占比"这个指标,得写两百多行代码去对齐两套坐标系和两张掩码。
所以统一方案的价值不在于"少训一个模型"这么简单,它真正改掉的是任务定义方式:不再用"任务"来切分模型,而是用"提示"来切分请求。你想要语义结果,就给类别文本提示;想要实例,就给点或框;想要全景,就把网格点铺满整图让模型自己吐所有目标。粒度变成调用侧的参数,而不是模型的结构。
1.2 把粒度、提示、模态塞进一次前向,真正的难点在哪
"统一"两个字说起来轻松,实际要跨过三道坎,每一道都能把方案卡死。
第一道是提示空间的统一。点、框、掩码、文本这四种输入的形态差异极大:点是两个数字,框是四个数字,掩码是一张二维图,文本是一串离散 token。要让一个解码器同时吃下它们,必须把它们投影到同一个维度的向量空间里,且语义上要能相加。这里的关键是"加法可交换"——一个框的提示和一个点的提示叠加后,解码器应该理解成"在框的约束下重点看这个点",而不是两个互不相干的信号。做不好这一步,多提示组合就会互相干扰,我早期实验里最典型的现象是"框 + 负点"组合后掩码直接变成整张图。
第二道是粒度歧义。用户点在一头牛的背上,他想要的是整头牛、牛的躯干,还是牛背上那块斑纹?这个歧义在单任务模型里被数据集定义掩盖了(实例分割数据集只标整头牛),但在统一模型里会直接暴露。解法通常是让模型一次输出多个候选掩码并给每个打分,把选择权交回调用侧。这个设计看着像偷懒,实际上是这套方案最实用的地方之一,后面 2.3 会展开。
第三道是模态对齐。文本分支的语义空间和视觉分支的语义空间天然错位,同一个词在图像里对应的区域大小可能差几十倍。这里最容易出的问题是"类别词能对齐、修饰词对不齐"——"车"能分对,"红色的车"就经常把旁边红色的广告牌也框进来;"裂缝"能分对,"细长的裂缝"反而因为形状先验被切碎。这个裂缝不是靠加大模型能补的,得靠数据侧的负样本设计和推理侧的提示改写,4.3 会讲具体手段。
提示:如果你评估一个统一分割方案时,对方只演示单点分割的效果,不给多提示组合、不给粒度歧义场景、不给同一张图的重复请求延迟,那基本可以判断这套方案还没过工程关。
1.3 "Everything"和"All-At-Once"的边界:这个方案不承诺什么
标题里的词很唬人,实际用起来必须先划清边界,否则期望管理会崩。
它不承诺"零样本覆盖一切"。所谓 Everything,指的是在同一次前向里尽可能多地吐出候选目标,而不是对世界上任何物体都有正确语义。遇到训练分布外的材质(比如高反光金属、透明玻璃、极细的纤维),它照样会给出边界模糊的结果。真正的覆盖度取决于数据引擎喂进去的分布,模型结构只是把"能接住多少"的天花板抬高了。
它不承诺"实时全图分割"。All-At-Once 说的是"一次请求内完成",不是"一帧内完成"。全图密集提示(比如每隔 16 像素撒一个点)在 1024 分辨率下是几千个提示,解码器再轻量也扛不住几毫秒,实际工程里必须做提示采样和去重。
它不承诺"输出就是最终标注"。全自动生成的掩码只适合做预标注,必须有人工审核环节。我在一个内容审核场景里试过纯自动跑,5% 左右的掩码存在"包含多余背景"或"缺一块"的问题,直接入库会导致下游统计口径漂移。
它也不替代检测模型。分类置信度和框回归这件事,专用检测器依然更准更快。统一分割系统的正确位置是"接在检测后面做精细化",或者"接在人工点击后面做边缘补全",而不是把检测器整个替掉。
把这三条边界想清楚,后面所有的结构设计和工程取舍才有落脚点。这也是我看任何新方案的第一反应:先问它不做什么,再问它怎么做。
2. 拆开多模态分割模型的三个舱室
2.1 图像编码器:贵在哪里,为什么只能跑一次
整个系统里最重的部分就是图像编码器,通常是一个视觉 Transformer 主干,把 H×W×3 的图变成 h×w×C 的特征图。以常见配置为例:输入 1024×1024,patch 尺寸 16,那么特征图是 64×64;如果主干是 24 层、宽度 1024 的规模,参数量在三亿量级,用半精度存权重就要 600MB 出头,推理时的激活峰值能到 4~6GB。要是换成更宽更深的版本,参数量翻倍,显存也跟着翻倍。
为什么它只能跑一次?因为它和提示无关。图像编码器看到的是图本身,不管用户问什么,这张图的特征表示是一样的。这套架构把"看图"和"答题"彻底分开:看图是一次性的重活,答题是轻量的活。这个拆分是整套方案能在交互式场景落地的根本原因。
算一笔账就明白了。假设图像编码在中等加速卡上耗时 120~180ms,解码器单次耗时 5~10ms。用户第一次点下去,总延迟是 150ms 左右;第二次、第三次点击,如果缓存了图像特征,延迟就只有 10ms 以内。从"每次点都等半秒"变成"只等第一次",交互手感完全是两个产品。我做过一个对比测试,同一张图连续点 8 个目标,不缓存的总耗时约 1.3 秒,缓存后约 0.35 秒。
成本控制上有三个经验点值得说。
其一,长宽比处理不要用 resize。直接把非正方形图拉伸到 1024×1024 会让物体变形,边界精度显著下降。正确做法是保持长边缩放到 1024,短边补零到 1024,同时在元数据里记录原始尺寸和有效区域,后处理时把掩码裁回有效区域再缩放到原图。这个"补零 + 记录有效区"的细节看着琐碎,但它是边界精度不掉的关键。补零的代价是短边方向有大量无效计算,一张 1920×1080 的图缩到 1024×576,补零后仍有 44% 的面积是空的,这部分算力纯属浪费。
其二,位置编码要支持插值。主干在固定分辨率上预训练,换分辨率时位置编码得做双线性插值,否则特征会错位。这个坑很隐蔽,表现是"换分辨率后掩码整体偏移几十像素",很多人第一反应是后处理坐标系错了,其实是位置编码没处理。
其三,主干可以选小一号。如果业务对边界精度要求没那么苛刻,宽度减半的版本能把显存压到 2GB 出头,速度快一倍多,掩码质量在常规物体上掉 2~3 个 IoU 点。我在移动端边缘设备上就用过小主干 + 后处理细化的组合,总体效果比大主干单跑更划算。
2.2 提示编码器:点、框、掩码、文本怎么挤进同一个256维空间
这是整套设计里最巧的一块,也是我最开始没看懂的地方。四种模态的输入,最后都要变成一组 256 维的向量,和解码器的 token 序列拼在一起。
点的处理:每个点有坐标和标签(正点表示"要这个",负点表示"不要这个")。坐标先做归一化,再用一组不同频率的正弦函数编码成高维向量,这样相邻位置在向量空间里也相邻;标签用一个可学习的嵌入向量表示,两者相加。这样"在 (320, 480) 的一个正点"就变成了一个 256 维向量,位置信息和意图信息都在里面。
框的处理:不把框当成四个数字去回归,而是用两个角点各自的编码加上一个表示"左上/右下"的嵌入。框还额外提供一个稠密提示——在框内填充、框外留白的低分辨率图,经过几层卷积压到 64×64×256,直接加在图像特征上。这个设计的意思是:框既作为稀疏 token 告诉解码器"大致范围",又作为稠密先验去调制特征图。实测下来,这个"稀疏 + 稠密"双路设计对小目标特别重要,只给稀疏 token 时小目标经常被漏掉。
掩码的处理:用户给的粗糙掩码(比如用笔刷涂的一片)先下采样到 256×256,再过几层卷积到 64×64,和图像特征的尺度对齐后相加。这里要注意,掩码输入的意义是"约束",不是"替代",所以它的权重不能太大,否则模型会完全沿用粗糙掩码的烂边缘。
文本的处理:分词后过文本编码器,得到每个 token 的向量,再投影到 256 维。这里有个工程细节:文本 token 的数量不固定,而解码器的 token 序列要定长,通常做法是保留一个固定的最大长度,不足补零,并加一个"有效位"标记。文本 token 和点框 token 在序列里的位置不同,注意力机制会自己学会区分。
| 提示类型 | 编码方式 | 输出维度 | 常见误用 |
|---|---|---|---|
| 点 | 位置正弦编码 + 标签嵌入 | 1×256 | 坐标忘记归一化,导致尺度偏差 |
| 框 | 双角点编码 + 框内稠密图 | 1×256 + 64×64×256 | 只给稀疏 token,小目标漏检 |
| 掩码 | 下采样 + 卷积投影 | 64×64×256 | 权重给太大,模型沿用烂边缘 |
| 文本 | 分词 + 文本编码器 + 投影 | N×256 | 长度未定长,批处理报错 |
多模态同时输入时,坑最多的是冲突仲裁。比如用户给了一个"红色"的文本提示,又给了一个落在蓝色区域的正点,模型该听谁的?实测下来,空间提示(点、框)的权重通常高于文本提示,因为空间提示更明确。如果业务上希望文本优先,得在推理时做提示加权,或者干脆拆成两次请求再合并。
2.3 掩码解码器:轻量双向注意力与"一次输出三个答案"的用意
解码器是整个系统里最轻的部分,通常只有两层 Transformer,每层四个注意力头,中间维度 2048。它的工作流程是:把提示 token、若干可学习的输出 token、图像特征拼成一个序列,做"token 到图像"和"图像到 token"的双向注意力,几轮之后,输出 token 上就带有了"这个提示对应哪个区域"的信息。
为什么要双向?单向注意力(token 只看图像)能解决"提示在哪",但解决不了"提示边界在哪",因为边界信息分散在图像各处。加了"图像到 token"的反向注意力后,图像特征会被提示调制,聚焦到相关区域,边界细节才出得来。这个设计在论文里往往一笔带过,但它是边界精度不掉的核心。
输出 token 经过几层感知机后,一路生成掩码,一路生成该掩码的质量分。掩码的生成方式是:先得到 256×256 的低分辨率掩码,再用两次转置卷积上采样到 1024×1024,中途和图像编码器的中间层特征做跳跃连接。这里为什么用转置卷积而不是直接双线性插值?因为边界细节需要在低分辨率阶段就学出来,双线性插值只能平滑,补不出细节。
一次输出三个掩码这个设计值得单独说。给定一个点,模型同时给出三个候选:整体、部分、子部分,每个配一个置信度分。举例来说,点在一辆车上,三个候选可能是"整辆车"、"车门"、"车窗"。这个设计解决的是粒度歧义,把"用户到底想要哪一层"的判断交给调用侧。实际用的时候,多数场景直接取置信度最高的那个;如果做标注辅助,三个候选都摆出来让人选,效率比重新点一遍高得多。
但这里有个容易被忽略的问题:置信度分是模型自己估的,不是真值校准过的。我做过统计,置信度 0.9 以上的掩码,实际 IoU 在 0.82~0.95 之间波动,在细长物体和透明物体上偏低。所以拿它做自动过滤时,阈值不能设太松,我一般设 0.88 起步,配合后文的稳定性检验。
解码器的另一个轻量化技巧是输出 token 数量固定且很少,通常四五个就够。这带来一个好处:批处理时不同样本的 token 数一致,不会出现变长序列的补齐浪费。
2.4 文本分支接入后的对齐裂缝
纯视觉提示(点、框、掩码)的系统相对好做,因为提示和输出在同一个视觉空间里。一旦接入文本,问题就变成"语言空间和像素空间怎么对齐"。
常规做法是先用大规模图文对做对比预训练,让文本编码器和视觉编码器在同一个语义空间里。这套预训练能解决"名词对齐",但解决不了三件事。
第一件是修饰词。"红色的车"里,"红色"这个词在预训练时更多和"红色物体"关联,而不是和"车的颜色"关联。结果就是模型会把画面里所有红色区域都拉进来。缓解办法是在训练数据里构造"同物体不同属性"的对比样本,让模型学会属性是绑定在物体上的,而不是全局的。
第二件是数量词和空间关系。"左边的螺丝"、"三颗芯片",这类描述需要模型理解空间布局和计数。这类能力靠分割数据本身很难训出来,通常得引入带空间关系标注的数据,或者在推理时把文本拆成"目标词 + 空间约束",空间约束转成框提示再和文本提示组合。
第三件是领域词。工业场景里的"焊点虚焊"、"丝印偏移",医学场景里的"磨玻璃影",这些词在通用图文对里几乎不存在。指望预训练模型认识它们不现实,必须在领域数据上做微调,而且要保证文本编码器也参与微调,不能只调解码器。我见过一个团队只调解码器,结果模型对领域词的反应和随机词差不多,白训了两周。
一个实用的折中方案是建同义词表。把领域黑话映射到模型认识的通用词上,比如把"虚焊"映射到"小的圆形凸起",把"丝印偏移"映射到"文字位置偏移"。这个土办法效果出乎意料地好,成本几乎为零,适合快速验证阶段。
3. 数据引擎:比网络结构更决定上限的那一半
3.1 辅助标注、半自动、全自动的三段式闭环
统一分割模型的效果上限,七成在数据,三成在结构。这句话我在不同项目里验证过好几次。数据引擎的核心思路是"模型帮人标,人帮模型改",分三段走。
第一段是辅助标注。人工在图上点几下,模型给出候选掩码,人工修边。这一步的模型可以很弱,甚至用传统算法都行,目的是把"从零画轮廓"变成"改轮廓"。实测下来,这一步能把单张图的标注时间压到原来的三分之一左右。关键指标不是模型的绝对精度,而是"人工修改量"——如果每次都要大改,省的时间就被抵消了。经验阈值是:模型给出的掩码和人工最终结果的 IoU 在 0.75 以上时,改起来才划算。
第二段是半自动。模型批量出候选,人工只做接受/拒绝/粗修三选一。这一步需要一个筛选机制,把明显烂的掩码提前扔掉,减少人工看的时间。筛选靠两个信号:模型自估的置信度分,以及掩码的稳定性——对同一提示做多次轻微扰动(比如把点挪动两像素、把框缩放 5%),看输出掩码的 IoU 波动。波动小于 0.05 的判为稳定。
第三段是全自动。在图上铺网格点或者用检测器出框,让模型自己生成掩码,再用稳定性 + 置信度 + 规则(面积上下限、长宽比、是否压边)过滤。这一步产出的数据质量参差,只能作为训练数据的补充,不能作为评测集。我们当时的比例是:全自动数据占训练集的 60%,半自动占 30%,纯人工的精标占 10%,精标那部分专门用来做验证和难样本挖掘。
这套闭环有个容易被忽略的点:每一段都要留人工抽检。全自动那段的抽检比例我一般设 2%,抽到的错误样本反向补充到难样本池里,下一轮训练重点关照。不设抽检的话,错误会静默累积,等你发现时模型已经在错误分布上训了很久。
提示:数据引擎里最贵的不是标注人力,是"返工"。把抽检机制和难样本回流的路径设计好,比多买几张卡有用得多。
3.2 损失函数组合与难样本重采样
分割任务的损失不是单一指标能覆盖的,常规组合是三项相加。
掩码损失用焦点损失加 Dice 损失的组合。焦点损失负责逐像素分类,处理正负样本极度不平衡(一张图里目标像素可能只占 2%),其中聚焦参数取 2、平衡参数取 0.25 是常用配置;Dice 损失负责整体重叠度,对边界不敏感但对"漏掉一整块"很敏感。两者权重一般是 20:1,焦点损失占大头。为什么差这么多?因为逐像素分类是基础,Dice 是修正项,权重给大了会让模型过度追求"整体覆盖"而忽略细节边界。
质量分损失用均方误差,把预测分和真实 IoU 拉近,权重通常设 0.05。这项权重小是有原因的:它只影响候选排序,不影响掩码本身,权重大了反而会干扰主任务。
难样本处理靠提示采样策略。训练时每张图不只用一个提示,而是采多个:一个来自真值掩码(模拟"用户点对了"),其余来自随机采样(模拟"用户点偏了"或"点在背景上")。这个比例大概是 1:10,也就是一个正样本配十个随机样本。这样训出来的模型对"点偏了"有鲁棒性,不会因为点落在物体边缘就崩掉。
我踩过的一个坑是难样本挖得太狠。有一版训练里,我把所有低 IoU 的样本加权三倍重复采样,结果模型在难样本上确实提升了,但在常规样本上退化了,整体指标反而降了两个点。原因是难样本里有一批是标注本身就有争议的(比如物体边界模糊、遮挡严重),模型在这些样本上学到的是噪声。后来的做法是难样本加权 1.5 倍封顶,并且人工过一遍剔除标注有争议的。
另一个经验是空掩码样本必须有。也就是"这个提示在图上没有任何对应目标"的情况,模型要能输出空结果。这类样本在训练集里占比建议 5%~10%。没有这类样本,模型遇到"点在纯背景上"时会强行编一个掩码出来,这在自动标注场景里是灾难——它会凭空造出目标。
3.3 小规模复现训练流水线与关键超参
想自己跑一版,不用从零开始。下面是我用过的一条可复现的流水线骨架,规模不大,单机八卡能跑起来。
# 伪代码:单轮训练主循环 for images, gt_masks, texts in loader: # 1. 图像编码(可梯度,也可冻结前几层) feats = image_encoder(images) # B×256×64×64 # 2. 提示采样:1 个来自真值,10 个随机 prompts = sample_prompts(gt_masks, k_pos=1, k_neg=10) # 3. 解码:每个提示输出 3 个候选掩码 + 3 个质量分 pred_masks, pred_iou = mask_decoder(feats, prompts) # 4. 多项匹配:候选和真值做匈牙利匹配,选损失最小的那个 matched = hungarian_match(pred_masks, gt_masks) # 5. 三项损失 loss = 20 * focal_loss(matched, gt_masks) \ + 1 * dice_loss(matched, gt_masks) \ + 0.05 * mse_loss(pred_iou, true_iou(matched, gt_masks)) loss.backward() optimizer.step()关键超参我列一下,都是试出来的:
| 参数 | 取值 | 说明 |
|---|---|---|
| 优化器 | AdamW | 权重衰减 0.1 |
| 主干学习率 | 1e-5 | 比解码器低一个量级 |
| 解码器学习率 | 1e-4 | 前期可以再高一档 |
| 批大小 | 每卡 4,梯度累积 4 | 等效批 128 |
| 输入分辨率 | 长边 1024,短边补零 | 不要拉伸 |
| 提示采样 | 正 1 负 10 | 负样本含背景点和随机框 |
| 训练轮数 | 30~50 轮 | 超过 60 轮开始过拟合 |
| 预热 | 前 2 轮线性预热 | 防早期梯度爆炸 |
关于冻结策略,我的建议是:前 5 轮冻结主干,只训解码器。理由是预训练主干的特征已经很好了,一开始就让大学习率去动它,容易把好特征破坏掉。5 轮之后再解冻,用很小的学习率微调,效果比全程联合训练稳。
还有一个容易忽略的细节:真值掩码的边界要处理。不同标注源的边界习惯不一样,有的贴紧物体,有的习惯往里收两像素。混在一起训,模型会学出一个"平均边界",结果两边都不像。我的做法是在数据加载时对边界做一致性处理,把外扩两像素以内的像素权重降到 0.5,让模型不必纠结这几像素的差异。
4. 推理侧问题排查:显存、分辨率与退化场景
4.1 分辨率和显存的死结,以及padding带来的隐性浪费
分辨率往上走,边界精度明显变好,但显存和耗时也线性甚至平方级上涨。这个矛盾没法完全消除,只能找平衡点。
我实测过几档分辨率的组合,用的是中等规模主干:
| 输入长边 | 显存峰值 | 单次编码耗时 | 边缘精度相对值 |
|---|---|---|---|
| 512 | 约 2.2 GB | 约 45 ms | 0.82 |
| 768 | 约 3.6 GB | 约 80 ms | 0.91 |
| 1024 | 约 6.0 GB | 约 150 ms | 1.00 |
| 1280 | 约 8.8 GB | 约 230 ms | 1.03 |
可以看到从 1024 到 1280,显存涨了 47%,耗时涨了 53%,精度只涨 3%。所以1024 是性价比拐点,除非业务是精细边缘(比如缺陷检测里的发丝级裂纹),否则没必要往上堆。
padding 的隐性浪费前面提过一半,这里补充一个优化思路:按批次内的最大长宽比补齐,而不是统一补到正方形。如果一批图都是宽幅(比如 1920×1080),统一补成 1024×1024 会让短边方向空白 44%,改成补到 1024×640 就能省掉这部分算力。实现上要用掩码注意力把空白区域屏蔽掉,否则空白区的特征会污染结果。
另一个必须做的事是图像特征缓存。缓存键用图片内容的哈希,不用文件路径(同名不同内容的图很常见)。缓存值存 64×64×256 的特征图,单张占用约 4MB(半精度),缓存 500 张就是 2GB,用内存或者本地 SSD 都行。命中率在标注场景里非常高,因为标注员会在一张图上反复点。我做过统计,开了缓存之后,标注平台的 GPU 平均利用率从 70% 降到 35%,同样的卡能多扛一倍的人。
4.2 小目标、细长结构、透明件的典型失败模式
这三类是我遇到问题最多的场景,每一类都有对应的缓解手段。
小目标:面积小于 32×32 像素的目标,掩码经常整个丢失或者只剩半个。根本原因是特征图是 64×64,一个小目标在特征图上不到一个格子,信息在编码阶段就被压没了。缓解手段有三个:提高输入分辨率(最直接但最贵)、在特征图上做多尺度融合(把浅层高分辨率特征接进来)、用框提示代替点提示(框的稠密先验能显著提升小目标召回)。第三个最省钱,实测点提示换成紧贴目标的框提示,小目标召回率能从 55% 提到 78%。
细长结构:电缆、裂纹、血管、细线条,掩码容易被切碎成好几段。原因是转置卷积的感受野偏方正,对 1~2 像素宽的细长结构不友好。缓解办法是后处理接一个形态学闭运算(核大小 3×3 或 5×5),把断开的段连起来;或者在训练数据里对上采样阶段做细化标注,让模型学到"细长不断"的先验。我试过直接改网络结构加方向卷积,收益不明显,性价比不如后处理。
透明件:玻璃、塑料膜、水面,边界几乎没有颜色差,模型给出的掩码往往偏大或者偏小。这类场景纯靠外观分割是做不到的,必须引入额外线索——比如偏振图、深度图,或者在提示里加入"边界在哪"的框约束。工程上更现实的做法是把它标成"不确定",交给人工,而不是硬让模型猜。
还有一个隐蔽的失败模式是大面积同色区域,比如白墙、天空、纯色布景。模型在这种情况下会把整个区域当成一个目标吐出来,甚至把不同物体连成一片。缓解办法是在提示里加负点,或者设面积上限(比如超过图像 40% 的掩码直接判为可疑,降级处理)。
4.3 文本提示歧义的消解手段
文本提示的好处是零门槛,坏处是歧义多。我在实际项目里总结了四条处理原则。
原则一,把模糊词换成可视觉化的词。"损坏"→"缺口或裂纹","脏污"→"深色小斑块","缺陷"→"形状不规则的小区域"。这一步本质上是把抽象判断转成外观描述,模型才能接住。
原则二,加空间锚点。与其说"螺丝",不如说"左上角那颗螺丝",具体做法是把空间词转成框提示,和文本提示一起送进去。这个组合的准确率比纯文本高出一截。
原则三,用负向提示排除干扰。想分"红色指示灯"但画面里红色太多,加几个负点落在其他红色区域上,效果立竿见影。这个技巧成本极低,但很多人不知道。
原则四,接受多候选。文本提示天然有多解,让模型出三个候选再筛,比反复改写提示更省事。我在一个仓储盘点场景里就这么干,"纸箱"这个提示会同时出"整箱"、"箱盖"、"箱上标签"三个候选,取第一个就够用了。
还有一个工程上的建议:把常用提示词做成模板库,而不是让业务方自由输入。自由输入听着灵活,实际上业务方写不出模型能懂的描述,反复试错的时间成本远高于维护一个几十条的模板库。
5. 工程落地:提示设计、缓存策略与三个真踩过的坑
5.1 同一张图多次提问的embedding缓存
前面提过缓存,这里讲清楚实现层面的三个细节。
缓存的粒度是"图 + 预处理参数"。同一张图,如果这次用 1024 补零、下次用 768 补零,特征是不一样的,缓存键必须带上分辨率和补齐策略。我最初的版本只用了图片哈希,结果换分辨率后拿到旧特征,掩码整体偏移,排查了半天才发现是缓存串了。
缓存的淘汰策略按"标注会话"来,不按 LRU。标注员的工作模式是"在一张图上点十几次,然后换下一张",典型的 LRU 会在换图时把刚用过的图淘汰掉,下次回来又得重算。改成会话级缓存(一次标注会话内不淘汰,会话结束统一释放)之后,命中率从 60% 提到了 90% 以上。
缓存要能失效。图片被替换、模型更新、预处理逻辑改动,这三种情况必须让缓存失效。做法是在缓存键里加一个版本号,模型或预处理一改就升版本,旧缓存自然失效。这个机制看着简单,但如果一开始没设计,后面清理脏缓存会非常痛苦。
5.2 与既有检测链路的分工与结果融合
我不建议用统一分割模型替掉检测器,正确姿势是分工。
检测器负责"有没有、在哪、是什么类",它的优势是速度快、类别置信度准、对小目标召回高。分割模型负责"精确边界",接收检测框作为提示,输出掩码。这个组合的好处是检测器的框天然解决了"分割模型不知道要看哪"的问题,而且框提示对小目标的分割质量提升明显。
融合时有两个技术点要注意。第一,框要略微外扩。检测框通常贴得比较紧,直接拿来当提示会切掉物体的边缘部分。我的经验是外扩 5%~10%,具体比例和物体尺寸有关,小物体外扩比例要大一些。第二,掩码要裁回框内再做后处理,否则掩码可能溢出框外,和相邻物体的掩码重叠。
还有一个容易被忽略的问题:检测器和分割模型的类别体系可能不一致。检测器有 20 类,分割模型靠文本提示,两者的类别名不完全对应。我的做法是维护一张映射表,把检测类别映射到分割模型认识的文本提示,映射不上的走"物体"这种泛化提示,宁可粗一点也不要报错。
5.3 三个踩坑记录
第一个坑:以为掩码质量分可以直接当阈值用。早期版本我把置信度 0.8 当过滤线,结果自动标注的数据里混进了大量"整个画面"的掩码。后来才明白,模型对"大面积简单区域"特别自信,因为这类样本在训练时损失很小。修正办法是加面积约束和压边检测,同时把置信度阈值提到 0.88,并且必须配合稳定性检验。这个坑的教训是:模型自估的分数反映的是"它有多确定",不是"它有多对",两者在分布外样本上会严重背离。
第二个坑:批量推理时提示数量不一致导致结果错位。一批图里有的图 5 个提示,有的 20 个,我图省事把提示拼成一个大 batch 送进去,再用索引切回来。结果因为补零和排序的关系,切回来的掩码和提示对不上,标注平台上出现了"点左边出右边"的诡异现象。排查了两天才定位到是批处理拼接的问题。修正办法是每个样本独立维护提示数量,输出时按样本 ID 回填,不做跨样本拼接。这个坑的教训是:批处理能省时间,但省出来的时间很容易在调试上还回去。
第三个坑:以为换了更强的文本编码器就能解决领域词问题。有个项目里模型对"焊点"这个词反应很差,我第一反应是换更大的文本编码器,折腾了一周没效果。后来发现根因是训练数据里"焊点"这个概念从来没有以文本形式出现过——所有标注都是点提示生成的,文本分支根本没被训到。补了一批"图 + 文本 + 掩码"的三元组数据之后,同样的编码器效果直接上来了。这个坑的教训是:先确认数据里有没有这个信号,再考虑换模型,顺序反了就是白烧算力。
还有个小技巧顺便说:同一张图做多次不同提示的请求时,把提示合并成一次前向。解码器很轻,合并后耗时几乎不增加,但省掉了多次调用的框架开销。我在一个批量标注任务里把 8 个提示合并,总耗时从 8×10ms 降到 15ms,提升非常明显。
我在实际项目里最大的体会是,这套多模态分割系统的价值不在于"一个模型干所有事"这种叙事,而在于它把分割从"训练一个专用模型"变成了"设计一组提示"。专用模型的门槛在数据和训练,提示工程的门槛在理解业务——后者的迭代速度快一个量级。真正难的部分从来不是那两层解码器怎么写,而是你怎么把业务里的模糊需求翻译成模型能接住的提示,以及你怎么设计一条能持续产出干净数据、持续发现模型盲区的回流链路。这两件事做扎实了,哪怕换个结构、换套权重,整套系统照样能跑;做不扎实,模型再新也只是个好看的 demo。