From bf91c32971c8f111954e2584745aec8a46774559 Mon Sep 17 00:00:00 2001 From: Cyber Dick Lang <286878701@qq.com> Date: Tue, 24 Jun 2025 19:06:57 +0800 Subject: [PATCH] =?UTF-8?q?v1.1.3:=20=E4=BF=AE=E5=A4=8D=20PurgeVRAM=5FUTK?= =?UTF-8?q?=20=E5=92=8C=20CheckMask=5FUTK=20=E8=8A=82=E7=82=B9=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E5=AE=8C=E6=88=90=E6=A8=A1=E5=9D=97=E5=8C=96?= =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 PurgeVRAM_UTK 节点类型不匹配问题,创建 AnyType 类实现 - 修复 CheckMask_UTK 节点 NoneType 错误,添加空值检查 - 完成模块化重构:将 common_utils.py 功能拆分到不同目录 - 创建 image_converters.py、color_utils.py、logging_utils.py、any_type.py - 删除 common_utils.py,避免依赖冲突 - 更新所有相关文件的导入语句 - 提高代码可维护性和模块化程度 --- AI_CODING_RULES.md | 43 +++- __init__.py | 161 ++++++------ node_category_report.txt | 1 + nodes/audio/audio_crop_process.py | 2 +- nodes/audio/load_audio.py | 2 +- nodes/image/check_mask.py | 33 ++- .../{common_utils.py => image/color_utils.py} | 120 +-------- nodes/image/crop_by_mask.py | 2 +- nodes/image/depth_map_blur.py | 2 +- nodes/image/empty_unit_generator.py | 10 +- nodes/image/image_and_mask_preview.py | 7 +- nodes/image/image_combine_alpha.py | 2 +- nodes/image/image_composite_masked.py | 127 +++++++++ nodes/image/image_concatenate.py | 4 +- nodes/image/image_concatenate_multi.py | 2 +- nodes/image/image_converters.py | 72 ++++++ nodes/image/image_mask_scale_as.py | 2 +- nodes/image/image_pad_for_outpaint_masked.py | 2 +- nodes/image/image_ratio_detector.py | 12 +- nodes/image/image_remove_alpha.py | 2 +- nodes/image/image_scale_by_aspect_ratio.py | 2 +- nodes/image/image_scale_restore.py | 2 +- nodes/image/imitation_hue_node.py | 240 +++--------------- nodes/image/purge_vram.py | 17 +- nodes/image/restore_crop_box.py | 2 +- nodes/mask/mask_operations.py | 6 +- nodes/tools/any_type.py | 16 ++ nodes/tools/fill_masked_area.py | 2 +- nodes/tools/logging_utils.py | 20 ++ nodes/tools/show_nodes.py | 12 +- pyproject.toml | 2 +- 31 files changed, 486 insertions(+), 443 deletions(-) create mode 100644 node_category_report.txt rename nodes/{common_utils.py => image/color_utils.py} (73%) create mode 100644 nodes/image/image_composite_masked.py create mode 100644 nodes/image/image_converters.py create mode 100644 nodes/tools/any_type.py create mode 100644 nodes/tools/logging_utils.py diff --git a/AI_CODING_RULES.md b/AI_CODING_RULES.md index 8c6e8f6..3ccc453 100644 --- a/AI_CODING_RULES.md +++ b/AI_CODING_RULES.md @@ -54,4 +54,45 @@ - 不要自动拉取外部仓库或依赖。 - 不要更改用户未授权的文件。 - 不要随意更改项目结构。 -- 不要在 requirements.txt/pyproject.toml 中写死所有依赖的精确版本号。 \ No newline at end of file +- 不要在 requirements.txt/pyproject.toml 中写死所有依赖的精确版本号。 + +# ComfyUI-UniversalToolkit 开发规范(AI_CODING_RULES) + +## 2024-07-13 重要改进与规则 + +### 1. 节点注册与导入(ComfyUI v3官方规范) +- 每个节点文件只导出自己的 `NODE_CLASS_MAPPINGS` 和 `NODE_DISPLAY_NAME_MAPPINGS`。 +- `__init__.py` 必须静态导入所有节点注册字典,禁止动态try/except导入和动态合并。 +- 只导出 `NODE_CLASS_MAPPINGS`、`NODE_DISPLAY_NAME_MAPPINGS`,对齐官方插件加载机制。 +- 节点注册顺序清晰,所有节点都必须被静态合并进主注册字典。 + +### 2. 节点分组与命名 +- 每个节点类必须有唯一且规范的 `CATEGORY` 属性,分组如 `UniversalToolkit/Image`、`UniversalToolkit/Mask`、`UniversalToolkit/Audio`、`UniversalToolkit/Tools`。 +- 节点类名、注册名、显示名必须唯一,全部带 `_UTK` 后缀,禁止与原生节点或其它插件重名。 +- 节点显示名统一加 `(UTK)` 后缀,保证界面分组风格一致。 + +### 3. pyproject.toml 规范 +- 必须包含 `[project]` 和 `[tool.comfy]` 两大段,字段严格对齐官方文档: + - `name`、`description`、`version`、`license`、`dependencies`、`Repository` + - `[tool.comfy]` 下 `PublisherId`、`DisplayName`、`Icon` 必填 +- `PublisherId` 必须与Registry注册一致,`DisplayName`为插件在ComfyUI-Manager/Registry中的显示名 + +### 4. 目录结构与分层 +- 按功能分为 `nodes/image`、`nodes/mask`、`nodes/audio`、`nodes/tools` 四大目录,每个节点独立py文件 +- 禁止使用绝对导入和跨目录utils模块,所有依赖应在本插件目录下 + +### 5. 节点输入输出与兼容性 +- 所有节点输入输出shape、类型、参数名、返回名必须严格遵循ComfyUI官方节点开发规范 +- 禁止随意更改节点实现、参数、shape,所有节点必须与ComfyUI原生节点和其它插件100%兼容 +- 任何涉及shape、类型、参数的修正,必须优先保证与ComfyUI主程序和主流插件生态兼容 + +### 6. 其它重要约定 +- 禁止在`__init__.py`中做复杂逻辑或动态注册,推荐静态声明所有节点映射 +- 禁止用旧版的`register_node`、`register_nodes`等动态注册API +- 所有节点分组、命名、注册、导入、依赖、pyproject.toml等必须随时对齐ComfyUI官方最新规范 + +--- + +**本规范为ComfyUI-UniversalToolkit插件开发的最高准则,所有贡献者和维护者必须严格遵守。** + +(如有新规范或官方更新,须第一时间同步修订本文件) \ No newline at end of file diff --git a/__init__.py b/__init__.py index 416b55c..68dff95 100644 --- a/__init__.py +++ b/__init__.py @@ -8,13 +8,29 @@ A comprehensive toolkit for ComfyUI that provides various utility nodes for imag :license: MIT, see LICENSE for more details. """ -__version__ = "1.1.2" +__version__ = "1.1.3" __author__ = "CyberDickLang" __email__ = "286878701@qq.com" __url__ = "https://github.com/whmc76" # 更新日志 CHANGELOG = { + "1.1.3": [ + "修复 PurgeVRAM_UTK 节点类型不匹配问题:", + "- 创建 AnyType 类实现,参考 ComfyUI-LayerStyle 项目", + "- 解决 'received_type(IMAGE) mismatch input_type(*)' 错误", + "- 支持接受任何类型输入并正确返回", + "修复 CheckMask_UTK 节点 NoneType 错误:", + "- 添加空值检查,防止 mask 为 None 时出错", + "- 改进 tensor2pil 转换失败的处理", + "- 增强错误处理和日志输出", + "完成模块化重构:", + "- 将 common_utils.py 功能拆分到不同目录", + "- 创建 image_converters.py、color_utils.py、logging_utils.py、any_type.py", + "- 删除 common_utils.py,避免依赖冲突", + "- 更新所有相关文件的导入语句", + "- 提高代码可维护性和模块化程度" + ], "1.1.2": [ "修复 DepthMapBlur_UTK 节点 kernel size 类型和 OpenCV 奇数断言问题,保证所有模糊核为正奇数,完全兼容 ComfyUI 规范。", "修正 EmptyUnitGenerator_UTK 输出 shape,所有节点输入输出严格遵循 ComfyUI 官方规范。", @@ -133,29 +149,22 @@ CHANGELOG = { # 导入节点模块 try: # 工具类节点 - from .nodes.tools.show_nodes import NODE_CLASS_MAPPINGS as SHOW_NODES_MAPPINGS - from .nodes.tools.show_nodes import NODE_DISPLAY_NAME_MAPPINGS as SHOW_NODES_DISPLAY_MAPPINGS + from .nodes.tools.show_nodes import NODE_CLASS_MAPPINGS as SHOW_NODES_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as SHOW_NODES_DISPLAY_MAPPINGS # 音频节点 - from .nodes.audio.audio_crop_process import NODE_CLASS_MAPPINGS as AUDIO_CROP_MAPPINGS - from .nodes.audio.audio_crop_process import NODE_DISPLAY_NAME_MAPPINGS as AUDIO_CROP_DISPLAY_MAPPINGS + from .nodes.audio.audio_crop_process import NODE_CLASS_MAPPINGS as AUDIO_CROP_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as AUDIO_CROP_DISPLAY_MAPPINGS # 掩码节点 - from .nodes.mask.mask_operations import NODE_CLASS_MAPPINGS as MASK_OPERATIONS_MAPPINGS - from .nodes.mask.mask_operations import NODE_DISPLAY_NAME_MAPPINGS as MASK_OPERATIONS_DISPLAY_MAPPINGS + from .nodes.mask.mask_operations import NODE_CLASS_MAPPINGS as MASK_OPERATIONS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as MASK_OPERATIONS_DISPLAY_MAPPINGS # 图像节点 - from .nodes.image.image_concatenate_multi import NODE_CLASS_MAPPINGS as CONCATENATE_MULTI_MAPPINGS - from .nodes.image.image_concatenate_multi import NODE_DISPLAY_NAME_MAPPINGS as CONCATENATE_MULTI_DISPLAY_MAPPINGS + from .nodes.image.image_concatenate_multi import NODE_CLASS_MAPPINGS as CONCATENATE_MULTI_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CONCATENATE_MULTI_DISPLAY_MAPPINGS - from .nodes.image.image_pad_for_outpaint_masked import NODE_CLASS_MAPPINGS as PAD_OUTPAINT_MAPPINGS - from .nodes.image.image_pad_for_outpaint_masked import NODE_DISPLAY_NAME_MAPPINGS as PAD_OUTPAINT_DISPLAY_MAPPINGS + from .nodes.image.image_pad_for_outpaint_masked import NODE_CLASS_MAPPINGS as PAD_OUTPAINT_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as PAD_OUTPAINT_DISPLAY_MAPPINGS - from .nodes.image.image_and_mask_preview import NODE_CLASS_MAPPINGS as AND_MASK_PREVIEW_MAPPINGS - from .nodes.image.image_and_mask_preview import NODE_DISPLAY_NAME_MAPPINGS as AND_MASK_PREVIEW_DISPLAY_MAPPINGS + from .nodes.image.image_and_mask_preview import NODE_CLASS_MAPPINGS as AND_MASK_PREVIEW_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as AND_MASK_PREVIEW_DISPLAY_MAPPINGS - from .nodes.image.imitation_hue_node import NODE_CLASS_MAPPINGS as IMITATION_HUE_MAPPINGS - from .nodes.image.imitation_hue_node import NODE_DISPLAY_NAME_MAPPINGS as IMITATION_HUE_DISPLAY_MAPPINGS + from .nodes.image.imitation_hue_node import NODE_CLASS_MAPPINGS as IMITATION_HUE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as IMITATION_HUE_DISPLAY_MAPPINGS except ImportError as e: print(f"导入错误: {e}") @@ -177,116 +186,101 @@ except ImportError as e: # 尝试导入其他可能有依赖的节点 try: - from .nodes.tools.fill_masked_area import NODE_CLASS_MAPPINGS as FILL_MASKED_MAPPINGS - from .nodes.tools.fill_masked_area import NODE_DISPLAY_NAME_MAPPINGS as FILL_MASKED_DISPLAY_MAPPINGS + from .nodes.tools.fill_masked_area import NODE_CLASS_MAPPINGS as FILL_MASKED_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as FILL_MASKED_DISPLAY_MAPPINGS except ImportError: FILL_MASKED_MAPPINGS = {} FILL_MASKED_DISPLAY_MAPPINGS = {} try: - from .nodes.audio.load_audio import NODE_CLASS_MAPPINGS as LOAD_AUDIO_MAPPINGS - from .nodes.audio.load_audio import NODE_DISPLAY_NAME_MAPPINGS as LOAD_AUDIO_DISPLAY_MAPPINGS + from .nodes.audio.load_audio import NODE_CLASS_MAPPINGS as LOAD_AUDIO_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as LOAD_AUDIO_DISPLAY_MAPPINGS except ImportError: LOAD_AUDIO_MAPPINGS = {} LOAD_AUDIO_DISPLAY_MAPPINGS = {} try: - from .nodes.image.empty_unit_generator import NODE_CLASS_MAPPINGS as IMAGE_GENERATOR_MAPPINGS - from .nodes.image.empty_unit_generator import NODE_DISPLAY_NAME_MAPPINGS as IMAGE_GENERATOR_DISPLAY_MAPPINGS + from .nodes.image.empty_unit_generator import NODE_CLASS_MAPPINGS as EMPTY_UNIT_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as EMPTY_UNIT_DISPLAY except ImportError: - IMAGE_GENERATOR_MAPPINGS = {} - IMAGE_GENERATOR_DISPLAY_MAPPINGS = {} + EMPTY_UNIT_MAPPINGS = {} + EMPTY_UNIT_DISPLAY = {} try: - from .nodes.image.image_ratio_detector import NODE_CLASS_MAPPINGS as IMAGE_DETECTOR_MAPPINGS - from .nodes.image.image_ratio_detector import NODE_DISPLAY_NAME_MAPPINGS as IMAGE_DETECTOR_DISPLAY_MAPPINGS + from .nodes.image.image_ratio_detector import NODE_CLASS_MAPPINGS as RATIO_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as RATIO_DISPLAY except ImportError: - IMAGE_DETECTOR_MAPPINGS = {} - IMAGE_DETECTOR_DISPLAY_MAPPINGS = {} + RATIO_MAPPINGS = {} + RATIO_DISPLAY = {} try: - from .nodes.image.depth_map_blur import NODE_CLASS_MAPPINGS as DEPTH_BLUR_MAPPINGS - from .nodes.image.depth_map_blur import NODE_DISPLAY_NAME_MAPPINGS as DEPTH_BLUR_DISPLAY_MAPPINGS + from .nodes.image.depth_map_blur import NODE_CLASS_MAPPINGS as DEPTH_BLUR_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as DEPTH_BLUR_DISPLAY except ImportError: DEPTH_BLUR_MAPPINGS = {} - DEPTH_BLUR_DISPLAY_MAPPINGS = {} + DEPTH_BLUR_DISPLAY = {} try: - from .nodes.image.image_concatenate import NODE_CLASS_MAPPINGS as CONCATENATE_MAPPINGS - from .nodes.image.image_concatenate import NODE_DISPLAY_NAME_MAPPINGS as CONCATENATE_DISPLAY_MAPPINGS + from .nodes.image.image_concatenate import NODE_CLASS_MAPPINGS as CONCAT_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CONCAT_DISPLAY except ImportError: - CONCATENATE_MAPPINGS = {} - CONCATENATE_DISPLAY_MAPPINGS = {} + CONCAT_MAPPINGS = {} + CONCAT_DISPLAY = {} try: - from .nodes.image.image_scale_by_aspect_ratio import NODE_CLASS_MAPPINGS as SCALE_ASPECT_MAPPINGS - from .nodes.image.image_scale_by_aspect_ratio import NODE_DISPLAY_NAME_MAPPINGS as SCALE_ASPECT_DISPLAY_MAPPINGS + from .nodes.image.image_scale_by_aspect_ratio import NODE_CLASS_MAPPINGS as SCALE_ASPECT_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as SCALE_ASPECT_DISPLAY except ImportError: SCALE_ASPECT_MAPPINGS = {} - SCALE_ASPECT_DISPLAY_MAPPINGS = {} + SCALE_ASPECT_DISPLAY = {} try: - from .nodes.image.image_mask_scale_as import NODE_CLASS_MAPPINGS as MASK_SCALE_MAPPINGS - from .nodes.image.image_mask_scale_as import NODE_DISPLAY_NAME_MAPPINGS as MASK_SCALE_DISPLAY_MAPPINGS + from .nodes.image.image_mask_scale_as import NODE_CLASS_MAPPINGS as MASK_SCALE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as MASK_SCALE_DISPLAY except ImportError: MASK_SCALE_MAPPINGS = {} - MASK_SCALE_DISPLAY_MAPPINGS = {} + MASK_SCALE_DISPLAY = {} try: - from .nodes.image.image_scale_restore import NODE_CLASS_MAPPINGS as SCALE_RESTORE_MAPPINGS - from .nodes.image.image_scale_restore import NODE_DISPLAY_NAME_MAPPINGS as SCALE_RESTORE_DISPLAY_MAPPINGS + from .nodes.image.image_scale_restore import NODE_CLASS_MAPPINGS as SCALE_RESTORE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as SCALE_RESTORE_DISPLAY except ImportError: SCALE_RESTORE_MAPPINGS = {} - SCALE_RESTORE_DISPLAY_MAPPINGS = {} + SCALE_RESTORE_DISPLAY = {} try: - from .nodes.image.image_remove_alpha import NODE_CLASS_MAPPINGS as REMOVE_ALPHA_MAPPINGS - from .nodes.image.image_remove_alpha import NODE_DISPLAY_NAME_MAPPINGS as REMOVE_ALPHA_DISPLAY_MAPPINGS + from .nodes.image.image_remove_alpha import NODE_CLASS_MAPPINGS as REMOVE_ALPHA_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as REMOVE_ALPHA_DISPLAY except ImportError: REMOVE_ALPHA_MAPPINGS = {} - REMOVE_ALPHA_DISPLAY_MAPPINGS = {} + REMOVE_ALPHA_DISPLAY = {} try: - from .nodes.image.image_combine_alpha import NODE_CLASS_MAPPINGS as COMBINE_ALPHA_MAPPINGS - from .nodes.image.image_combine_alpha import NODE_DISPLAY_NAME_MAPPINGS as COMBINE_ALPHA_DISPLAY_MAPPINGS + from .nodes.image.image_combine_alpha import NODE_CLASS_MAPPINGS as COMBINE_ALPHA_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as COMBINE_ALPHA_DISPLAY except ImportError: COMBINE_ALPHA_MAPPINGS = {} - COMBINE_ALPHA_DISPLAY_MAPPINGS = {} + COMBINE_ALPHA_DISPLAY = {} try: - from .nodes.image.check_mask import NODE_CLASS_MAPPINGS as CHECK_MASK_MAPPINGS - from .nodes.image.check_mask import NODE_DISPLAY_NAME_MAPPINGS as CHECK_MASK_DISPLAY_MAPPINGS + from .nodes.image.check_mask import NODE_CLASS_MAPPINGS as CHECK_MASK_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CHECK_MASK_DISPLAY except ImportError: CHECK_MASK_MAPPINGS = {} - CHECK_MASK_DISPLAY_MAPPINGS = {} + CHECK_MASK_DISPLAY = {} try: - from .nodes.image.purge_vram import NODE_CLASS_MAPPINGS as PURGE_VRAM_MAPPINGS - from .nodes.image.purge_vram import NODE_DISPLAY_NAME_MAPPINGS as PURGE_VRAM_DISPLAY_MAPPINGS + from .nodes.image.purge_vram import NODE_CLASS_MAPPINGS as PURGE_VRAM_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as PURGE_VRAM_DISPLAY except ImportError: PURGE_VRAM_MAPPINGS = {} - PURGE_VRAM_DISPLAY_MAPPINGS = {} + PURGE_VRAM_DISPLAY = {} try: - from .nodes.image.crop_by_mask import NODE_CLASS_MAPPINGS as CROP_MASK_MAPPINGS - from .nodes.image.crop_by_mask import NODE_DISPLAY_NAME_MAPPINGS as CROP_MASK_DISPLAY_MAPPINGS + from .nodes.image.crop_by_mask import NODE_CLASS_MAPPINGS as CROP_MASK_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as CROP_MASK_DISPLAY except ImportError: CROP_MASK_MAPPINGS = {} - CROP_MASK_DISPLAY_MAPPINGS = {} + CROP_MASK_DISPLAY = {} try: - from .nodes.image.restore_crop_box import NODE_CLASS_MAPPINGS as RESTORE_CROP_MAPPINGS - from .nodes.image.restore_crop_box import NODE_DISPLAY_NAME_MAPPINGS as RESTORE_CROP_DISPLAY_MAPPINGS + from .nodes.image.restore_crop_box import NODE_CLASS_MAPPINGS as RESTORE_CROP_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as RESTORE_CROP_DISPLAY except ImportError: RESTORE_CROP_MAPPINGS = {} - RESTORE_CROP_DISPLAY_MAPPINGS = {} + RESTORE_CROP_DISPLAY = {} # 合并所有节点映射 NODE_CLASS_MAPPINGS = {} -NODE_CLASS_MAPPINGS.update(IMAGE_GENERATOR_MAPPINGS) -NODE_CLASS_MAPPINGS.update(IMAGE_DETECTOR_MAPPINGS) +NODE_CLASS_MAPPINGS.update(EMPTY_UNIT_MAPPINGS) +NODE_CLASS_MAPPINGS.update(RATIO_MAPPINGS) NODE_CLASS_MAPPINGS.update(DEPTH_BLUR_MAPPINGS) -NODE_CLASS_MAPPINGS.update(CONCATENATE_MAPPINGS) +NODE_CLASS_MAPPINGS.update(CONCAT_MAPPINGS) NODE_CLASS_MAPPINGS.update(CONCATENATE_MULTI_MAPPINGS) NODE_CLASS_MAPPINGS.update(PAD_OUTPAINT_MAPPINGS) NODE_CLASS_MAPPINGS.update(AND_MASK_PREVIEW_MAPPINGS) @@ -302,29 +296,29 @@ NODE_CLASS_MAPPINGS.update(CROP_MASK_MAPPINGS) NODE_CLASS_MAPPINGS.update(RESTORE_CROP_MAPPINGS) NODE_CLASS_MAPPINGS.update(SHOW_NODES_MAPPINGS) NODE_CLASS_MAPPINGS.update(FILL_MASKED_MAPPINGS) +NODE_CLASS_MAPPINGS.update(MASK_OPERATIONS_MAPPINGS) NODE_CLASS_MAPPINGS.update(LOAD_AUDIO_MAPPINGS) NODE_CLASS_MAPPINGS.update(AUDIO_CROP_MAPPINGS) -NODE_CLASS_MAPPINGS.update(MASK_OPERATIONS_MAPPINGS) # 合并显示名称映射 NODE_DISPLAY_NAME_MAPPINGS = {} -NODE_DISPLAY_NAME_MAPPINGS.update(IMAGE_GENERATOR_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(IMAGE_DETECTOR_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(DEPTH_BLUR_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(CONCATENATE_DISPLAY_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(EMPTY_UNIT_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(RATIO_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(DEPTH_BLUR_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(CONCAT_DISPLAY) NODE_DISPLAY_NAME_MAPPINGS.update(CONCATENATE_MULTI_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(PAD_OUTPAINT_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(AND_MASK_PREVIEW_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(IMITATION_HUE_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(SCALE_ASPECT_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(MASK_SCALE_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(SCALE_RESTORE_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(REMOVE_ALPHA_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(COMBINE_ALPHA_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(CHECK_MASK_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(PURGE_VRAM_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(CROP_MASK_DISPLAY_MAPPINGS) -NODE_DISPLAY_NAME_MAPPINGS.update(RESTORE_CROP_DISPLAY_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(SCALE_ASPECT_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(MASK_SCALE_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(SCALE_RESTORE_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(REMOVE_ALPHA_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(COMBINE_ALPHA_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(CHECK_MASK_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(PURGE_VRAM_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(CROP_MASK_DISPLAY) +NODE_DISPLAY_NAME_MAPPINGS.update(RESTORE_CROP_DISPLAY) NODE_DISPLAY_NAME_MAPPINGS.update(SHOW_NODES_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(FILL_MASKED_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(LOAD_AUDIO_DISPLAY_MAPPINGS) @@ -371,4 +365,13 @@ __all__ = [ "__email__", "__url__", "CHANGELOG", -] +] + +# 调试:输出所有注册节点的分组属性 +if __name__ == "__main__": + with open("node_category_report.txt", "w", encoding="utf-8") as f: + f.write("=== UniversalToolkit 节点分组属性清单 ===\n") + for k, v in NODE_CLASS_MAPPINGS.items(): + cat = getattr(v, 'CATEGORY', '无CATEGORY') + f.write(f"{k}: CATEGORY = {cat}\n") + print("节点分组清单已导出到 node_category_report.txt") diff --git a/node_category_report.txt b/node_category_report.txt new file mode 100644 index 0000000..53a8f13 --- /dev/null +++ b/node_category_report.txt @@ -0,0 +1 @@ +=== UniversalToolkit 节点分组属性清单 === diff --git a/nodes/audio/audio_crop_process.py b/nodes/audio/audio_crop_process.py index 29d5e28..32010d2 100644 --- a/nodes/audio/audio_crop_process.py +++ b/nodes/audio/audio_crop_process.py @@ -13,7 +13,7 @@ import torch FLOAT_MAX = 99999999999999999.0 class AudioCropProcessUTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Audio" @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/audio/load_audio.py b/nodes/audio/load_audio.py index 198ac9e..d6ad803 100644 --- a/nodes/audio/load_audio.py +++ b/nodes/audio/load_audio.py @@ -18,7 +18,7 @@ import librosa FLOAT_MAX = 99999999999999999.0 class LoadAudioPlusFromPath_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Audio" @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/image/check_mask.py b/nodes/image/check_mask.py index 1610248..83c3ac3 100644 --- a/nodes/image/check_mask.py +++ b/nodes/image/check_mask.py @@ -2,26 +2,31 @@ Check Mask Node ~~~~~~~~~~~~~~ -Checks if a mask is valid based on white area percentage. +Check if a mask is valid and provide information about it. :copyright: (c) 2024 by May :license: MIT, see LICENSE for more details. """ import torch -from PIL import Image import numpy as np -from ...common_utils import log, tensor2pil, pil2tensor +from PIL import Image +import cv2 + +from ..tools.logging_utils import log +from .image_converters import tensor2pil, pil2tensor def mask_white_area(mask, white_point): """Calculate the percentage of white area in mask""" + if mask is None: + return 0.0 mask_array = np.array(mask) white_pixels = np.sum(mask_array > white_point) total_pixels = mask_array.size return white_pixels / total_pixels if total_pixels > 0 else 0 class CheckMask_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): @@ -41,14 +46,24 @@ class CheckMask_UTK: def check_mask(self, mask, white_point, area_percent,): + if mask is None: + log("CheckMask_UTK: mask is None", message_type="warning") + return (False,) + if mask.dim() == 2: mask = torch.unsqueeze(mask, 0) - mask = tensor2pil(mask[0]) - if mask.width * mask.height > 262144: + + mask_pil = tensor2pil(mask[0]) + if mask_pil is None: + log("CheckMask_UTK: Failed to convert mask to PIL", message_type="warning") + return (False,) + + if mask_pil.width * mask_pil.height > 262144: target_width = 512 - target_height = int(target_width * mask.height / mask.width) - mask = mask.resize((target_width, target_height), Image.LANCZOS) - ret = mask_white_area(mask, white_point) * 100 > area_percent + target_height = int(target_width * mask_pil.height / mask_pil.width) + mask_pil = mask_pil.resize((target_width, target_height), Image.LANCZOS) + + ret = mask_white_area(mask_pil, white_point) * 100 > area_percent log(f"CheckMask_UTK:{ret}", message_type="finish") return (ret,) diff --git a/nodes/common_utils.py b/nodes/image/color_utils.py similarity index 73% rename from nodes/common_utils.py rename to nodes/image/color_utils.py index 5646499..3900de5 100644 --- a/nodes/common_utils.py +++ b/nodes/image/color_utils.py @@ -1,133 +1,29 @@ """ -ComfyUI Universal Toolkit - Common Utilities -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Color Utilities for UniversalToolkit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Common utility functions and constants used across all nodes. +Color processing utilities for UniversalToolkit. :copyright: (c) 2024 by May :license: MIT, see LICENSE for more details. """ -import torch -import torch.nn.functional as F import numpy as np -import re -import math -import random -import os -import json import cv2 -from comfy.utils import ProgressBar, common_upscale -from PIL import Image -from PIL.PngImagePlugin import PngInfo -# Import ComfyUI modules with fallbacks -MAX_RESOLUTION = 8192 -SaveImage = None -ImageCompositeMasked = None -args = None -folder_paths = None - -try: - from nodes import MAX_RESOLUTION, SaveImage -except ImportError: - pass - -try: - from comfy_extras.nodes_mask import ImageCompositeMasked -except ImportError: - pass - -try: - from comfy.cli_args import args -except ImportError: - pass - -try: - import folder_paths -except ImportError: - pass - -def log(message, message_type='info'): - """简单的日志函数""" - if message_type == 'error': - print(f"❌ Error: {message}") - elif message_type == 'warning': - print(f"⚠️ Warning: {message}") - elif message_type == 'finish': - print(f"✅ {message}") - else: - print(f"ℹ️ {message}") - -def tensor2pil(image): - """将torch张量转换为PIL图像""" - if image.dim() == 4: - image = image.squeeze(0) - if image.dim() == 3: - if image.shape[0] == 1: # 灰度图 - image = image.squeeze(0) - image = (image * 255).clamp(0, 255).to(torch.uint8) - return Image.fromarray(image.cpu().numpy(), mode='L') - else: # RGB图 - image = image.permute(1, 2, 0) - image = (image * 255).clamp(0, 255).to(torch.uint8) - return Image.fromarray(image.cpu().numpy(), mode='RGB') - return None - -def pil2tensor(image): - """将PIL图像转换为torch张量""" - if image.mode == 'L': - image = image.convert('RGB') - image = np.array(image).astype(np.float32) / 255.0 - image = torch.from_numpy(image) - if image.dim() == 3: - image = image.permute(2, 0, 1) - return image - -def image2mask(image): - """将PIL图像转换为掩码张量""" - if image.mode == 'L': - return torch.tensor([pil2tensor(image)[0, :, :].tolist()]) - else: - image = image.convert('RGB').split()[0] - return torch.tensor([pil2tensor(image)[0, :, :].tolist()]) - -def tensor2cv2(image: torch.Tensor) -> np.array: - """将torch张量转换为OpenCV格式""" - if image.dim() == 4: - image = image.squeeze() - npimage = image.numpy() - cv2image = np.uint8(npimage * 255 / npimage.max()) - return cv2.cvtColor(cv2image, cv2.COLOR_RGB2BGR) - -def pil2cv2(pil_img): - """将PIL图像转换为OpenCV格式""" - np_img_array = np.asarray(pil_img) - return cv2.cvtColor(np_img_array, cv2.COLOR_RGB2BGR) - -def cv22pil(cv2_img): - """将OpenCV图像转换为PIL图像""" - cv2_img = cv2.cvtColor(cv2_img, cv2.COLOR_BGR2RGB) - return Image.fromarray(cv2_img) - -def tensor2np(tensor): - """将torch张量转换为numpy数组""" - if len(tensor.shape) == 3: # Single image - return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8) - else: # Batch of images - return [np.clip(255.0 * t.cpu().numpy(), 0, 255).astype(np.uint8) for t in tensor] - -# Color transfer functions for ImitationHueNode def image_stats(image): + """计算图像的统计信息""" return np.mean(image[:, :, 1:], axis=(0, 1)), np.std(image[:, :, 1:], axis=(0, 1)) def is_skin_or_lips(lab_image): + """检测皮肤和嘴唇区域""" l, a, b = lab_image[:, :, 0], lab_image[:, :, 1], lab_image[:, :, 2] skin = (l > 20) & (l < 250) & (a > 120) & (a < 180) & (b > 120) & (b < 190) lips = (l > 20) & (l < 200) & (a > 150) & (b > 140) return (skin | lips).astype(np.float32) def adjust_brightness(image, factor, mask=None): + """调整图像亮度""" hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) v = hsv[:, :, 2].astype(np.float32) if mask is not None: @@ -139,6 +35,7 @@ def adjust_brightness(image, factor, mask=None): return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) def adjust_saturation(image, factor, mask=None): + """调整图像饱和度""" hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) s = hsv[:, :, 1].astype(np.float32) if mask is not None: @@ -150,6 +47,7 @@ def adjust_saturation(image, factor, mask=None): return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) def adjust_contrast(image, factor, mask=None): + """调整图像对比度""" mean = np.mean(image) adjusted = image.astype(np.float32) if mask is not None: @@ -161,6 +59,7 @@ def adjust_contrast(image, factor, mask=None): return adjusted.astype(np.uint8) def adjust_tone(source, target, tone_strength=0.7, mask=None): + """调整图像影调""" h, w = target.shape[:2] source = cv2.resize(source, (w, h)) lab_image = cv2.cvtColor(target, cv2.COLOR_BGR2LAB).astype(np.float32) @@ -206,6 +105,7 @@ def adjust_tone(source, target, tone_strength=0.7, mask=None): def color_transfer(source, target, mask=None, strength=1.0, skin_protection=0.2, auto_brightness=True, brightness_range=0.5, auto_contrast=False, contrast_range=0.5, auto_saturation=False, saturation_range=0.5, auto_tone=False, tone_strength=0.7): + """色彩迁移函数""" source_lab = cv2.cvtColor(source, cv2.COLOR_BGR2LAB).astype(np.float32) target_lab = cv2.cvtColor(target, cv2.COLOR_BGR2LAB).astype(np.float32) diff --git a/nodes/image/crop_by_mask.py b/nodes/image/crop_by_mask.py index acbe335..a640d59 100644 --- a/nodes/image/crop_by_mask.py +++ b/nodes/image/crop_by_mask.py @@ -54,7 +54,7 @@ def draw_rect(image, x, y, width, height, line_color="#FF0000", line_width=2): return image class CropByMask_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): diff --git a/nodes/image/depth_map_blur.py b/nodes/image/depth_map_blur.py index 7dd520a..5fb70ae 100644 --- a/nodes/image/depth_map_blur.py +++ b/nodes/image/depth_map_blur.py @@ -15,7 +15,7 @@ import numpy as np import folder_paths class DepthMapBlur_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(s): diff --git a/nodes/image/empty_unit_generator.py b/nodes/image/empty_unit_generator.py index f15b0d6..73b6b25 100644 --- a/nodes/image/empty_unit_generator.py +++ b/nodes/image/empty_unit_generator.py @@ -2,7 +2,7 @@ Empty Unit Generator Node ~~~~~~~~~~~~~~~~~~~~~~~~~ -Generates empty images, masks, and latents with various preset ratios and configurations. +Generate empty units for testing and development. :copyright: (c) 2024 by May :license: MIT, see LICENSE for more details. @@ -12,10 +12,12 @@ import torch import numpy as np import re from PIL import Image -from ..common_utils import log +import cv2 + +from ..tools.logging_utils import log class EmptyUnitGenerator_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): @@ -108,5 +110,5 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "EmptyUnitGenerator_UTK": "Empty Unit Generator", + "EmptyUnitGenerator_UTK": "Empty Unit Generator (UTK)", } \ No newline at end of file diff --git a/nodes/image/image_and_mask_preview.py b/nodes/image/image_and_mask_preview.py index 1f1e95a..567e56a 100644 --- a/nodes/image/image_and_mask_preview.py +++ b/nodes/image/image_and_mask_preview.py @@ -9,9 +9,12 @@ Preview an image or a mask, when both inputs are used composites the mask on top """ import random -from nodes import SaveImage, ImageCompositeMasked +from nodes import SaveImage import folder_paths +# 导入本地的 ImageCompositeMasked 实现 +from .image_composite_masked import ImageCompositeMasked + class ImageAndMaskPreview_UTK(SaveImage): def __init__(self): self.output_dir = folder_paths.get_temp_directory() @@ -37,7 +40,7 @@ class ImageAndMaskPreview_UTK(SaveImage): RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("composite",) FUNCTION = "execute" - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" DESCRIPTION = """ Preview an image or a mask, when both inputs are used composites the mask on top of the image. diff --git a/nodes/image/image_combine_alpha.py b/nodes/image/image_combine_alpha.py index 7ccc4df..5ef9634 100644 --- a/nodes/image/image_combine_alpha.py +++ b/nodes/image/image_combine_alpha.py @@ -31,7 +31,7 @@ def image_channel_merge(channels, mode): return Image.merge(mode, channels) class ImageCombineAlpha_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): diff --git a/nodes/image/image_composite_masked.py b/nodes/image/image_composite_masked.py new file mode 100644 index 0000000..9545a9a --- /dev/null +++ b/nodes/image/image_composite_masked.py @@ -0,0 +1,127 @@ +""" +Image Composite Masked Node +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Local implementation of ImageCompositeMasked for UniversalToolkit. + +:copyright: (c) 2024 by May +:license: MIT, see LICENSE for more details. +""" + +import torch +import torch.nn.functional as F + +class ImageCompositeMasked: + """ + Local implementation of ImageCompositeMasked for compositing images with masks. + Based on KJNodes implementation. + """ + + @staticmethod + def composite(self, image, mask_image, x, y, resize_mask, mask): + """ + Composite an image with a mask at specified position. + + Args: + image: Base image tensor (B, H, W, C) + mask_image: Mask image tensor (B, H, W, C) + x: X offset + y: Y offset + resize_mask: Whether to resize mask to match image + mask: Alpha mask tensor (B, H, W) + + Returns: + Composited image tensor + """ + if image is None: + return (mask_image,) + + if mask_image is None: + return (image,) + + # Ensure tensors are on the same device + device = image.device + mask_image = mask_image.to(device) + mask = mask.to(device) if mask is not None else None + + # Get dimensions + batch_size, image_height, image_width, channels = image.shape + mask_batch_size, mask_height, mask_width, mask_channels = mask_image.shape + + # Handle batch size mismatch + if batch_size != mask_batch_size: + if batch_size == 1: + mask_image = mask_image[:1] + elif mask_batch_size == 1: + mask_image = mask_image.expand(batch_size, -1, -1, -1) + else: + raise ValueError("Batch sizes must match or one must be 1") + + # Resize mask if needed + if resize_mask and (mask_height != image_height or mask_width != image_width): + mask_image = F.interpolate( + mask_image.permute(0, 3, 1, 2), # (B, C, H, W) + size=(image_height, image_width), + mode='bilinear', + align_corners=False + ).permute(0, 2, 3, 1) # (B, H, W, C) + + # Apply mask if provided + if mask is not None: + if mask.shape[1:] != (image_height, image_width): + mask = F.interpolate( + mask.unsqueeze(1), # (B, 1, H, W) + size=(image_height, image_width), + mode='bilinear', + align_corners=False + ).squeeze(1) # (B, H, W) + + # Expand mask to match channels + mask = mask.unsqueeze(-1).expand(-1, -1, -1, channels) + mask_image = mask_image * mask + + # Calculate crop region + if x < 0: + crop_x = -x + x = 0 + else: + crop_x = 0 + + if y < 0: + crop_y = -y + y = 0 + else: + crop_y = 0 + + # Crop mask image if needed + if crop_x > 0 or crop_y > 0: + mask_image = mask_image[:, crop_y:, crop_x:, :] + + # Calculate final dimensions + mask_height, mask_width = mask_image.shape[1:3] + + # Check bounds + if x + mask_width > image_width: + mask_width = image_width - x + mask_image = mask_image[:, :, :mask_width, :] + + if y + mask_height > image_height: + mask_height = image_height - y + mask_image = mask_image[:, :mask_height, :, :] + + # Create output image + result = image.clone() + + # Composite mask image onto result + if mask is not None: + # Use alpha blending + alpha = mask[:, y:y+mask_height, x:x+mask_width, :] + result[:, y:y+mask_height, x:x+mask_width, :] = ( + result[:, y:y+mask_height, x:x+mask_width, :] * (1 - alpha) + + mask_image * alpha + ) + else: + # Direct replacement + result[:, y:y+mask_height, x:x+mask_width, :] = mask_image + + return (result,) \ No newline at end of file diff --git a/nodes/image/image_concatenate.py b/nodes/image/image_concatenate.py index a45c80b..26a3442 100644 --- a/nodes/image/image_concatenate.py +++ b/nodes/image/image_concatenate.py @@ -13,7 +13,7 @@ import torch.nn.functional as F from ..common_utils import log class ImageConcatenate_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): @@ -201,5 +201,5 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "ImageConcatenate_UTK": "Image Concatenate", + "ImageConcatenate_UTK": "Image Concatenate (UTK)", } \ No newline at end of file diff --git a/nodes/image/image_concatenate_multi.py b/nodes/image/image_concatenate_multi.py index f768dd2..0a77672 100644 --- a/nodes/image/image_concatenate_multi.py +++ b/nodes/image/image_concatenate_multi.py @@ -11,7 +11,7 @@ Concatenates multiple images in various directions and layouts. import torch class ImageConcatenateMulti_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): diff --git a/nodes/image/image_converters.py b/nodes/image/image_converters.py new file mode 100644 index 0000000..e906847 --- /dev/null +++ b/nodes/image/image_converters.py @@ -0,0 +1,72 @@ +""" +Image Converters for UniversalToolkit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Image format conversion utilities for UniversalToolkit. + +:copyright: (c) 2024 by May +:license: MIT, see LICENSE for more details. +""" + +import torch +import numpy as np +import cv2 +from PIL import Image + +def tensor2pil(image): + """将torch张量转换为PIL图像""" + if image.dim() == 4: + image = image.squeeze(0) + if image.dim() == 3: + if image.shape[0] == 1: # 灰度图 + image = image.squeeze(0) + image = (image * 255).clamp(0, 255).to(torch.uint8) + return Image.fromarray(image.cpu().numpy(), mode='L') + else: # RGB图 + image = image.permute(1, 2, 0) + image = (image * 255).clamp(0, 255).to(torch.uint8) + return Image.fromarray(image.cpu().numpy(), mode='RGB') + return None + +def pil2tensor(image): + """将PIL图像转换为torch张量""" + if image.mode == 'L': + image = image.convert('RGB') + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image) + if image.dim() == 3: + image = image.permute(2, 0, 1) + return image + +def image2mask(image): + """将PIL图像转换为掩码张量""" + if image.mode == 'L': + return torch.tensor([pil2tensor(image)[0, :, :].tolist()]) + else: + image = image.convert('RGB').split()[0] + return torch.tensor([pil2tensor(image)[0, :, :].tolist()]) + +def tensor2cv2(image: torch.Tensor) -> np.array: + """将torch张量转换为OpenCV格式""" + if image.dim() == 4: + image = image.squeeze() + npimage = image.numpy() + cv2image = np.uint8(npimage * 255 / npimage.max()) + return cv2.cvtColor(cv2image, cv2.COLOR_RGB2BGR) + +def pil2cv2(pil_img): + """将PIL图像转换为OpenCV格式""" + np_img_array = np.asarray(pil_img) + return cv2.cvtColor(np_img_array, cv2.COLOR_RGB2BGR) + +def cv22pil(cv2_img): + """将OpenCV图像转换为PIL图像""" + cv2_img = cv2.cvtColor(cv2_img, cv2.COLOR_BGR2RGB) + return Image.fromarray(cv2_img) + +def tensor2np(tensor): + """将torch张量转换为numpy数组""" + if len(tensor.shape) == 3: # Single image + return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8) + else: # Batch of images + return [np.clip(255.0 * t.cpu().numpy(), 0, 255).astype(np.uint8) for t in tensor] \ No newline at end of file diff --git a/nodes/image/image_mask_scale_as.py b/nodes/image/image_mask_scale_as.py index 876c134..65cecf9 100644 --- a/nodes/image/image_mask_scale_as.py +++ b/nodes/image/image_mask_scale_as.py @@ -52,7 +52,7 @@ def fit_resize_image(image, target_width, target_height, fit_mode, resize_sample return image.resize((target_width, target_height), resize_sampler) class ImageMaskScaleAs_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): diff --git a/nodes/image/image_pad_for_outpaint_masked.py b/nodes/image/image_pad_for_outpaint_masked.py index e6b2b6a..b29a361 100644 --- a/nodes/image/image_pad_for_outpaint_masked.py +++ b/nodes/image/image_pad_for_outpaint_masked.py @@ -14,7 +14,7 @@ import torch.nn.functional as F MAX_RESOLUTION = 8192 class ImagePadForOutpaintMasked_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): diff --git a/nodes/image/image_ratio_detector.py b/nodes/image/image_ratio_detector.py index 576a632..982d18a 100644 --- a/nodes/image/image_ratio_detector.py +++ b/nodes/image/image_ratio_detector.py @@ -1,18 +1,20 @@ """ Image Ratio Detector Node -~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~ -Detects and analyzes image aspect ratios and dimensions. +Detect the aspect ratio of an image. :copyright: (c) 2024 by May :license: MIT, see LICENSE for more details. """ +import torch import math -from ..common_utils import log + +from ..tools.logging_utils import log class ImageRatioDetector_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): @@ -73,5 +75,5 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "ImageRatioDetector_UTK": "Image Ratio Detector", + "ImageRatioDetector_UTK": "Image Ratio Detector (UTK)", } \ No newline at end of file diff --git a/nodes/image/image_remove_alpha.py b/nodes/image/image_remove_alpha.py index 10ec4c9..4a45439 100644 --- a/nodes/image/image_remove_alpha.py +++ b/nodes/image/image_remove_alpha.py @@ -13,7 +13,7 @@ from PIL import Image from ...common_utils import log, tensor2pil, pil2tensor class ImageRemoveAlpha_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): diff --git a/nodes/image/image_scale_by_aspect_ratio.py b/nodes/image/image_scale_by_aspect_ratio.py index 55eff2f..8e3982e 100644 --- a/nodes/image/image_scale_by_aspect_ratio.py +++ b/nodes/image/image_scale_by_aspect_ratio.py @@ -54,7 +54,7 @@ def fit_resize_image(image, target_width, target_height, fit_mode, resize_sample return image.resize((target_width, target_height), resize_sampler) class ImageScaleByAspectRatio_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): diff --git a/nodes/image/image_scale_restore.py b/nodes/image/image_scale_restore.py index 7fa7726..591b12d 100644 --- a/nodes/image/image_scale_restore.py +++ b/nodes/image/image_scale_restore.py @@ -13,7 +13,7 @@ from PIL import Image from ...common_utils import log, tensor2pil, pil2tensor, image2mask class ImageScaleRestore_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): diff --git a/nodes/image/imitation_hue_node.py b/nodes/image/imitation_hue_node.py index a898872..f630404 100644 --- a/nodes/image/imitation_hue_node.py +++ b/nodes/image/imitation_hue_node.py @@ -12,187 +12,8 @@ import torch import numpy as np import cv2 -def image_stats(image): - return np.mean(image[:, :, 1:], axis=(0, 1)), np.std(image[:, :, 1:], axis=(0, 1)) - -def is_skin_or_lips(lab_image): - l, a, b = lab_image[:, :, 0], lab_image[:, :, 1], lab_image[:, :, 2] - skin = (l > 20) & (l < 250) & (a > 120) & (a < 180) & (b > 120) & (b < 190) - lips = (l > 20) & (l < 200) & (a > 150) & (b > 140) - return (skin | lips).astype(np.float32) - -def adjust_brightness(image, factor, mask=None): - hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) - v = hsv[:, :, 2].astype(np.float32) - if mask is not None: - mask = mask.squeeze() - v = np.where(mask > 0, np.clip(v * factor, 0, 255), v) - else: - v = np.clip(v * factor, 0, 255) - hsv[:, :, 2] = v.astype(np.uint8) - return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) - -def adjust_saturation(image, factor, mask=None): - hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) - s = hsv[:, :, 1].astype(np.float32) - if mask is not None: - mask = mask.squeeze() - s = np.where(mask > 0, np.clip(s * factor, 0, 255), s) - else: - s = np.clip(s * factor, 0, 255) - hsv[:, :, 1] = s.astype(np.uint8) - return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) - -def adjust_contrast(image, factor, mask=None): - mean = np.mean(image) - adjusted = image.astype(np.float32) - if mask is not None: - mask = mask.squeeze() - mask = np.repeat(mask[:, :, np.newaxis], 3, axis=2) - adjusted = np.where(mask > 0, np.clip((adjusted - mean) * factor + mean, 0, 255), adjusted) - else: - adjusted = np.clip((adjusted - mean) * factor + mean, 0, 255) - return adjusted.astype(np.uint8) - -def adjust_tone(source, target, tone_strength=0.7, mask=None): - h, w = target.shape[:2] - source = cv2.resize(source, (w, h)) - lab_image = cv2.cvtColor(target, cv2.COLOR_BGR2LAB).astype(np.float32) - lab_source = cv2.cvtColor(source, cv2.COLOR_BGR2LAB).astype(np.float32) - l_image = lab_image[:,:,0] - l_source = lab_source[:,:,0] - - if mask is not None: - mask = cv2.resize(mask, (w, h)) - mask = mask.astype(np.float32) / 255.0 - l_adjusted = np.copy(l_image) - mean_source = np.mean(l_source[mask > 0]) - std_source = np.std(l_source[mask > 0]) - mean_target = np.mean(l_image[mask > 0]) - std_target = np.std(l_image[mask > 0]) - l_adjusted[mask > 0] = (l_image[mask > 0] - mean_target) * (std_source / (std_target + 1e-6)) * 0.7 + mean_source - l_adjusted[mask > 0] = np.clip(l_adjusted[mask > 0], 0, 255) - clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8,8)) - l_enhanced = clahe.apply(l_adjusted.astype(np.uint8)) - l_final = cv2.addWeighted(l_adjusted, 0.7, l_enhanced.astype(np.float32), 0.3, 0) - l_final = np.clip(l_final, 0, 255) - l_contrast = cv2.addWeighted(l_final, 1.3, l_final, 0, -20) - l_contrast = np.clip(l_contrast, 0, 255) - l_image[mask > 0] = l_image[mask > 0] * (1 - tone_strength) + l_contrast[mask > 0] * tone_strength - else: - mean_source = np.mean(l_source) - std_source = np.std(l_source) - l_mean = np.mean(l_image) - l_std = np.std(l_image) - l_adjusted = (l_image - l_mean) * (std_source / (l_std + 1e-6)) * 0.7 + mean_source - l_adjusted = np.clip(l_adjusted, 0, 255) - clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8,8)) - l_enhanced = clahe.apply(l_adjusted.astype(np.uint8)) - l_final = cv2.addWeighted(l_adjusted, 0.7, l_enhanced.astype(np.float32), 0.3, 0) - l_final = np.clip(l_final, 0, 255) - l_contrast = cv2.addWeighted(l_final, 1.3, l_final, 0, -20) - l_contrast = np.clip(l_contrast, 0, 255) - l_image = l_image * (1 - tone_strength) + l_contrast * tone_strength - - lab_image[:,:,0] = l_image - return cv2.cvtColor(lab_image.astype(np.uint8), cv2.COLOR_LAB2BGR) - -def tensor2cv2(image: torch.Tensor) -> np.array: - if image.dim() == 4: - image = image.squeeze() - npimage = image.numpy() - cv2image = np.uint8(npimage * 255 / npimage.max()) - return cv2.cvtColor(cv2image, cv2.COLOR_RGB2BGR) - -def color_transfer(source, target, mask=None, strength=1.0, skin_protection=0.2, auto_brightness=True, - brightness_range=0.5, auto_contrast=False, contrast_range=0.5, - auto_saturation=False, saturation_range=0.5, auto_tone=False, tone_strength=0.7): - source_lab = cv2.cvtColor(source, cv2.COLOR_BGR2LAB).astype(np.float32) - target_lab = cv2.cvtColor(target, cv2.COLOR_BGR2LAB).astype(np.float32) - - src_means, src_stds = image_stats(source_lab) - tar_means, tar_stds = image_stats(target_lab) - - skin_lips_mask = is_skin_or_lips(target_lab.astype(np.uint8)) - skin_lips_mask = cv2.GaussianBlur(skin_lips_mask, (5, 5), 0) - - if mask is not None: - mask = cv2.resize(mask, (target.shape[1], target.shape[0])) - mask = mask.astype(np.float32) / 255.0 - - result_lab = target_lab.copy() - for i in range(1, 3): - adjusted_channel = (target_lab[:, :, i] - tar_means[i - 1]) * (src_stds[i - 1] / (tar_stds[i - 1] + 1e-6)) + \ - src_means[i - 1] - adjusted_channel = np.clip(adjusted_channel, 0, 255) - - if mask is not None: - result_lab[:, :, i] = target_lab[:, :, i] * (1 - mask) + \ - (target_lab[:, :, i] * skin_lips_mask * skin_protection + \ - adjusted_channel * skin_lips_mask * (1 - skin_protection) + \ - adjusted_channel * (1 - skin_lips_mask)) * mask - else: - result_lab[:, :, i] = target_lab[:, :, i] * skin_lips_mask * skin_protection + \ - adjusted_channel * skin_lips_mask * (1 - skin_protection) + \ - adjusted_channel * (1 - skin_lips_mask) - - result_bgr = cv2.cvtColor(result_lab.astype(np.uint8), cv2.COLOR_LAB2BGR) - final_result = cv2.addWeighted(target, 1 - strength, result_bgr, strength, 0) - - if mask is not None: - mask = cv2.resize(mask, (target.shape[1], target.shape[0])) - mask = mask.astype(np.float32) / 255.0 - if auto_brightness: - source_brightness = np.mean(cv2.cvtColor(source, cv2.COLOR_BGR2GRAY)) - target_brightness = np.mean(cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)) - brightness_difference = source_brightness - target_brightness - brightness_factor = 1.0 + np.clip(brightness_difference / 255 * brightness_range, brightness_range*-1, brightness_range) - final_result = adjust_brightness(final_result, brightness_factor, mask) - if auto_contrast: - source_gray = cv2.cvtColor(source, cv2.COLOR_BGR2GRAY) - target_gray = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY) - source_contrast = np.std(source_gray) - target_contrast = np.std(target_gray) - contrast_difference = source_contrast - target_contrast - contrast_factor = 1.0 + np.clip(contrast_difference / 255, contrast_range*-1, contrast_range) - final_result = adjust_contrast(final_result, contrast_factor, mask) - if auto_saturation: - source_hsv = cv2.cvtColor(source, cv2.COLOR_BGR2HSV) - target_hsv = cv2.cvtColor(target, cv2.COLOR_BGR2HSV) - source_saturation = np.mean(source_hsv[:, :, 1]) - target_saturation = np.mean(target_hsv[:, :, 1]) - saturation_difference = source_saturation - target_saturation - saturation_factor = 1.0 + np.clip(saturation_difference / 255, saturation_range*-1, saturation_range) - final_result = adjust_saturation(final_result, saturation_factor, mask) - if auto_tone: - final_result = adjust_tone(source, final_result, tone_strength, mask) - else: - if auto_brightness: - source_brightness = np.mean(cv2.cvtColor(source, cv2.COLOR_BGR2GRAY)) - target_brightness = np.mean(cv2.cvtColor(target, cv2.COLOR_BGR2GRAY)) - brightness_difference = source_brightness - target_brightness - brightness_factor = 1.0 + np.clip(brightness_difference / 255 * brightness_range, brightness_range*-1, brightness_range) - final_result = adjust_brightness(final_result, brightness_factor) - if auto_contrast: - source_gray = cv2.cvtColor(source, cv2.COLOR_BGR2GRAY) - target_gray = cv2.cvtColor(target, cv2.COLOR_BGR2GRAY) - source_contrast = np.std(source_gray) - target_contrast = np.std(target_gray) - contrast_difference = source_contrast - target_contrast - contrast_factor = 1.0 + np.clip(contrast_difference / 255, contrast_range*-1, contrast_range) - final_result = adjust_contrast(final_result, contrast_factor) - if auto_saturation: - source_hsv = cv2.cvtColor(source, cv2.COLOR_BGR2HSV) - target_hsv = cv2.cvtColor(target, cv2.COLOR_BGR2HSV) - source_saturation = np.mean(source_hsv[:, :, 1]) - target_saturation = np.mean(target_hsv[:, :, 1]) - saturation_difference = source_saturation - target_saturation - saturation_factor = 1.0 + np.clip(saturation_difference / 255, saturation_range*-1, saturation_range) - final_result = adjust_saturation(final_result, saturation_factor) - if auto_tone: - final_result = adjust_tone(source, final_result, tone_strength) - - return final_result +from .color_utils import color_transfer +from .image_converters import tensor2cv2 class ImitationHueNode_UTK: @classmethod @@ -217,34 +38,49 @@ class ImitationHueNode_UTK: }, } - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" RETURN_TYPES = ("IMAGE",) RETURN_NAMES = ("image",) FUNCTION = "imitation_hue" + DESCRIPTION = """ +Performs color transfer and imitation between images with skin protection. +""" def imitation_hue(self, imitation_image, target_image, strength, skin_protection, auto_brightness, brightness_range, - auto_contrast, contrast_range, auto_saturation, saturation_range, auto_tone, tone_strength, - mask=None): - for img in imitation_image: - img_cv1 = tensor2cv2(img) - - for img in target_image: - img_cv2 = tensor2cv2(img) - - img_cv3 = None + auto_contrast, contrast_range, auto_saturation, saturation_range, auto_tone, tone_strength, + mask=None): + # Convert tensors to OpenCV format + imitation_cv2 = tensor2cv2(imitation_image) + target_cv2 = tensor2cv2(target_image) + + # Convert mask if provided + mask_cv2 = None if mask is not None: - for img3 in mask: - img_cv3 = img3.cpu().numpy() - img_cv3 = (img_cv3 * 255).astype(np.uint8) - - result_img = color_transfer(img_cv1, img_cv2, img_cv3, strength, skin_protection, auto_brightness, - brightness_range,auto_contrast, contrast_range, auto_saturation, - saturation_range, auto_tone, tone_strength) - result_img = cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB) - rst = torch.from_numpy(result_img.astype(np.float32) / 255.0).unsqueeze(0) - - return (rst,) + mask_cv2 = (mask.cpu().numpy() * 255).astype(np.uint8) + + # Perform color transfer + result = color_transfer( + source=imitation_cv2, + target=target_cv2, + mask=mask_cv2, + strength=strength, + skin_protection=skin_protection, + auto_brightness=auto_brightness, + brightness_range=brightness_range, + auto_contrast=auto_contrast, + contrast_range=contrast_range, + auto_saturation=auto_saturation, + saturation_range=saturation_range, + auto_tone=auto_tone, + tone_strength=tone_strength + ) + + # Convert back to tensor + result_rgb = cv2.cvtColor(result, cv2.COLOR_BGR2RGB) + result_tensor = torch.from_numpy(result_rgb.astype(np.float32) / 255.0) + + return (result_tensor,) # Node mappings NODE_CLASS_MAPPINGS = { diff --git a/nodes/image/purge_vram.py b/nodes/image/purge_vram.py index 681ef5e..7d150c3 100644 --- a/nodes/image/purge_vram.py +++ b/nodes/image/purge_vram.py @@ -2,15 +2,20 @@ Purge VRAM Node ~~~~~~~~~~~~~~ -Purges GPU memory and optionally unloads models. +Purge GPU memory to free up VRAM. :copyright: (c) 2024 by May :license: MIT, see LICENSE for more details. """ -import torch.cuda +import torch import gc -from ...common_utils import log + +from ..tools.logging_utils import log +from ..tools.any_type import AnyType + +# 创建 AnyType 实例 +any = AnyType("*") def clear_memory(): """Clear GPU memory""" @@ -19,13 +24,13 @@ def clear_memory(): gc.collect() class PurgeVRAM_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): return { "required": { - "anything": ("*", {}), + "anything": (any, {}), "purge_cache": ("BOOLEAN", {"default": True}), "purge_models": ("BOOLEAN", {"default": True}), }, @@ -33,7 +38,7 @@ class PurgeVRAM_UTK: } } - RETURN_TYPES = ("*",) + RETURN_TYPES = (any,) RETURN_NAMES = ("anything",) FUNCTION = "purge_vram" OUTPUT_NODE = True diff --git a/nodes/image/restore_crop_box.py b/nodes/image/restore_crop_box.py index 702f62f..cacf060 100644 --- a/nodes/image/restore_crop_box.py +++ b/nodes/image/restore_crop_box.py @@ -13,7 +13,7 @@ from PIL import Image from ...common_utils import log, tensor2pil, pil2tensor, image2mask class RestoreCropBox_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Image" @classmethod def INPUT_TYPES(cls): diff --git a/nodes/mask/mask_operations.py b/nodes/mask/mask_operations.py index edfac11..64ca761 100644 --- a/nodes/mask/mask_operations.py +++ b/nodes/mask/mask_operations.py @@ -11,7 +11,7 @@ Performs logical operations on masks. import torch class MaskAnd_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Mask" @classmethod def INPUT_TYPES(cls): return {"required": {"mask1": ("MASK",), "mask2": ("MASK",)}} @@ -25,7 +25,7 @@ class MaskAnd_UTK: return (mask1 * mask2,) class MaskSub_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Mask" @classmethod def INPUT_TYPES(cls): return {"required": {"mask1": ("MASK",), "mask2": ("MASK",)}} @@ -39,7 +39,7 @@ class MaskSub_UTK: return (torch.clamp(mask1 - mask2, 0, 1),) class MaskAdd_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Mask" @classmethod def INPUT_TYPES(cls): return {"required": {"mask1": ("MASK",), "mask2": ("MASK",)}} diff --git a/nodes/tools/any_type.py b/nodes/tools/any_type.py new file mode 100644 index 0000000..a603647 --- /dev/null +++ b/nodes/tools/any_type.py @@ -0,0 +1,16 @@ +""" +AnyType for UniversalToolkit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +AnyType class for accepting any input type in nodes. + +:copyright: (c) 2024 by May +:license: MIT, see LICENSE for more details. +""" + +class AnyType(str): + """A special class that is always equal in not equal comparisons. Credit to pythongosssss""" + def __eq__(self, __value: object) -> bool: + return True + def __ne__(self, __value: object) -> bool: + return False \ No newline at end of file diff --git a/nodes/tools/fill_masked_area.py b/nodes/tools/fill_masked_area.py index 1b0ec83..52a586e 100644 --- a/nodes/tools/fill_masked_area.py +++ b/nodes/tools/fill_masked_area.py @@ -32,7 +32,7 @@ def mask_blur(mask, feathering): return mask class FillMaskedArea_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Tools" @classmethod def INPUT_TYPES(cls): return { diff --git a/nodes/tools/logging_utils.py b/nodes/tools/logging_utils.py new file mode 100644 index 0000000..c33d3f6 --- /dev/null +++ b/nodes/tools/logging_utils.py @@ -0,0 +1,20 @@ +""" +Logging Utilities for UniversalToolkit +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Logging utilities for UniversalToolkit. + +:copyright: (c) 2024 by May +:license: MIT, see LICENSE for more details. +""" + +def log(message, message_type='info'): + """简单的日志函数""" + if message_type == 'error': + print(f"❌ Error: {message}") + elif message_type == 'warning': + print(f"⚠️ Warning: {message}") + elif message_type == 'finish': + print(f"✅ {message}") + else: + print(f"ℹ️ {message}") \ No newline at end of file diff --git a/nodes/tools/show_nodes.py b/nodes/tools/show_nodes.py index d4f9907..6e21295 100644 --- a/nodes/tools/show_nodes.py +++ b/nodes/tools/show_nodes.py @@ -11,7 +11,7 @@ Display and preview nodes for various data types. import torch class Show_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Tools" @classmethod def INPUT_TYPES(cls): return {"required": {"input": ("STRING", "INT", "FLOAT", "LIST", "MASK", "IMAGE", "LATENT")}} @@ -39,7 +39,7 @@ class Show_UTK: return tuple(outs) class ShowInt_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Tools" @classmethod def INPUT_TYPES(cls): return {"required": {"int_val": ("INT",)}} @@ -53,7 +53,7 @@ class ShowInt_UTK: return (int_val,) class ShowFloat_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Tools" @classmethod def INPUT_TYPES(cls): return {"required": {"float_val": ("FLOAT",)}} @@ -67,7 +67,7 @@ class ShowFloat_UTK: return (float_val,) class ShowList_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Tools" @classmethod def INPUT_TYPES(cls): return {"required": {"list_val": ("LIST",)}} @@ -81,7 +81,7 @@ class ShowList_UTK: return (list_val,) class ShowText_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Tools" @classmethod def INPUT_TYPES(cls): return {"required": {"text": ("STRING",)}} @@ -95,7 +95,7 @@ class ShowText_UTK: return (text,) class PreviewMask_UTK: - CATEGORY = "UniversalToolkit" + CATEGORY = "UniversalToolkit/Tools" @classmethod def INPUT_TYPES(cls): return {"required": {"mask": ("MASK",)}} diff --git a/pyproject.toml b/pyproject.toml index ca0d19f..d51b09f 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.1.2" +version = "1.1.3" license = {file = "LICENSE"} dependencies = ["torch", "numpy", "Pillow", "opencv-python", "scipy", "tqdm"] requires-python = ">=3.8"