开头:BiRefNet 到底在做什么#

BiRefNet 可以先粗略理解成一个高质量前景分割模型。它拿到一张图片之后,不是直接输出一张透明 PNG,而是先输出一张 mask:

白色区域:要保留的主体
黑色区域:要删除的背景
灰色区域:边缘、不确定或半透明过渡

如果我们要做商品抠图,最终流程通常是:

原始图片
BiRefNet 预测 mask
mask 作为 alpha 通道
得到透明背景 PNG

比如一副眼镜放在白色桌面上,BiRefNet 要判断每个像素属于眼镜还是背景。真正的难点不在于“整副眼镜在哪里”,而在于鼻托、镜腿、镜框内侧边缘、镜片孔洞这些细节是否干净、连续、圆润。

这篇文章按一个完全不懂深度学习的人也能理解的方式,解释 BiRefNet 的工作链路,并重点讨论一个很实际的问题:如果模型整体已经抠得很准,但镜架鼻托边缘还有毛刺和不规整,下一步应该怎么修。

总览:BiRefNet 的基本分工#

可以先把 BiRefNet 拆成两个大部分:

图片
backbone:提取视觉特征
decoder / refinement:融合特征并生成 mask
输出前景 mask

更通俗一点:

backbone = 看图并提取证据
decoder = 根据证据画出最终 mask

backbone 不直接输出“这个像素是前景、那个像素是背景”。它输出的是很多层特征图。decoder 才负责把这些特征图融合起来,变成最终可用的分割结果。

Backbone:不是空模型,而是视觉特征提取器#

很多人第一次听到 backbone,会以为它是一个空权重模型。其实不是。

backbone 是整个网络里的视觉主干,它负责把原始 RGB 像素转换成模型内部能理解的特征。以一张 1024x1024 图片为例,原始输入大概是:

[3, 1024, 1024]

这里的 3 是 RGB 三个颜色通道。进入 backbone 后,它可能输出多层特征:

x1: [192, 256, 256]
x2: [384, 128, 128]
x3: [768, 64, 64]
x4: [1536, 32, 32]

这些不是普通图片,而是特征图。可以粗略理解为:

浅层特征:边缘、颜色变化、纹理、细线条
中层特征:局部结构,比如镜框、镜腿、鼻托轮廓
深层特征:物体部件,比如左右镜片、鼻梁连接区域
最深层特征:整体语义,比如这是一副眼镜,主体大概在哪里

BiRefNet 官方默认常见配置使用的是 swin_v1_l,也就是 Swin Transformer Large 作为 backbone。它通常会加载 ImageNet 预训练权重,例如 swin_large_patch4_window12_384_22kto1k.pth。这意味着 backbone 不是从零开始学看图,而是已经在大规模图像数据上学过通用视觉特征。

Decoder:把特征融合成最终 mask#

backbone 只负责提取线索,decoder 才负责最终判断。

对于眼镜图,backbone 可能已经提取出这些信息:

这里有强边缘
这里有细长黑色结构
这里像镜腿
这里像鼻托
这里可能是桌面背景纹理
这里是整副眼镜的主体范围

但这些线索本身还不是最终答案。decoder 会把多层特征融合起来:

深层语义:整副眼镜在哪里
中层结构:镜片、镜腿、鼻托大概在哪里
浅层细节:边缘、纹理、孔洞、细线在哪里

然后逐步放大并输出 mask:

32x32 粗略主体区域
64x64 更清楚
128x128 加入局部结构
256x256 加入边缘细节
最终高分辨率 mask

所以准确地说,不是“边缘在哪里全靠 decoder 从零判断”,而是:

backbone 提供边缘、纹理、结构、语义线索
decoder 融合这些线索并做最终 mask 决策

为什么产品抠图容易有白边和毛刺#

商品抠图里很常见两个问题:白边和毛刺。

白边不一定是模型看错了。常见原因有:

mask 边界偏外,保留了一点背景
训练标注本身外扩,模型学到了保守边界
resize 或插值导致边缘污染
原图边缘像素本身混有白色背景
alpha 合成时没有做颜色去污染

比如黑色镜框在白底上拍摄,镜框边缘像素可能不是纯黑,而是黑色主体和白色背景混合后的灰白边缘。哪怕 mask 已经很准,透明 PNG 放到深色背景上时也可能出现白边。

毛刺则更偏向 mask 边界质量:

局部边缘抖动
小噪点被当成前景
鼻托曲线不够平滑
细长结构边缘有锯齿
孔洞边界不规整

对于镜架来说,鼻托是一个典型难点。它面积小、边缘曲线细、透明或半透明材质常见,而且和背景颜色可能接近。模型整体已经抠准时,鼻托边缘仍然可能看起来“不舒服”。

官方训练默认有没有边缘圆润奖惩#

BiRefNet 官方训练默认更关注整体 mask 是否准确。常见 loss 包括:

BCE loss:逐像素判断前景/背景
IoU loss:约束整体区域重叠
SSIM loss:约束局部结构相似性

官方配置里还能看到类似 contour 或 structure 的项,但默认权重可能是 0。也就是说,默认训练并没有一个非常明确的目标叫做:

边缘曲线要圆润
镜架鼻托要平滑
轮廓不能轻微起伏
细长结构必须连续
孔洞拓扑必须保持

BCEIoUSSIM 可以让整体分割变准,但它们不是专门为“形态学观感”设计的。对于普通主体分割已经够用,但对于镜架鼻托这种产品级细节,进步空间可能会卡在边缘质量上。

如果模型已经很准,下一步不要优先大改 backbone#

当模型还分不清主体时,可能需要更强的 backbone、更好的数据、更大的训练集。

但如果现在的问题是:

整体抠图准确性已经不错
只有鼻托部分有毛刺
曲线有轻微起伏
局部边缘不够规整

那问题更像是在 decoder、loss、标注一致性和后处理上。

这时不建议优先动 backbone。backbone 是通用视觉特征提取器,动它成本高,也容易把已经学好的能力破坏掉。更务实的方向是:

冻结 backbone
只训练 decoder / refiner / mask head
加入形态相关的辅助分支和 loss
用小规模高质量镜架困难样本 finetune

把后处理的形态学理解迁移进 decoder#

传统后处理可以做这些事:

去小噪点
小半径 opening 去毛刺
小半径 closing 补缺口
边缘轻微 feather
局部 contour 平滑

但如果想让模型自己学到这些倾向,就要把后处理思路转换成可训练目标。核心方法有两类:

1. 让 decoder 多预测形态相关的东西
2. 用 loss 奖惩这些形态是否合理

对镜架鼻托比较实用的是下面几类 loss。

Boundary Loss:让 decoder 明确学习边界#

第一步最推荐加 boundary branch。

原来 decoder 可能只输出 mask:

decoder feature
└─ mask head -> pred_mask

可以改成:

decoder feature
├─ mask head -> pred_mask
└─ boundary head -> pred_boundary

GT boundary 不需要人工重新标,可以从 GT mask 自动生成:

GT mask
dilate(GT) - erode(GT)
GT boundary

训练时加入:

L_boundary = BCE(pred_boundary, gt_boundary)

它的作用是让 decoder 不只知道“哪里是前景”,还要明确知道“边界应该在哪里”。对于鼻托、镜框内侧、镜腿这种细结构,它通常比单纯加大 BCE 权重更直接。

Edge-Aware Smoothness Loss:只在边缘附近减少抖动#

如果直接对全图 mask 做平滑,副作用很大:细节会糊,镜腿可能变细,鼻托可能被磨掉。

更合理的是只在边缘带上加平滑约束:

GT boundary 附近:约束预测边缘不要乱抖
主体内部和背景远处:不强行平滑

一个直观形式是:

L_smooth = edge_weight * (|dx(pred_mask)| + |dy(pred_mask)|)

这里的 edge_weight 来自 GT 边界附近区域。它不是让整个 mask 都变平,而是让边界附近的变化更规整。

权重要小。对于已经训练得不错的模型,可以从:

0.01 ~ 0.05

这种级别开始试。如果权重太大,鼻托边缘会顺一点,但也可能变钝、变短、变糊。

Curvature / Laplacian Loss:惩罚不自然的边缘起伏#

如果你的核心问题是“鼻托曲线有一点起伏,看起来不圆润”,可以考虑二阶差分或 Laplacian 类 loss。

一阶差分看的是边缘变化:

相邻像素变化有多大

二阶差分更像是在看变化是否突然抖动:

边缘方向是否一会儿凸、一会儿凹
曲线是否有异常小波动

可微近似可以写成:

L_curvature = |dxx(pred_mask)| + |dyy(pred_mask)| + |dxy(pred_mask)|

同样建议只在边界窄带上算。全图计算会让模型倾向输出过于平滑的结果,损伤细结构。

Morph Consistency Loss:把 opening / closing 思路变成训练目标#

后处理中常用 opening 去毛刺、closing 补小缺口。想迁移到训练里,可以让预测 mask 和 GT mask 在形态变换后也保持一致。

例如:

L_morph = L1(open(pred), open(gt)) + L1(close(pred), close(gt))

普通形态学操作不可微,但可以用池化近似:

dilate(x) ≈ max_pool(x)
erode(x) ≈ -max_pool(-x)
open(x) = dilate(erode(x))
close(x) = erode(dilate(x))

这个 loss 的目标不是让模型死板执行后处理,而是让它学到:

小毛刺不应该存在
小缺口不应该把连续边缘打断
鼻托边缘应该更稳定

Skeleton / Connectivity Loss:适合细长结构断裂#

如果鼻托或镜腿有断裂问题,可以加 skeleton 分支。

流程是:

GT mask
skeletonize
GT skeleton

decoder 多预测一个 skeleton:

decoder feature
├─ mask head
├─ boundary head
└─ skeleton head

loss:

L_skeleton = BCE(pred_skeleton, gt_skeleton)

这类 loss 更适合解决连续性问题。如果只是边缘有毛刺,而没有断裂,可以先不加 skeleton,避免系统复杂度过早上升。

一个比较务实的训练配方#

如果已经有一个不错的 BiRefNet 镜架模型,我会按下面顺序试:

第一轮:官方 loss + boundary loss
第二轮:官方 loss + boundary loss + edge-aware smoothness
第三轮:再加 curvature / Laplacian loss
第四轮:最后尝试 morph consistency loss

不要一次把所有 loss 全开。否则结果变好或变坏时,很难判断是哪一项造成的。

一个保守的初始权重可以是:

L_total = L_official
        + 1.0   * L_boundary
        + 0.02  * L_edge_smooth
        + 0.01  * L_curvature
        + 0.1   * L_morph

如果出现这些现象,可以这样调:

边缘还是毛刺:略微提高 smooth 或 morph
鼻托变糊:降低 smooth / curvature
细结构变短:降低 morph 或 curvature
边界位置漂移:提高 boundary,降低平滑类 loss
整体准确性下降:降低所有新增 loss,混入更多正常样本

可以冻结 backbone 只训练 decoder 吗#

可以,而且对于这种局部边缘优化很合理。

冻结策略大概是:

backbone:冻结
decoder:训练
refiner:训练
mask head:训练
新增 boundary / skeleton head:训练

代码思路类似:

for name, p in model.named_parameters():
    if name.startswith("bb."):
        p.requires_grad = False

optimizer = AdamW(
    [p for p in model.parameters() if p.requires_grad],
    lr=1e-5,
    weight_decay=0.01,
)

这样做的好处是:

保留已经学好的视觉特征
减少小数据 finetune 把模型训坏的风险
训练更快,占用更少显存
更适合只修 decoder 输出边界质量

注意,不建议只训练最后一层 head。只训练最后一层通常能力不够,修不了复杂形态。更合理的是训练 decoder、refiner、mask head 和新增分支。

MacBook M4 16GB 能不能训练#

如果是完整重训 BiRefNet,不现实。如果是冻结 backbone,只做小规模 decoder finetune,可以尝试。

比较稳的本地配置是:

设备:Apple MPS
backbone:冻结
batch size:1
输入尺寸:512 起步,最多尝试 768
学习率:1e-5 ~ 5e-5
数据量:几十到几百张困难样本 + 少量正常样本
epoch:5 ~ 20
num_workers:0 ~ 2

即使冻结 backbone,前向计算仍然要经过 backbone,所以它还是占内存。冻结只是省掉 backbone 的梯度和反向传播开销。

如果使用默认 swin_v1_l

512:比较有希望
768:可能可行,但要看实现和 batch
1024:16GB 上大概率吃紧

本地 Mac 更适合验证 loss 是否有效,不适合作为正式高分辨率训练环境。正式训练最好还是用 NVIDIA GPU,显存 24GB 以上会舒服很多。

标注一致性比继续堆训练更重要#

当模型已经很准时,鼻托边缘不稳定,常见原因不是模型不够大,而是 GT 标注不一致。

例如同样是鼻托:

有的样本贴边标
有的样本外扩 1px
有的样本边缘硬
有的样本带灰度过渡
有的样本保留透明部分
有的样本把透明部分切掉

模型会学到这些标注的平均结果,最后表现为边缘轻微抖动或不规整。

更有效的做法是整理一个小型高质量集:

100 ~ 300 张鼻托困难样本
统一边缘规则
统一透明/半透明区域处理方式
统一孔洞和细结构保留规则
再混入一部分正常样本防止退化

对于产品级抠图,数据标注的一致性经常比 loss 设计更关键。

实战建议:模型负责准确,后处理负责漂亮#

即使把形态学 loss 加进 decoder,也不要完全放弃后处理。

产品级输出通常需要两层:

模型:负责主体是否准确、结构是否完整
后处理:负责边缘观感、白边处理、alpha 过渡、颜色去污染

针对镜架鼻托,实际输出链路可以是:

BiRefNet 输出 mask
边缘带局部 smoothing
小半径 opening / closing
必要时局部 contour 平滑
alpha feather 0.5 ~ 1px
RGB 边缘去白边 / 去背景色污染
导出透明 PNG

这样比单纯要求模型输出完美透明图更稳定。

总结#

BiRefNet 的核心链路可以概括为:

backbone 提取多尺度视觉特征
decoder 融合特征并生成 mask
loss 决定模型被奖励和惩罚的方向
后处理负责产品级边缘观感

如果现在模型已经能很好地抠出镜架,只是鼻托边缘有毛刺、曲线不够圆润,那么下一步不应该优先大改 backbone,而是:

冻结 backbone
训练 decoder / refiner / head
加入 boundary loss
小权重加入 edge-aware smoothness
必要时加入 curvature / morph consistency
整理高一致性的鼻托困难样本
保留轻量后处理做最终观感打磨

这条路线更符合问题的真实位置:不是“模型不会看懂眼镜”,而是“模型输出边界还没有被形态学和产品观感充分约束”。

参考: