结构优化与依赖修复,节点功能完善,调整工具节点分类,完善.gitignore
This commit is contained in:
+26
-1
@@ -72,4 +72,29 @@ env/
|
||||
Thumbs.db
|
||||
|
||||
# Reference code directory - ignore updates
|
||||
reference_code/
|
||||
reference_code/
|
||||
|
||||
# 临时目录
|
||||
temp_*/
|
||||
temp_mingnodes/
|
||||
|
||||
# 测试文件
|
||||
test_nodes.py
|
||||
|
||||
# Python缓存
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
*.so
|
||||
|
||||
# IDE文件
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
+44
-11
@@ -31,6 +31,8 @@
|
||||
## 依赖管理
|
||||
- 避免严格的依赖版本限制。除非有兼容性或安全性要求,否则建议只指定主版本或不指定版本。
|
||||
- 依赖声明与参考项目一致,不随意更改依赖版本。
|
||||
- 禁止跨文件导入自定义工具函数(如 color_utils、common_utils),所有通用函数应直接内置到节点文件。
|
||||
- 如需参考第三方实现,务必将原始代码存放于 `reference_code/`,主线代码只保留必要部分。
|
||||
|
||||
## 变更限制
|
||||
- 不允许删除或覆盖用户已有的自定义节点。
|
||||
@@ -43,18 +45,49 @@
|
||||
- 如需引入第三方实现,必须先征得用户同意。
|
||||
- 任何自动化操作前,需先说明理由和影响。
|
||||
|
||||
## 示例
|
||||
**应该做:**
|
||||
- 优化已有节点的参数校验和注释。
|
||||
- 按照原项目实现方式重构 fill mask 相关逻辑。
|
||||
- 新增节点时同步更新注册和文档。
|
||||
- 依赖项如无特殊需求,不要写死具体小版本号。
|
||||
## 目录结构与分层(UniversalToolkit 专项)
|
||||
|
||||
**不应该做:**
|
||||
- 不要自动拉取外部仓库或依赖。
|
||||
- 不要更改用户未授权的文件。
|
||||
- 不要随意更改项目结构。
|
||||
- 不要在 requirements.txt/pyproject.toml 中写死所有依赖的精确版本号。
|
||||
```
|
||||
nodes/
|
||||
├── image/ # 图像处理相关节点
|
||||
│ ├── imitation_hue_node.py
|
||||
│ ├── image_concatenate.py
|
||||
│ └── ...(其他 image 节点)
|
||||
├── tools/ # 工具类节点
|
||||
│ ├── purge_vram.py
|
||||
│ ├── fill_masked_area.py
|
||||
│ └── ...(其他工具节点)
|
||||
reference_code/ # 官方/第三方参考实现存放目录
|
||||
```
|
||||
- 图像处理节点统一分类为 `UniversalToolkit/Image`
|
||||
- 工具节点统一分类为 `Tools`
|
||||
|
||||
## 节点注册与命名规范
|
||||
- 每个节点文件需包含如下注册方式:
|
||||
```python
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"节点类名": 节点类,
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"节点类名": "节点显示名",
|
||||
}
|
||||
```
|
||||
- 节点类名建议统一加 `_UTK` 后缀,显示名加 `(UTK)`。
|
||||
|
||||
## 测试与验证
|
||||
- 所有节点必须能通过 `test_nodes.py` 脚本批量导入测试。
|
||||
- 新增节点或重构后,需补充测试用例,确保无依赖缺失和导入错误。
|
||||
|
||||
## 参考实现与同步
|
||||
- 如需同步第三方(如 MingNodes)实现,务必:
|
||||
- 保持参数、输入输出、算法与官方一致
|
||||
- 分类、注册方式可按 UniversalToolkit 规范适配
|
||||
- 参考代码完整保留在 `reference_code/`,主线代码只保留实际用到部分
|
||||
|
||||
## 其它约定
|
||||
- 禁止使用已废弃的 `common_utils.py`、`color_utils.py` 等历史文件。
|
||||
- 所有节点文件需自包含所需的工具函数,避免循环依赖和导入混乱。
|
||||
- 重要变更需在文件头部注明来源、变更说明和版权信息。
|
||||
|
||||
# ComfyUI-UniversalToolkit 开发规范(AI_CODING_RULES)
|
||||
|
||||
|
||||
+1
-1
@@ -258,7 +258,7 @@ except ImportError:
|
||||
CHECK_MASK_DISPLAY = {}
|
||||
|
||||
try:
|
||||
from .nodes.image.purge_vram import NODE_CLASS_MAPPINGS as PURGE_VRAM_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as PURGE_VRAM_DISPLAY
|
||||
from .nodes.tools.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 = {}
|
||||
|
||||
+17
-1
@@ -6,4 +6,20 @@ Image processing nodes for ComfyUI Universal Toolkit.
|
||||
|
||||
:copyright: (c) 2024 by May
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
"""
|
||||
|
||||
import os
|
||||
import importlib
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
# 自动导入本目录下所有节点文件的注册表
|
||||
for filename in os.listdir(os.path.dirname(__file__)):
|
||||
if filename.endswith('.py') and filename not in ('__init__.py', 'image_utils.py', 'image_converters.py'):
|
||||
modulename = filename[:-3]
|
||||
module = importlib.import_module(f'.{modulename}', __package__)
|
||||
if hasattr(module, 'NODE_CLASS_MAPPINGS'):
|
||||
NODE_CLASS_MAPPINGS.update(getattr(module, 'NODE_CLASS_MAPPINGS'))
|
||||
if hasattr(module, 'NODE_DISPLAY_NAME_MAPPINGS'):
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(getattr(module, 'NODE_DISPLAY_NAME_MAPPINGS'))
|
||||
@@ -1,194 +0,0 @@
|
||||
"""
|
||||
Color Utilities for UniversalToolkit
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Color processing utilities for UniversalToolkit.
|
||||
|
||||
:copyright: (c) 2024 by May
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
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 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
|
||||
@@ -9,9 +9,20 @@ Crops images based on mask detection with various detection modes.
|
||||
"""
|
||||
|
||||
import torch
|
||||
from PIL import Image, ImageDraw
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
import numpy as np
|
||||
from ...common_utils import log, tensor2pil, pil2tensor, image2mask
|
||||
from ..image_utils import tensor2pil, pil2tensor, image2mask
|
||||
|
||||
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 mask2image(mask):
|
||||
"""Convert mask tensor to PIL image"""
|
||||
@@ -101,7 +112,6 @@ class CropByMask_UTK:
|
||||
|
||||
_mask = mask2image(mask_for_crop)
|
||||
try:
|
||||
from PIL import ImageFilter
|
||||
bluredmask = gaussian_blur(_mask, 20).convert('L')
|
||||
except ImportError:
|
||||
bluredmask = _mask.convert('L')
|
||||
|
||||
@@ -10,7 +10,18 @@ Combines RGB image with mask to create RGBA image.
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
from ...common_utils import log, tensor2pil, pil2tensor
|
||||
from ..image_utils import tensor2pil, pil2tensor
|
||||
|
||||
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 image_channel_split(image, mode):
|
||||
"""Split image into channels"""
|
||||
|
||||
@@ -10,7 +10,6 @@ Concatenates two images side by side or vertically with various options.
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from ..common_utils import log
|
||||
|
||||
class ImageConcatenate_UTK:
|
||||
CATEGORY = "UniversalToolkit/Image"
|
||||
|
||||
@@ -10,7 +10,18 @@ Scales images and masks to match the dimensions of a reference image.
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
from ...common_utils import log, tensor2pil, pil2tensor, image2mask
|
||||
from ..image_utils import tensor2pil, pil2tensor, image2mask
|
||||
|
||||
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 fit_resize_image(image, target_width, target_height, fit_mode, resize_sampler, background_color="#000000"):
|
||||
"""Resize image according to fit mode"""
|
||||
|
||||
@@ -10,7 +10,18 @@ Removes alpha channel from RGBA images with optional background filling.
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
from ...common_utils import log, tensor2pil, pil2tensor
|
||||
from ..image_utils import tensor2pil, pil2tensor
|
||||
|
||||
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}")
|
||||
|
||||
class ImageRemoveAlpha_UTK:
|
||||
CATEGORY = "UniversalToolkit/Image"
|
||||
|
||||
@@ -11,13 +11,24 @@ Scales images to specific aspect ratios with various fitting modes.
|
||||
import torch
|
||||
from PIL import Image
|
||||
import math
|
||||
from ...common_utils import log, tensor2pil, pil2tensor, image2mask
|
||||
from ..image_utils import log, tensor2pil, pil2tensor, image2mask, num_round_up_to_multiple, fit_resize_image, is_valid_mask
|
||||
|
||||
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 num_round_up_to_multiple(num, multiple):
|
||||
"""Round up to the nearest multiple"""
|
||||
return ((num + multiple - 1) // multiple) * multiple
|
||||
|
||||
def fit_resize_image(image, target_width, target_height, fit_mode, resize_sampler):
|
||||
def fit_resize_image(image, target_width, target_height, fit_mode, resize_sampler, background_color):
|
||||
"""Resize image according to fit mode"""
|
||||
if fit_mode == 'letterbox':
|
||||
# Calculate scaling factor to fit within target dimensions
|
||||
@@ -29,7 +40,7 @@ def fit_resize_image(image, target_width, target_height, fit_mode, resize_sample
|
||||
resized = image.resize((new_width, new_height), resize_sampler)
|
||||
|
||||
# Create new image with target dimensions and paste resized image
|
||||
result = Image.new(image.mode, (target_width, target_height), (0, 0, 0))
|
||||
result = Image.new(image.mode, (target_width, target_height), background_color)
|
||||
paste_x = (target_width - new_width) // 2
|
||||
paste_y = (target_height - new_height) // 2
|
||||
result.paste(resized, (paste_x, paste_y))
|
||||
@@ -62,21 +73,22 @@ class ImageScaleByAspectRatio_UTK:
|
||||
fit_mode = ['letterbox', 'crop', 'fill']
|
||||
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
|
||||
multiple_list = ['8', '16', '32', '64', '128', '256', '512', 'None']
|
||||
|
||||
scale_to_list = ['None', 'longest', 'shortest', 'width', 'height', 'total_pixel(kilo pixel)']
|
||||
return {
|
||||
"required": {
|
||||
"aspect_ratio": (ratio_list,),
|
||||
"proportional_width": ("INT", {"default": 2, "min": 1, "max": 999, "step": 1}),
|
||||
"proportional_height": ("INT", {"default": 1, "min": 1, "max": 999, "step": 1}),
|
||||
"proportional_width": ("INT", {"default": 1, "min": 1, "max": 1e8, "step": 1}),
|
||||
"proportional_height": ("INT", {"default": 1, "min": 1, "max": 1e8, "step": 1}),
|
||||
"fit": (fit_mode,),
|
||||
"method": (method_mode,),
|
||||
"round_to_multiple": (multiple_list,),
|
||||
"scale_to_longest_side": ("BOOLEAN", {"default": False}), # 是否按长边缩放
|
||||
"longest_side": ("INT", {"default": 1024, "min": 4, "max": 999999, "step": 1}),
|
||||
"scale_to_side": (scale_to_list,),
|
||||
"scale_to_length": ("INT", {"default": 1024, "min": 4, "max": 1e8, "step": 1}),
|
||||
"background_color": ("STRING", {"default": "#000000"}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",), #
|
||||
"mask": ("MASK",), #
|
||||
"image": ("IMAGE",),
|
||||
"mask": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +97,9 @@ class ImageScaleByAspectRatio_UTK:
|
||||
FUNCTION = 'image_scale_by_aspect_ratio'
|
||||
|
||||
def image_scale_by_aspect_ratio(self, aspect_ratio, proportional_width, proportional_height,
|
||||
fit, method, round_to_multiple, scale_to_longest_side, longest_side,
|
||||
image=None, mask = None,
|
||||
):
|
||||
fit, method, round_to_multiple, scale_to_side, scale_to_length,
|
||||
background_color,
|
||||
image=None, mask=None):
|
||||
orig_images = []
|
||||
orig_masks = []
|
||||
orig_width = 0
|
||||
@@ -107,17 +119,21 @@ class ImageScaleByAspectRatio_UTK:
|
||||
mask = torch.unsqueeze(mask, 0)
|
||||
for m in mask:
|
||||
m = torch.unsqueeze(m, 0)
|
||||
orig_masks.append(m)
|
||||
_width, _height = tensor2pil(orig_masks[0]).size
|
||||
if (orig_width > 0 and orig_width != _width) or (orig_height > 0 and orig_height != _height):
|
||||
log(f"Error: ImageScaleByAspectRatio_UTK skipped, because the mask is does'nt match image.", message_type='error')
|
||||
return (None, None, None, 0, 0,)
|
||||
elif orig_width + orig_height == 0:
|
||||
orig_width = _width
|
||||
orig_height = _height
|
||||
if not is_valid_mask(m) and m.shape == torch.Size([1, 64, 64]):
|
||||
log(f"Warning: ImageScaleByAspectRatio_UTK input mask is empty, ignore it.", message_type='warning')
|
||||
else:
|
||||
orig_masks.append(m)
|
||||
if len(orig_masks) > 0:
|
||||
_width, _height = tensor2pil(orig_masks[0]).size
|
||||
if (orig_width > 0 and orig_width != _width) or (orig_height > 0 and orig_height != _height):
|
||||
log(f"Error: ImageScaleByAspectRatio_UTK execute failed, because the mask is does'nt match image.", message_type='error')
|
||||
return (None, None, None, 0, 0,)
|
||||
elif orig_width + orig_height == 0:
|
||||
orig_width = _width
|
||||
orig_height = _height
|
||||
|
||||
if orig_width + orig_height == 0:
|
||||
log(f"Error: ImageScaleByAspectRatio_UTK skipped, because the image or mask at least one must be input.", message_type='error')
|
||||
log(f"Error: ImageScaleByAspectRatio_UTK execute failed, because the image or mask at least one must be input.", message_type='error')
|
||||
return (None, None, None, 0, 0,)
|
||||
|
||||
if aspect_ratio == 'original':
|
||||
@@ -129,27 +145,48 @@ class ImageScaleByAspectRatio_UTK:
|
||||
ratio = int(s[0]) / int(s[1])
|
||||
|
||||
# calculate target width and height
|
||||
if orig_width > orig_height:
|
||||
if scale_to_longest_side:
|
||||
target_width = longest_side
|
||||
if ratio > 1:
|
||||
if scale_to_side == 'longest':
|
||||
target_width = scale_to_length
|
||||
target_height = int(target_width / ratio)
|
||||
elif scale_to_side == 'shortest':
|
||||
target_height = scale_to_length
|
||||
target_width = int(target_height * ratio)
|
||||
elif scale_to_side == 'width':
|
||||
target_width = scale_to_length
|
||||
target_height = int(target_width / ratio)
|
||||
elif scale_to_side == 'height':
|
||||
target_height = scale_to_length
|
||||
target_width = int(target_height * ratio)
|
||||
elif scale_to_side == 'total_pixel(kilo pixel)':
|
||||
target_width = math.sqrt(ratio * scale_to_length * 1000)
|
||||
target_height = target_width / ratio
|
||||
target_width = int(target_width)
|
||||
target_height = int(target_height)
|
||||
else:
|
||||
target_width = orig_width
|
||||
target_height = int(target_width / ratio)
|
||||
target_height = int(target_width / ratio)
|
||||
else:
|
||||
if scale_to_longest_side:
|
||||
target_height = longest_side
|
||||
if scale_to_side == 'longest':
|
||||
target_height = scale_to_length
|
||||
target_width = int(target_height * ratio)
|
||||
elif scale_to_side == 'shortest':
|
||||
target_width = scale_to_length
|
||||
target_height = int(target_width / ratio)
|
||||
elif scale_to_side == 'width':
|
||||
target_width = scale_to_length
|
||||
target_height = int(target_width / ratio)
|
||||
elif scale_to_side == 'height':
|
||||
target_height = scale_to_length
|
||||
target_width = int(target_height * ratio)
|
||||
elif scale_to_side == 'total_pixel(kilo pixel)':
|
||||
target_width = math.sqrt(ratio * scale_to_length * 1000)
|
||||
target_height = target_width / ratio
|
||||
target_width = int(target_width)
|
||||
target_height = int(target_height)
|
||||
else:
|
||||
target_height = orig_height
|
||||
target_width = int(target_height * ratio)
|
||||
|
||||
if ratio < 1:
|
||||
if scale_to_longest_side:
|
||||
_r = longest_side / target_height
|
||||
target_height = longest_side
|
||||
else:
|
||||
_r = orig_height / target_height
|
||||
target_height = orig_height
|
||||
target_width = int(target_width * _r)
|
||||
target_width = int(target_height * ratio)
|
||||
|
||||
if round_to_multiple != 'None':
|
||||
multiple = int(round_to_multiple)
|
||||
@@ -174,22 +211,22 @@ class ImageScaleByAspectRatio_UTK:
|
||||
if len(orig_images) > 0:
|
||||
for i in orig_images:
|
||||
_image = tensor2pil(i).convert('RGB')
|
||||
_image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler)
|
||||
_image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler, background_color)
|
||||
ret_images.append(pil2tensor(_image))
|
||||
if len(orig_masks) > 0:
|
||||
for m in orig_masks:
|
||||
_mask = tensor2pil(m).convert('L')
|
||||
_mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L')
|
||||
_mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler, background_color).convert('L')
|
||||
ret_masks.append(image2mask(_mask))
|
||||
if len(ret_images) > 0 and len(ret_masks) >0:
|
||||
if len(ret_images) > 0 and len(ret_masks) > 0:
|
||||
log(f"ImageScaleByAspectRatio_UTK Processed {len(ret_images)} image(s).", message_type='finish')
|
||||
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),[orig_width, orig_height], target_width, target_height,)
|
||||
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,)
|
||||
elif len(ret_images) > 0 and len(ret_masks) == 0:
|
||||
log(f"ImageScaleByAspectRatio_UTK Processed {len(ret_images)} image(s).", message_type='finish')
|
||||
return (torch.cat(ret_images, dim=0), None,[orig_width, orig_height], target_width, target_height,)
|
||||
return (torch.cat(ret_images, dim=0), None, [orig_width, orig_height], target_width, target_height,)
|
||||
elif len(ret_images) == 0 and len(ret_masks) > 0:
|
||||
log(f"ImageScaleByAspectRatio_UTK Processed {len(ret_masks)} image(s).", message_type='finish')
|
||||
return (None, torch.cat(ret_masks, dim=0),[orig_width, orig_height], target_width, target_height,)
|
||||
return (None, torch.cat(ret_masks, dim=0), [orig_width, orig_height], target_width, target_height,)
|
||||
else:
|
||||
log(f"Error: ImageScaleByAspectRatio_UTK skipped, because the available image or mask is not found.", message_type='error')
|
||||
return (None, None, None, 0, 0,)
|
||||
|
||||
@@ -10,7 +10,18 @@ Restores images to original size or scales them with specified parameters.
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
from ...common_utils import log, tensor2pil, pil2tensor, image2mask
|
||||
from ..image_utils import tensor2pil, pil2tensor, image2mask
|
||||
|
||||
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}")
|
||||
|
||||
class ImageScaleRestore_UTK:
|
||||
CATEGORY = "UniversalToolkit/Image"
|
||||
|
||||
@@ -8,12 +8,199 @@ Performs color transfer and imitation between images with skin protection.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
import cv2
|
||||
import torch
|
||||
|
||||
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
|
||||
@@ -50,37 +237,25 @@ 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):
|
||||
# Convert tensors to OpenCV format
|
||||
imitation_cv2 = tensor2cv2(imitation_image)
|
||||
target_cv2 = tensor2cv2(target_image)
|
||||
|
||||
# Convert mask if provided
|
||||
mask_cv2 = None
|
||||
for img in imitation_image:
|
||||
img_cv1 = tensor2cv2(img)
|
||||
|
||||
for img in target_image:
|
||||
img_cv2 = tensor2cv2(img)
|
||||
|
||||
img_cv3 = None
|
||||
if mask is not None:
|
||||
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,)
|
||||
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,)
|
||||
|
||||
# Node mappings
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
|
||||
@@ -10,7 +10,18 @@ Restores cropped images back to their original background.
|
||||
|
||||
import torch
|
||||
from PIL import Image
|
||||
from ...common_utils import log, tensor2pil, pil2tensor, image2mask
|
||||
from ..image_utils import tensor2pil, pil2tensor, image2mask
|
||||
|
||||
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}")
|
||||
|
||||
class RestoreCropBox_UTK:
|
||||
CATEGORY = "UniversalToolkit/Image"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Image Utilities for UniversalToolkit
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Image processing utility functions for UniversalToolkit nodes.
|
||||
|
||||
:copyright: (c) 2024 by May
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
def tensor2pil(t_image: torch.Tensor) -> Image:
|
||||
"""将 PyTorch tensor 转换为 PIL Image"""
|
||||
return Image.fromarray(np.clip(255.0 * t_image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
||||
|
||||
def pil2tensor(image: Image) -> torch.Tensor:
|
||||
"""将 PIL Image 转换为 PyTorch tensor"""
|
||||
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
||||
|
||||
def image2mask(image: Image) -> torch.Tensor:
|
||||
"""将图像转换为掩码格式"""
|
||||
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 tensor2np(tensor: torch.Tensor) -> np.ndarray:
|
||||
"""将 PyTorch tensor 转换为 numpy 数组"""
|
||||
return np.clip(255.0 * tensor.cpu().numpy(), 0, 255).astype(np.uint8)
|
||||
|
||||
def np2tensor(np_array: np.ndarray) -> torch.Tensor:
|
||||
"""将 numpy 数组转换为 PyTorch tensor"""
|
||||
return torch.from_numpy(np_array.astype(np.float32) / 255.0).unsqueeze(0)
|
||||
|
||||
def log(message: str, message_type: str = 'info'):
|
||||
name = 'LayerStyle'
|
||||
if message_type == 'error':
|
||||
message = '\033[1;41m' + str(message) + '\033[m'
|
||||
elif message_type == 'warning':
|
||||
message = '\033[1;31m' + str(message) + '\033[m'
|
||||
elif message_type == 'finish':
|
||||
message = '\033[1;32m' + str(message) + '\033[m'
|
||||
else:
|
||||
message = '\033[1;33m' + str(message) + '\033[m'
|
||||
print(f"# 😺dzNodes: {name} -> {message}")
|
||||
|
||||
def num_round_up_to_multiple(number: int, multiple: int) -> int:
|
||||
return ((number + multiple - 1) // multiple) * multiple
|
||||
|
||||
def fit_resize_image(image, target_width, target_height, fit, resize_sampler, background_color='#000000'):
|
||||
image = image.convert('RGB')
|
||||
orig_width, orig_height = image.size
|
||||
if fit == 'letterbox':
|
||||
if orig_width / orig_height > target_width / target_height:
|
||||
fit_width = target_width
|
||||
fit_height = int(target_width / orig_width * orig_height)
|
||||
else:
|
||||
fit_height = target_height
|
||||
fit_width = int(target_height / orig_height * orig_width)
|
||||
fit_image = image.resize((fit_width, fit_height), resize_sampler)
|
||||
ret_image = Image.new('RGB', size=(target_width, target_height), color=background_color)
|
||||
ret_image.paste(fit_image, box=((target_width - fit_width)//2, (target_height - fit_height)//2))
|
||||
elif fit == 'crop':
|
||||
if orig_width / orig_height > target_width / target_height:
|
||||
fit_width = int(orig_height * target_width / target_height)
|
||||
fit_image = image.crop(
|
||||
((orig_width - fit_width)//2, 0, (orig_width - fit_width)//2 + fit_width, orig_height))
|
||||
else:
|
||||
fit_height = int(orig_width * target_height / target_width)
|
||||
fit_image = image.crop(
|
||||
(0, (orig_height-fit_height)//2, orig_width, (orig_height-fit_height)//2 + fit_height))
|
||||
ret_image = fit_image.resize((target_width, target_height), resize_sampler)
|
||||
else:
|
||||
ret_image = image.resize((target_width, target_height), resize_sampler)
|
||||
return ret_image
|
||||
|
||||
def is_valid_mask(tensor: torch.Tensor) -> bool:
|
||||
return tensor.sum().item() > 0
|
||||
@@ -10,9 +10,8 @@ Purge GPU memory to free up VRAM.
|
||||
|
||||
import torch
|
||||
import gc
|
||||
|
||||
from ..tools.logging_utils import log
|
||||
from ..tools.any_type import AnyType
|
||||
from .logging_utils import log
|
||||
from .any_type import AnyType
|
||||
|
||||
# 创建 AnyType 实例
|
||||
any = AnyType("*")
|
||||
@@ -24,7 +23,7 @@ def clear_memory():
|
||||
gc.collect()
|
||||
|
||||
class PurgeVRAM_UTK:
|
||||
CATEGORY = "UniversalToolkit/Image"
|
||||
CATEGORY = "UniversalToolkit/Tools"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -0,0 +1,85 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加路径
|
||||
sys.path.append('.')
|
||||
|
||||
print("🔧 测试所有修复后的节点...\n")
|
||||
|
||||
# 测试图像拼接节点
|
||||
try:
|
||||
from nodes.image.image_concatenate import NODE_CLASS_MAPPINGS as CONCAT_MAPPINGS
|
||||
print("✅ ImageConcatenate_UTK 节点映射:")
|
||||
for k, v in CONCAT_MAPPINGS.items():
|
||||
print(f" {k}: {v.__name__}")
|
||||
except Exception as e:
|
||||
print(f"❌ 导入 ImageConcatenate_UTK 失败: {e}")
|
||||
|
||||
try:
|
||||
from nodes.image.image_concatenate_multi import NODE_CLASS_MAPPINGS as CONCAT_MULTI_MAPPINGS
|
||||
print("\n✅ ImageConcatenateMulti_UTK 节点映射:")
|
||||
for k, v in CONCAT_MULTI_MAPPINGS.items():
|
||||
print(f" {k}: {v.__name__}")
|
||||
except Exception as e:
|
||||
print(f"❌ 导入 ImageConcatenateMulti_UTK 失败: {e}")
|
||||
|
||||
# 测试 ImitationHueNode_UTK
|
||||
try:
|
||||
from nodes.image.imitation_hue_node import NODE_CLASS_MAPPINGS as IMITATION_MAPPINGS
|
||||
print("\n✅ ImitationHueNode_UTK 节点映射:")
|
||||
for k, v in IMITATION_MAPPINGS.items():
|
||||
print(f" {k}: {v.__name__}")
|
||||
except Exception as e:
|
||||
print(f"❌ 导入 ImitationHueNode_UTK 失败: {e}")
|
||||
|
||||
# 测试其他修复的节点
|
||||
test_nodes = [
|
||||
("restore_crop_box", "RestoreCropBox_UTK"),
|
||||
("image_scale_restore", "ImageScaleRestore_UTK"),
|
||||
("image_scale_by_aspect_ratio", "ImageScaleByAspectRatio_UTK"),
|
||||
("image_remove_alpha", "ImageRemoveAlpha_UTK"),
|
||||
("image_mask_scale_as", "ImageMaskScaleAs_UTK"),
|
||||
("image_combine_alpha", "ImageCombineAlpha_UTK"),
|
||||
("crop_by_mask", "CropByMask_UTK"),
|
||||
]
|
||||
|
||||
print("\n🔧 测试其他修复的节点:")
|
||||
for node_file, node_class in test_nodes:
|
||||
try:
|
||||
module = __import__(f"nodes.image.{node_file}", fromlist=[node_class])
|
||||
node_class_obj = getattr(module, node_class)
|
||||
print(f"✅ {node_class}: 导入成功")
|
||||
except Exception as e:
|
||||
print(f"❌ {node_class}: 导入失败 - {e}")
|
||||
|
||||
# 测试 tools 目录下的节点
|
||||
print("\n🔧 测试 tools 目录下的节点:")
|
||||
tools_nodes = [
|
||||
("purge_vram", "PurgeVRAM_UTK"),
|
||||
("fill_masked_area", "FillMaskedArea_UTK"),
|
||||
("show_nodes", "Show_UTK"),
|
||||
]
|
||||
|
||||
for node_file, node_class in tools_nodes:
|
||||
try:
|
||||
module = __import__(f"nodes.tools.{node_file}", fromlist=[node_class])
|
||||
node_class_obj = getattr(module, node_class)
|
||||
print(f"✅ {node_class}: 导入成功")
|
||||
except Exception as e:
|
||||
print(f"❌ {node_class}: 导入失败 - {e}")
|
||||
|
||||
print("\n🎉 所有节点修复完成!")
|
||||
print("现在您应该能在 ComfyUI 中看到以下节点:")
|
||||
print(" - Image Concatenate (UTK)")
|
||||
print(" - Image Concatenate Multi (UTK)")
|
||||
print(" - Imitation Hue Node (UTK) - 已同步 MingNodes 实现")
|
||||
print(" - Restore Crop Box (UTK)")
|
||||
print(" - Image Scale Restore (UTK)")
|
||||
print(" - Image Scale By Aspect Ratio (UTK)")
|
||||
print(" - Image Remove Alpha (UTK)")
|
||||
print(" - Image Mask Scale As (UTK)")
|
||||
print(" - Image Combine Alpha (UTK)")
|
||||
print(" - Crop By Mask (UTK)")
|
||||
print(" - Purge VRAM (UTK) - 现在在 Tools 分类下")
|
||||
print(" - Fill Masked Area (UTK) - 在 Tools 分类下")
|
||||
print(" - Show Nodes (UTK) - 在 Tools 分类下")
|
||||
Reference in New Issue
Block a user