import glob import json import os import subprocess from pathlib import Path import numpy as np import torch from PIL import Image from PIL.PngImagePlugin import PngInfo NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} def register_node(identifier: str, display_name: str): def decorator(cls): NODE_CLASS_MAPPINGS[identifier] = cls NODE_DISPLAY_NAME_MAPPINGS[identifier] = display_name return cls return decorator def load_image(path): img = Image.open(path).convert("RGB") img = np.array(img).astype(np.float32) / 255.0 img = torch.from_numpy(img).unsqueeze(0) return img @register_node("BatchLoadImage", "[DEPRECATED] Batch Load Image") class _: """ Batch-load images in a given folder. To avoid loading too many images at once, you can use `paginate_size` and `paginate_page` to load a subset of the images. To disable pagination functionality, leave `paginate_size` and `paginate_page` at 0. """ CATEGORY = "jamesWalker55" INPUT_TYPES = lambda: { "required": { "image_dir": ("STRING", {"default": "./images", "multiline": False}), "glob_pattern": ("STRING", {"default": "*.png", "multiline": False}), "paginate_size": ("INT", {"default": 0, "min": 0}), "paginate_page": ("INT", {"default": 0, "min": 0}), } } RETURN_NAMES = ("IMAGE", "FRAME_COUNT", "FILENAMES") RETURN_TYPES = ("IMAGE", "INT", "STRING") FUNCTION = "execute" def execute( self, image_dir: str, glob_pattern: str, paginate_size: int, paginate_page: int ): assert isinstance(image_dir, str) assert isinstance(glob_pattern, str) assert isinstance(paginate_size, int) assert isinstance(paginate_page, int) # get paths relative to root dir paths = glob.glob(glob_pattern, root_dir=image_dir, recursive=True) # convert paths to be relative to here paths = [os.path.join(image_dir, x) for x in paths] # sort paths alphabetically paths.sort() if len(paths) == 0: raise FileNotFoundError( f"No images found in folder matching pattern {glob_pattern!r}" ) if paginate_size > 0: start_offset = paginate_page * paginate_size if start_offset > len(paths): raise StopIteration( f"No more images in folder at page {paginate_page}!" ) paths = paths[start_offset : start_offset + paginate_size] filenames = [os.path.splitext(os.path.basename(x))[0] for x in paths] imgs = [] for p in paths: img = load_image(p) # img.shape => torch.Size([1, 768, 768, 3]) imgs.append(img) imgs = torch.cat(imgs, dim=0) assert len(imgs) == len(filenames) return (imgs, len(imgs), "\n".join(filenames)) @register_node("BatchSaveImage", "[DEPRECATED] Batch Save Image") class _: CATEGORY = "jamesWalker55" INPUT_TYPES = lambda: { "required": { "images": ("IMAGE",), "output_dir": ("STRING", {"default": "./", "multiline": False}), "name_prefix": ("STRING", {"default": ""}), "name_suffix": ("STRING", {"default": ""}), "numbering_start": ( "INT", {"default": 1, "min": 0, "step": 1}, ), "numbering_digits": ("INT", {"default": 4, "min": 1, "step": 1}), "render_video_fps": ("INT", {"default": 8, "min": 0, "step": 1}), }, "optional": { "filenames": ("STRING", {"multiline": True, "dynamicPrompts": False}), }, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } RETURN_TYPES = () OUTPUT_NODE = True FUNCTION = "main" def main( self, images: torch.Tensor, output_dir: str, name_prefix: str, name_suffix: str, numbering_start: int, numbering_digits: int, render_video_fps: int, filenames: str | None = None, prompt=None, extra_pnginfo=None, ): if filenames is not None: filenames = [x.strip() for x in filenames.splitlines()] filenames = [x for x in filenames if len(x) > 0] if len(filenames) != len(images): raise ValueError( f"Number of images ({len(images)}) and filenames ({len(filenames)}) must be the same" ) filenames = filenames.copy() filenames.reverse() output_dir: Path = Path(output_dir) output_dir.mkdir(exist_ok=True) ui_results = [] for i, img in enumerate(images): num = i + numbering_start if filenames is not None: filename = filenames.pop() filename = f"{filename}.png" else: filename = f"{name_prefix}{num:0{numbering_digits}d}{name_suffix}.png" output_path = output_dir / filename ui = self.save_image( img, output_path, prompt=prompt, extra_pnginfo=extra_pnginfo ) ui_results.append(ui) if render_video_fps > 0: subprocess.run( [ "python", R"D:\Programming\bin\render-img-sequence.py", "-i", str(output_dir), "-r", str(render_video_fps), ] ) return {"ui": {"images": ui_results}} @staticmethod def save_image(img: torch.Tensor, path, prompt=None, extra_pnginfo: dict = None): path = str(path) img = 255.0 * img.cpu().numpy() img = Image.fromarray(np.clip(img, 0, 255).astype(np.uint8)) metadata = PngInfo() if prompt is not None: metadata.add_text("prompt", json.dumps(prompt)) if extra_pnginfo is not None: for k, v in extra_pnginfo.items(): metadata.add_text(k, json.dumps(v)) img.save(path, pnginfo=metadata, compress_level=4) subfolder, filename = os.path.split(path) return {"filename": filename, "subfolder": subfolder, "type": "output"}