feat(nodes): 集成 ImitationHueNode 追色节点 (v1.0.8)
基于 ComfyUI-MingNodes 项目集成追色功能 - 支持图像色彩迁移和追色 - 支持皮肤保护参数 - 支持自动亮度、对比度、饱和度调节 - 支持影调模仿功能 - 支持区域色彩迁移(通过掩码) - 添加 opencv-python 依赖支持
This commit is contained in:
+15
-2
@@ -8,13 +8,23 @@ A comprehensive toolkit for ComfyUI that provides various utility nodes for imag
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__version__ = "1.0.7"
|
||||
__version__ = "1.0.8"
|
||||
__author__ = "CyberDickLang"
|
||||
__email__ = "286878701@qq.com"
|
||||
__url__ = "https://github.com/whmc76"
|
||||
|
||||
# 更新日志
|
||||
CHANGELOG = {
|
||||
"1.0.8": [
|
||||
"新增 ImitationHueNode_UTK 节点(追色节点):",
|
||||
"- 基于 ComfyUI-MingNodes 项目集成",
|
||||
"- 支持图像色彩迁移和追色功能",
|
||||
"- 支持皮肤保护参数,避免肤色失真",
|
||||
"- 支持自动亮度、对比度、饱和度调节",
|
||||
"- 支持影调模仿功能",
|
||||
"- 支持区域色彩迁移(通过掩码)",
|
||||
"- 添加 opencv-python 依赖支持",
|
||||
],
|
||||
"1.0.7": [
|
||||
"改进 ImagePadForOutpaintMasked (UTK) 节点:",
|
||||
"- 新增数据模式(data_mode)参数,支持 'pixel' 和 'percent' 两种模式",
|
||||
@@ -74,7 +84,7 @@ CHANGELOG = {
|
||||
]
|
||||
}
|
||||
|
||||
from .nodes.image_nodes_utk import EmptyUnitGenerator_UTK, ImageRatioDetector_UTK, DepthMapBlur_UTK, ImageConcatenate_UTK, ImageConcatenateMulti_UTK, ImagePadForOutpaintMasked_UTK, ImageAndMaskPreview_UTK
|
||||
from .nodes.image_nodes_utk import EmptyUnitGenerator_UTK, ImageRatioDetector_UTK, DepthMapBlur_UTK, ImageConcatenate_UTK, ImageConcatenateMulti_UTK, ImagePadForOutpaintMasked_UTK, ImageAndMaskPreview_UTK, ImitationHueNode_UTK
|
||||
from .nodes.tool_nodes_utk import ShowInt_UTK, ShowFloat_UTK, ShowList_UTK, ShowText_UTK, PreviewMask_UTK, FillMaskedArea_UTK
|
||||
from .nodes.audio_nodes_utk import LoadAudioPlusFromPath_UTK, AudioCropProcessUTK
|
||||
from .nodes.mask_nodes_utk import MaskAnd_UTK, MaskSub_UTK, MaskAdd_UTK
|
||||
@@ -98,6 +108,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"MaskAnd_UTK": MaskAnd_UTK,
|
||||
"MaskSub_UTK": MaskSub_UTK,
|
||||
"MaskAdd_UTK": MaskAdd_UTK,
|
||||
"ImitationHueNode_UTK": ImitationHueNode_UTK,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -119,6 +130,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"MaskAnd_UTK": "Mask And (UTK)",
|
||||
"MaskSub_UTK": "Mask Sub (UTK)",
|
||||
"MaskAdd_UTK": "Mask Add (UTK)",
|
||||
"ImitationHueNode_UTK": "Imitation Hue Node (UTK)",
|
||||
}
|
||||
|
||||
NODE_CATEGORIES = {
|
||||
@@ -136,6 +148,7 @@ NODE_CATEGORIES = {
|
||||
"MaskAnd_UTK",
|
||||
"MaskSub_UTK",
|
||||
"MaskAdd_UTK",
|
||||
"ImitationHueNode_UTK",
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+249
-1
@@ -6,6 +6,7 @@ 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
|
||||
@@ -754,4 +755,251 @@ nodes for example.
|
||||
preview, = ImageCompositeMasked.composite(self, image, mask_image, 0, 0, True, mask_adjusted)
|
||||
if pass_through:
|
||||
return (preview, )
|
||||
return(self.save_images(preview, filename_prefix, prompt, extra_pnginfo))
|
||||
return(self.save_images(preview, filename_prefix, prompt, extra_pnginfo))
|
||||
|
||||
# -----------------------------------------------------------------------------------
|
||||
# ComfyUI-MingNodes - ImitationHueNode
|
||||
# https://github.com/mingsky-ai/ComfyUI-MingNodes
|
||||
# -----------------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
|
||||
|
||||
class ImitationHueNode_UTK:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"imitation_image": ("IMAGE",),
|
||||
"target_image": ("IMAGE",),
|
||||
"strength": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 1.0, "step": 0.1}),
|
||||
"skin_protection": ("FLOAT", {"default": 0.2, "min": 0, "max": 1.0, "step": 0.1}),
|
||||
"auto_brightness": ("BOOLEAN", {"default": True}),
|
||||
"brightness_range": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.1}),
|
||||
"auto_contrast": ("BOOLEAN", {"default": False}),
|
||||
"contrast_range": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.1}),
|
||||
"auto_saturation": ("BOOLEAN", {"default": False}),
|
||||
"saturation_range": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.1}),
|
||||
"auto_tone": ("BOOLEAN", {"default": False}),
|
||||
"tone_strength": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.1}),
|
||||
},
|
||||
"optional": {
|
||||
"mask": ("MASK", {"default": None}),
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "UniversalToolkit"
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "imitation_hue"
|
||||
|
||||
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
|
||||
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,)
|
||||
Reference in New Issue
Block a user