Files
rui40000-RUI-Nodes/unmult_node.py
T
rui40000andClaude Opus 4.8 bc433bc24a 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>
2026-08-17 14:10:04 +08:00

135 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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",
}