|
|
|
|
|
|
|
|
|
|
|
|
|
import os |
|
import folder_paths |
|
from PIL import Image, ImageFont |
|
import torch |
|
import numpy as np |
|
import re |
|
from pathlib import Path |
|
import typing as t |
|
from dataclasses import dataclass |
|
from .functions_xygrid import create_images_grid_by_columns, Annotation |
|
from ..categories import icons |
|
|
|
def tensor_to_pillow(image: t.Any) -> Image.Image: |
|
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) |
|
|
|
def pillow_to_tensor(image: Image.Image) -> t.Any: |
|
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) |
|
|
|
def find_highest_numeric_value(directory, filename_prefix): |
|
highest_value = -1 |
|
|
|
|
|
for filename in os.listdir(directory): |
|
if filename.startswith(filename_prefix): |
|
try: |
|
|
|
numeric_part = filename[len(filename_prefix):] |
|
numeric_str = re.search(r'\d+', numeric_part).group() |
|
numeric_value = int(numeric_str) |
|
|
|
if numeric_value > highest_value: |
|
highest_value = int(numeric_value) |
|
except ValueError: |
|
|
|
continue |
|
|
|
return highest_value |
|
|
|
|
|
class CR_XYList: |
|
|
|
@classmethod |
|
def INPUT_TYPES(s): |
|
return {"required":{ |
|
"index": ("INT", {"default": 0.0, "min": 0.0, "max": 9999.0, "step": 1.0,}), |
|
"list1": ("STRING", {"multiline": True, "default": "x"}), |
|
"x_prepend": ("STRING", {"multiline": False, "default": ""}), |
|
"x_append": ("STRING", {"multiline": False, "default": ""}), |
|
"x_annotation_prepend": ("STRING", {"multiline": False, "default": ""}), |
|
"list2": ("STRING", {"multiline": True, "default": "y"}), |
|
"y_prepend": ("STRING", {"multiline": False, "default": ""}), |
|
"y_append": ("STRING", {"multiline": False, "default": ""}), |
|
"y_annotation_prepend": ("STRING", {"multiline": False, "default": ""}), |
|
} |
|
} |
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "BOOLEAN", "STRING", ) |
|
RETURN_NAMES = ("X", "Y", "x_annotation", "y_annotation", "trigger", "show_help", ) |
|
FUNCTION = "cross_join" |
|
CATEGORY = icons.get("Comfyroll/XY Grid") |
|
|
|
def cross_join(self, list1, list2, x_prepend, x_append, x_annotation_prepend, |
|
y_prepend, y_append, y_annotation_prepend, index): |
|
|
|
|
|
index -=1 |
|
|
|
trigger = False |
|
|
|
|
|
|
|
listx = re.split(r',(?=(?:[^"]*"[^"]*")*[^"]*$)', list1) |
|
listy = re.split(r',(?=(?:[^"]*"[^"]*")*[^"]*$)', list2) |
|
|
|
listx = [item.strip() for item in listx] |
|
listy = [item.strip() for item in listy] |
|
|
|
lenx = len(listx) |
|
leny = len(listy) |
|
|
|
grid_size = lenx * leny |
|
|
|
x = index % lenx |
|
y = int(index / lenx) |
|
|
|
x_out = x_prepend + listx[x] + x_append |
|
y_out = y_prepend + listy[y] + y_append |
|
|
|
x_ann_out = "" |
|
y_ann_out = "" |
|
|
|
if index + 1 == grid_size: |
|
x_ann_out = [x_annotation_prepend + item + ";" for item in listx] |
|
y_ann_out = [y_annotation_prepend + item + ";" for item in listy] |
|
x_ann_out = "".join([str(item) for item in x_ann_out]) |
|
y_ann_out = "".join([str(item) for item in y_ann_out]) |
|
trigger = True |
|
|
|
show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/XY-Grid-Nodes#cr-xy-list" |
|
|
|
return (x_out, y_out, x_ann_out, y_ann_out, trigger, show_help, ) |
|
|
|
|
|
class CR_XYInterpolate: |
|
|
|
@classmethod |
|
def INPUT_TYPES(s): |
|
gradient_profiles = ["Lerp"] |
|
|
|
return {"required": {"x_columns":("INT", {"default": 5.0, "min": 0.0, "max": 9999.0, "step": 1.0,}), |
|
"x_start_value": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 9999.0, "step": 0.01,}), |
|
"x_step": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 9999.0, "step": 0.01,}), |
|
"x_annotation_prepend": ("STRING", {"multiline": False, "default": ""}), |
|
"y_rows":("INT", {"default": 5.0, "min": 0.0, "max": 9999.0, "step": 1.0,}), |
|
"y_start_value": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 9999.0, "step": 0.01,}), |
|
"y_step": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 9999.0, "step": 0.01,}), |
|
"y_annotation_prepend": ("STRING", {"multiline": False, "default": ""}), |
|
"index": ("INT", {"default": 0.0, "min": 0.0, "max": 9999.0, "step": 1.0,}), |
|
"gradient_profile": (gradient_profiles,) |
|
} |
|
} |
|
|
|
RETURN_TYPES = ("FLOAT", "FLOAT", "STRING", "STRING", "BOOLEAN", "STRING", ) |
|
RETURN_NAMES = ("X", "Y", "x_annotation", "y_annotation", "trigger", "show_help", ) |
|
FUNCTION = "gradient" |
|
CATEGORY = icons.get("Comfyroll/XY Grid") |
|
|
|
def gradient(self, x_columns, x_start_value, x_step, x_annotation_prepend, |
|
y_rows, y_start_value, y_step, y_annotation_prepend, |
|
index, gradient_profile): |
|
|
|
|
|
index -=1 |
|
trigger = False |
|
grid_size = x_columns * y_rows |
|
|
|
x = index % x_columns |
|
y = int(index / x_columns) |
|
|
|
x_float_out = round(x_start_value + x * x_step, 3) |
|
y_float_out = round(y_start_value + y * y_step, 3) |
|
|
|
x_ann_out = "" |
|
y_ann_out = "" |
|
|
|
if index + 1 == grid_size: |
|
for i in range(0, x_columns): |
|
x = index % x_columns |
|
x_float_out = x_start_value + i * x_step |
|
x_float_out = round(x_float_out, 3) |
|
x_ann_out = x_ann_out + x_annotation_prepend + str(x_float_out) + "; " |
|
for j in range(0, y_rows): |
|
y = int(index / x_columns) |
|
y_float_out = y_start_value + j * y_step |
|
y_float_out = round(y_float_out, 3) |
|
y_ann_out = y_ann_out + y_annotation_prepend + str(y_float_out) + "; " |
|
|
|
x_ann_out = x_ann_out[:-1] |
|
y_ann_out = y_ann_out[:-1] |
|
print(x_ann_out,y_ann_out) |
|
trigger = True |
|
|
|
show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/XY-Grid-Nodes#cr-xy-interpolate" |
|
|
|
return (x_float_out, y_float_out, x_ann_out, y_ann_out, trigger, show_help, ) |
|
|
|
|
|
class CR_XYIndex: |
|
|
|
@classmethod |
|
def INPUT_TYPES(s): |
|
gradient_profiles = ["Lerp"] |
|
|
|
return {"required": {"x_columns":("INT", {"default": 5.0, "min": 0.0, "max": 9999.0, "step": 1.0,}), |
|
"y_rows":("INT", {"default": 5.0, "min": 0.0, "max": 9999.0, "step": 1.0,}), |
|
"index": ("INT", {"default": 0.0, "min": 0.0, "max": 9999.0, "step": 1.0,}), |
|
} |
|
} |
|
|
|
RETURN_TYPES = ("INT", "INT", "STRING", ) |
|
RETURN_NAMES = ("x", "y", "show_help", ) |
|
FUNCTION = "index" |
|
CATEGORY = icons.get("Comfyroll/XY Grid") |
|
|
|
def index(self, x_columns, y_rows, index): |
|
|
|
|
|
index -=1 |
|
|
|
x = index % x_columns |
|
y = int(index / x_columns) |
|
|
|
show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/XY-Grid-Nodes#cr-xy-index" |
|
|
|
return (x, y, show_help, ) |
|
|
|
|
|
class CR_XYFromFolder: |
|
|
|
@classmethod |
|
def INPUT_TYPES(cls) -> dict[str, t.Any]: |
|
|
|
input_dir = folder_paths.output_directory |
|
image_folder = [name for name in os.listdir(input_dir) if os.path.isdir(os.path.join(input_dir,name))] |
|
|
|
return {"required": |
|
{"image_folder": (sorted(image_folder), ), |
|
"start_index": ("INT", {"default": 1, "min": 0, "max": 10000}), |
|
"end_index": ("INT", {"default": 1, "min": 1, "max": 10000}), |
|
"max_columns": ("INT", {"default": 1, "min": 1, "max": 10000}), |
|
"x_annotation": ("STRING", {"multiline": True}), |
|
"y_annotation": ("STRING", {"multiline": True}), |
|
"font_size": ("INT", {"default": 50, "min": 1}), |
|
"gap": ("INT", {"default": 0, "min": 0}), |
|
}, |
|
"optional": { |
|
"trigger": ("BOOLEAN", {"default": False},), |
|
} |
|
} |
|
|
|
RETURN_TYPES = ("IMAGE", "BOOLEAN", "STRING", ) |
|
RETURN_NAMES = ("IMAGE", "trigger", "show_help", ) |
|
FUNCTION = "load_images" |
|
CATEGORY = icons.get("Comfyroll/XY Grid") |
|
|
|
def load_images(self, image_folder, start_index, end_index, max_columns, x_annotation, y_annotation, font_size, gap, trigger=False): |
|
show_help = "https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes/wiki/XY-Grid-Nodes#cr-xy-from-folder" |
|
|
|
if trigger == False: |
|
return((), False, show_help, ) |
|
|
|
input_dir = folder_paths.output_directory |
|
image_path = os.path.join(input_dir, image_folder) |
|
file_list = sorted(os.listdir(image_path), key=lambda s: sum(((s, int(n)) for s, n in re.findall(r'(\D+)(\d+)', 'a%s0' % s)), ())) |
|
|
|
sample_frames = [] |
|
pillow_images = [] |
|
|
|
if len(file_list) < end_index: |
|
end_index = len(file_list) |
|
|
|
for num in range(start_index, end_index + 1): |
|
i = Image.open(os.path.join(image_path, file_list[num - 1])) |
|
image = i.convert("RGB") |
|
image = np.array(image).astype(np.float32) / 255.0 |
|
image = torch.from_numpy(image)[None,] |
|
image = image.squeeze() |
|
sample_frames.append(image) |
|
|
|
resolved_font_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "fonts\Roboto-Regular.ttf") |
|
font = ImageFont.truetype(str(resolved_font_path), size=font_size) |
|
|
|
start_x_ann = (start_index % max_columns) - 1 |
|
start_y_ann = int(start_index / max_columns) |
|
|
|
column_list = x_annotation.split(";")[start_x_ann:] |
|
row_list = y_annotation.split(";")[start_y_ann:] |
|
|
|
column_list = [item.strip() for item in column_list] |
|
row_list = [item.strip() for item in row_list] |
|
|
|
annotation = Annotation(column_texts=column_list, row_texts=row_list, font=font) |
|
images = torch.stack(sample_frames) |
|
|
|
pillow_images = [tensor_to_pillow(i) for i in images] |
|
pillow_grid = create_images_grid_by_columns( |
|
images=pillow_images, |
|
gap=gap, |
|
annotation=annotation, |
|
max_columns=max_columns, |
|
) |
|
tensor_grid = pillow_to_tensor(pillow_grid) |
|
|
|
return (tensor_grid, trigger, show_help, ) |
|
|
|
|
|
class CR_XYSaveGridImage: |
|
|
|
|
|
def __init__(self): |
|
self.type = "output" |
|
|
|
@classmethod |
|
def INPUT_TYPES(cls): |
|
|
|
output_dir = folder_paths.output_directory |
|
output_folders = [name for name in os.listdir(output_dir) if os.path.isdir(os.path.join(output_dir,name))] |
|
|
|
return { |
|
"required": {"mode": (["Save", "Preview"],), |
|
"output_folder": (sorted(output_folders), ), |
|
"image": ("IMAGE", ), |
|
"filename_prefix": ("STRING", {"default": "CR"}), |
|
"file_format": (["webp", "jpg", "png", "tif"],), |
|
}, |
|
"optional": {"output_path": ("STRING", {"default": '', "multiline": False}), |
|
"trigger": ("BOOLEAN", {"default": False},), |
|
} |
|
} |
|
|
|
RETURN_TYPES = () |
|
FUNCTION = "save_image" |
|
OUTPUT_NODE = True |
|
CATEGORY = icons.get("Comfyroll/XY Grid") |
|
|
|
def save_image(self, mode, output_folder, image, file_format, output_path='', filename_prefix="CR", trigger=False): |
|
|
|
if trigger == False: |
|
return () |
|
|
|
output_dir = folder_paths.get_output_directory() |
|
out_folder = os.path.join(output_dir, output_folder) |
|
|
|
|
|
if output_path != '': |
|
if not os.path.exists(output_path): |
|
print(f"[Warning] CR Save XY Grid Image: The input_path `{output_path}` does not exist") |
|
return ("",) |
|
out_path = output_path |
|
else: |
|
out_path = os.path.join(output_dir, out_folder) |
|
|
|
if mode == "Preview": |
|
out_path = folder_paths.temp_directory |
|
|
|
print(f"[Info] CR Save XY Grid Image: Output path is `{out_path}`") |
|
|
|
|
|
counter = find_highest_numeric_value(out_path, filename_prefix) + 1 |
|
|
|
|
|
|
|
output_image = image[0].cpu().numpy() |
|
img = Image.fromarray(np.clip(output_image * 255.0, 0, 255).astype(np.uint8)) |
|
|
|
output_filename = f"{filename_prefix}_{counter:05}" |
|
img_params = {'png': {'compress_level': 4}, |
|
'webp': {'method': 6, 'lossless': False, 'quality': 80}, |
|
'jpg': {'format': 'JPEG'}, |
|
'tif': {'format': 'TIFF'} |
|
} |
|
self.type = "output" if mode == "Save" else 'temp' |
|
|
|
resolved_image_path = os.path.join(out_path, f"{output_filename}.{file_format}") |
|
img.save(resolved_image_path, **img_params[file_format]) |
|
print(f"[Info] CR Save XY Grid Image: Saved to {output_filename}.{file_format}") |
|
out_filename = f"{output_filename}.{file_format}" |
|
preview = {"ui": {"images": [{"filename": out_filename,"subfolder": out_path,"type": self.type,}]}} |
|
|
|
return preview |
|
|
|
|
|
|
|
|
|
|
|
|
|
''' |
|
NODE_CLASS_MAPPINGS = { |
|
# XY Grid |
|
"CR XY List":CR_XYList, |
|
"CR XY Index":CR_XYIndex, |
|
"CR XY Interpolate":CR_XYInterpolate, |
|
"CR XY From Folder":CR_XYFromFolder, |
|
"CR XY Save Grid Image":CR_XYSaveGridImage, |
|
} |
|
''' |
|
|
|
|
|
|
|
|