diff --git a/README.md b/README.md index 402c042..93a5f4a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# πŸ”— Comfyui : Bjornulf_custom_nodes v0.32 πŸ”— +# πŸ”— Comfyui : Bjornulf_custom_nodes v0.34 πŸ”— # ❀️ Coffee : β˜•β˜•β˜•β˜•β˜• 5/5 @@ -81,6 +81,7 @@ wget --content-disposition -P /workspace/ComfyUI/models/checkpoints "https://civ - **v0.31**: ❗Sorry, Breaking changes for Write/Show text nodes, cleaner system : 1 simple write text and the other is 1 advanced with console and special syntax. Also Show can now manage INT, FLOAT, TEXT. - **v0.32**: Quick rename to avoid breaking loop_text node. - **v0.33**: Control random on paused nodes, fix pydub sound bug permissions on Windows. +- **v0.34**: Two new nodes : Load Images from output folder and Select an Image, Pick. # πŸ“ Nodes descriptions @@ -440,4 +441,37 @@ Just take a trio at random from a load checkpoint node. ![pick input](screenshots/loop_checkpoint.png) **Description:** -Loop over all the trios from several checkpoint node. \ No newline at end of file +Loop over all the trios from several checkpoint node. + +### 42 - πŸ“‚πŸ–Ό Load Images from output folder + +![pick input](screenshots/load_images_folder.png) + +**Description:** +Quickly select all images from a folder inside the output folder. (Not recursively.) +So... As you can see from the screenshot the images are split based on their resolution. +It is not a choice I made, it is something that is part of the comfyui environment. +It's also not possible to edit dynamically the number of outputs, so I just picked a number : 4. +The node will separate the images based on their resolution, so with this node you can have 4 different resolutions per folder. (If you have more than that, maybe you should have another folder...) +To avoid error or crash if you have less than 4 resolutions in a folder, the node will just output white tensors. (white square image.) +So this node is a little hacky for now, but i can select my different characters in less than a second. +If you want to know how i personnaly save my images for a specific character, here is part of my workflow (Notice that i personnaly use / for folders because I'm on linux) : +![pick input](screenshots/character_save.png) +In this example I put "character/" as a string and then combine with "nothing". But it's the same if you do "character" and then combine with "/". (I just like having a / at the end of my folder's name...) + +If you are satisfied with this logic, you can then select all these nodes, right click and `Convert to Group Node`, you can then have you own customized "save character node" : +![pick input](screenshots/bjornulf_save_character_group.png) + +Here is another example of the same thing but excluding the save folder node : +![pick input](screenshots/bjornulf_save_character_group2.png) + +### 43 - πŸ–ΌπŸ” Select an Image, Pick + +![pick input](screenshots/select_image.png) + +**Description:** +Select an image from a list of images. +Useful in combination with my Load images from folder and preview image nodes. + +You can also of course make a group node, like this one, which is the same as the screenshot above : +![pick input](screenshots/select_image_group.png) \ No newline at end of file diff --git a/__init__.py b/__init__.py index 6c913e3..fbd3706 100644 --- a/__init__.py +++ b/__init__.py @@ -47,8 +47,9 @@ from .random_image import RandomImage from .loop_model_clip_vae import LoopModelClipVae from .write_text_advanced import WriteTextAdvanced from .loop_write_text import LoopWriteText -# from .load_images_from_folder import LoadImagesFromSelectedFolder +from .load_images_from_folder import LoadImagesFromSelectedFolder # from .show import ShowWhatever +from .select_image_from_list import SelectImageFromList # from .pass_preview_image import PassPreviewImage # from .check_black_image import CheckBlackImage @@ -59,8 +60,9 @@ from .loop_write_text import LoopWriteText NODE_CLASS_MAPPINGS = { # "Bjornulf_CustomStringType": CustomStringType, "Bjornulf_ollamaLoader": ollamaLoader, + "Bjornulf_SelectImageFromList": SelectImageFromList, "Bjornulf_WriteText": WriteText, - # "Bjornulf_LoadImagesFromSelectedFolder": LoadImagesFromSelectedFolder, + "Bjornulf_LoadImagesFromSelectedFolder": LoadImagesFromSelectedFolder, # "Bjornulf_ShowWhatever": ShowWhatever, "Bjornulf_LoopModelClipVae": LoopModelClipVae, # "Bjoenulf_RandomCheckpoint": RandomCheckpoint, @@ -162,9 +164,10 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Bjornulf_ollamaLoader": "πŸ¦™ Ollama (Description)", "Bjornulf_FreeVRAM": "🧹 Free VRAM hack", "Bjornulf_TextToSpeech": "πŸ”Š TTS - Text to Speech", - "Bjornulf_PickInput": "βΈοΈπŸ” Paused. Select input, Pick one", - "Bjornulf_PauseResume": "⏸️ Paused. Resume or Stop ?", - # "Bjornulf_LoadImagesFromSelectedFolder": "πŸ“‚πŸ–Ό Load Images from folder", + "Bjornulf_PickInput": "⏸️ Paused. Select input, Pick πŸ‘‡", + "Bjornulf_PauseResume": "⏸️ Paused. Resume or Stop, Pick πŸ‘‡", + "Bjornulf_LoadImagesFromSelectedFolder": "πŸ“‚πŸ–Ό Load Images from output folder", + "Bjornulf_SelectImageFromList": "πŸ–ΌπŸ” Select an Image, Pick", } WEB_DIRECTORY = "./web" diff --git a/combine_texts.py b/combine_texts.py index fde304c..e76baf8 100644 --- a/combine_texts.py +++ b/combine_texts.py @@ -4,7 +4,7 @@ class CombineTexts: return { "required": { "number_of_inputs": ("INT", {"default": 2, "min": 2, "max": 50, "step": 1}), - "delimiter": (["newline", "comma", "space", "slash"], {"default": "newline"}), + "delimiter": (["newline", "comma", "space", "slash", "nothing"], {"default": "newline"}), "text_1": ("STRING", {"forceInput": True}), "text_2": ("STRING", {"forceInput": True}), }, @@ -44,5 +44,7 @@ class CombineTexts: return " " elif delimiter == "slash": return "/" + elif delimiter == "nothing": + return "" else: return "\n" \ No newline at end of file diff --git a/load_images_from_folder.py b/load_images_from_folder.py new file mode 100644 index 0000000..8d4f33b --- /dev/null +++ b/load_images_from_folder.py @@ -0,0 +1,96 @@ +import os +import numpy as np +from PIL import Image, ImageSequence, ImageOps +import torch + +class LoadImagesFromSelectedFolder: + @classmethod + def INPUT_TYPES(cls): + # Get the directory where this script is located + script_dir = os.path.dirname(os.path.abspath(__file__)) + comfyui_root = os.path.abspath(os.path.join(script_dir, '..', '..')) + output_dir = os.path.join(comfyui_root, 'output') + + def count_images(folder_path): + # Count the number of image files in the folder + return len([f for f in os.listdir(folder_path) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp'))]) + + folders = [] + for root, dirs, files in os.walk(output_dir): + rel_path = os.path.relpath(root, output_dir) + if rel_path == '.': + continue + image_count = count_images(root) + if image_count > 0: + folder_name = f"{rel_path} ({image_count} images)" + folders.append((folder_name, rel_path)) + + # Sort folders alphabetically, case-insensitive + folders.sort(key=lambda x: x[0].lower()) + + return { + "required": { + "selected_folder": ([folder[0] for folder in folders],), + } + } + + RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE") + RETURN_NAMES = ("Images resolution 1", "Images resolution 2", "Images resolution 3", "Images resolution 4") + FUNCTION = "load_images_from_selected_folder" + CATEGORY = "Bjornulf" + + def load_images_from_selected_folder(self, selected_folder): + script_dir = os.path.dirname(os.path.abspath(__file__)) + comfyui_root = os.path.abspath(os.path.join(script_dir, '..', '..')) + output_dir = os.path.join(comfyui_root, 'output') + folder_path = os.path.join(output_dir, selected_folder.split(" (")[0]) + + images_by_resolution = {} + + # Check if the folder exists and contains images + if not os.path.exists(folder_path): + print(f"Folder {folder_path} does not exist.") + return (None, None, None) + + image_files = [f for f in os.listdir(folder_path) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp'))] + if not image_files: + print(f"No images found in folder {folder_path}.") + return (None, None, None) + + for image_file in image_files: + image_path = os.path.join(folder_path, image_file) + img = Image.open(image_path) + + for i in ImageSequence.Iterator(img): + i = ImageOps.exif_transpose(i) + + if i.mode == 'I': + i = i.point(lambda i: i * (1 / 255)) + image = i.convert("RGB") + + resolution = image.size + + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + + if resolution not in images_by_resolution: + images_by_resolution[resolution] = [] + + images_by_resolution[resolution].append(image) + + # Sort resolutions by total pixel count (width * height) + sorted_resolutions = sorted(images_by_resolution.keys(), key=lambda r: r[0] * r[1], reverse=True) + + outputs = [] + for i in range(4): # Return up to 4 different resolutions + if i < len(sorted_resolutions): + resolution = sorted_resolutions[i] + output_image = torch.cat(images_by_resolution[resolution], dim=0) + outputs.append(output_image) + else: + # Create a placeholder tensor filled with 11111111111111111111111 + H, W, C = 64, 64, 3 + placeholder_image = torch.ones((1, H, W, C), dtype=torch.float32) + outputs.append(placeholder_image) + + return tuple(outputs) diff --git a/pyproject.toml b/pyproject.toml index 9196a1d..4e884ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "bjornulf_custom_nodes" description = "Nodes: Ollama, Text to Speech, Combine Texts, Random Texts, Save image for Bjornulf LobeChat, Text with random Seed, Random line from input, Combine images, Image to grayscale (black & white), Remove image Transparency (alpha), Resize Image, ..." -version = "0.33" +version = "0.34" license = {file = "LICENSE"} [project.urls] diff --git a/screenshots/bjornulf_save_character_group.png b/screenshots/bjornulf_save_character_group.png new file mode 100644 index 0000000..9438aa7 Binary files /dev/null and b/screenshots/bjornulf_save_character_group.png differ diff --git a/screenshots/bjornulf_save_character_group2.png b/screenshots/bjornulf_save_character_group2.png new file mode 100644 index 0000000..3865dd1 Binary files /dev/null and b/screenshots/bjornulf_save_character_group2.png differ diff --git a/screenshots/character_save.png b/screenshots/character_save.png new file mode 100644 index 0000000..824d83d Binary files /dev/null and b/screenshots/character_save.png differ diff --git a/screenshots/load_images_folder.png b/screenshots/load_images_folder.png new file mode 100644 index 0000000..f912a04 Binary files /dev/null and b/screenshots/load_images_folder.png differ diff --git a/screenshots/select_image.png b/screenshots/select_image.png new file mode 100644 index 0000000..595f192 Binary files /dev/null and b/screenshots/select_image.png differ diff --git a/screenshots/select_image_group.png b/screenshots/select_image_group.png new file mode 100644 index 0000000..04e099d Binary files /dev/null and b/screenshots/select_image_group.png differ diff --git a/select_image_from_list.py b/select_image_from_list.py new file mode 100644 index 0000000..b6dee7a --- /dev/null +++ b/select_image_from_list.py @@ -0,0 +1,32 @@ +import torch + +class Everything(str): + def __ne__(self, __value: object) -> bool: + return False + +class SelectImageFromList: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "all_images": ("IMAGE", {}), + "selection": ("INT", {"default": 1, "min": 1, "max": 999, "step": 1}), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("selected_image",) + FUNCTION = "select_an_image" + CATEGORY = "Bjornulf" + + def select_an_image(self, all_images, selection): + # Ensure the selection is within bounds + selection = max(1, min(selection, all_images.shape[0])) + + # Adjust selection to 0-based index + index = selection - 1 + + # Select the image at the specified index + selected_image = all_images[index].unsqueeze(0) + + return (selected_image,) \ No newline at end of file