#---------------------------------------------------------------------------------------------------------------------# # Comfyroll Studio custom nodes by RockOfFire and Akatsuzi https://github.com/Suzie1/ComfyUI_Comfyroll_CustomNodes # for ComfyUI https://github.com/comfyanonymous/ComfyUI #---------------------------------------------------------------------------------------------------------------------# # based on https://github.com/LEv145/images-grid-comfy-plugin 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 # Initialize with a value lower than possible numeric values # Iterate through all files in the directory for filename in os.listdir(directory): if filename.startswith(filename_prefix): try: # Extract numeric part of the filename numeric_part = filename[len(filename_prefix):] numeric_str = re.search(r'\d+', numeric_part).group() numeric_value = int(numeric_str) # Check if the current numeric value is higher than the highest found so far if numeric_value > highest_value: highest_value = int(numeric_value) except ValueError: # If the numeric part is not a valid integer, ignore the file 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"}), #"forceInput": True}), "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 values for all XY nodes start from 1 index -=1 trigger = False #listx = list1.split(",") #listy = list2.split(",") 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 values for all XY nodes start from 1 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 values for all XY nodes start from 1 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: # originally based on SaveImageSequence by mtb 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) # Set the output path 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}`") # Set the counter counter = find_highest_numeric_value(out_path, filename_prefix) + 1 #print(f"[Debug] counter {counter}") # Output image 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 #---------------------------------------------------------------------------------------------------------------------# # MAPPINGS #---------------------------------------------------------------------------------------------------------------------# # For reference only, actual mappings are in __init__.py # 0 nodes released ''' 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, } '''