diff --git a/README.ZH_CN.md b/README.ZH_CN.md index 6c5b25a..9ac52ec 100644 --- a/README.ZH_CN.md +++ b/README.ZH_CN.md @@ -21,6 +21,9 @@ git clone https://github.com/1hew/ComfyUI-1hewNodes ## 📜 更新日志 +**v3.29.1** +- feat(image):`Image Alpha Split` 的 `background_color` 新增 `sm` 智能背景模式;对每张带 alpha 的图片从黑、白和 `#808080` 中选择更利于主体及半透明边缘辨识的底色 + **v3.29.0** - feat(image_resize):新增 `Image Resize Area`,按面积保持原比例缩放:将 `√(宽 × 高)` 夹到可配置区间并对齐倍数,同步处理可选遮罩,兼容 `Image Resize Universal` 的 `fit` / `pad_color` 行为,并且只在小图时懒加载可选 `upscale_model` - fix(image):`Image Alpha Join` 在 `image` 输入为空(未连接或空批次)时改为输出空,不再输出透明画布;空批次的 `mask` 按未输入处理 diff --git a/README.md b/README.md index ed38e44..a74647e 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ git clone https://github.com/1hew/ComfyUI-1hewNodes ## 📜 Changelog +**v3.29.1** +- feat(image): Add `sm` smart-background mode to `Image Alpha Split` `background_color`; for each alpha image it selects black, white, or `#808080` to improve subject and translucent-edge visibility + **v3.29.0** - feat(image_resize): Add `Image Resize Area`, an aspect-preserving area normalizer: clamp and align `sqrt(width × height)` to configurable bounds, synchronize optional masks, provide Image Resize Universal-compatible `fit` / `pad_color` handling, and lazily use an optional `upscale_model` only for undersized inputs - fix(image): Make `Image Alpha Join` return an empty output whenever the `image` input is empty (unconnected or a zero-length batch) instead of a transparent canvas, and treat a zero-length `mask` batch as no mask diff --git a/nodes/image/image_alpha_split.py b/nodes/image/image_alpha_split.py index 460d9af..7989c0c 100644 --- a/nodes/image/image_alpha_split.py +++ b/nodes/image/image_alpha_split.py @@ -42,7 +42,8 @@ class ImageAlphaSplit(io.ComfyNode): if int(images.shape[0]) <= 0: return io.NodeOutput(None, None) - bg_rgb = torch.tensor( + smart_background = str(background_color).strip().lower() == "sm" + bg_rgb = None if smart_background else torch.tensor( cls.parse_color(background_color), dtype=torch.float32, device=images.device, @@ -51,7 +52,9 @@ class ImageAlphaSplit(io.ComfyNode): output_images: list[torch.Tensor] = [] output_masks: list[torch.Tensor] = [] for index in range(int(images.shape[0])): - flat_image, alpha_mask = cls._split_single(images[index], bg_rgb) + frame = images[index] + frame_bg_rgb = cls._choose_smart_background(frame) if smart_background else bg_rgb + flat_image, alpha_mask = cls._split_single(frame, frame_bg_rgb) output_images.append(flat_image) output_masks.append(alpha_mask) @@ -59,6 +62,51 @@ class ImageAlphaSplit(io.ComfyNode): result_masks = torch.stack(output_masks, dim=0).to(dtype=torch.float32, device=image.device) return io.NodeOutput(result_images, result_masks) + @classmethod + def _choose_smart_background(cls, frame: torch.Tensor) -> torch.Tensor: + """Choose black, white, or #808080 for a transparent image. + + Candidates are scored from the *composited* subject pixels, weighted by + opacity and alpha-boundary pixels so fine translucent outlines remain + visible. RGB-only input has no background to fill, so it uses white. + """ + current = frame.to(torch.float32).clamp(0.0, 1.0) + if current.ndim != 3 or int(current.shape[2]) < 4: + return torch.ones((1, 1, 3), dtype=torch.float32, device=current.device) + + alpha = current[:, :, 3] + if not torch.any(alpha > 0.0) or torch.all(alpha >= 1.0): + return torch.ones((1, 1, 3), dtype=torch.float32, device=current.device) + + rgb = current[:, :, :3] + # Give contour pixels extra influence without morphology dependencies. + alpha_pad = torch.nn.functional.pad(alpha.unsqueeze(0).unsqueeze(0), (1, 1, 1, 1), mode="replicate")[0, 0] + edge = torch.maximum( + (alpha - alpha_pad[1:-1, :-2]).abs(), + torch.maximum( + (alpha - alpha_pad[1:-1, 2:]).abs(), + torch.maximum((alpha - alpha_pad[:-2, 1:-1]).abs(), (alpha - alpha_pad[2:, 1:-1]).abs()), + ), + ) + weight = alpha * (1.0 + 2.0 * edge) + candidates = torch.tensor( + ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0), (128.0 / 255.0,) * 3), + dtype=torch.float32, + device=current.device, + ) + # Subject/background distance after alpha compositing. The 10th + # percentile prevents a rare color or thin stroke from being ignored. + scores: list[torch.Tensor] = [] + for candidate in candidates: + composite = rgb * alpha.unsqueeze(2) + candidate.view(1, 1, 3) * (1.0 - alpha.unsqueeze(2)) + distance = torch.sqrt(torch.sum((composite - candidate.view(1, 1, 3)) ** 2, dim=2)) + visible = distance[weight > 0.0] + visible_weight = weight[weight > 0.0] + average = torch.sum(visible * visible_weight) / torch.sum(visible_weight) + low_tail = torch.quantile(visible, 0.10) if visible.numel() > 1 else visible[0] + scores.append(0.75 * average + 0.25 * low_tail) + return candidates[int(torch.argmax(torch.stack(scores)))].view(1, 1, 3) + @classmethod def _split_single( cls, diff --git a/pyproject.toml b/pyproject.toml index 996077e..17e7390 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ComfyUI-1hewNodes" description = "This is a custom node collection for ComfyUI that provides some utility nodes." -version = "3.29.0" +version = "3.29.1" license = {file = "LICENSE"} [project.urls] diff --git a/web/docs/1hew_ImageAlphaSplit/en.md b/web/docs/1hew_ImageAlphaSplit/en.md index ba10232..6e5bbb9 100644 --- a/web/docs/1hew_ImageAlphaSplit/en.md +++ b/web/docs/1hew_ImageAlphaSplit/en.md @@ -7,7 +7,7 @@ | Name | Port | Type | Default | Range | Description | | ---- | ---- | ---- | ------- | ----- | ----------- | | `image` | - | IMAGE | - | - | Input image, supports single image or batch; RGBA inputs extract alpha and composite against the background color; both outputs are empty when it is empty | -| `background_color` | - | STRING | `1.0` | same formats as `Image Solid` color: named / `#RRGGBB` / `r,g,b` / `0~1` grayscale / single-letter shorthand | Background color used to fill transparent areas | +| `background_color` | - | STRING | `1.0` | named / `#RRGGBB` / `r,g,b` / `0~1` grayscale / single-letter shorthand / `sm` | Background color used to fill transparent areas; `sm` selects black, white, or `#808080` per frame for subject and edge visibility | ## Outputs @@ -19,7 +19,7 @@ ## Features - Empty `image` (unconnected or a zero-length batch): both the `image` and `mask` outputs are empty. -- RGBA input: returns the original alpha as `mask` and composites transparency onto `background_color`. +- RGBA input: returns the original alpha as `mask` and composites transparency onto `background_color`; with `sm`, it selects black, white, or `#808080` per frame based on composited subject and translucent-edge visibility. - RGB input: passes `image` through unchanged and returns a full-white `mask`. - Batch support: image batches are processed frame by frame and return synchronized `image` and `mask` outputs. - Color parsing follows the same rules as the `Image Solid` node. diff --git a/web/docs/1hew_ImageAlphaSplit/zh.md b/web/docs/1hew_ImageAlphaSplit/zh.md index f182289..12d2c8a 100644 --- a/web/docs/1hew_ImageAlphaSplit/zh.md +++ b/web/docs/1hew_ImageAlphaSplit/zh.md @@ -7,7 +7,7 @@ | 参数名称 | 入端选择 | 数据类型 | 默认值 | 取值范围 | 描述 | | -------- | -------- | -------- | ------ | -------- | ---- | | `image` | - | IMAGE | - | - | 输入图像,支持单图或批量;RGBA 会提取 alpha 并与底色合成;为空时两个输出都为空 | -| `background_color` | - | STRING | `1.0` | 与 `Image Solid` 的 `color` 相同:颜色名 / `#RRGGBB` / `r,g,b` / `0~1` 灰度 / 单字母颜色简写 | 透明区域要铺上的底色 | +| `background_color` | - | STRING | `1.0` | 颜色名 / `#RRGGBB` / `r,g,b` / `0~1` 灰度 / 单字母颜色简写 / `sm` | 透明区域要铺上的底色;`sm` 会逐张从黑、白、`#808080` 中选择更易辨识主体与边缘的颜色 | ## 输出 @@ -19,7 +19,7 @@ ## 功能说明 - `image` 为空(未连接或空批次):输出空,`image` 与 `mask` 两个输出都为空。 -- RGBA 输入:输出 `mask` 为原始 alpha,同时使用 `background_color` 对透明区域做标准合成。 +- RGBA 输入:输出 `mask` 为原始 alpha,同时使用 `background_color` 对透明区域做标准合成;填写 `sm` 时,会逐张在黑、白和 `#808080` 间依照合成后的主体、半透明边缘可见性选择底色。 - RGB 输入:`image` 原样输出,`mask` 返回全白。 - 批量支持:输入是 image batch 时,会逐张处理并同步输出 image 与 mask。 - 颜色输入兼容:与 `Image Solid` 的 `color` 输入规则保持一致。