docs: 核实 Unmult 节点并补充 README(含实测适用边界)

节点为纯数学去底(等效 AE Unmult):纯色底合成图满足
C = aF + (1-a)B,背景色已知即可反解出前景色与 alpha,
无需模型推理,且能真实保留半透明层次。

核实结果:
- 已正确注册进 __init__.py(无外部依赖,裸 import 与仓库惯例一致)
- 4 个参数均带中文 tooltip,符合本仓库的统一规则
- 分类 Rui-Node/抠图 与其余抠图节点一致,支持批量

验证(构造已知合成图反推真值):
- 发光素材在黑底/白底/绿幕下,alpha 与前景色平均误差均为 0.0000,
  数学上是精确解
- 黑白点参数、批量输入、RGBA/MASK 双输出均正常

同时测出一条必须写进文档的适用边界:
黑底 unmult 本质是拿亮度当不透明度,这对发光物成立、对实体不成立。
同一实体素材(alpha 真值恒为 1)实测——
  黑底:亮部 0.900,暗部仅 0.080(黑头发/深色衣服/鞋会被扣穿)
  绿幕:亮部 0.900,暗部 0.940,正常
故黑底的角色素材仍应走 Lucida / FeyNobg 等语义抠图,
unmult 留给光效、火焰、粒子与绿幕素材。README 补了四个抠图节点的选型表。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rui40000
2026-08-17 14:10:04 +08:00
co-authored by Claude Opus 4.8
parent cea14ef327
commit bc433bc24a
3 changed files with 191 additions and 0 deletions
+52
View File
@@ -19,6 +19,7 @@ Rui-Node🐶 是一个功能丰富的 ComfyUI 节点集合,提供图像处理
- [满屏文字水印 / Full-Screen Text Watermark](#21-满屏文字水印--full-screen-text-watermark)
- [像素化 / Pixelate](#24-像素化--pixelate)
- [八方向序列拆分 / 8-Direction Sprite Split](#25-八方向序列拆分--8-direction-sprite-split)
- [半透明抠图 / Unmult Matting](#27-半透明抠图--unmult-matting)
### 📁 文件存储与加载类
- [按路径加载图像 / Load Image By Path](#3-按路径加载图像--load-image-by-path)
@@ -1190,6 +1191,57 @@ Unity/Godot 归一化 pivot(左下为原点):(0.5411, 0.0699)
---
### 27. 半透明抠图 / Unmult Matting
**分类**: `Rui-Node🐶/抠图✂️`
**功能描述**:
纯数学去底,等效 After Effects 的 Unmult。纯色背景上的合成图满足 `C = αF + (1-α)B`,背景色 B 已知时可**反解**出前景色 F 与透明度 α。不需要模型推理,速度快、结果是精确解,且能真实保留半透明层次——**这是语义抠图模型给不了的**。
**输入参数**:
- `image` (IMAGE): 待去底图像,支持批量(序列帧/视频帧逐帧处理)
- `bg_color` (STRING): 要去除的背景色,`#RRGGBB`。常用 `#000000` / `#FFFFFF` / `#00FF00` / `#FF00FF`
- `alpha_low` (FLOAT): 黑点,低于此值的 alpha 归零,用于清除背景残留噪点
- `alpha_high` (FLOAT): 白点,高于此值的 alpha 归一,用于让主体更实
**输出**: `rgba_image` (IMAGE,4 通道) / `alpha` (MASK)
**实测(构造已知合成图反推,验证还原精度)**:
| 场景 | alpha 平均误差 | 前景色平均误差 |
|:-----|-------------:|-------------:|
| 发光素材 @ 黑底 | **0.0000** | **0.0000** |
| 发光素材 @ 白底 | **0.0000** | **0.0000** |
| 发光素材 @ 绿幕 | **0.0000** | **0.0000** |
数学上是精确解,三种底色都能完美还原。
**⚠ 适用边界(很重要,选错底色会翻车)**:
| 素材 | 结果 |
|:-----|:-----|
| 光效、火焰、烟雾、粒子、UI 特效 | ✅ 最佳选择,半透明层次完整保留 |
| 绿幕/品红等**与素材反差大**的底色 | ✅ 实体素材也能扣干净 |
| **黑底 + 暗色实体** | ❌ **暗部会被当成背景扣掉** |
实测同一个实体素材(alpha 真值恒为 1):
- 黑底:亮部还原 alpha 0.900,**暗部只有 0.080** —— 黑头发、深色衣服、鞋子会被扣穿
- 绿幕:亮部 0.900,**暗部 0.940** ✓ 正常
原因是黑底 unmult 本质在用"亮度当不透明度",这对发光物成立,对实体不成立。**所以黑底的角色素材请用 [Lucida](#23-lucida-抠图--lucida-matting) 或 [FeyNobg](#22-feynobg-抠图--feynobg-matting) 这类语义抠图**,unmult 留给光效。
**四个抠图节点怎么选**:
| 节点 | 原理 | 适用 |
|:-----|:-----|:-----|
| **Unmult** | 纯数学反解 | 纯色底的光效/火焰/粒子;绿幕素材 |
| **Lucida** | 语义模型 | 文字/Logo、插画、玻璃、伪装物体 |
| **FeyNobg** | 语义模型 | 常规主体照片,要求背景剥离干净 |
| **SDMatte** | 语义模型 + 提示 | 画面里多个主体、只抠其中一个 |
---
## 🐕 关于 Rui-Node🐶
Rui-Node🐶 致力于为 ComfyUI 用户提供实用、高效的节点工具集。🐶 是我们的项目标志,代表着忠诚、友好和可靠。
+5
View File
@@ -101,6 +101,9 @@ except Exception as _e:
print(f"[Ruinode] Lucida 抠图 节点未加载:{_e}")
LUCIDA_NODE_CLASS_MAPPINGS = {}
LUCIDA_NODE_DISPLAY_NAME_MAPPINGS = {}
# 新增:半透明抠图节点(Unmult 纯数学去底,适合光效/火焰/粒子/UI 特效)
from .unmult_node import NODE_CLASS_MAPPINGS as UNMULT_NODE_CLASS_MAPPINGS
from .unmult_node import NODE_DISPLAY_NAME_MAPPINGS as UNMULT_NODE_DISPLAY_NAME_MAPPINGS
# 新增:满屏文字水印节点(平铺文字 + 旋转/密度/透明度/颜色可调)
try:
from .watermark_node import NODE_CLASS_MAPPINGS as WATERMARK_NODE_CLASS_MAPPINGS
@@ -138,6 +141,7 @@ NODE_CLASS_MAPPINGS.update(FEYNOBG_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(LUCIDA_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(PIXELATE_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(EIGHTDIR_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(UNMULT_NODE_CLASS_MAPPINGS)
# 合并节点显示名称映射
NODE_DISPLAY_NAME_MAPPINGS = {}
@@ -167,5 +171,6 @@ NODE_DISPLAY_NAME_MAPPINGS.update(FEYNOBG_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(LUCIDA_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(PIXELATE_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(EIGHTDIR_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(UNMULT_NODE_DISPLAY_NAME_MAPPINGS)
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
+134
View File
@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
"""
半透明抠图节点(Unmult)—— Ruinode
==================================
纯数学去底算法,等效 AE Unmult 效果。
适用于渲染在纯色底上的光效、火焰、烟雾、粒子、UI 特效等半透明素材。
原理:纯色背景合成图满足 C = αF + (1-α)B,
其中 B 为已知背景色。通过各通道与背景色的差异反推 α 和前景色 F。
支持任意背景色(黑/白/绿幕/自定义),纯数学变换,无模型推理。
支持批量输入(序列帧/视频帧),逐帧处理后堆叠输出。
"""
import numpy as np
import torch
def _hex_to_rgb01(hex_str: str) -> tuple:
"""将 #RRGGBB 格式的颜色字符串转为 (r, g, b) 浮点元组,值域 [0,1]。"""
h = hex_str.strip().lstrip("#")
if len(h) == 3:
h = h[0] * 2 + h[1] * 2 + h[2] * 2
if len(h) != 6:
raise ValueError(f"无效的颜色格式:{hex_str},需要 #RRGGBB")
r = int(h[0:2], 16) / 255.0
g = int(h[2:4], 16) / 255.0
b = int(h[4:6], 16) / 255.0
return (r, g, b)
def _unmult_frame(rgb: np.ndarray, bg_color: tuple,
alpha_low: float, alpha_high: float,
epsilon: float = 1e-6) -> tuple:
"""对单帧图像执行 Unmult 去底。
参数:
rgb: float32 [H,W,3] 值域 [0,1]
bg_color: (R,G,B) 值域 [0,1]
alpha_low: 黑点(低于此值的 alpha 映射为 0)
alpha_high: 白点(高于此值的 alpha 映射为 1)
返回:
(foreground [H,W,3], alpha [H,W]) 均 float32
"""
bg = np.array(bg_color, dtype=np.float32).reshape(1, 1, 3)
diff = rgb.astype(np.float32) - bg
scale = np.array([max(bg_color[c], 1.0 - bg_color[c], epsilon)
for c in range(3)], dtype=np.float32).reshape(1, 1, 3)
norm_diff = np.abs(diff) / scale
alpha = np.max(norm_diff, axis=-1)
alpha = np.clip(alpha, 0.0, 1.0)
if alpha_low > 0.0 or alpha_high < 1.0:
span = max(alpha_high - alpha_low, epsilon)
alpha = np.clip((alpha - alpha_low) / span, 0.0, 1.0)
alpha_safe = np.maximum(alpha, epsilon)
foreground = bg + diff / alpha_safe[..., np.newaxis]
foreground = np.clip(foreground, 0.0, 1.0)
transparent = alpha < epsilon
foreground[transparent] = 0.0
return foreground.astype(np.float32), alpha.astype(np.float32)
class RuiUnmult:
"""半透明抠图(Unmult):指定背景色,纯数学去底,输出 RGBA 图像 + Alpha 蒙版。"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE", {
"tooltip": "待去底的图像,支持批量(序列帧/视频帧逐帧处理)。"
}),
"bg_color": ("STRING", {
"default": "#000000",
"tooltip": "要去除的背景色,#RRGGBB 格式。\n"
"常用值:#000000(黑底)、#FFFFFF(白底)、"
"#00FF00(绿幕)、#FF00FF(品红)。"
}),
"alpha_low": ("FLOAT", {
"default": 0.0, "min": 0.0, "max": 0.5, "step": 0.01,
"tooltip": "黑点(主体保护):低于此值的 alpha 强制归零。\n"
"调高可清除背景残留噪点,但过高会丢失边缘细节。"
}),
"alpha_high": ("FLOAT", {
"default": 1.0, "min": 0.3, "max": 1.0, "step": 0.01,
"tooltip": "白点(主体保护):高于此值的 alpha 强制归一。\n"
"调低可让主体更实、减少半透明损失,但过低会让边缘硬化。"
}),
},
}
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("rgba_image", "alpha")
FUNCTION = "unmult"
CATEGORY = "Rui-Node🐶/抠图✂️"
def unmult(self, image, bg_color, alpha_low, alpha_high):
bg_rgb = _hex_to_rgb01(bg_color)
B, H, W, C = image.shape
if C == 4:
image = image[..., :3]
elif C == 1:
image = image.repeat(1, 1, 1, 3)
fg_list = []
alpha_list = []
for i in range(B):
frame = image[i].cpu().numpy().astype(np.float32)
fg, a = _unmult_frame(frame, bg_rgb,
float(alpha_low), float(alpha_high))
rgba = np.concatenate([fg, a[..., np.newaxis]], axis=-1)
fg_list.append(torch.from_numpy(rgba))
alpha_list.append(torch.from_numpy(a))
rgba_out = torch.stack(fg_list) # [B, H, W, 4]
alpha_out = torch.stack(alpha_list) # [B, H, W]
return (rgba_out, alpha_out)
NODE_CLASS_MAPPINGS = {
"RuiUnmult": RuiUnmult,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RuiUnmult": "半透明抠图 / Unmult Matting",
}