feat: Unmult 增加「主体保护」开关,控制是否采纳 subject_mask

subject_mask 的合并逻辑此前已存在,本次补上开关:
- 启用(默认):alpha 取 max(unmult 结果, subject_mask)
- 关闭:即便上游已连线也完全不采纳,等同纯 Unmult
想对比「有无 AI 介入」的差别时拨开关即可,不必拔线。
兼容旧英文名 use_subject_mask。

顺带把黑点/白点 tooltip 里的「(主体保护)」字样去掉 —— 那是描述
参数作用的措辞,和新开关撞名会让人以为是同一功能。

实测(黑底 + 暗色实体,alpha 真值恒为 1,下半近纯黑):
  不接 mask              暗部 alpha 0.080  ← 黑衣黑发被扣穿
  接 mask + 保护启用      暗部 alpha 1.000
  接 mask + 保护关闭      暗部 alpha 0.080,与不接 mask 逐元素一致
即开关确实起作用,且关闭时行为与纯 Unmult 完全等价。

这同时解掉了上次记录的适用边界:黑底 unmult 本质是拿亮度当不透明度,
对实体不成立;现在接一路语义抠图的 alpha 进 subject_mask 即可破解 ——
语义模型负责「哪里是主体」,Unmult 负责「边缘有多透」。README 已按此改写。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rui40000
2026-08-18 15:44:32 +08:00
co-authored by Claude Opus 4.8
parent adb1de6329
commit a1f52de801
2 changed files with 36 additions and 13 deletions
+16 -10
View File
@@ -1203,6 +1203,8 @@ Unity/Godot 归一化 pivot(左下为原点):(0.5411, 0.0699)
- `bg_color` (STRING): 要去除的背景色,`#RRGGBB`。常用 `#000000` / `#FFFFFF` / `#00FF00` / `#FF00FF`
- `黑点` (FLOAT, 滑块): 低于此值的 alpha 归零,用于清除背景残留噪点。调太高会丢边缘细节
- `白点` (FLOAT, 滑块): 高于此值的 alpha 归一,用于让主体更实。调太低会让边缘硬化
- `主体保护` (BOOLEAN): 是否采纳 `subject_mask`。**关闭时即便已连线也完全不采纳**,等同纯 Unmult——想对比「有无 AI 介入」时拨这个开关即可,不必拔线
- `subject_mask` (MASK, 可选): 接抠图节点输出的 alpha,节点执行 `max(unmult_α, subject_mask)` 合并
后两项用中文参数名 + `display: slider`,界面上就是两条滑块,与 LayerStyle 的 BiRefNet Ultra 观感一致。函数内部用 `**kwargs` 接收(中文名不能直接做函数形参),并兼容旧的 `alpha_low`/`alpha_high` 调用。
@@ -1218,20 +1220,24 @@ Unity/Godot 归一化 pivot(左下为原点):(0.5411, 0.0699)
数学上是精确解,三种底色都能完美还原。
**⚠ 适用边界(很重要,选错底色会翻车)**:
**⚠ 适用边界与破解办法**:
| 素材 | 结果 |
|:-----|:-----|
| 光效、火焰、烟雾、粒子、UI 特效 | ✅ 最佳选择,半透明层次完整保留 |
| 绿幕/品红等**与素材反差大**的底色 | ✅ 实体素材也能扣干净 |
| **黑底 + 暗色实体** | ❌ **暗部会被当成背景扣掉** |
| 素材 | 纯 Unmult | 接 `subject_mask` 后 |
|:-----|:---------|:--------------------|
| 光效、火焰、烟雾、粒子、UI 特效 | ✅ 最佳选择,半透明层次完整保留 | 一般不需要 |
| 绿幕/品红等**与素材反差大**的底色 | ✅ 实体素材也能扣干净 | 一般不需要 |
| **黑底 + 暗色实体** | ❌ **暗部会被当成背景扣掉** | ✅ **已解决** |
实测同一个实体素材(alpha 真值恒为 1):
实测同一个实体素材(alpha 真值恒为 1,下半部分接近纯黑):
- 黑底:亮部还原 alpha 0.900,**暗部只有 0.080** —— 黑头发、深色衣服、鞋子会被扣穿
- 绿幕:亮部 0.900,**暗部 0.940** ✓ 正常
| 条件 | 暗部还原 alpha |
|:-----|-------------:|
| 黑底,不接 mask | **0.080** ← 黑头发、深色衣服、鞋子被扣穿 |
| 黑底,接 mask 且「主体保护」启用 | **1.000** ✅ |
| 黑底,接 mask 但「主体保护」关闭 | 0.080(与不接完全一致,开关确实生效)|
| 绿幕,不接 mask | 0.940 ✓ 本来就正常 |
原因是黑底 unmult 本质在用"亮度当不透明度",这对发光物成立,对实体不成立。**所以黑底的角色素材请用 [Lucida](#23-lucida-抠图--lucida-matting) 或 [FeyNobg](#22-feynobg-抠图--feynobg-matting) 这类语义抠图**,unmult 留给光效。
根因是黑底 unmult 本质在用"亮度当不透明度",这对发光物成立、对实体不成立。**破法是接一路语义抠图(Lucida / FeyNobg / BiRefNet)的 alpha 进 `subject_mask`**:主体区域强制不透明,主体之外仍走 Unmult 的精确半透明。两者各取所长——语义模型负责"哪里是主体",Unmult 负责"边缘有多透"。
**四个抠图节点怎么选**:
+20 -3
View File
@@ -11,6 +11,7 @@
AI 主体保护(可选):接入 BiRefNet / FeyNobg 等抠图节点输出的 subject_mask,
用 max(unmult_α, subject_mask) 合并,防止主体中与背景色相近的区域被误判为透明。
该合并由「主体保护」开关控制,关闭时即便连了 mask 也不采纳。
典型场景:黑底光效中人物穿黑衣 → 纯 Unmult 会让黑衣半透明,
接入主体 mask 后黑衣区域强制保留为不透明。
@@ -96,15 +97,27 @@ class RuiUnmult:
"黑点": ("FLOAT", {
"default": 0.0, "min": 0.0, "max": 0.5, "step": 0.01,
"display": "slider",
"tooltip": "黑点(主体保护):低于此值的 alpha 强制归零。\n"
"tooltip": "低于此值的 alpha 强制归零。\n"
"调高可清除背景残留噪点,但过高会丢失边缘细节。"
}),
"白点": ("FLOAT", {
"default": 1.0, "min": 0.3, "max": 1.0, "step": 0.01,
"display": "slider",
"tooltip": "白点(主体保护):高于此值的 alpha 强制归一。\n"
"tooltip": "高于此值的 alpha 强制归一。\n"
"调低可让主体更实、减少半透明损失,但过低会让边缘硬化。"
}),
"主体保护": ("BOOLEAN", {
"default": True,
"label_on": "启用",
"label_off": "关闭",
"tooltip": "是否采纳下方接入的 subject_mask。\n\n"
"启用:alpha 取 max(unmult 结果, subject_mask),\n"
" 主体区域强制不透明,光效边缘仍保留半透明。\n"
" 黑底素材里的黑衣、黑发全靠它保住。\n\n"
"关闭:即便已经连了 subject_mask 也完全不采纳,\n"
" 等同于纯 Unmult。想对比「有无 AI 介入」的差别时,\n"
" 拨这个开关即可,不必拔线。"
}),
},
"optional": {
"subject_mask": ("MASK", {
@@ -114,7 +127,7 @@ class RuiUnmult:
"主体内部强制不透明,光效边缘保留半透明。\n\n"
"典型场景:黑底光效中人物穿黑衣 →\n"
"纯 Unmult 会让黑衣半透明,接入主体 mask 后黑衣保留。\n\n"
"不连接时等同于纯 Unmult,无 AI 介入。"
"不连接、或上方「主体保护」关闭时,等同于纯 Unmult,无 AI 介入。"
}),
},
}
@@ -129,7 +142,11 @@ class RuiUnmult:
bg_color = kwargs.get("bg_color", "#000000")
alpha_low = kwargs.get("黑点", kwargs.get("alpha_low", 0.0))
alpha_high = kwargs.get("白点", kwargs.get("alpha_high", 1.0))
# 主体保护开关:关闭时彻底忽略 subject_mask,哪怕上游已经连线
use_subject = kwargs.get("主体保护", kwargs.get("use_subject_mask", True))
subject_mask_tensor = kwargs.get("subject_mask", None)
if not use_subject:
subject_mask_tensor = None
bg_rgb = _hex_to_rgb01(bg_color)