Files

1638 lines
64 KiB
Python

import logging
import folder_paths
from nodes import LoadImage, LoadImageMask
from comfy_extras.nodes_mask import ImageCompositeMasked
from comfy.cli_args import args
import comfy.model_management
import comfy.clip_vision
import comfy.controlnet
import comfy.utils
import comfy.sd
import comfy.sample
import comfy.samplers
import comfy.diffusers_load
import torch
import yaml
import os
import sys
import numpy as np
from PIL import Image, ImageEnhance
from .color_correct import ColorCorrectOfUtils
import cv2
from comfy_extras.nodes_upscale_model import ImageUpscaleWithModel
from math import dist
import folder_paths
from .utils import tensor2cv
import hashlib
config_dir = os.path.join(folder_paths.base_path, "config")
if not os.path.exists(config_dir):
os.makedirs(config_dir)
sys.path.insert(0, os.path.join(
os.path.dirname(os.path.realpath(__file__)), "comfy"))
logger = logging.getLogger(__file__)
RESOLUTION_PRESETS_1K = [
(704, 1408), (704, 1344), (768, 1344), (768, 1280), (832, 1216), (832, 1152),
(896, 1152), (896, 1088), (960, 1088), (960, 1024), (1024, 1024), (1024, 960),
(1088, 960), (1088, 896), (1152, 896), (1152, 832), (1216, 832), (1280, 768),
(1344, 768), (1344, 704), (1408, 704), (1472, 704), (1536, 640), (1600, 640),
(1664, 576), (1728, 576)
]
ASPECT_RATIO_RESOLUTIONS = {
"1:1": (1024, 1024),
"1:2": (704, 1408),
"3:5": (768, 1280),
"5:3": (1280, 768),
"2:1": (1408, 704),
"3:1": (1728, 576),
"1:3": (576, 1728),
"9:16": (768, 1344),
"16:9": (1344, 768),
"3:4": (896, 1152),
"4:3": (1152, 896),
"2:3": (832, 1216),
"3:2": (1216, 832),
"9:21": (640, 1536),
"21:9": (1536, 640),
}
SIZE_MULTIPLIERS = {"1k": 1.0, "2k": 2.0, "4k": 4.0}
class LoadImageWithSwitch(LoadImage):
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(
os.path.join(input_dir, f))]
return {"required":
{"image": (sorted(files), {"image_upload": True})},
"optional": {
"enabled": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
}
}
CATEGORY = "utils/image"
RETURN_TYPES = ("IMAGE", "MASK", "BOOLEAN")
RETURN_NAMES = ("image", "mask", "enabled")
FUNCTION = "load_image_with_switch"
def load_image_with_switch(self, image, enabled=True):
logger.debug("start load image")
if not enabled:
return None, None, enabled
return self.load_image(image) + (enabled, )
class LoadImageWithoutListDir(LoadImage):
@classmethod
def INPUT_TYPES(s):
return {"required":
{"image": ([], {"image_upload": True})},
"optional": {
"enabled": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"input_image_path": ("STRING",),
}
}
CATEGORY = "utils/image"
RETURN_TYPES = ("IMAGE", "MASK", "BOOLEAN", "STRING", "INT", "INT")
RETURN_NAMES = ("image", "mask", "enabled", "filename", "width", "height")
FUNCTION = "load_image_with_switch"
def load_image_with_switch(self, image, enabled=True, input_image_path=""):
logger.debug("start load image")
if not enabled:
return None, None, enabled, "", 0, 0
if input_image_path:
image = input_image_path
output_image, output_mask = self.load_image(image)
return (output_image, output_mask, enabled, image, output_image.shape[2], output_image.shape[1])
@classmethod
def IS_CHANGED(s, image, enabled, input_image_path=""):
if not enabled:
return ""
image_path = folder_paths.get_annotated_filepath(image)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())
return m.digest().hex()
class LoadImageMaskWithSwitch(LoadImageMask):
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(
os.path.join(input_dir, f))]
return {"required":
{"image": (sorted(files), {"image_upload": True}),
"channel": (["red", "green", "blue", "alpha"], ), },
"optional": {
"enabled": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
},
}
CATEGORY = "utils/mask"
RETURN_TYPES = ("MASK", "BOOLEAN")
RETURN_NAMES = ("mask", "enabled")
FUNCTION = "load_image_with_switch"
def load_image_with_switch(self, image, channel, enabled=True):
if not enabled:
return (None, enabled)
return self.load_image(image, channel) + (enabled, )
@classmethod
def VALIDATE_INPUTS(s, image, enabled):
if not enabled:
return True
if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image)
return True
class LoadImageMaskWithoutListDir(LoadImageMask):
@classmethod
def INPUT_TYPES(s):
return {"required":
{"image": ([], {"image_upload": True}),
"channel": (["red", "green", "blue", "alpha"], ), },
"optional": {
"enabled": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"mask_repeat_number":("INT", {"default": 1, "min": 1, "step": 1}),
},
}
CATEGORY = "utils/mask"
RETURN_TYPES = ("MASK", "BOOLEAN")
RETURN_NAMES = ("mask", "enabled")
FUNCTION = "load_image_with_switch"
def load_image_with_switch(self, image, channel, enabled=True, mask_repeat_number=1):
if not enabled:
return (None, enabled)
mask = self.load_image(image, channel)[0]
mask = mask.unsqueeze(0) if mask.dim() == 2 else mask
new_mask = mask.expand(mask_repeat_number, -1, -1)
return (new_mask, enabled)
@classmethod
def VALIDATE_INPUTS(s, image, enabled):
if not enabled:
return True
if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image)
return True
@classmethod
def IS_CHANGED(s, image, channel, enabled=True, mask_repeat_number=1):
if not enabled:
return ""
image_path = folder_paths.get_annotated_filepath(image)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())
return m.digest().hex()
class ImageBatchOneOrMore:
@classmethod
def INPUT_TYPES(s):
return {"required": {"image1": ("IMAGE",)},
"optional": {"image2": ("IMAGE",), "image3": ("IMAGE",), "image4": ("IMAGE",), "image5": ("IMAGE",), "image6": ("IMAGE",)}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "batch"
CATEGORY = "utils/image"
def batch(self, image1, image2=None, image3=None, image4=None, image5=None, image6=None):
images = [image1]
for other_image in [image2, image3, image4, image5, image6]:
if other_image is not None:
try:
if image1.shape[1:] != other_image.shape[1:]:
other_image = comfy.utils.common_upscale(
other_image.movedim(-1, 1), image1.shape[2], image1.shape[1], "bilinear", "center").movedim(1, -1)
images.append(other_image)
except Exception as e:
logger.exception(e)
s = torch.cat(images, dim=0)
return (s,)
class ConcatTextOfUtils:
"""
This node will concatenate two strings together
"""
@ classmethod
def INPUT_TYPES(cls):
return {"required": {
"text1": ("STRING", {"multiline": True, "defaultBehavior": "input"}),
"text2": ("STRING", {"multiline": True, "defaultBehavior": "input"}),
"separator": ("STRING", {"multiline": False, "default": ","}),
},
"optional": {
"text3": ("STRING", {"multiline": True, "defaultBehavior": "input"}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "fun"
CATEGORY = "utils/text"
@ staticmethod
def fun(text1, separator, text2, text3=""):
texts = [text1, text2, text3]
texts = [text for text in texts if text]
return (separator.join(texts),)
class GenderWordsConfig:
file_path = os.path.join(config_dir, "gender_words_config.yaml")
if not os.path.exists(file_path):
gender_map = {
'F': {
'man': 'woman', 'men': 'women', 'sir': 'madam', 'father': 'mother',
'husband': 'wife', 'son': 'daughter', 'boy': 'girl', 'brother': 'sister',
'uncle': 'aunt', 'grandfather': 'grandmother', 'nephew': 'niece',
'groom': 'bride', 'waiter': 'waitress', 'king': 'queen', 'gentleman': 'lady',
'prince': 'princess', 'male': 'female', 'fiance': 'fiancee',
'actor': 'actress', 'hero': 'heroine', 'he': 'she', 'his': 'her',
'him': 'her', 'himself': 'herself', "he's": "she's",
}
}
gender_map['M'] = {value: key for key,
value in gender_map['F'].items()}
config = {"gender_map": gender_map, "gender_add_words": {
"M": ["male",], "F": ["female"], },"error_words": {"person",}}
with open(file_path, 'w') as file:
yaml.dump(config, file)
config = {}
@staticmethod
def load_config():
with open(GenderWordsConfig.file_path, 'r') as file:
GenderWordsConfig.config = yaml.safe_load(file)
@staticmethod
def get_config():
return GenderWordsConfig.config
@staticmethod
def update_config(new_config):
GenderWordsConfig.config.update(new_config)
GenderWordsConfig.save_config()
@staticmethod
def save_config():
with open(GenderWordsConfig.file_path, 'w') as file:
yaml.dump(GenderWordsConfig.config, file)
class ModifyTextGender:
"""
This node will modify the prompt string according gender. gender words include M, F
"""
@ classmethod
def INPUT_TYPES(cls):
return {"required": {
"gender_prior": (["", "M", "F"],),
"text": ("STRING", {"forceInput": True}),
},
"optional": {
"gender_prior_weight": ("FLOAT", {"default": 1, "min": 0, "max": 3, "step": 0.1}),
"gender_alternative": ("STRING", {"forceInput": True}),
"enabled": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
},
"hidden": {
"age": ("INT", {"default": -1, "min": -1, "max": 120, "step": 1}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "fun"
CATEGORY = "utils/text"
GenderWordsConfig.load_config()
@ staticmethod
def fun(text, gender_prior="", gender_alternative=None, age=-1, enabled=True,gender_prior_weight=1.0):
gender= gender_prior if gender_prior else gender_alternative
weight = gender_prior_weight if gender_prior else 1.0
gender_map = GenderWordsConfig.get_config().get("gender_map", {})
if not enabled or text is None or gender is None or gender.upper() not in gender_map:
return (text,)
result = ModifyTextGender.gender_swap(text, gender, gender_map)
result = ModifyTextGender.gender_add_words(result, gender, weight = weight)
logger.info(f"ModifyTextGender result:{result}")
return (result,)
@ staticmethod
def gender_add_words(text, gender, weight = 1.0):
gender_add_map = GenderWordsConfig.get_config().get("gender_add_words", {})
prefixes = gender_add_map[gender.upper()]
if weight != 1.0:
prefixes = [f"({prefix}:{weight:.1f})" for prefix in prefixes]
result = ", ".join(prefixes + [text])
return result
@ staticmethod
def gender_swap(text, gender, gender_map):
words = text.split()
mappings = gender_map[gender.upper()]
for i, word in enumerate(words):
masks = ""
case = 'lower'
original_word = word.lower()
if original_word in GenderWordsConfig.get_config().get("error_words", {}):
continue
if word.endswith(".") or word.endswith(",") or word.endswith("'") or word.endswith('"') or word.endswith(":"):
case = "masks"
original_word, masks = original_word[:-1], original_word[-1]
replacement = None
for key, value in mappings.items():
if len(key) == 2:
if original_word == key:
replacement = value
break
elif original_word.startswith(key) or original_word.endswith(key):
replacement = original_word.replace(key, value)
break
if replacement is not None:
if case == "masks":
replacement = replacement + masks
words[i] = replacement
return ' '.join(words)
class GenderControlOutput:
"""
This node will modify the prompt string according gender. gender words include M, F
"""
@ classmethod
def INPUT_TYPES(cls):
return {"required": {
"gender_prior": (["", "M", "F"],),
"male_text": ("STRING", {"multiline": True, "defaultBehavior": "input"}),
"male_float": ("FLOAT", {"default": 1, "step": 0.1}),
"male_int": ("INT", {"default": 1, "step": 1}),
"female_text": ("STRING", {"multiline": True, "defaultBehavior": "input"}),
"female_float": ("FLOAT", {"default": 1, "step": 0.1}),
"female_int": ("INT", {"default": 1, "step": 1}),
},
"optional": {
"gender_alternative": ("STRING", {"forceInput": True}),
}
}
RETURN_TYPES = ("STRING","FLOAT","INT","BOOLEAN","BOOLEAN")
RETURN_NAMES = ("gender_text","float","int","is_male","is_female")
FUNCTION = "fun"
CATEGORY = "utils/text"
@ staticmethod
def fun(gender_prior,male_text,male_float,male_int,female_text,female_float,female_int, gender_alternative=None):
gender= gender_prior if gender_prior else gender_alternative
if gender is None or gender.upper() not in ["M", "F"]:
raise Exception("can't get any gender input.")
if gender.upper()== "M":
return (male_text, male_float, male_int, True, False)
else:
return (female_text, female_float, female_int, False, True)
class ImageConcanateOfUtils:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"image1": ("IMAGE",),
"image2": ("IMAGE",),
"direction": (
['right',
'down',
'left',
'up',
],
{
"default": 'right'
}),
},
"optional":{
"image3": ("IMAGE",),
"image4": ("IMAGE",),
"image5": ("IMAGE",),
"image6": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "concanate"
CATEGORY = "utils/image"
def concanate(self, image1, image2=None, image3=None, image4=None, image5=None, image6=None, direction='right'):
images = [image1]
# 添加非空的image3-6到列表中
for img in [image2,image3, image4, image5, image6]:
if img is not None:
images.append(img)
# 如果只有一张图片,直接返回
if len(images) == 1:
return (images[0],)
# 根据拼接方向调整图片尺寸
if direction in ['right', 'left']:
# 左右拼接:调整到同一高度
target_height = images[0].shape[1]
for i in range(1, len(images)):
if images[i].shape[1] != target_height:
# 计算等比例缩放后的宽度
aspect_ratio = images[i].shape[2] / images[i].shape[1]
target_width = int(target_height * aspect_ratio)
images[i] = comfy.utils.common_upscale(
images[i].movedim(-1, 1), target_width, target_height, "bilinear", "center").movedim(1, -1)
row = torch.cat(images if direction == 'right' else [i for i in reversed(images)], dim=2)
elif direction in ['down', 'up']:
# 上下拼接:调整到同一宽度
target_width = images[0].shape[2]
for i in range(1, len(images)):
if images[i].shape[2] != target_width:
# 计算等比例缩放后的高度
aspect_ratio = images[i].shape[1] / images[i].shape[2]
target_height = int(target_width * aspect_ratio)
images[i] = comfy.utils.common_upscale(
images[i].movedim(-1, 1), target_width, target_height, "bilinear", "center").movedim(1, -1)
row = torch.cat(images if direction == 'down' else [i for i in reversed(images)], dim=1)
return (row,)
class SplitMask:
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"mask_prior": ("MASK", ),
},
"optional": {
"mask_alternative": ("MASK", )
}
}
RETURN_TYPES = ("MASK", "MASK",)
RETURN_NAMES = ("mask", "mask",)
FUNCTION = 'split_mask'
CATEGORY = 'utils/mask'
def split_mask(self, mask_prior, mask_alternative=None):
mask = mask_prior if mask_prior is not None else mask_alternative
if mask is None:
return [torch.zeros((64, 64)).unsqueeze(0)] * 2
ret_masks = []
gray_image = mask[0].detach().cpu().numpy()
# 对灰度图像进行阈值化处理,将白色区域转换为二进制掩码
_, binary_mask = cv2.threshold(gray_image.astype(
np.uint8), 0.5, 255, cv2.THRESH_BINARY)
# 寻找白色区域的轮廓
contours, _ = cv2.findContours(
binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
logger.info(f"find mask areas:{len(contours)}")
if contours is not None and len(contours) > 0:
# 根据轮廓的面积对其进行排序
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:2]
contours = sorted(contours, key=lambda c: cv2.boundingRect(c)[0])
for i, contour in enumerate(contours):
# 创建一个新的同样尺寸的空图像
new_mask = np.zeros_like(gray_image)
# 在空图像中绘制当前轮廓
cv2.drawContours(
new_mask, [contour], -1, (255), thickness=cv2.FILLED)
ret_masks.append(torch.tensor(new_mask/255))
else:
# 如果未找到轮廓,则返回空 tensor
ret_masks = [torch.tensor(np.zeros_like(gray_image))] * 2
if len(ret_masks) < 2:
ret_masks.extend(
[torch.tensor(np.zeros_like(gray_image))]*(2-len(ret_masks)))
ret_masks = [torch.unsqueeze(m, 0) for m in ret_masks]
return ret_masks
class MaskFastGrow:
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"mask": ("MASK", ),
"invert_mask": ("BOOLEAN", {"default": True}), # 反转mask
"grow": ("INT", {"default": 4, "min": -999, "max": 999, "step": 1}),
"blur": ("INT", {"default": 4, "min": 0, "max": 999, "step": 1}),
},
"optional": {
"low_limit": ("FLOAT", {"default": 0, "min": 0, "max": 1, "step": 0.01}),
"high_limit": ("FLOAT", {"default": 1, "min": 0, "max": 1, "step": 0.01}),
"enabled": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
}
}
RETURN_TYPES = ("MASK",)
FUNCTION = 'mask_grow'
CATEGORY = 'utils/mask'
def mask_grow(self, mask, invert_mask, grow, blur, low_limit=0, high_limit=1, enabled=True):
if not enabled:
return (mask,)
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
c = 0
kernel = np.array([[c, 1, c],
[1, 1, 1],
[c, 1, c]], dtype=np.uint8)
mask = mask.reshape((-1, mask.shape[-2], mask.shape[-1]))
out = []
for m in mask:
if invert_mask:
m = 1 - m
output = m.numpy().astype(np.float32)
# Scale the float mask to [0, 255] for OpenCV processing
output_scaled = (output * 255).astype(np.uint8)
if grow > 0:
output_scaled = cv2.dilate(
output_scaled, kernel, iterations=grow)
else:
output_scaled = cv2.erode(
output_scaled, kernel, iterations=-grow)
# Apply Gaussian blur using OpenCV
if blur > 0:
output_blurred = cv2.GaussianBlur(
output_scaled, (blur*2+1, blur*2+1), 0)
else:
output_blurred = output_scaled
# Scale back to [0, 1]
output = output_blurred.astype(np.float32) / 255.0
if low_limit > 0:
output = np.clip(output, low_limit, 1)
if high_limit < 1:
output = np.clip(output, 0, high_limit)
out.append(torch.from_numpy(output))
result = torch.stack(out, dim=0)
if result.dim() == 2:
result = torch.unsqueeze(result, 0)
return (result,)
class MaskFromFaceModel:
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE",),
"max_face_number": ("INT", {"default": -1, "min": -1, "max": 99, "step": 1}),
"add_bbox_upper_points": ("BOOLEAN", {"default": False}), # 新增参数
},
"optional": {
"faceanalysis": ("FACEANALYSIS", ),
"face_model": ("FACE_MODEL", ),
"cant_detect_mask_mode": (["black", "white", "none"], {"default": "black"}),
"enabled": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"face_confidence": ("FLOAT", {"default": 0.75, "min": 0.0, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("MASK","STRING")
RETURN_NAMES = ("mask","genders")
FUNCTION = 'mask_get'
CATEGORY = 'utils/mask'
def mask_get(self, image, max_face_number, add_bbox_upper_points, faceanalysis=None, face_model=None, cant_detect_mask_mode="black", enabled=True, face_confidence=0.75):
if not enabled:
return (None,)
h, w = image.shape[-3:-1]
cant_detect_result = None
if cant_detect_mask_mode == "black":
cant_detect_result = torch.zeros((1, h, w), dtype=torch.uint8)
elif cant_detect_mask_mode == "white":
cant_detect_result = torch.ones((1, h, w), dtype=torch.uint8)
if faceanalysis is None and face_model is None:
raise Exception("both faceanalysis and face_model are none!")
if face_model is None:
image_np = tensor2cv(image)
image_np = image_np[0] if isinstance(image_np, list) else image_np
face_model = self.analyze_faces(faceanalysis, image_np)
if not isinstance(face_model,list):
if face_model is None:
face_models = []
else:
face_models = [face_model]
else:
face_models = face_model
if len(face_models) == 0:
return (cant_detect_result,)
# 过滤低置信度的face
det_scores = [f.det_score for f in face_models]
if len(face_models) > 1 and max(det_scores) > face_confidence:
face_models = [f for f in face_models if f.det_score > face_confidence]
if max_face_number !=-1 and len(face_model) > max_face_number:
face_models = self.remove_unavaible_face_models(face_models=face_models,max_people_number=max_face_number)
face_models = sorted(face_models, key=lambda x:x.bbox[0])
result = np.zeros((h, w), dtype=np.uint8)
genders = []
for face in face_models:
genders.append(face.sex)
points = face.landmark_2d_106.astype(np.int32) # Convert landmarks to integer format
if add_bbox_upper_points:
# 获取bbox的坐标
x1, y1 = face.bbox[0:2]
x2, y2 = face.bbox[2:4]
# 计算上边的1/4和3/4位置的点
width = x2 - x1
left_quarter_x = x1 + width // 4
right_quarter_x = x2 - width // 4
# 创建两个新点
left_quarter_point = np.array([left_quarter_x, y1], dtype=np.int32)
right_quarter_point = np.array([right_quarter_x, y1], dtype=np.int32)
# 将两个点添加到landmarks中
points = np.vstack((points, left_quarter_point, right_quarter_point))
points = points.reshape((-1, 1, 2)) # Reshape for cv2.drawContours
# Compute the convex hull for the landmarks
hull = cv2.convexHull(points)
# Draw the convex hull on the mask as well
cv2.drawContours(result, [hull], contourIdx=-1, color=255, thickness=cv2.FILLED)
result = torch.unsqueeze(torch.tensor(np.clip(result/255, 0, 1)), 0)
return (result, ",".join(genders))
def remove_unavaible_face_models(self, face_models, max_people_number):
max_lengths = []
# Calculate the maximum length for each group of keypoints
kpss = [f.kps for f in face_models]
for keypoints in kpss:
max_length = self.get_max_distance(keypoints)
max_lengths.append(max_length)
sorted_touple = sorted(zip(face_models, max_lengths), key=lambda x:x[1], reverse=True)
# Filter out keypoints groups that have a maximum length less than one-fourth of the largest maximum length
filtered_face_models = [
face_model for face_model, _ in sorted_touple[:max_people_number]
]
return filtered_face_models
def get_max_distance(self, keypoints):
max_distance = 0
# Calculate the distance between every pair of keypoints
for i in range(len(keypoints)):
for j in range(i + 1, len(keypoints)):
if keypoints[i] is not None and keypoints[j] is not None:
distance = dist(keypoints[i], keypoints[j])
max_distance = max(max_distance, distance)
return max_distance
def analyze_faces(self, insightface, img_data: np.ndarray):
for size in [(size, size) for size in range(640, 310, -320)]:
insightface.det_model.input_size = size
face = insightface.get(img_data)
if face:
if 640 not in size:
print(f"\033[33mINFO: InsightFace detection resolution lowered to {size}.\033[0m")
break
return face
class MaskofCenter:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"width": ("INT", {"default": 1024, "min": 0, "max": 8096, "step": 8}),
"height": ("INT", {"default": 1024, "min": 0, "max": 8096, "step": 8}),
"top": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}),
"bottom": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}),
"left": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}),
"right": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 1.0, "step": 0.01}),
"redius": ("FLOAT", {"default": 0.05, "min": 0.0, "max": 0.5, "step": 0.01}),
},
"optional": {
"size_as": ("IMAGE",),
}
}
RETURN_TYPES = ("MASK","INT","INT")
RETURN_NAMES = ("mask","width","height")
FUNCTION = 'mask_get'
CATEGORY = 'utils/mask'
def mask_get(self, size_as=None, width=1024, height=1024, top=0.25, bottom=0.25, left=0.25, right=0.25, redius=0.05):
if size_as is not None:
height, width = size_as.shape[-3:-1]
# 创建全白色mask
mask = torch.ones((1, height, width), dtype=torch.float32)
# 计算各边界的像素值
top_pixels = int(height * top)
bottom_pixels = int(height * bottom)
left_pixels = int(width * left)
right_pixels = int(width * right)
# 将边界区域设为黑色(0)
if top > 0:
mask[:, :top_pixels, :] = 0
if bottom > 0:
mask[:, -bottom_pixels:, :] = 0
if left > 0:
mask[:, :, :left_pixels] = 0
if right > 0:
mask[:, :, -right_pixels:] = 0
# 添加圆弧效果
if redius > 0:
mask = self.add_corner_radius(mask[0], top_pixels, bottom_pixels, left_pixels, right_pixels, redius)
mask = torch.unsqueeze(mask, 0)
return (mask, width, height)
def add_corner_radius(self, mask, top_pixels, bottom_pixels, left_pixels, right_pixels, radius_percent):
"""在mask的白色区域四个角添加圆弧效果
Args:
mask: 2D tensor mask
top_pixels: 顶部黑色区域高度
bottom_pixels: 底部黑色区域高度
left_pixels: 左侧黑色区域宽度
right_pixels: 右侧黑色区域宽度
radius_percent: 圆弧半径(相对于较短边的百分比)
"""
height, width = mask.shape
min_side = min(height, width)
radius = int(min_side * radius_percent)
if radius <= 0:
return mask
# 创建圆形kernel
y, x = torch.meshgrid(torch.arange(radius), torch.arange(radius))
# 计算到圆心的距离
dist_from_center = torch.sqrt((x - (radius-1))**2 + (y - (radius-1))**2).float()
# 圆内为1,圆外为0
circle = (dist_from_center <= radius).float()
# 左上角
mask[top_pixels:top_pixels+radius, left_pixels:left_pixels+radius] = circle
# 右上角
mask[top_pixels:top_pixels+radius, width-right_pixels-radius:width-right_pixels] = torch.flip(circle, [1])
# 左下角
mask[height-bottom_pixels-radius:height-bottom_pixels, left_pixels:left_pixels+radius] = torch.flip(circle, [0])
# 右下角
mask[height-bottom_pixels-radius:height-bottom_pixels, width-right_pixels-radius:width-right_pixels] = torch.flip(circle, [0, 1])
return mask
class MaskCoverFourCorners:
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"width": ("INT", {"default": 1024, "min": 0, "max": 8096, "step": 8}),
"height": ("INT", {"default": 1024, "min": 0, "max": 8096, "step": 8}),
"radius": ("INT", {"default": 100, "min": 0, "max": 8096, "step": 5}),
"draw_top_left": ("BOOLEAN", {"default": False}),
"draw_top_right": ("BOOLEAN", {"default": False}),
"draw_bottom_right": ("BOOLEAN", {"default": True}),
"draw_bottom_left": ("BOOLEAN", {"default": False}),
},
"optional": {
"size_as": ("IMAGE",),
}
}
RETURN_TYPES = ("MASK",)
FUNCTION = 'mask_get'
CATEGORY = 'utils/mask'
def mask_get(self, size_as=None, width=1024, height=1024, radius=100,
draw_top_left=False, draw_top_right=False, draw_bottom_right=True, draw_bottom_left=False):
if size_as is not None:
height, width = size_as.shape[-3:-1]
result = self.create_mask_with_arcs(width, height, radius,draw_top_left, draw_top_right,draw_bottom_right,draw_bottom_left)
result = torch.unsqueeze(torch.tensor(np.clip(result/255, 0, 1)), 0)
return (result,)
def create_mask_with_arcs(self, width=None, height=None, radius=50,
draw_top_left=True, draw_top_right=True, draw_bottom_right=True, draw_bottom_left=True):
"""
Creates a mask with circular arcs at the corners.
:param width: Width of the mask.
:param height: Height of the mask.
:param radius: Radius of the circular arcs.
:param draw_top_left: Boolean indicating whether to draw an arc at the top-left corner.
:param draw_top_right: Boolean indicating whether to draw an arc at the top-right corner.
:param draw_bottom_right: Boolean indicating whether to draw an arc at the bottom-right corner.
:param draw_bottom_left: Boolean indicating whether to draw an arc at the bottom-left corner.
:return: Mask image with arcs drawn.
"""
# Create a white mask
mask = np.ones((height, width), dtype=np.uint8) * 255
# Draw arcs on the mask, filling them with black color
if draw_top_left: # Top-left corner
cv2.ellipse(mask, (0, 0), (radius, radius), 0, 0, 90, 0, -1) # Fill with black
if draw_top_right: # Top-right corner
cv2.ellipse(mask, (width, 0), (radius, radius), 0, 90, 180, 0, -1) # Fill with black
if draw_bottom_right: # Bottom-right corner
cv2.ellipse(mask, (width, height), (radius, radius), 0, 180, 270, 0, -1) # Fill with black
if draw_bottom_left: # Bottom-left corner
cv2.ellipse(mask, (0, height), (radius, radius), 0, 0, -90, 0, -1) # Fill with black
return mask
class MaskAutoSelector:
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"mask_prior": ("MASK", ),
},
"optional": {
"mask_alternative": ("MASK", ),
"mask_third": ("MASK", )
}
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = 'select_mask'
CATEGORY = 'utils/mask'
def select_mask(self, mask_prior=None, mask_alternative=None, mask_third=None):
if mask_prior is not None:
mask = mask_prior
elif mask_alternative is not None:
mask = mask_alternative
else:
mask = mask_third
if mask is None:
raise RuntimeError("all mask inputs is None")
if mask.dim() == 2:
mask = torch.unsqueeze(mask, 0)
return (mask,)
class FloatMultipleAddLiteral:
RETURN_TYPES = ("FLOAT", "FLOAT", "INT")
RETURN_NAMES = ("x", "ax + b", "ax + b(int)")
FUNCTION = "get_float"
CATEGORY = "utils/numbers"
@classmethod
def INPUT_TYPES(cls):
return {"required": {"number": ("FLOAT", {"default": 0, "min": 0, "max": 1000000})},
"optional": {"a_aign": (["positive", "negative"], {"default": "positive"}),
"a": ("FLOAT", {"default": 1.0, "step": 0.001}), "b": ("FLOAT", {"default": 1, "step": 0.001}),
}
}
def get_float(self, number, a, b, a_aign):
if a_aign == "negative":
a = - a
return (number, a*number + b, int(a*number + b))
class IntMultipleAddLiteral:
RETURN_TYPES = ("INT", "INT", "FLOAT")
RETURN_NAMES = ("x", "ax + b", "ax + b(float)")
FUNCTION = "get_int"
CATEGORY = "utils/numbers"
@classmethod
def INPUT_TYPES(cls):
return {"required": {"number": ("INT", {"default": 0, "min": 0, "max": 1000000})},
"optional": {"a_aign": (["positive", "negative"], {"default": "positive"}),
"a": ("FLOAT", {"default": 1.0, "step": 0.001}), "b": ("INT", {"default": 1, "step": 1}),
}
}
def get_int(self, number, a, b, a_aign):
if a_aign == "negative":
a = - a
return (number, int(a*number + b), a*number + b)
MAX_RESOLUTION = 16384
try:
from comfy_api.internal import _ComfyNodeInternal
from comfy_api.latest import _io as IO
from dataclasses import replace
_HAS_V3_SUPPORT = True
except ImportError:
_HAS_V3_SUPPORT = False
_ComfyNodeInternal = None
IO = None
replace = None
def _is_parent_v3():
try:
is_v3 = _HAS_V3_SUPPORT and issubclass(ImageCompositeMasked, _ComfyNodeInternal)
if _HAS_V3_SUPPORT:
logger.info(f"ImageCompositeMaskedWithSwitch: 检测父节点类型 - V3支持={_HAS_V3_SUPPORT}, 父节点是V3={is_v3}")
return is_v3
except (TypeError, AttributeError) as e:
logger.warning(f"ImageCompositeMaskedWithSwitch: 检测父节点类型时出错 - {e}")
return False
_IS_PARENT_V3 = _is_parent_v3()
if _IS_PARENT_V3:
logger.info("ImageCompositeMaskedWithSwitch: 使用 V3 Schema 模式")
else:
logger.info("ImageCompositeMaskedWithSwitch: 使用 V1 INPUT_TYPES 模式")
class ImageCompositeMaskedWithSwitch(ImageCompositeMasked):
if _IS_PARENT_V3:
@classmethod
def define_schema(cls):
logger.debug("ImageCompositeMaskedWithSwitch: 调用 define_schema (V3模式)")
parent_schema = super().define_schema()
parent_inputs = list(parent_schema.inputs) if parent_schema.inputs else []
parent_inputs.extend([
IO.Boolean.Input("enabled", default=True),
IO.Boolean.Input("invert_mask", default=False),
])
logger.debug(f"ImageCompositeMaskedWithSwitch: Schema定义完成,共{len(parent_inputs)}个输入")
return replace(
parent_schema,
node_id="ImageCompositeMaskedWithSwitch",
category="utils/image",
inputs=parent_inputs,
)
@classmethod
def execute(cls, destination, source, x, y, resize_source, mask=None, enabled=True, invert_mask=False):
logger.debug(f"ImageCompositeMaskedWithSwitch: 执行 execute (V3模式) - enabled={enabled}, invert_mask={invert_mask}")
if not enabled:
logger.debug("ImageCompositeMaskedWithSwitch: enabled=False, 直接返回destination")
return IO.NodeOutput(destination)
if invert_mask and mask is not None:
logger.debug("ImageCompositeMaskedWithSwitch: 反转mask")
mask = 1.0 - mask
return super().execute(destination, source, x, y, resize_source, mask)
else:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"destination": ("IMAGE",),
"source": ("IMAGE",),
"x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
"y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
"resize_source": ("BOOLEAN", {"default": False}),
},
"optional": {
"mask": ("MASK",),
"enabled": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"invert_mask": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "composite_with_switch"
CATEGORY = "utils/image"
def composite_with_switch(self, destination, source, x, y, resize_source, mask=None, enabled=True, invert_mask=False):
logger.debug(f"ImageCompositeMaskedWithSwitch: 执行 composite_with_switch (V1模式) - enabled={enabled}, invert_mask={invert_mask}")
if not enabled:
logger.debug("ImageCompositeMaskedWithSwitch: enabled=False, 直接返回destination")
return (destination, )
if invert_mask:
logger.debug("ImageCompositeMaskedWithSwitch: 反转mask")
mask = 1.0 - mask
return self.composite(destination, source, x, y, resize_source, mask)
class CheckpointLoaderSimpleWithSwitch:
@classmethod
def INPUT_TYPES(s):
return {"required": {"ckpt_name": (folder_paths.get_filename_list("checkpoints"), ),
},
"optional": {
"load_model": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"load_clip": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
"load_vae": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
}}
RETURN_TYPES = ("MODEL", "CLIP", "VAE")
FUNCTION = "load_checkpoint"
CATEGORY = "utils/loaders"
def load_checkpoint(self, ckpt_name, load_model, load_clip, load_vae):
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
out = comfy.sd.load_checkpoint_guess_config(ckpt_path, output_model=load_model, output_vae=load_vae,
output_clip=load_clip, embedding_directory=folder_paths.get_folder_paths("embeddings"))
return out[:3]
class ImageResizeTo8x:
def __init__(self):
pass
ACTION_TYPE_RESIZE = "resize only"
ACTION_TYPE_CROP = "crop to ratio"
ACTION_TYPE_PAD = "pad to ratio"
RESIZE_MODE_DOWNSCALE = "reduce size only"
RESIZE_MODE_UPSCALE = "increase size only"
RESIZE_MODE_ANY = "any"
RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT")
RETURN_NAMES = ("image", "mask", "width", "height")
FUNCTION = "resize"
CATEGORY = "utils/image"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"pixels": ("IMAGE",),
"action": ([s.ACTION_TYPE_RESIZE, s.ACTION_TYPE_CROP, s.ACTION_TYPE_PAD],),
"smaller_side": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}),
"larger_side": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}),
"target_width": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 1}),
"target_height": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 1}),
"scale_factor": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.1}),
"resize_mode": ([s.RESIZE_MODE_DOWNSCALE, s.RESIZE_MODE_UPSCALE, s.RESIZE_MODE_ANY],),
"side_ratio": ("STRING", {"default": "4:3"}),
"crop_pad_position": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"pad_feathering": ("INT", {"default": 20, "min": 0, "max": 8192, "step": 1}),
"all_szie_8x": (["disable", "crop", "resize"],),
},
"optional": {
"mask_optional": ("MASK",),
"all_size_16x": (["disable", "crop", "resize"],),
},
}
@classmethod
def VALIDATE_INPUTS(s, action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio,target_width,target_height, **_):
if side_ratio is not None:
if action != s.ACTION_TYPE_RESIZE and s.parse_side_ratio(side_ratio) is None:
return f"Invalid side ratio: {side_ratio}"
if smaller_side is not None and larger_side is not None and scale_factor is not None:
if int(smaller_side > 0) + int(larger_side > 0) + int(scale_factor > 0) > 1:
return f"At most one scaling rule (smaller_side, larger_side, scale_factor) should be enabled by setting a non-zero value"
if scale_factor is not None:
if resize_mode == s.RESIZE_MODE_DOWNSCALE and scale_factor > 1.0:
return f"For resize_mode {s.RESIZE_MODE_DOWNSCALE}, scale_factor should be less than one but got {scale_factor}"
if resize_mode == s.RESIZE_MODE_UPSCALE and scale_factor > 0.0 and scale_factor < 1.0:
return f"For resize_mode {s.RESIZE_MODE_UPSCALE}, scale_factor should be larger than one but got {scale_factor}"
if (target_width == 0 and target_height != 0) or (target_width != 0 and target_height == 0):
return f"targe_width and target_height should be set or unset simultaneously"
return True
@classmethod
def parse_side_ratio(s, side_ratio):
try:
x, y = map(int, side_ratio.split(":", 1))
if x < 1 or y < 1:
raise Exception("Ratio factors have to be positive numbers")
return float(x) / float(y)
except:
return None
def vae_encode_crop_pixels(self, pixels, ratio=8):
dims = pixels.shape[1:3]
for d in range(len(dims)):
x = (dims[d] // ratio) * ratio
x_offset = (dims[d] % ratio) // 2
if x != dims[d]:
pixels = pixels.narrow(d + 1, x_offset, x)
return pixels
def resize_a_little_to_ratio(self, image, mask,ratio=8):
in_h, in_w = image.shape[1:3]
out_h = (in_h // ratio) * ratio
out_w = (in_w // ratio) * ratio
if in_h != out_h or in_w != out_w:
image, mask = self.interpolate_to_target_size(image, mask, out_h, out_w)
return image, mask
def interpolate_to_target_size(self, image, mask, height, width):
image = torch.nn.functional.interpolate(
image.movedim(-1, 1), size=(height, width), mode="bicubic", antialias=True).movedim(1, -1).clamp(0.0, 1.0)
mask = torch.nn.functional.interpolate(mask.unsqueeze(
0), size=(height, width), mode="bicubic", antialias=True).squeeze(0).clamp(0.0, 1.0)
return image, mask
def resize(self, pixels, action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio, crop_pad_position, pad_feathering, mask_optional=None, all_szie_8x="disable",target_width=0,target_height=0,all_size_16x="disable"):
validity = self.VALIDATE_INPUTS(
action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio,target_width,target_height)
if validity is not True:
raise Exception(validity)
height, width = pixels.shape[1:3]
if mask_optional is None:
mask = torch.zeros(1, height, width, dtype=torch.float32)
else:
mask = mask_optional
if len(mask.shape) == 2:
mask = mask.unsqueeze(0)
if mask.shape[1] != height or mask.shape[2] != width:
mask = torch.nn.functional.interpolate(mask.unsqueeze(0), size=(
height, width), mode="bicubic").squeeze(0).clamp(0.0, 1.0)
crop_x, crop_y, pad_x, pad_y = (0.0, 0.0, 0.0, 0.0)
if action == self.ACTION_TYPE_CROP:
target_ratio = target_width / target_height if target_width != 0 and target_height!=0 else self.parse_side_ratio(side_ratio)
if height * target_ratio < width:
crop_x = width - height * target_ratio
else:
crop_y = height - width / target_ratio
elif action == self.ACTION_TYPE_PAD:
target_ratio = target_width / target_height if target_width != 0 and target_height!=0 else self.parse_side_ratio(side_ratio)
if height * target_ratio > width:
pad_x = height * target_ratio - width
else:
pad_y = width / target_ratio - height
if smaller_side > 0:
if width + pad_x - crop_x > height + pad_y - crop_y:
scale_factor = float(smaller_side) / (height + pad_y - crop_y)
else:
scale_factor = float(smaller_side) / (width + pad_x - crop_x)
if larger_side > 0:
if width + pad_x - crop_x > height + pad_y - crop_y:
scale_factor = float(larger_side) / (width + pad_x - crop_x)
else:
scale_factor = float(larger_side) / (height + pad_y - crop_y)
if (resize_mode == self.RESIZE_MODE_DOWNSCALE and scale_factor >= 1.0) or (resize_mode == self.RESIZE_MODE_UPSCALE and scale_factor <= 1.0):
scale_factor = 0.0
if scale_factor > 0.0:
pixels = torch.nn.functional.interpolate(
pixels.movedim(-1, 1), scale_factor=scale_factor, mode="bicubic", antialias=True).movedim(1, -1).clamp(0.0, 1.0)
mask = torch.nn.functional.interpolate(mask.unsqueeze(
0), scale_factor=scale_factor, mode="bicubic", antialias=True).squeeze(0).clamp(0.0, 1.0)
height, width = pixels.shape[1:3]
crop_x *= scale_factor
crop_y *= scale_factor
pad_x *= scale_factor
pad_y *= scale_factor
if crop_x > 0.0 or crop_y > 0.0:
remove_x = (round(crop_x * crop_pad_position), round(crop_x *
(1 - crop_pad_position))) if crop_x > 0.0 else (0, 0)
remove_y = (round(crop_y * crop_pad_position), round(crop_y *
(1 - crop_pad_position))) if crop_y > 0.0 else (0, 0)
pixels = pixels[:, remove_y[0]:height -
remove_y[1], remove_x[0]:width - remove_x[1], :]
mask = mask[:, remove_y[0]:height - remove_y[1],
remove_x[0]:width - remove_x[1]]
elif pad_x > 0.0 or pad_y > 0.0:
add_x = (round(pad_x * crop_pad_position), round(pad_x *
(1 - crop_pad_position))) if pad_x > 0.0 else (0, 0)
add_y = (round(pad_y * crop_pad_position), round(pad_y *
(1 - crop_pad_position))) if pad_y > 0.0 else (0, 0)
new_pixels = torch.zeros(pixels.shape[0], height + add_y[0] + add_y[1],
width + add_x[0] + add_x[1], pixels.shape[3], dtype=torch.float32)
new_pixels[:, add_y[0]:height + add_y[0],
add_x[0]:width + add_x[0], :] = pixels
pixels = new_pixels
new_mask = torch.ones(
mask.shape[0], height + add_y[0] + add_y[1], width + add_x[0] + add_x[1], dtype=torch.float32)
new_mask[:, add_y[0]:height + add_y[0],
add_x[0]:width + add_x[0]] = mask
mask = new_mask
if pad_feathering > 0:
for i in range(mask.shape[0]):
for j in range(pad_feathering):
feather_strength = (
1 - j / pad_feathering) * (1 - j / pad_feathering)
if add_x[0] > 0 and j < width:
for k in range(height):
mask[i, k, add_x[0] +
j] = max(mask[i, k, add_x[0] + j], feather_strength)
if add_x[1] > 0 and j < width:
for k in range(height):
mask[i, k, width + add_x[0] - j - 1] = max(
mask[i, k, width + add_x[0] - j - 1], feather_strength)
if add_y[0] > 0 and j < height:
for k in range(width):
mask[i, add_y[0] + j,
k] = max(mask[i, add_y[0] + j, k], feather_strength)
if add_y[1] > 0 and j < height:
for k in range(width):
mask[i, height + add_y[0] - j - 1, k] = max(
mask[i, height + add_y[0] - j - 1, k], feather_strength)
if target_width != 0 and target_height!=0:
pixels, mask = self.interpolate_to_target_size(pixels, mask, target_height, target_width)
if all_size_16x == "crop":
pixels = self.vae_encode_crop_pixels(pixels,16)
mask = self.vae_encode_crop_pixels(mask,16)
elif all_size_16x == "resize":
pixels, mask = self.resize_a_little_to_ratio(pixels, mask, ratio=16)
elif all_szie_8x == "crop":
pixels = self.vae_encode_crop_pixels(pixels)
mask = self.vae_encode_crop_pixels(mask)
elif all_szie_8x == "resize":
pixels, mask = self.resize_a_little_to_ratio(pixels, mask, ratio=8)
height, width = pixels.shape[1:3]
return (pixels, mask, width, height)
class TextPreview:
"""this node code comes from ComfyUI-Custom-Scripts\py\show_text.py. thanks the orininal writer."""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
INPUT_IS_LIST = True
RETURN_TYPES = ("STRING",)
FUNCTION = "notify"
OUTPUT_NODE = True
OUTPUT_IS_LIST = (True,)
CATEGORY = "utils/text"
def notify(self, text, unique_id=None, extra_pnginfo=None):
if unique_id is not None and extra_pnginfo is not None:
if not isinstance(extra_pnginfo, list):
logger.warn("Error: extra_pnginfo is not a list")
elif (
not isinstance(extra_pnginfo[0], dict)
or "workflow" not in extra_pnginfo[0]
):
logger.warn(
"Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
else:
workflow = extra_pnginfo[0]["workflow"]
node = next(
(x for x in workflow["nodes"] if str(
x["id"]) == str(unique_id[0])),
None,
)
if node:
node["widgets_values"] = [text]
return {"ui": {"text": text}, "result": (text,)}
class TextInputAutoSelector:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"component_input": ("STRING", {"multiline": True}),
},
"optional":{
"alternative_input": ("STRING",{"forceInput": True}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "select_input"
CATEGORY = "utils/text"
def select_input(self, component_input, alternative_input=""):
# 去除组件输入两端的空白字符
component_input = component_input.strip()
# 如果组件输入为空或只包含空白字符,选择外部输入
if not component_input:
selected_input = alternative_input
else:
selected_input = component_input
return (selected_input,)
class AspectRatioSizeNodeOfUtils:
@classmethod
def INPUT_TYPES(cls):
aspect_options = list(ASPECT_RATIO_RESOLUTIONS.keys()) + ["auto"]
return {
"required": {
"aspect_ratio": (aspect_options,),
"size": (list(SIZE_MULTIPLIERS.keys()),),
"divisible_by": (["none", "8", "16"],),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4096, "step": 1}),
},
"optional": {
"input_width": ("INT", {"default": 1024, "min": 1, "max": 8192, "step": 1}),
"input_height": ("INT", {"default": 1024, "min": 1, "max": 8192, "step": 1}),
}
}
RETURN_TYPES = ("INT", "INT", "INT")
RETURN_NAMES = ("width", "height", "batch_size")
FUNCTION = "calculate_size"
CATEGORY = "utils/numbers"
def calculate_size(self, aspect_ratio, size, divisible_by, batch_size, input_width=1024, input_height=1024):
if aspect_ratio == "auto":
width, height = input_width, input_height
else:
base_w, base_h = ASPECT_RATIO_RESOLUTIONS[aspect_ratio]
multiplier = SIZE_MULTIPLIERS[size]
width, height = int(base_w * multiplier), int(base_h * multiplier)
width, height = self._apply_divisibility(width, height, divisible_by)
return (width, height, batch_size)
def _apply_divisibility(self, width, height, divisible_by):
if divisible_by == "none":
return width, height
divisor = int(divisible_by)
width = (width // divisor) * divisor
height = (height // divisor) * divisor
return max(divisor, width), max(divisor, height)
class MatchImageRatioToPreset:
def __init__(self):
self.presets = RESOLUTION_PRESETS_1K
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"width_offset": ("INT", {"default": 0, "min": -128, "max": 128, "step": 8}),
"height_offset": ("INT", {"default": 0, "min": -128, "max": 128, "step": 8}),
}
}
RETURN_TYPES = ("INT", "INT", "INT", "INT")
RETURN_NAMES = ("standard_width", "standard_height", "min", "max")
FUNCTION = "forward"
CATEGORY = "utils/image"
def forward(self, image, width_offset=0, height_offset=0):
h, w = image.shape[1:-1]
aspect_ratio = w / h
# 计算每个预设的宽高比,并与输入图像的宽高比进行比较
distances = [abs(aspect_ratio - w/h) for w,h in self.presets]
closest_index = np.argmin(distances)
# 选择最接近的预设尺寸
target_w, target_h = self.presets[closest_index]
if width_offset != 0:
target_w += width_offset
if height_offset != 0:
target_h += height_offset
max_v, min_v = max(target_h, target_w), min(target_h, target_w)
logger.debug((target_w, target_h, min_v, max_v))
return (target_w, target_h, min_v, max_v)
class UpscaleImageWithModelIfNeed(ImageUpscaleWithModel):
def __init__(self) -> None:
super().__init__()
@classmethod
def INPUT_TYPES(s):
return {"required": {"upscale_model": ("UPSCALE_MODEL",),
"image": ("IMAGE",),
"threshold_of_xl_area": ("FLOAT", {"default": 0.9, "min": 0.0, "max": 64.0, "step": 0.01}),
},
"hidden":{
"tile_size": ("INT", {"default": 512, "min": 128, "max": 10000}),
}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "forward"
CATEGORY = "utils/image"
def forward(self, image, upscale_model, threshold_of_xl_area=0.9):
h, w = image.shape[1:-1]
percent = h * w / (1024 * 1024)
if percent > threshold_of_xl_area:
return (image,)
return self.upscale(upscale_model, image)
class ImageAutoSelector:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
},
"optional": {
"image_prior": ("IMAGE",),
"image_alternative": ("IMAGE",),
"image_third": ("IMAGE",)
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "select_image"
CATEGORY = "utils/image"
def select_image(self, image_prior=None, image_alternative=None, image_third=None):
if image_prior is not None:
image = image_prior
elif image_alternative is not None:
image = image_alternative
else:
image = image_third
if image is None:
raise RuntimeError("all image inputs are None")
return (image,)
class BooleanControlOutput:
"""
This node will output different values based on a boolean input
"""
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"boolean_input": ("BOOLEAN", {"default": True, "label_on": "True", "label_off": "False"}),
"true_text": ("STRING", {"multiline": True, "defaultBehavior": "input"}),
"true_float": ("FLOAT", {"default": 1, "step": 0.05}),
"true_int": ("INT", {"default": 1, "step": 1}),
"false_text": ("STRING", {"multiline": True, "defaultBehavior": "input"}),
"false_float": ("FLOAT", {"default": 0, "step": 0.1}),
"false_int": ("INT", {"default": 0, "step": 1}),
}}
RETURN_TYPES = ("STRING", "FLOAT", "INT", "BOOLEAN", "BOOLEAN")
RETURN_NAMES = ("text", "float", "int", "is_true", "is_false")
FUNCTION = "fun"
CATEGORY = "utils/text"
@staticmethod
def fun(boolean_input, true_text, true_float, true_int, false_text, false_float, false_int):
if boolean_input:
return (true_text, true_float, true_int, True, False)
else:
return (false_text, false_float, false_int, False, True)
NODE_CLASS_MAPPINGS = {
#image
"LoadImageWithSwitch": LoadImageWithSwitch,
"LoadImageMaskWithSwitch": LoadImageMaskWithSwitch,
"LoadImageWithoutListDir": LoadImageWithoutListDir,
"LoadImageMaskWithoutListDir": LoadImageMaskWithoutListDir,
"ImageCompositeMaskedWithSwitch": ImageCompositeMaskedWithSwitch,
"ImageBatchOneOrMore": ImageBatchOneOrMore,
"ImageConcanateOfUtils": ImageConcanateOfUtils,
"ColorCorrectOfUtils": ColorCorrectOfUtils,
"UpscaleImageWithModelIfNeed": UpscaleImageWithModelIfNeed,
"ImageResizeTo8x": ImageResizeTo8x,
"ImageAutoSelector": ImageAutoSelector,
# text
"ConcatTextOfUtils": ConcatTextOfUtils,
"ModifyTextGender": ModifyTextGender,
"GenderControlOutput": GenderControlOutput,
"TextPreview": TextPreview,
"TextInputAutoSelector": TextInputAutoSelector,
"BooleanControlOutput": BooleanControlOutput,
# numbers
"AspectRatioSizeNodeOfUtils": AspectRatioSizeNodeOfUtils,
"MatchImageRatioToPreset": MatchImageRatioToPreset,
"FloatMultipleAddLiteral": FloatMultipleAddLiteral,
"IntMultipleAddLiteral": IntMultipleAddLiteral,
# mask
"SplitMask": SplitMask,
"MaskFastGrow": MaskFastGrow,
"MaskAutoSelector": MaskAutoSelector,
"MaskFromFaceModel": MaskFromFaceModel,
"MaskCoverFourCorners": MaskCoverFourCorners,
"MaskofCenter": MaskofCenter,
#loader
"CheckpointLoaderSimpleWithSwitch": CheckpointLoaderSimpleWithSwitch,
"ImageAutoSelector": ImageAutoSelector,
}
NODE_DISPLAY_NAME_MAPPINGS = {
# Image
"LoadImageWithSwitch": "Load Image with Switch",
"LoadImageMaskWithSwitch": "Load Image as Mask with Switch",
"LoadImageWithoutListDir": "Load Image without Listing Input Dir",
"LoadImageMaskWithoutListDir": "Load Image as Mask without Listing Input Dir",
"ImageCompositeMaskedWithSwitch": "Image Composite Masked with Switch",
"ImageBatchOneOrMore": "Batch Images One or More",
"ImageConcanateOfUtils": "Image Concatenate of Utils",
"ColorCorrectOfUtils": "Color Correct of Utils",
"UpscaleImageWithModelIfNeed": "Upscale Image Using Model if Need",
"ImageResizeTo8x": "Image Resize to 8x",
"ImageAutoSelector": "Image Auto Selector",
# Text
"ConcatTextOfUtils": "Concat Text",
"ModifyTextGender": "Modify Text Gender",
"GenderControlOutput": "Gender Control Output",
"TextPreview": "Preview Text",
"TextInputAutoSelector": "Text Input Auto Selector",
"BooleanControlOutput": "Boolean Control Output",
# Number
"AspectRatioSizeNodeOfUtils": "Aspect Ratio Size Selector",
"MatchImageRatioToPreset": "Match Image Ratio to Standard Size",
"FloatMultipleAddLiteral": "Float Multiple and Add Literal",
"IntMultipleAddLiteral": "Int Multiple and Add Literal",
# Mask
"SplitMask": "Split Mask by Contours",
"MaskFastGrow": "Mask Grow Fast",
"MaskAutoSelector": "Mask Auto Selector",
"MaskFromFaceModel": "Mask from FaceModel",
"MaskCoverFourCorners": "Mask Cover Four Corners",
"MaskofCenter": "Mask of Center",
# Loader
"CheckpointLoaderSimpleWithSwitch": "Load Checkpoint with Switch",
}