feat(nodes): 集成 ImitationHueNode 追色节点 (v1.0.8)

基于 ComfyUI-MingNodes 项目集成追色功能

- 支持图像色彩迁移和追色

- 支持皮肤保护参数

- 支持自动亮度、对比度、饱和度调节

- 支持影调模仿功能

- 支持区域色彩迁移(通过掩码)

- 添加 opencv-python 依赖支持
This commit is contained in:
Cyber Dick Lang
2025-06-23 19:33:02 +08:00
parent f9f8f60f8d
commit 1c44578d9a
2 changed files with 264 additions and 3 deletions
+15 -2
View File
@@ -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
View File
@@ -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,)