diff --git a/README.md b/README.md index 811e679..307ab22 100644 --- a/README.md +++ b/README.md @@ -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()`,保证离线可用。 diff --git a/example_workflow/FeyNobg抠图.json b/example_workflow/FeyNobg抠图.json index d70126a..d02a199 100644 --- a/example_workflow/FeyNobg抠图.json +++ b/example_workflow/FeyNobg抠图.json @@ -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" diff --git a/feynobg/birefnet/image_processing_birefnet.py b/feynobg/birefnet/image_processing_birefnet.py index d61acf6..aeb283b 100644 --- a/feynobg/birefnet/image_processing_birefnet.py +++ b/feynobg/birefnet/image_processing_birefnet.py @@ -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)} 张图" diff --git a/feynobg_node.py b/feynobg_node.py index 11abed4..81970d1 100644 --- a/feynobg_node.py +++ b/feynobg_node.py @@ -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