feat: FeyNobg 抠图开放半透明控制与长图形变补救参数
针对「主体整片半透明发灰」的实际反馈,模型侧无可调参数 (架构参数训练时已固化),故在后处理层开放: - alpha_threshold / alpha_softness:以阈值为中心、柔和度为宽度 取一段区间线性拉伸到 [0,1],压掉灰雾同时保留边缘过渡。 默认 (0.5, 1.0) 区间恰为 [0,1],是恒等变换,行为与之前逐位一致; 实测 (0.35, 0.3) 半透明像素占比 1.49% -> 0.31%,主体均值几乎不变 - keep_aspect_ratio:模型固定吃 1024 方图,长图默认被拉伸压扁。 开启后改为等比缩放 + 边缘延展补边(不填黑,避免造出假轮廓), 推理后按有效区域裁掉补边 同步更新示例工作流的 widgets_values(widget 由 5 个增至 8 个, 不同步会导致参数错位)与 Note 说明。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
01a26baea9
commit
be93fd36a3
@@ -865,6 +865,9 @@ WAS Node Suite 的「Text Multiline」会把 `#` 开头的行**当注释删除**
|
||||
- `resolution` (选择): 推理分辨率,默认 **1024**(模型原生训练分辨率)。调低省显存但边缘变粗;调高不一定更好,可能出现结构断裂
|
||||
- `precision` (选择): `fp32`(默认)/ `fp16`。**实测两者输出一致**(同图 alpha 均值均为 0.657),fp16 显存减半且明显更快,推荐优先用 fp16
|
||||
- `device` (选择): `auto` / `cpu`
|
||||
- `alpha_threshold` (FLOAT, 可选): 前景判定阈值,默认 0.5。见下方「主体半透明发灰怎么救」
|
||||
- `alpha_softness` (FLOAT, 可选): 阈值两侧过渡带宽度,默认 **1.0 = 完全不处理**
|
||||
- `keep_aspect_ratio` (BOOLEAN, 可选): 保持宽高比(等比缩放 + 边缘延展补边),默认关闭
|
||||
- `invert_mask` (BOOLEAN, 可选): 反转 alpha,默认前景为白
|
||||
|
||||
**输出**:
|
||||
@@ -881,6 +884,24 @@ WAS Node Suite 的「Text Multiline」会把 `#` 开头的行**当注释删除**
|
||||
|
||||
发丝、飘带、细链条等高频细节均能完整分离,边缘为自然的半透明过渡而非硬边。
|
||||
|
||||
**主体「整片半透明发灰」怎么救**:
|
||||
|
||||
模型对拿不准的区域会输出 0.5 上下的中间值,表现为整个人物/物体呈半透明。模型本身**没有**开放任何控制该行为的参数(`use_gradient_attention` 等是训练时固化的架构参数,推理期不可调),因此节点在后处理层提供了一对色阶参数:
|
||||
|
||||
| alpha_threshold | alpha_softness | 效果 |
|
||||
|:---------------:|:--------------:|:-----|
|
||||
| 0.5 | 1.0 | **默认**,原样输出,一个像素都不动 |
|
||||
| 0.35 | 0.3 | **推荐**,半透明像素占比 1.49% → 0.31%(降 79%),主体均值几乎不变 |
|
||||
| 0.5 | 0.0 | 硬二值化,锯齿硬边,抠头发/玻璃慎用 |
|
||||
|
||||
原理是以 `threshold` 为中心、`softness` 为宽度取一段区间线性拉伸到 [0,1]:区间以下压成全透明,以上提成全不透明,区间内保留平滑过渡。默认参数下该区间恰好是 [0,1],等于恒等变换。
|
||||
|
||||
两点边界必须说明:
|
||||
- **只对已有一定响应的区域有效**。模型压根没认出来的地方 alpha 接近 0,再降阈值也救不回来——那属于语义判断差异(BiRefNet-General 倾向保留画面全部前景,FeyNobg 更强调「找主要主体」),需要换模型或改用 SDMatte 指定目标。
|
||||
- `softness` 越小,发丝等**真实**半透明细节损失越多,是一对权衡。
|
||||
|
||||
**长图形变**:模型固定吃 1024×1024,默认把图直接拉伸成正方形(与官方训练方式一致)。手机截图这类 1:2 以上的长图横向会被压到一半,可开 `keep_aspect_ratio` 改为等比缩放 + 边缘延展补边、推理后裁掉补边。实测 2.36:1 的图半透明占比 0.1192 → 0.1041。该选项与训练分布不同,属试验性,常规比例建议保持关闭。
|
||||
|
||||
**实现说明**(两个坑,都已在节点内处理):
|
||||
|
||||
1. **预处理依赖**:上游 `nobg` 的预处理模块继承 `transformers>=5.4` 的 `TorchvisionBackend`,而 ComfyUI 常见环境仍是 transformers 4.x,直接引入会报 `No module named 'transformers.image_processing_backends'`。本节点内嵌了 nobg 推理子集(`feynobg/`)并**重写了预处理**,数值规格与官方逐项对齐(1024 双线性抗锯齿缩放 + ImageNet 标准化;后处理先 sigmoid 再缩放),**无需升级 transformers**。同时绕开了上游 `AutoModel` 里会联网查 tags 的 `model_info()`,保证离线可用。
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"id": 2,
|
||||
"type": "RuiFeyNobg",
|
||||
"pos": [-1240, 400],
|
||||
"size": [320, 170],
|
||||
"size": [340, 260],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
@@ -66,7 +66,7 @@
|
||||
"aux_id": "rui40000/Ruinode",
|
||||
"Node name for S&R": "RuiFeyNobg"
|
||||
},
|
||||
"widgets_values": ["FeyNobg", 1024, "fp16", "auto", false],
|
||||
"widgets_values": ["FeyNobg", 1024, "fp16", "auto", 0.5, 1, false, false],
|
||||
"color": "#232",
|
||||
"bgcolor": "#353",
|
||||
"title": "② FeyNobg 抠图(全自动,无需提示)"
|
||||
@@ -247,7 +247,7 @@
|
||||
"outputs": [],
|
||||
"properties": {},
|
||||
"widgets_values": [
|
||||
"FeyNobg 抠图 —— 使用要点\n\n【模型】首次运行会自动下载到 ComfyUI/models/nobg/FeyNobg(约 1.05GB)。\n 也可手动下载 https://huggingface.co/feyninc/FeyNobg 的三个文件放进去:\n config.json / preprocessor_config.json / model.safetensors\n\n【precision】推荐 fp16\n 实测 fp16 与 fp32 输出完全一致(同图 alpha 均值都是 0.657),\n 但 fp16 显存减半、速度快数倍。默认给 fp32 只是为老显卡留退路。\n\n【resolution】保持 1024\n 这是模型原生训练分辨率。降到 768 差别很小(前景占比 0.656 vs 0.659)\n 且更省显存;调高不一定更好,可能出现结构断裂。\n\n【适用范围】画面主体明确时效果最好\n 实测发丝、飘带、细链条等高频细节都能完整分离,边缘是自然的半透明过渡。\n 但它是「找出画面主体」的模型:给一张没有明确主体的图(例如 UI 截图、\n 纯风景),输出接近全黑是正常反应,不是故障。\n\n【与 SDMatte 的分工】\n · FeyNobg:全自动、快,适合批量去背景 —— 主体明确时首选\n · SDMatte:需要框/掩码提示指定目标,适合画面里有多个主体、只抠其中一个\n\n【透明 PNG】cutout 输出是黑底 RGB。要透明背景需走下面这条链路:\n alpha → InvertMask → JoinImageWithAlpha(该节点内部会再反一次,故须先反相)\n\n【invert_mask】节点自带的反转开关只影响 alpha 本身,\n 与上面为 JoinImageWithAlpha 做的反相是两回事,别混用。"
|
||||
"FeyNobg 抠图 —— 使用要点\n\n【模型】首次运行会自动下载到 ComfyUI/models/nobg/FeyNobg(约 1.05GB)。\n 也可手动下载 https://huggingface.co/feyninc/FeyNobg 的三个文件放进去:\n config.json / preprocessor_config.json / model.safetensors\n\n【precision】推荐 fp16\n 实测 fp16 与 fp32 输出完全一致,但显存减半、速度快数倍。\n 默认给 fp32 只是为老显卡留退路。\n\n【resolution】保持 1024,这是模型原生训练分辨率。\n\n■ 主体「整片半透明发灰」怎么救 ————————————————\n\n模型对拿不准的区域会输出 0.5 上下的中间值,表现为整个人物/物体半透明。\n用下面两个参数拉一次色阶即可:\n\n【alpha_threshold】多大置信度算前景,默认 0.5\n【alpha_softness】过渡带宽度,默认 1.0 = 完全不处理\n\n · 默认 (0.5, 1.0) —— 原样输出,一个像素都不动\n · 推荐 (0.35, 0.3) —— 实测半透明像素占比从 1.49% 降到 0.31%,\n 主体均值几乎不变,即只压灰雾、不吃主体\n · 硬边 (0.5, 0.0) —— 直接二值化,锯齿硬边,抠头发/玻璃慎用\n\n ⚠ 只对已有一定响应的区域有效。模型压根没认出来的地方 alpha 接近 0,\n 再降阈值也救不回来 —— 那属于语义判断差异,得换模型或改用 SDMatte 指定目标。\n ⚠ softness 越小,发丝等真实半透明细节损失越多,是一对权衡。\n\n【keep_aspect_ratio】长图形变的补救,默认关闭\n 模型固定吃 1024×1024,默认把图直接拉伸成正方形(与官方训练一致)。\n 手机截图这类 1:2 以上的长图会被压得很扁,可开启此项改为等比缩放 + 补边。\n 属试验性选项(与训练分布不同),常规比例建议保持关闭。\n\n【适用范围】它是「找出画面主体」的模型\n 发丝、飘带、细链条等高频细节都能完整分离。但给一张没有明确主体的图\n (UI 截图、纯风景),输出接近全黑是正常反应,不是故障。\n\n【与 SDMatte 的分工】\n · FeyNobg:全自动、快,适合批量去背景 —— 主体明确时首选\n · SDMatte:需要框/掩码提示指定目标,适合画面里有多个主体、只抠其中一个\n\n【透明 PNG】cutout 输出是黑底 RGB。要透明背景需走下面这条链路:\n alpha → InvertMask → JoinImageWithAlpha(该节点内部会再反一次,故须先反相)\n\n【invert_mask】节点自带的反转开关只影响 alpha 本身,\n 与上面为 JoinImageWithAlpha 做的反相是两回事,别混用。"
|
||||
],
|
||||
"color": "#432",
|
||||
"bgcolor": "#653"
|
||||
|
||||
@@ -65,25 +65,47 @@ class BiRefNetImageProcessor:
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------- 预处理
|
||||
def preprocess_tensor(self, images, device=None, dtype=torch.float32):
|
||||
def preprocess_tensor(self, images, device=None, dtype=torch.float32,
|
||||
keep_aspect=False):
|
||||
"""
|
||||
ComfyUI 原生张量入口,避免 PIL 往返。
|
||||
|
||||
images: (B, H, W, 3) float,值域 [0,1](ComfyUI 的 IMAGE 约定)
|
||||
返回: (B, 3, size_h, size_w),已 ImageNet 标准化
|
||||
keep_aspect: False 走官方路径,直接拉伸成正方形(与训练一致);
|
||||
True 则按长边等比缩放后补边,保留原始比例。
|
||||
|
||||
返回 (pixel_values, meta):
|
||||
pixel_values (B, 3, size_h, size_w),已 ImageNet 标准化
|
||||
meta keep_aspect 时为有效区域 (nh, nw),供后处理裁掉补边;否则 None
|
||||
"""
|
||||
x = images.permute(0, 3, 1, 2).contiguous().float() # BHWC -> BCHW
|
||||
# 官方走 torchvision 的 resize,对张量默认开抗锯齿;缩小到 1024 时
|
||||
# 是否抗锯齿对细边缘影响可见,这里保持一致
|
||||
x = F.interpolate(
|
||||
x, size=(self.size["height"], self.size["width"]),
|
||||
mode="bilinear", align_corners=False, antialias=True,
|
||||
)
|
||||
th, tw = self.size["height"], self.size["width"]
|
||||
meta = None
|
||||
|
||||
if keep_aspect:
|
||||
_, _, H, W = x.shape
|
||||
scale = min(th / H, tw / W)
|
||||
nh = max(1, min(th, int(round(H * scale))))
|
||||
nw = max(1, min(tw, int(round(W * scale))))
|
||||
x = F.interpolate(x, size=(nh, nw), mode="bilinear",
|
||||
align_corners=False, antialias=True)
|
||||
# 补边放在右/下侧,用边缘像素延展而非填黑:填黑会凭空造出一条
|
||||
# 高对比直边,模型容易把它当成物体轮廓
|
||||
if nh < th or nw < tw:
|
||||
x = F.pad(x, (0, tw - nw, 0, th - nh), mode="replicate")
|
||||
meta = (nh, nw)
|
||||
else:
|
||||
# 官方走 torchvision 的 resize,对张量默认开抗锯齿;缩小到 1024 时
|
||||
# 是否抗锯齿对细边缘影响可见,这里保持一致
|
||||
x = F.interpolate(x, size=(th, tw), mode="bilinear",
|
||||
align_corners=False, antialias=True)
|
||||
|
||||
mean = torch.tensor(self.image_mean, dtype=torch.float32).view(1, 3, 1, 1)
|
||||
std = torch.tensor(self.image_std, dtype=torch.float32).view(1, 3, 1, 1)
|
||||
x = (x - mean) / std
|
||||
return x.to(device=device, dtype=dtype) if device is not None \
|
||||
x = x.to(device=device, dtype=dtype) if device is not None \
|
||||
else x.to(dtype=dtype)
|
||||
return x, meta
|
||||
|
||||
def __call__(self, images, return_tensors="pt", **kwargs):
|
||||
"""PIL 入口,兼容官方 README 的用法:processor(image, return_tensors='pt')。"""
|
||||
@@ -97,17 +119,21 @@ class BiRefNetImageProcessor:
|
||||
im = im.convert("RGB")
|
||||
arrs.append(np.asarray(im, dtype=np.float32) * self.rescale_factor)
|
||||
batch = torch.from_numpy(np.stack(arrs)) # (B,H,W,3) in [0,1]
|
||||
return {"pixel_values": self.preprocess_tensor(batch)}
|
||||
return {"pixel_values": self.preprocess_tensor(batch)[0]}
|
||||
|
||||
# ---------------------------------------------------------------- 后处理
|
||||
def post_process_alpha_matting(self, outputs, target_sizes=None):
|
||||
def post_process_alpha_matting(self, outputs, target_sizes=None, crop=None):
|
||||
"""
|
||||
原始 logits -> 每图 alpha([0,1],形状 (H, W))。
|
||||
|
||||
outputs: 含 "logits" 的 dict 或带 .logits 的对象,logits 形状 (B,1,H,W)
|
||||
target_sizes: [(h, w), ...],逐图缩放回原尺寸
|
||||
crop: 预处理若做过等比补边,这里传 (nh, nw) 把补出来的部分裁掉
|
||||
"""
|
||||
logits = outputs["logits"] if isinstance(outputs, dict) else outputs.logits
|
||||
if crop is not None:
|
||||
nh, nw = crop
|
||||
logits = logits[:, :, :nh, :nw]
|
||||
if target_sizes is not None and len(logits) != len(target_sizes):
|
||||
raise ValueError(
|
||||
f"给了 {len(target_sizes)} 个目标尺寸,但批次里有 {len(logits)} 张图"
|
||||
|
||||
+48
-4
@@ -228,6 +228,29 @@ class RuiFeyNobg:
|
||||
"device": (["auto", "cpu"], {"default": "auto"}),
|
||||
},
|
||||
"optional": {
|
||||
"alpha_threshold": ("FLOAT", {
|
||||
"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"tooltip": "多大置信度才算前景。模型对拿不准的区域会输出 0.5 上下的\n"
|
||||
"中间值,表现为「整片主体半透明发灰」。\n"
|
||||
"把阈值调低(如 0.3)可把这类区域拉回不透明。\n"
|
||||
"注意:只对已有一定响应的区域有效;模型压根没认出来的\n"
|
||||
"地方 alpha 接近 0,再降阈值也救不回来。"
|
||||
}),
|
||||
"alpha_softness": ("FLOAT", {
|
||||
"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01,
|
||||
"tooltip": "阈值两侧过渡带的宽度,决定边缘软硬。\n"
|
||||
"1.0 = 完全不处理,原样输出模型结果(默认)\n"
|
||||
"0.2~0.4 = 压掉灰雾但保留发丝级过渡(推荐从 0.3 试)\n"
|
||||
"0 = 硬二值化,边缘变成锯齿硬边,抠玻璃/头发慎用"
|
||||
}),
|
||||
"keep_aspect_ratio": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "模型固定吃 1024×1024,默认会把图直接拉伸成正方形\n"
|
||||
"(与官方训练方式一致)。长图/宽图形变严重时可开启此项,\n"
|
||||
"改为等比缩放 + 边缘延展补边,推理后再裁掉补边部分。\n"
|
||||
"注意这与训练分布不同,属于试验性选项:\n"
|
||||
"极端长宽比(如手机截图 1:2 以上)通常有改善,常规比例建议关闭。"
|
||||
}),
|
||||
"invert_mask": ("BOOLEAN", {
|
||||
"default": False,
|
||||
"tooltip": "反转 alpha:默认前景为白(1),开启后前景为黑。"
|
||||
@@ -245,8 +268,26 @@ class RuiFeyNobg:
|
||||
"""模型列表随目录变化,宽松放行,运行时兜底(含自动下载)。"""
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _remap_alpha(alpha, threshold, softness):
|
||||
"""
|
||||
按阈值 / 柔和度重新映射 alpha,相当于给遮罩拉一次色阶。
|
||||
|
||||
以 threshold 为中心、softness 为宽度取一段区间线性拉伸到 [0,1]:
|
||||
区间以下压成全透明,以上提成全不透明,区间内保留平滑过渡。
|
||||
默认 (0.5, 1.0) 时区间恰好是 [0,1],等于原样返回。
|
||||
"""
|
||||
low = threshold - softness / 2.0
|
||||
high = threshold + softness / 2.0
|
||||
if high <= low: # softness=0:硬二值化
|
||||
return (alpha >= threshold).to(alpha.dtype)
|
||||
if abs(low) < 1e-6 and abs(high - 1.0) < 1e-6:
|
||||
return alpha # 默认参数,一个像素都不动
|
||||
return ((alpha - low) / (high - low)).clamp(0.0, 1.0)
|
||||
|
||||
def matting(self, image, model_name, resolution, precision, device,
|
||||
invert_mask=False):
|
||||
alpha_threshold=0.5, alpha_softness=1.0,
|
||||
keep_aspect_ratio=False, invert_mask=False):
|
||||
import comfy.model_management
|
||||
|
||||
from .feynobg import BiRefNetImageProcessor
|
||||
@@ -274,18 +315,21 @@ class RuiFeyNobg:
|
||||
alphas = []
|
||||
for i in range(B):
|
||||
# 逐张推理:1024 分辨率下 Swin-Large 峰值显存不低,整批一次容易 OOM
|
||||
pixel_values = proc.preprocess_tensor(
|
||||
image[i:i + 1], device=dev, dtype=dtype)
|
||||
pixel_values, meta = proc.preprocess_tensor(
|
||||
image[i:i + 1], device=dev, dtype=dtype,
|
||||
keep_aspect=bool(keep_aspect_ratio))
|
||||
with torch.no_grad():
|
||||
outputs = model(pixel_values=pixel_values)
|
||||
# fp16 推理出的 logits 先转回 fp32 再 sigmoid/缩放,避免精度损失
|
||||
if isinstance(outputs, dict):
|
||||
outputs = {"logits": outputs["logits"].float()}
|
||||
alpha = proc.post_process_alpha_matting(
|
||||
outputs, target_sizes=[(H, W)])[0]
|
||||
outputs, target_sizes=[(H, W)], crop=meta)[0]
|
||||
alphas.append(alpha.clamp(0, 1).cpu())
|
||||
|
||||
alpha = torch.stack(alphas) # [B,H,W]
|
||||
alpha = self._remap_alpha(alpha, float(alpha_threshold),
|
||||
float(alpha_softness))
|
||||
if invert_mask:
|
||||
alpha = 1.0 - alpha
|
||||
|
||||
|
||||
Reference in New Issue
Block a user