feat(image): add smart alpha background mode

This commit is contained in:
1hew
2026-09-29 17:50:16 +08:00
parent d410cd783e
commit bc966a63b6
6 changed files with 61 additions and 7 deletions
+3
View File
@@ -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` 按未输入处理
+3
View File
@@ -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
+50 -2
View File
@@ -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,
+1 -1
View File
@@ -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]
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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` 输入规则保持一致。