v1.3.4: 新增Lazy Switch KJ节点和重要功能修复

- 新增Lazy Switch KJ (UTK)节点:支持懒加载评估的条件流程控制
- 支持任意数据类型的条件切换,提供真正的懒加载机制
- 修复Crop By Mask (UTK)节点批处理逻辑:现在正确支持图像和mask批次对应
- 改进批处理算法:每个图像使用对应位置的mask进行独立裁剪
- 智能处理批次数量不匹配:自动重复或截断mask以匹配图像数量
- 增强日志输出:每个图像的裁剪信息单独记录,便于调试
- 保持向后兼容性:单图像+单mask的使用方式保持不变
This commit is contained in:
Cyber Dick Lang
2025-09-21 01:36:57 +08:00
parent 909ef05d14
commit e28f505574
4 changed files with 223 additions and 22 deletions
+24 -1
View File
@@ -8,13 +8,24 @@ A comprehensive toolkit for ComfyUI that provides various utility nodes for imag
:license: MIT, see LICENSE for more details.
"""
__version__ = "1.3.3"
__version__ = "1.3.4"
__author__ = "CyberDickLang"
__email__ = "286878701@qq.com"
__url__ = "https://github.com/whmc76"
# 更新日志
CHANGELOG = {
"1.3.4": [
"新增Lazy Switch KJ节点和重要功能修复:",
"- 新增Lazy Switch KJ (UTK)节点:支持懒加载评估的条件流程控制",
"- 支持任意数据类型的条件切换,提供真正的懒加载机制",
"- 修复Crop By Mask (UTK)节点批处理逻辑:现在正确支持图像和mask批次对应",
"- 改进批处理算法:每个图像使用对应位置的mask进行独立裁剪",
"- 智能处理批次数量不匹配:自动重复或截断mask以匹配图像数量",
"- 增强日志输出:每个图像的裁剪信息单独记录,便于调试",
"- 保持向后兼容性:单图像+单mask的使用方式保持不变",
"- 优化性能:避免不必要的计算,特别适用于条件工作流",
],
"1.3.3": [
"新增多个kjnodes节点移植和架构优化:",
"- 新增Color Match (UTK)节点:支持6种颜色匹配算法,用于图像间色彩转移",
@@ -596,6 +607,15 @@ except ImportError:
COLOR_TO_MASK_MAPPINGS = {}
COLOR_TO_MASK_DISPLAY = {}
try:
from .nodes.tools.lazy_switch import \
NODE_CLASS_MAPPINGS as LAZY_SWITCH_MAPPINGS
from .nodes.tools.lazy_switch import \
NODE_DISPLAY_NAME_MAPPINGS as LAZY_SWITCH_DISPLAY
except ImportError:
LAZY_SWITCH_MAPPINGS = {}
LAZY_SWITCH_DISPLAY = {}
# 合并所有节点映射
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS.update(EMPTY_UNIT_MAPPINGS)
@@ -629,6 +649,7 @@ NODE_CLASS_MAPPINGS.update(LORA_INFO_MAPPINGS)
NODE_CLASS_MAPPINGS.update(KONTEXT_PRESETS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(PROMPT_HELPER_MAPPINGS)
NODE_CLASS_MAPPINGS.update(COLOR_TO_MASK_MAPPINGS)
NODE_CLASS_MAPPINGS.update(LAZY_SWITCH_MAPPINGS)
# 合并显示名称映射
NODE_DISPLAY_NAME_MAPPINGS = {}
@@ -663,6 +684,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update(LORA_INFO_DISPLAY_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(KONTEXT_PRESETS_DISPLAY_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(PROMPT_HELPER_DISPLAY_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(COLOR_TO_MASK_DISPLAY)
NODE_DISPLAY_NAME_MAPPINGS.update(LAZY_SWITCH_DISPLAY)
NODE_CATEGORIES = {
"UniversalToolkit": [
@@ -698,6 +720,7 @@ NODE_CATEGORIES = {
"LoraInfo_UTK",
"LoadKontextPresets_UTK",
"ColorToMask_UTK",
"LazySwitchKJ_UTK",
]
}
+73 -20
View File
@@ -132,26 +132,42 @@ class CropByMask_UTK:
l_images = []
l_masks = []
# 处理图像批次
for l in image:
l_images.append(torch.unsqueeze(l, 0))
# 处理mask批次
if mask_for_crop.dim() == 2:
mask_for_crop = torch.unsqueeze(mask_for_crop, 0)
# 如果有多张mask输入,使用第一张
if mask_for_crop.shape[0] > 1:
log(
f"Warning: Multiple mask inputs, using the first.",
message_type="warning",
)
mask_for_crop = torch.unsqueeze(mask_for_crop[0], 0)
# 反转mask(如果需要)
if invert_mask:
mask_for_crop = 1 - mask_for_crop
l_masks.append(tensor2pil(torch.unsqueeze(mask_for_crop, 0)).convert("L"))
# 将所有mask转换为PIL图像
for i in range(mask_for_crop.shape[0]):
l_masks.append(tensor2pil(torch.unsqueeze(mask_for_crop[i], 0)).convert("L"))
# 如果mask数量少于图像数量,重复使用最后一个mask
while len(l_masks) < len(l_images):
l_masks.append(l_masks[-1])
# 如果mask数量多于图像数量,截断到图像数量
if len(l_masks) > len(l_images):
l_masks = l_masks[:len(l_images)]
log(f"Warning: More masks than images, using first {len(l_images)} masks.", message_type="warning")
_mask = mask2image(mask_for_crop)
# 获取画布尺寸
canvas_width, canvas_height = (
tensor2pil(torch.unsqueeze(image[0], 0)).convert("RGB").size
)
# 存储所有的裁剪框用于预览(使用第一个mask)
first_mask = l_masks[0]
try:
bluredmask = gaussian_blur(_mask, 20).convert("L")
bluredmask = gaussian_blur(first_mask, 20).convert("L")
except ImportError:
bluredmask = _mask.convert("L")
bluredmask = first_mask.convert("L")
x = 0
y = 0
@@ -162,14 +178,11 @@ class CropByMask_UTK:
elif detect == "max_inscribed_rect":
(x, y, width, height) = max_inscribed_rect(bluredmask)
else:
(x, y, width, height) = mask_area(_mask)
(x, y, width, height) = mask_area(first_mask)
width = num_round_up_to_multiple(width, 8)
height = num_round_up_to_multiple(height, 8)
log(f"CropByMask_UTK: Box detected. x={x},y={y},width={width},height={height}")
canvas_width, canvas_height = (
tensor2pil(torch.unsqueeze(image[0], 0)).convert("RGB").size
)
x1 = x - left_reserve if x - left_reserve > 0 else 0
y1 = y - top_reserve if y - top_reserve > 0 else 0
x2 = (
@@ -182,7 +195,9 @@ class CropByMask_UTK:
if y + height + bottom_reserve < canvas_height
else canvas_height
)
preview_image = tensor2pil(mask_for_crop).convert("RGB")
# 创建预览图像
preview_image = first_mask.convert("RGB")
preview_image = draw_rect(
preview_image,
x,
@@ -201,12 +216,50 @@ class CropByMask_UTK:
line_color="#00F000",
line_width=(width + height) // 200,
)
crop_box = (x1, y1, x2, y2)
# 处理每个图像和对应的mask
for i in range(len(l_images)):
_canvas = tensor2pil(l_images[i]).convert("RGB")
_mask = l_masks[0]
ret_images.append(pil2tensor(_canvas.crop(crop_box)))
ret_masks.append(image2mask(_mask.crop(crop_box)))
_mask = l_masks[i] # 使用对应的mask而不是第一个
# 对每个mask单独计算裁剪区域
try:
current_bluredmask = gaussian_blur(_mask, 20).convert("L")
except ImportError:
current_bluredmask = _mask.convert("L")
curr_x, curr_y, curr_width, curr_height = 0, 0, 0, 0
if detect == "min_bounding_rect":
(curr_x, curr_y, curr_width, curr_height) = min_bounding_rect(current_bluredmask)
elif detect == "max_inscribed_rect":
(curr_x, curr_y, curr_width, curr_height) = max_inscribed_rect(current_bluredmask)
else:
(curr_x, curr_y, curr_width, curr_height) = mask_area(_mask)
curr_width = num_round_up_to_multiple(curr_width, 8)
curr_height = num_round_up_to_multiple(curr_height, 8)
curr_x1 = curr_x - left_reserve if curr_x - left_reserve > 0 else 0
curr_y1 = curr_y - top_reserve if curr_y - top_reserve > 0 else 0
curr_x2 = (
curr_x + curr_width + right_reserve
if curr_x + curr_width + right_reserve < canvas_width
else canvas_width
)
curr_y2 = (
curr_y + curr_height + bottom_reserve
if curr_y + curr_height + bottom_reserve < canvas_height
else canvas_height
)
current_crop_box = (curr_x1, curr_y1, curr_x2, curr_y2)
ret_images.append(pil2tensor(_canvas.crop(current_crop_box)))
ret_masks.append(image2mask(_mask.crop(current_crop_box)))
log(f"CropByMask_UTK: Image {i+1} - Box detected. x={curr_x},y={curr_y},width={curr_width},height={curr_height}")
log(
f"CropByMask_UTK Processed {len(ret_images)} image(s).",
+125
View File
@@ -0,0 +1,125 @@
"""
Lazy Switch Node for ComfyUI Universal Toolkit
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Lazy switch functionality adapted from kjnodes.
Controls flow of execution based on a boolean switch with lazy evaluation.
:copyright: (c) 2024 by May
:license: MIT, see LICENSE for more details.
"""
# Try to import IO.ANY from ComfyUI's typing system
try:
from comfy.comfy_types.node_typing import IO
ANY_TYPE = IO.ANY
except ImportError:
# Fallback for older ComfyUI versions or different typing systems
try:
from comfy_extras.nodes_custom_sampler import AnyType
ANY_TYPE = AnyType("*")
except ImportError:
# Create a simple ANY type fallback
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
ANY_TYPE = AnyType("*")
class LazySwitchKJ_UTK:
"""
Lazy Switch node that controls flow of execution based on a boolean switch.
This node implements lazy evaluation, meaning it only evaluates the branch
that will actually be used based on the switch value. This can improve
performance by avoiding unnecessary computations.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"switch": ("BOOLEAN", {"tooltip": "Boolean value to control which input is returned"}),
"on_false": (ANY_TYPE, {
"lazy": True,
"tooltip": "Value returned when switch is False"
}),
"on_true": (ANY_TYPE, {
"lazy": True,
"tooltip": "Value returned when switch is True"
}),
},
}
RETURN_TYPES = (ANY_TYPE,)
RETURN_NAMES = ("output",)
FUNCTION = "switch"
CATEGORY = "UniversalToolkit/Tools"
DESCRIPTION = """
Controls flow of execution based on a boolean switch.
This node implements lazy evaluation - it only processes the input
that will actually be used based on the switch value. This can
significantly improve performance by avoiding unnecessary computations
in complex workflows.
Features:
- **Lazy Evaluation**: Only evaluates the selected branch
- **Any Type Support**: Works with any data type (images, masks, strings, etc.)
- **Flow Control**: Essential for conditional workflow execution
- **Performance Optimization**: Reduces unnecessary processing
Usage:
- Connect your boolean condition to the 'switch' input
- Connect the value for False condition to 'on_false'
- Connect the value for True condition to 'on_true'
- The node will output the appropriate value based on the switch
Common use cases:
- Conditional image processing pipelines
- A/B testing different parameters
- Workflow branching based on user input
- Performance optimization in complex workflows
"""
def check_lazy_status(self, switch, on_false=None, on_true=None):
"""
Check which inputs are needed for lazy evaluation.
This method tells ComfyUI which inputs it needs to evaluate
based on the current switch value.
"""
if switch and on_true is None:
return ["on_true"]
if not switch and on_false is None:
return ["on_false"]
def switch(self, switch, on_false=None, on_true=None):
"""
Switch between two values based on a boolean condition.
Args:
switch: Boolean value determining which input to return
on_false: Value to return when switch is False
on_true: Value to return when switch is True
Returns:
Tuple containing the selected value
"""
value = on_true if switch else on_false
return (value,)
# Node registration
NODE_CLASS_MAPPINGS = {
"LazySwitchKJ_UTK": LazySwitchKJ_UTK,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LazySwitchKJ_UTK": "Lazy Switch KJ (UTK)",
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "universaltoolkit"
description = "A comprehensive toolkit based on ComfyUI, providing image, mask, audio, and tools nodes, fully modular and v3 compatible."
version = "1.3.3"
version = "1.3.4"
license = {file = "LICENSE"}
dependencies = [
"torch",