feat(image): add smart alpha background mode
This commit is contained in:
@@ -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` 按未输入处理
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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` 输入规则保持一致。
|
||||
|
||||
Reference in New Issue
Block a user