Files
whmc76-ComfyUI-UniversalToo…/ize 与 pad 方法表现相同的问题
T
2025-11-01 01:50:01 +08:00

20 KiB

diff --git a/README.md b/README.md
index e0ec9b9..2a0873a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# ComfyUI-UniversalToolkit
 
-[![Version](https://img.shields.io/badge/version-1.3.2-blue.svg)](https://github.com/whmc76/ComfyUI-UniversalToolkit)
+[![Version](https://img.shields.io/badge/version-1.4.7-blue.svg)](https://github.com/whmc76/ComfyUI-UniversalToolkit)
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
[![ComfyUI](https://img.shields.io/badge/ComfyUI-v3+-orange.svg)](https://github.com/comfyanonymous/ComfyUI)
 
@@ -52,9 +52,10 @@ tqdm
- **ImageConcatenate_UTK**:水平或垂直拼接两张图像
- **ImageConcatenateMulti_UTK**:智能拼接多张图像,支持2-4图自动布局
 
-#### 图像变换与调整
-- **ImageScaleByAspectRatio_UTK**:按指定宽高比缩放图像
-- **ImageMaskScaleAs_UTK**:按参考图像尺寸缩放图像
+- #### 图像变换与调整
+- **ResizeImageVerKJ_UTK**:KJ v2 风格的高兼容缩放,支持 stretch/resize/pad/pad_edge/pad_edge_pixel/crop/pillarbox_blur/total_pixels 与 `crop_position`
+- **ImageScaleByAspectRatio_UTK**:按指定宽高比缩放图像(已支持与 KJ v2 一致的 fit 模式与 `crop_position`,背景色为预设清单)
+- **ImageMaskScaleAs_UTK**:按参考图像尺寸缩放图像(已支持与 KJ v2 一致的 fit 模式与 `crop_position`,pad_color 为预设清单)
- **ImageScaleRestore_UTK**:将图像恢复到原始尺寸
- **ImageRemoveAlpha_UTK**:移除图像的Alpha通道
- **ImageCombineAlpha_UTK**:合并Alpha通道到图像
@@ -81,6 +82,7 @@ tqdm
- **MaskAnd_UTK**:掩码与运算
- **MaskSub_UTK**:掩码减法运算
- **MaskAdd_UTK**:掩码加法运算
+- **BlockifyMask_UTK**:将掩码按 block_size 马赛克化(支持 cpu/cuda;可选二值化)
 
### 🛠️ 工具节点
 
@@ -91,6 +93,7 @@ tqdm
 
#### 数学与逻辑
- **MathExpression_UTK**:数学表达式计算,支持复杂公式和函数
+- **BestContextWindow_UTK**:最佳滑动窗口帧数计算(满足 4n+1,最小化补帧;输出 best_window/padding/padded_total/segments)
 
#### 系统工具
- **PurgeVRAM_UTK**:显存清理,支持选择性清理缓存和模型
@@ -176,7 +179,22 @@ AudioCropProcess_UTK
 
## 📋 版本历史
 
-### v1.3.2 (最新)
+### v1.4.7 (最新)
+- 修复 resize 与 pad 方法表现相同的问题:
+ - ResizeImageVerKJ (UTK):resize 模式只等比缩放不填充,pad 模式填充到目标尺寸
+ - ImageMaskScaleAs (UTK):resize 返回实际缩放尺寸,pad 填充到目标尺寸并正确输出尺寸
+ - ImageScaleByAspectRatio (UTK):resize 返回实际缩放尺寸,pad 填充到目标尺寸并正确输出尺寸
+ - resize:等比缩放,输出尺寸 = 缩放后尺寸(可能小于目标尺寸)
+ - pad:等比缩放 + 背景填充,输出尺寸 = 目标尺寸(固定尺寸)
+
+### v1.4.6
+- 新增 `Resize Image ver KJ (UTK)`,完整对齐 KJ v2 调整模式,支持 `crop_position` 与 mask 同步缩放;pad_edge/pad_edge_pixel 行为与 KJ 对齐
+- 升级 `Image Mask Scale As (UTK)` 与 `Image Scale By Aspect Ratio (UTK)`:支持同样的 fit 模式、`crop_position`,并将背景色改为预设清单
+- 新增 `Blockify Mask (UTK)`:掩码块化,支持二值化
+- 新增 `Best Context Window (UTK)`:计算满足 4n+1 的最佳窗口,最小化补帧
+- 统一分类命名:`UniversalToolkit/Tools`
+
+### v1.3.2
- 新增电商应用类,重新组织预设分类结构
- 创建专门的电商应用类,包含6个专业电商功能:
- Ecommerce-Professional Product Photography (专业产品图)
diff --git a/__init__.py b/__init__.py
index a72fc01..2b6dda9 100644
--- a/__init__.py
+++ b/__init__.py
@@ -8,13 +8,43 @@ A comprehensive toolkit for ComfyUI that provides various utility nodes for imag
:license: MIT, see LICENSE for more details.
"""
 
-__version__ = "1.4.4"
+__version__ = "1.4.7"
__author__ = "CyberDickLang"
__email__ = "286878701@qq.com"
__url__ = "https://github.com/whmc76"
 
# 更新日志
CHANGELOG = {
+ "1.4.7": [
+ "修复 resize 与 pad 方法表现相同的问题:",
+ "- ResizeImageVerKJ (UTK):resize 模式只等比缩放不填充,pad 模式填充到目标尺寸",
+ "- ImageMaskScaleAs (UTK):resize 返回实际缩放尺寸,pad 填充到目标尺寸并正确输出尺寸",
+ "- ImageScaleByAspectRatio (UTK):resize 返回实际缩放尺寸,pad 填充到目标尺寸并正确输出尺寸",
+ "- resize:等比缩放,输出尺寸 = 缩放后尺寸(可能小于目标尺寸)",
+ "- pad:等比缩放 + 背景填充,输出尺寸 = 目标尺寸(固定尺寸)",
+ ],
+ "1.4.6": [
+ "新增 Resize Image ver KJ (UTK):",
+ "- 复刻 KJ v2 的调整模式:stretch/resize/pad/pad_edge/pad_edge_pixel/crop/pillarbox_blur/total_pixels",
+ "- 支持 mask 同步缩放与对齐,pad_edge/pad_edge_pixel 行为与 KJ 对齐",
+ "升级 Image Mask Scale As (UTK):",
+ "- fit 与 KJ v2 对齐,新增 crop_position,支持预设 pad_color",
+ "升级 Image Scale By Aspect Ratio (UTK):",
+ "- fit 与 KJ v2 对齐,新增 crop_position,background_color 改为预设清单",
+ "修正 pad_edge 与 pad_edge_pixel 的边缘与角点处理逻辑,匹配 KJ 视觉表现",
+ ],
+ "1.4.5": [
+ "新增Best Context Window (UTK)节点:",
+ "- 计算满足4n+1且位于[min,max]区间的最佳窗口,以最小化补帧",
+ "- 输出best_window、padding、padded_total、segments",
+ "- 分类:UniversalToolkit/Tools",
+ "新增Blockify Mask (UTK)节点:",
+ "- 将掩码按block_size块化,支持cpu/cuda",
+ "- 可选二值化binarize与threshold",
+ "- 分类:UniversalToolkit/Mask",
+ "统一分类命名:将UniversalToolkit/tools合并为UniversalToolkit/Tools",
+ "修复:Get Image or Mask Range From Batch (UTK) 分类名不一致问题",
+ ],
"1.4.4": [
"新增Get Image or Mask Range From Batch (UTK)节点:",
"- 支持从图像批次或遮罩批次中提取指定范围的元素",
@@ -742,9 +772,27 @@ try:
NODE_CLASS_MAPPINGS as GET_IMAGE_RANGE_MAPPINGS
from .nodes.tools.get_image_range_from_batch import \
NODE_DISPLAY_NAME_MAPPINGS as GET_IMAGE_RANGE_DISPLAY
+ from .nodes.tools.optimal_context_window_node import \
+ NODE_CLASS_MAPPINGS as BEST_CONTEXT_WINDOW_MAPPINGS
+ from .nodes.tools.optimal_context_window_node import \
+ NODE_DISPLAY_NAME_MAPPINGS as BEST_CONTEXT_WINDOW_DISPLAY
+ from .nodes.mask.blockify_mask import \
+ NODE_CLASS_MAPPINGS as BLOCKIFY_MASK_MAPPINGS
+ from .nodes.mask.blockify_mask import \
+ NODE_DISPLAY_NAME_MAPPINGS as BLOCKIFY_MASK_DISPLAY
+ from .nodes.image.resize_image_ver_kj import \
+ NODE_CLASS_MAPPINGS as RESIZE_VER_KJ_MAPPINGS
+ from .nodes.image.resize_image_ver_kj import \
+ NODE_DISPLAY_NAME_MAPPINGS as RESIZE_VER_KJ_DISPLAY
except ImportError:
GET_IMAGE_RANGE_MAPPINGS = {}
GET_IMAGE_RANGE_DISPLAY = {}
+ BEST_CONTEXT_WINDOW_MAPPINGS = {}
+ BEST_CONTEXT_WINDOW_DISPLAY = {}
+ BLOCKIFY_MASK_MAPPINGS = {}
+ BLOCKIFY_MASK_DISPLAY = {}
+ RESIZE_VER_KJ_MAPPINGS = {}
+ RESIZE_VER_KJ_DISPLAY = {}
 
 
# 合并所有节点映射
@@ -786,6 +834,9 @@ NODE_CLASS_MAPPINGS.update(COLOR_TO_MASK_MAPPINGS)
NODE_CLASS_MAPPINGS.update(LAZY_SWITCH_MAPPINGS)
NODE_CLASS_MAPPINGS.update(TEXT_TRANSLATOR_API_MAPPINGS)
NODE_CLASS_MAPPINGS.update(GET_IMAGE_RANGE_MAPPINGS)
+NODE_CLASS_MAPPINGS.update(BEST_CONTEXT_WINDOW_MAPPINGS)
+NODE_CLASS_MAPPINGS.update(BLOCKIFY_MASK_MAPPINGS)
+NODE_CLASS_MAPPINGS.update(RESIZE_VER_KJ_MAPPINGS)
 
# 合并显示名称映射
NODE_DISPLAY_NAME_MAPPINGS = {}
@@ -826,6 +877,9 @@ NODE_DISPLAY_NAME_MAPPINGS.update(COLOR_TO_MASK_DISPLAY)
NODE_DISPLAY_NAME_MAPPINGS.update(LAZY_SWITCH_DISPLAY)
NODE_DISPLAY_NAME_MAPPINGS.update(TEXT_TRANSLATOR_API_DISPLAY)
NODE_DISPLAY_NAME_MAPPINGS.update(GET_IMAGE_RANGE_DISPLAY)
+NODE_DISPLAY_NAME_MAPPINGS.update(BEST_CONTEXT_WINDOW_DISPLAY)
+NODE_DISPLAY_NAME_MAPPINGS.update(BLOCKIFY_MASK_DISPLAY)
+NODE_DISPLAY_NAME_MAPPINGS.update(RESIZE_VER_KJ_DISPLAY)
 
NODE_CATEGORIES = {
"UniversalToolkit": [
@@ -867,6 +921,9 @@ NODE_CATEGORIES = {
"APIImageGenerator_UTK",
"TextTranslatorAPI_UTK",
"GetImageRangeFromBatch_UTK",
+ "BestContextWindow_UTK",
+ "BlockifyMask_UTK",
+ "ResizeImageVerKJ_UTK",
]
}
 
diff --git a/nodes/image/image_mask_scale_as.py b/nodes/image/image_mask_scale_as.py
index 7debdaa..46af644 100644
--- a/nodes/image/image_mask_scale_as.py
+++ b/nodes/image/image_mask_scale_as.py
@@ -9,7 +9,7 @@ Scales images and masks to match the dimensions of a reference image.
"""
 
import torch
-from PIL import Image
+from PIL import Image, ImageFilter
 
from ..image_utils import image2mask, pil2tensor, tensor2pil
 
@@ -32,10 +32,18 @@ def fit_resize_image(
target_height,
fit_mode,
resize_sampler,
- background_color="#000000",
+ background_color="black",
+ crop_position="center",
):
"""Resize image according to fit mode"""
- if fit_mode == "letterbox":
+ if fit_mode == "resize":
+ # resize: 只等比缩放,不填充,直接返回缩放后的图像
+ scale = min(target_width / image.width, target_height / image.height)
+ new_width = int(image.width * scale)
+ new_height = int(image.height * scale)
+ return image.resize((new_width, new_height), resize_sampler)
+
+ if fit_mode in ["letterbox", "pad", "pad_edge", "pad_edge_pixel", "pillarbox_blur"]:
# Calculate scaling factor to fit within target dimensions
scale = min(target_width / image.width, target_height / image.height)
new_width = int(image.width * scale)
@@ -44,13 +52,164 @@ def fit_resize_image(
# Resize image
resized = image.resize((new_width, new_height), resize_sampler)
 
- # Create new image with target dimensions and paste resized image
- if image.mode == "RGB":
- result = Image.new("RGB", (target_width, target_height), background_color)
+ # Create background
+ if fit_mode == "pillarbox_blur":
+ # create scaled background then blur and dim
+ scale_fill = max(target_width / max(1, image.width), target_height / max(1, image.height))
+ bg_w = max(1, int(round(image.width * scale_fill)))
+ bg_h = max(1, int(round(image.height * scale_fill)))
+ bg = image.resize((bg_w, bg_h), Image.BILINEAR)
+ # center crop to canvas
+ x0 = max(0, (bg_w - target_width) // 2)
+ y0 = max(0, (bg_h - target_height) // 2)
+ bg = bg.crop((x0, y0, x0 + target_width, y0 + target_height))
+ sigma = max(1.0, 0.006 * float(min(target_width, target_height)))
+ bg = bg.filter(ImageFilter.GaussianBlur(radius=sigma))
+ # desaturate slightly if RGB
+ if bg.mode == "RGB":
+ r, g, b = bg.split()
+ # simple luminance
+ l = r.point(lambda v: int(0.2126 * v))
+ l = Image.merge("RGB", (l, l, l))
+ def mix(a, b, t=0.2):
+ return Image.blend(a, b, t)
+ bg = mix(bg, l)
+ # dim
+ bg = bg.point(lambda v: int(v * 0.35))
+ result = bg
+ elif fit_mode in ["pad_edge", "pad_edge_pixel"]:
+ # start with empty canvas
+ result = Image.new("RGB" if image.mode == "RGB" else "L", (target_width, target_height))
else:
- result = Image.new("L", (target_width, target_height), 0)
- paste_x = (target_width - new_width) // 2
- paste_y = (target_height - new_height) // 2
+ if image.mode == "RGB":
+ # preset color names
+ preset_colors = {
+ "black": "#000000",
+ "white": "#FFFFFF",
+ "gray": "#808080",
+ "red": "#FF0000",
+ "green": "#00FF00",
+ "blue": "#0000FF",
+ "yellow": "#FFFF00",
+ "cyan": "#00FFFF",
+ "magenta": "#FF00FF",
+ }
+ fill_color = preset_colors.get(str(background_color).lower(), background_color)
+ result = Image.new("RGB", (target_width, target_height), fill_color)
+ else:
+ result = Image.new("L", (target_width, target_height), 0)
+
+ # paste location
+ if crop_position == "center":
+ paste_x = (target_width - new_width) // 2
+ paste_y = (target_height - new_height) // 2
+ elif crop_position == "top":
+ paste_x = (target_width - new_width) // 2
+ paste_y = 0
+ elif crop_position == "bottom":
+ paste_x = (target_width - new_width) // 2
+ paste_y = target_height - new_height
+ elif crop_position == "left":
+ paste_x = 0
+ paste_y = (target_height - new_height) // 2
+ elif crop_position == "right":
+ paste_x = target_width - new_width
+ paste_y = (target_height - new_height) // 2
+ else:
+ paste_x = (target_width - new_width) // 2
+ paste_y = (target_height - new_height) // 2
+ # specialized edge padding behaviors
+ if fit_mode == "pad_edge" or fit_mode == "pad_edge_pixel":
+ left_pad = paste_x
+ right_pad = target_width - (paste_x + new_width)
+ top_pad = paste_y
+ bottom_pad = target_height - (paste_y + new_height)
+
+ # left/right stripes from image columns
+ if left_pad > 0:
+ col = resized.crop((0, 0, 1, new_height))
+ if fit_mode == "pad_edge_pixel":
+ col = col.resize((left_pad, new_height), Image.NEAREST)
+ result.paste(col, (0, paste_y))
+ else:
+ # mean color of left edge
+ if col.mode == "RGB":
+ pixels = list(col.getdata())
+ r = sum(p[0] for p in pixels) // len(pixels)
+ g = sum(p[1] for p in pixels) // len(pixels)
+ b = sum(p[2] for p in pixels) // len(pixels)
+ fill = (r, g, b)
+ else:
+ v = sum(col.getdata()) // len(col.getdata())
+ fill = v
+ Image.Image.paste(result, Image.new(result.mode, (left_pad, new_height), fill), (0, paste_y))
+
+ if right_pad > 0:
+ col = resized.crop((new_width - 1, 0, new_width, new_height))
+ if fit_mode == "pad_edge_pixel":
+ col = col.resize((right_pad, new_height), Image.NEAREST)
+ result.paste(col, (paste_x + new_width, paste_y))
+ else:
+ if col.mode == "RGB":
+ pixels = list(col.getdata())
+ r = sum(p[0] for p in pixels) // len(pixels)
+ g = sum(p[1] for p in pixels) // len(pixels)
+ b = sum(p[2] for p in pixels) // len(pixels)
+ fill = (r, g, b)
+ else:
+ v = sum(col.getdata()) // len(col.getdata())
+ fill = v
+ Image.Image.paste(result, Image.new(result.mode, (right_pad, new_height), fill), (paste_x + new_width, paste_y))
+
+ # top/bottom stripes from image rows
+ if top_pad > 0:
+ row = resized.crop((0, 0, new_width, 1))
+ if fit_mode == "pad_edge_pixel":
+ row = row.resize((new_width, top_pad), Image.NEAREST)
+ result.paste(row, (paste_x, 0))
+ # corners by corner pixels
+ if left_pad > 0:
+ c = resized.getpixel((0, 0))
+ Image.Image.paste(result, Image.new(result.mode, (left_pad, top_pad), c), (0, 0))
+ if right_pad > 0:
+ c = resized.getpixel((new_width - 1, 0