From 88a070d8c46dfd25d58ad8342a41e2ade44f9b7b Mon Sep 17 00:00:00 2001 From: Fill Date: Fri, 23 May 2025 03:33:23 +0900 Subject: [PATCH] added list output for directory crawl and image randomizer + added image blank node --- __init__.py | 3 ++ nodes/FL_DirectoryCrawl.py | 67 +++++++++++++++++++++++------------- nodes/FL_Image_Blank.py | 36 +++++++++++++++++++ nodes/FL_Image_Randomizer.py | 25 ++++++-------- pyproject.toml | 2 +- 5 files changed, 95 insertions(+), 38 deletions(-) create mode 100644 nodes/FL_Image_Blank.py diff --git a/__init__.py b/__init__.py index ea4279f..d9eb09a 100644 --- a/__init__.py +++ b/__init__.py @@ -127,9 +127,11 @@ from .nodes.FL_GPT_Image1_ADV import FL_GPT_Image1_ADV from .nodes.FL_ImageBatch import FL_ImageBatch from .nodes.FL_Hedra_API import FL_Hedra_API from .nodes.FL_RunwayImageAPI import FL_RunwayImageAPI +from .nodes.FL_Image_Blank import FL_ImageBlank NODE_CLASS_MAPPINGS = { + "FL_ImageBlank": FL_ImageBlank, "FL_ImageRandomizer": FL_ImageRandomizer, "FL_ImageCaptionSaver": FL_ImageCaptionSaver, "FL_VideoCaptionSaver": FL_VideoCaptionSaver, @@ -265,6 +267,7 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { + "FL_ImageBlank": "FL Image Blank", "FL_ImageRandomizer": "FL Image Randomizer", "FL_ImageCaptionSaver": "FL Image Caption Saver", "FL_VideoCaptionSaver": "FL Video Caption Saver", diff --git a/nodes/FL_DirectoryCrawl.py b/nodes/FL_DirectoryCrawl.py index 48f0a08..1f3d6fe 100644 --- a/nodes/FL_DirectoryCrawl.py +++ b/nodes/FL_DirectoryCrawl.py @@ -17,7 +17,9 @@ class FL_DirectoryCrawl: } } - RETURN_TYPES = ("IMAGE", "STRING") # Output a batch of images or list of text contents + RETURN_TYPES = ("IMAGE", "STRING", "IMAGE") + RETURN_NAMES = ("image_batch", "text_content", "image_list") + OUTPUT_IS_LIST = (False, False, True) FUNCTION = "load_batch" CATEGORY = "🏵️Fill Nodes/utility" @@ -27,52 +29,71 @@ class FL_DirectoryCrawl: file_paths = self.crawl_directories(directory_path, file_type) if not file_paths: - raise ValueError(f"No {file_type} found in the specified directory and its subdirectories.") + if file_type == "images": + return (torch.empty(0), "", []) # Return empty for all if no files + else: # text + return (torch.empty(0), "", []) + file_paths = file_paths[:max_files] # Limit the number of files if file_type == "images": - return self.load_image_batch(file_paths) - else: - return self.load_text_batch(file_paths) + batch_tensor, image_list_tensors = self.load_images_data(file_paths) + return (batch_tensor, "", image_list_tensors) + else: # text + text_content = self.load_text_data(file_paths) + return (torch.empty(0), text_content, []) - def load_image_batch(self, image_paths): - batch_images = [] + def load_images_data(self, image_paths): + individual_images_np = [] + individual_image_tensors = [] pbar = ProgressBar(len(image_paths)) + + if not image_paths: + return torch.empty(0), [] + for idx, img_path in enumerate(image_paths): image = Image.open(img_path) image = ImageOps.exif_transpose(image) # Correct orientation image = image.convert("RGB") image_np = np.array(image).astype(np.float32) / 255.0 - batch_images.append(image_np) + individual_images_np.append(image_np) + # Create tensor for the list output (B, H, W, C) -> (1, H, W, C) + individual_image_tensors.append(torch.from_numpy(image_np)[None,]) pbar.update_absolute(idx) - # Pad images to the largest dimensions - max_h = max(img.shape[0] for img in batch_images) - max_w = max(img.shape[1] for img in batch_images) + # Pad images for batch output + max_h = max(img.shape[0] for img in individual_images_np) + max_w = max(img.shape[1] for img in individual_images_np) - padded_images = [] - for img in batch_images: - h, w, c = img.shape + padded_images_for_batch = [] + for img_np in individual_images_np: + h, w, c = img_np.shape padded = np.zeros((max_h, max_w, c), dtype=np.float32) - padded[:h, :w, :] = img - padded_images.append(padded) + padded[:h, :w, :] = img_np + padded_images_for_batch.append(padded) - batch_images_np = np.stack(padded_images, axis=0) + batch_images_np = np.stack(padded_images_for_batch, axis=0) batch_images_tensor = torch.from_numpy(batch_images_np) - return (batch_images_tensor, "") + return batch_images_tensor, individual_image_tensors - def load_text_batch(self, text_paths): + def load_text_data(self, text_paths): text_contents = [] + if not text_paths: + return "" pbar = ProgressBar(len(text_paths)) for idx, txt_path in enumerate(text_paths): - with open(txt_path, 'r', encoding='utf-8') as file: - content = file.read() - text_contents.append(content) + try: + with open(txt_path, 'r', encoding='utf-8') as file: + content = file.read() + text_contents.append(content) + except Exception as e: + print(f"Warning: Could not read text file {txt_path}: {e}") + text_contents.append(f"Error reading file: {txt_path}") pbar.update_absolute(idx) - return (torch.zeros(1), "\n---\n".join(text_contents)) # Return empty tensor for IMAGE type + return "\n---\n".join(text_contents) def crawl_directories(self, directory, file_type): if file_type == "images": diff --git a/nodes/FL_Image_Blank.py b/nodes/FL_Image_Blank.py new file mode 100644 index 0000000..d0b9ba6 --- /dev/null +++ b/nodes/FL_Image_Blank.py @@ -0,0 +1,36 @@ +import torch +import numpy as np +from PIL import Image + +class FL_ImageBlank: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "width": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 64}), + "height": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 64}), + "red": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), + "green": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), + "blue": ("INT", {"default": 255, "min": 0, "max": 255, "step": 1}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "create_blank_image" + CATEGORY = "🏵️Fill Nodes/Image" + + def create_blank_image(self, width, height, red, green, blue): + # Create a 3-channel image (H, W, C) + image_np = np.full((height, width, 3), [red, green, blue], dtype=np.uint8) + + # Convert to PIL Image first to handle potential mode issues if needed, then to numpy float32 + # Though for a simple color, direct numpy to tensor is fine. + # image_pil = Image.fromarray(image_np, 'RGB') + # image_np_float = np.array(image_pil).astype(np.float32) / 255.0 + + image_np_float = image_np.astype(np.float32) / 255.0 + + # Convert to tensor and add batch dimension (B, H, W, C) + image_tensor = torch.from_numpy(image_np_float)[None,] + + return (image_tensor,) \ No newline at end of file diff --git a/nodes/FL_Image_Randomizer.py b/nodes/FL_Image_Randomizer.py index e5e7bf3..8ed862f 100644 --- a/nodes/FL_Image_Randomizer.py +++ b/nodes/FL_Image_Randomizer.py @@ -17,7 +17,9 @@ class FL_ImageRandomizer: } } - RETURN_TYPES = ("IMAGE", "PATH") + RETURN_TYPES = ("IMAGE", "PATH", "IMAGE") + RETURN_NAMES = ("image_batch", "selected_path", "image_list") + OUTPUT_IS_LIST = (False, False, True) FUNCTION = "select_media" CATEGORY = "🏵️Fill Nodes/Image" @@ -26,11 +28,13 @@ class FL_ImageRandomizer: raise ValueError("Directory path is not provided.") if mode == "Image": - return self.select_image(directory_path, seed, search_subdirectories) + image_tensor, selected_path = self.select_image_data(directory_path, seed, search_subdirectories) + return (image_tensor, selected_path, [image_tensor]) else: # Video mode - return self.select_video_frames(directory_path, seed, search_subdirectories) + frames_tensor, selected_path = self.select_video_data(directory_path, seed, search_subdirectories) + return (frames_tensor, selected_path, [frames_tensor]) # Video frames are already a batch, but we wrap in list for consistency - def select_image(self, directory_path, seed, search_subdirectories=False): + def select_image_data(self, directory_path, seed, search_subdirectories=False): images = self.load_files(directory_path, search_subdirectories, file_type="image") if not images: raise ValueError("No images found in the specified directory.") @@ -46,9 +50,9 @@ class FL_ImageRandomizer: image_np = np.array(image).astype(np.float32) / 255.0 image_tensor = torch.from_numpy(image_np)[None,] - return (image_tensor, selected_image_path) + return image_tensor, selected_image_path - def select_video_frames(self, directory_path, seed, search_subdirectories=False): + def select_video_data(self, directory_path, seed, search_subdirectories=False): videos = self.load_files(directory_path, search_subdirectories, file_type="video") if not videos: raise ValueError("No videos found in the specified directory.") @@ -58,27 +62,21 @@ class FL_ImageRandomizer: selected_video_path = videos[selected_index] - # Open the video file cap = cv2.VideoCapture(selected_video_path) if not cap.isOpened(): raise ValueError(f"Could not open video file: {selected_video_path}") - # Get video properties frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) if frame_count <= 0: raise ValueError(f"No frames found in video: {selected_video_path}") - # Extract all frames from the video frames = [] success = True while success: success, frame = cap.read() if success: - # Convert BGR to RGB (OpenCV uses BGR by default) frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # Normalize frame_np = np.array(frame).astype(np.float32) / 255.0 frames.append(frame_np) @@ -87,10 +85,9 @@ class FL_ImageRandomizer: if not frames: raise ValueError(f"Failed to extract frames from video: {selected_video_path}") - # Convert list of frames to tensor with batch dimension frames_tensor = torch.from_numpy(np.stack(frames)) - return (frames_tensor, selected_video_path) + return frames_tensor, selected_video_path def load_files(self, directory, search_subdirectories=False, file_type="image"): if file_type == "image": diff --git a/pyproject.toml b/pyproject.toml index 3c099c7..7975880 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_fill-nodes" description = "Fill-Nodes is a versatile collection of custom nodes for ComfyUI that extends functionality across multiple domains. Features include advanced image processing (pixelation, slicing, masking), visual effects generation (glitch, halftone, pixel art), comprehensive file handling (PDF creation/extraction, Google Drive integration), AI model interfaces (GPT, DALL-E, Hugging Face), utility nodes for workflow enhancement, and specialized tools for video processing, captioning, and batch operations. The pack provides both practical workflow solutions and creative tools within a unified node collection." -version = "1.5.4" +version = "1.5.5" license = "LICENSE" dependencies = ["diffusers", "librosa", "sounddevice", "glitch_this", "PyOpenGL", "glfw", "scipy>=1.13.1", "requests", "aiohttp", "moviepy", "matplotlib", "reportlab", "openai", "PyPDF2", "pdf2image", "PyMuPDF", "reportlab", "PyPDF2", "ollama", "kornia", "opencv-python", "gdown", "open_clip_torch", "google-genai"]