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:
co-authored by
Claude Opus 4.8
parent
cea14ef327
commit
bc433bc24a
@@ -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 用户提供实用、高效的节点工具集。🐶 是我们的项目标志,代表着忠诚、友好和可靠。
|
||||
|
||||
@@ -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
@@ -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",
|
||||
}
|
||||
Reference in New Issue
Block a user