diff --git a/__init__.py b/__init__.py index 2e98420..222d8df 100644 --- a/__init__.py +++ b/__init__.py @@ -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", ] } diff --git a/nodes/image/crop_by_mask.py b/nodes/image/crop_by_mask.py index 0e820a2..2ee315c 100644 --- a/nodes/image/crop_by_mask.py +++ b/nodes/image/crop_by_mask.py @@ -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).", diff --git a/nodes/tools/lazy_switch.py b/nodes/tools/lazy_switch.py new file mode 100644 index 0000000..6926ae5 --- /dev/null +++ b/nodes/tools/lazy_switch.py @@ -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)", +} diff --git a/pyproject.toml b/pyproject.toml index 7cc7824..84f0328 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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",