v1.1.3: 修复 PurgeVRAM_UTK 和 CheckMask_UTK 节点问题,完成模块化重构

- 修复 PurgeVRAM_UTK 节点类型不匹配问题,创建 AnyType 类实现
- 修复 CheckMask_UTK 节点 NoneType 错误,添加空值检查
- 完成模块化重构:将 common_utils.py 功能拆分到不同目录
- 创建 image_converters.py、color_utils.py、logging_utils.py、any_type.py
- 删除 common_utils.py,避免依赖冲突
- 更新所有相关文件的导入语句
- 提高代码可维护性和模块化程度
This commit is contained in:
Cyber Dick Lang
2025-06-24 19:06:57 +08:00
parent d9aa3e8855
commit bf91c32971
31 changed files with 486 additions and 443 deletions
+42 -1
View File
@@ -54,4 +54,45 @@
- 不要自动拉取外部仓库或依赖。
- 不要更改用户未授权的文件。
- 不要随意更改项目结构。
- 不要在 requirements.txt/pyproject.toml 中写死所有依赖的精确版本号。
- 不要在 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插件开发的最高准则,所有贡献者和维护者必须严格遵守。**
(如有新规范或官方更新,须第一时间同步修订本文件)
+82 -79
View File
@@ -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")
+1
View File
@@ -0,0 +1 @@
=== UniversalToolkit 节点分组属性清单 ===
+1 -1
View File
@@ -13,7 +13,7 @@ import torch
FLOAT_MAX = 99999999999999999.0
class AudioCropProcessUTK:
CATEGORY = "UniversalToolkit"
CATEGORY = "UniversalToolkit/Audio"
@classmethod
def INPUT_TYPES(cls):
return {
+1 -1
View File
@@ -18,7 +18,7 @@ import librosa
FLOAT_MAX = 99999999999999999.0
class LoadAudioPlusFromPath_UTK:
CATEGORY = "UniversalToolkit"
CATEGORY = "UniversalToolkit/Audio"
@classmethod
def INPUT_TYPES(cls):
return {
+24 -9
View File
@@ -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,)
@@ -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)
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -15,7 +15,7 @@ import numpy as np
import folder_paths
class DepthMapBlur_UTK:
CATEGORY = "UniversalToolkit"
CATEGORY = "UniversalToolkit/Image"
@classmethod
def INPUT_TYPES(s):
+6 -4
View File
@@ -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)",
}
+5 -2
View File
@@ -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.
+1 -1
View File
@@ -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):
+127
View File
@@ -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,)
+2 -2
View File
@@ -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)",
}
+1 -1
View File
@@ -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):
+72
View File
@@ -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]
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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):
+7 -5
View File
@@ -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)",
}
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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):
+38 -202
View File
@@ -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 = {
+11 -6
View File
@@ -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
+1 -1
View File
@@ -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):
+3 -3
View File
@@ -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",)}}
+16
View File
@@ -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
+1 -1
View File
@@ -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 {
+20
View File
@@ -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}")
+6 -6
View File
@@ -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",)}}
+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.1.2"
version = "1.1.3"
license = {file = "LICENSE"}
dependencies = ["torch", "numpy", "Pillow", "opencv-python", "scipy", "tqdm"]
requires-python = ">=3.8"