diff --git a/README.md b/README.md index f2a0ee5..b4aec82 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# πŸ”— Comfyui : Bjornulf_custom_nodes v0.2 πŸ”— +# πŸ”— Comfyui : Bjornulf_custom_nodes v0.3 πŸ”— # Dependencies @@ -6,8 +6,9 @@ # πŸ“ Changelog -## v0.2 -- **Ollama**: Improve ollama node with system prompt + model selection. +- **v0.2 Ollama**: Improve ollama node with system prompt + model selection. +- **v0.3 Save Image to Folder**: Add a new node : Save image to a chosen folder. +- **v0.3 Save Images**: Add comfyui Metadata / workflow to all my image-related nodes. # πŸ“ Nodes descriptions @@ -106,7 +107,15 @@ The name will start at `api_00001.png`, then `api_00002.png`, etc... **Description:** Save image for short-term use : ./output/tmp_api.png βš οΈπŸ’£ -## 18 - πŸ¦™ Ollama + +## 18 - πŸ–Ό Save image to a chosen folder name +![Save Temporary API](screenshots/save_image_to_folder.png) + +**Description:** +Save image in a specific folder : `my_folder/00001.png`, `my_folder/00002.png`, etc... +Also allow multiple nested folders, like for example : `animal/dog/small`. + +## 19 - πŸ¦™ Ollama ![Show Text](screenshots/ollama.png) **Description:** @@ -117,13 +126,13 @@ I recommend using `mistral-nemo` if you can run it, but it's up to you. (Might h **Description:** Straight forward node to write and show text. -## 18 - πŸ“Ή Video Ping Pong +## 20 - πŸ“Ή Video Ping Pong ![Video Ping Pong](screenshots/video_pingpong.png) **Description:** Create a ping-pong effect from a list of images (from a video) by reversing the playback direction when reaching the last frame. Good for an "infinity loop" effect. -## 19 - πŸ“Ή Images to Video +## 21 - πŸ“Ή Images to Video ![Images to Video](screenshots/imgs2video.png) **Description:** diff --git a/__init__.py b/__init__.py index a69f338..d91ded6 100644 --- a/__init__.py +++ b/__init__.py @@ -22,6 +22,7 @@ from .save_text import SaveText from .save_tmp_image import SaveTmpImage from .save_image_path import SaveImagePath from .save_api_image import SaveApiImage +from .save_img_to_folder import SaveImageToFolder from .resize_image import ResizeImage from .loop_my_combos_samplers_schedulers import LoopCombosSamplersSchedulers @@ -40,6 +41,7 @@ NODE_CLASS_MAPPINGS = { "Bjornulf_ShowFloat": ShowFloat, "Bjornulf_SaveText": SaveText, "Bjornulf_ResizeImage": ResizeImage, + "Bjornulf_SaveImageToFolder": SaveImageToFolder, "Bjornulf_SaveTmpImage": SaveTmpImage, "Bjornulf_SaveImagePath": SaveImagePath, "Bjornulf_SaveApiImage": SaveApiImage, @@ -65,6 +67,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "Bjornulf_ShowFloat": "πŸ‘ Show (Float)", "Bjornulf_ResizeImage": "πŸ“ Resize Image", "Bjornulf_SaveImagePath": "πŸ–Ό Save Image (exact path, exact name) βš οΈπŸ’£", + "Bjornulf_SaveImageToFolder": "πŸ–ΌπŸ“ Save Image to a folder", "Bjornulf_SaveTmpImage": "πŸ–Ό Save Image (tmp_api.png) βš οΈπŸ’£", "Bjornulf_SaveApiImage": "πŸ–Ό Save Image (./output/api_00001.png...)", "Bjornulf_SaveText": "πŸ’Ύ Save Text", #Make SaveCharacter, SaveLocation, SaveCamera, SaveAction, SaveClothes, SaveEmotion... diff --git a/resize_image.py b/resize_image.py index 1466b23..b2e42bb 100644 --- a/resize_image.py +++ b/resize_image.py @@ -10,15 +10,16 @@ class ResizeImage: "image": ("IMAGE", {}), "width": ("INT", {"default": 256}), "height": ("INT", {"default": 256}), - } + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } FUNCTION = "resize_image" - RETURN_TYPES = ("IMAGE",) + RETURN_TYPES = ("IMAGE", "PROMPT", "EXTRA_PNGINFO") OUTPUT_NODE = True CATEGORY = "Bjornulf" - def resize_image(self, image, width=256, height=256): + def resize_image(self, image, width=256, height=256, prompt=None, extra_pnginfo=None): # Ensure the input image is on CPU and convert to numpy array image_np = image.cpu().numpy() @@ -38,7 +39,7 @@ class ResizeImage: # Stack the resized images back into a batch resized_batch = np.stack(resized_images) # Convert to torch tensor - return (torch.from_numpy(resized_batch),) + resized_tensor = torch.from_numpy(resized_batch) else: # If it's a single image, process it directly # Convert to PIL Image @@ -51,4 +52,11 @@ class ResizeImage: if image.dim() == 4: resized_np = np.expand_dims(resized_np, axis=0) # Convert to torch tensor - return (torch.from_numpy(resized_np),) \ No newline at end of file + resized_tensor = torch.from_numpy(resized_np) + + # Update metadata if needed + if extra_pnginfo is not None: + extra_pnginfo["resized_width"] = width + extra_pnginfo["resized_height"] = height + + return (resized_tensor, prompt, extra_pnginfo) \ No newline at end of file diff --git a/save_api_image.py b/save_api_image.py index 4df46a9..2967d70 100644 --- a/save_api_image.py +++ b/save_api_image.py @@ -1,6 +1,8 @@ import os import numpy as np from PIL import Image +import json +from PIL.PngImagePlugin import PngInfo class SaveApiImage: @classmethod @@ -8,7 +10,8 @@ class SaveApiImage: return { "required": { "image": ("IMAGE", {"forceInput": True}), - } + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } FUNCTION = "save_api_image" @@ -16,7 +19,7 @@ class SaveApiImage: OUTPUT_NODE = True CATEGORY = "Bjornulf" - def save_api_image(self, image): + def save_api_image(self, image, prompt=None, extra_pnginfo=None): # Ensure the output directory exists os.makedirs("./output/", exist_ok=True) @@ -37,11 +40,21 @@ class SaveApiImage: break counter += 1 - # Save the image with the determined filename - img.save(filename, format="PNG") + # Prepare metadata + 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)) + + # Save the image with the determined filename and metadata + img.save(filename, format="PNG", pnginfo=metadata) # Write the number of the last image to a text file with leading zeroes with open("./output/api_next_image.txt", "w") as f: f.write(f"api_{counter+1:05d}.png") - return () + print(f"Image saved as: {filename}") + + return {"ui": {"images": [{"filename": filename, "type": "output"}]}} \ No newline at end of file diff --git a/save_image_path.py b/save_image_path.py index e475d4a..9b7f06a 100644 --- a/save_image_path.py +++ b/save_image_path.py @@ -1,6 +1,8 @@ import os import numpy as np from PIL import Image +import json +from PIL.PngImagePlugin import PngInfo class SaveImagePath: @classmethod @@ -8,8 +10,9 @@ class SaveImagePath: return { "required": { "image": ("IMAGE", {"forceInput": True}), - "path": ("STRING", {"default":"./output/default.png"}), # Add path input - } + "path": ("STRING", {"default": "./output/default.png"}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } FUNCTION = "save_image_path" @@ -17,7 +20,7 @@ class SaveImagePath: OUTPUT_NODE = True CATEGORY = "Bjornulf" - def save_image_path(self, image, path): + def save_image_path(self, image, path, prompt=None, extra_pnginfo=None): # Ensure the output directory exists os.makedirs(os.path.dirname(path), exist_ok=True) @@ -33,7 +36,17 @@ class SaveImagePath: img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - # Save the image, overwriting if it exists - img.save(path, format="PNG") + # Create PngInfo object for metadata + 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)) - return () + # Save the image with metadata, overwriting if it exists + img.save(path, format="PNG", pnginfo=metadata) + + print(f"Image saved as: {path}") + + return {"ui": {"images": [{"filename": path, "type": "output"}]}} \ No newline at end of file diff --git a/save_img_to_folder.py b/save_img_to_folder.py new file mode 100644 index 0000000..098f6fc --- /dev/null +++ b/save_img_to_folder.py @@ -0,0 +1,50 @@ +import os +import numpy as np +from PIL import Image +import json +from PIL.PngImagePlugin import PngInfo + +class SaveImageToFolder: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE", ), + "folder_name": ("STRING", {"default": "my_folder"}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + FUNCTION = "save_image_to_folder" + RETURN_TYPES = () + OUTPUT_NODE = True + CATEGORY = "Bjornulf" + + def save_image_to_folder(self, images, folder_name, prompt=None, extra_pnginfo=None): + output_dir = os.path.join("./output", folder_name) + os.makedirs(output_dir, exist_ok=True) + + results = [] + for image in images: + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 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)) + + counter = 1 + while True: + filename = os.path.join(output_dir, f"{counter:05d}.png") + if not os.path.exists(filename): + break + counter += 1 + + img.save(filename, format="PNG", pnginfo=metadata) + print(f"Image saved as: {filename}") + results.append({"filename": filename}) + + return {"ui": {"images": results}} \ No newline at end of file diff --git a/save_tmp_image.py b/save_tmp_image.py index 9838dde..10c0a3f 100644 --- a/save_tmp_image.py +++ b/save_tmp_image.py @@ -1,6 +1,8 @@ import os import numpy as np from PIL import Image +import json +from PIL.PngImagePlugin import PngInfo class SaveTmpImage: @classmethod @@ -8,7 +10,8 @@ class SaveTmpImage: return { "required": { "image": ("IMAGE", {"forceInput": True}), - } + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } FUNCTION = "save_image" @@ -16,12 +19,11 @@ class SaveTmpImage: OUTPUT_NODE = True CATEGORY = "Bjornulf" - def save_image(self, image): + def save_image(self, image, prompt=None, extra_pnginfo=None): # Ensure the output directory exists - # os.makedirs("./output", exist_ok=True) + os.makedirs("./output", exist_ok=True) # Convert the image from ComfyUI format to PIL Image - # Assuming the first two dimensions are extra, and we need to keep the last two i = 255. * image.cpu().numpy() # Reshape the image if it's not in the expected format, remove any leading dimensions of size 1 if i.ndim > 3: @@ -32,7 +34,18 @@ class SaveTmpImage: img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) - # Save the image, overwriting if it exists - img.save("./output/tmp_api.png", format="PNG") + # Prepare metadata + 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)) - return () + # Save the image with metadata, overwriting if it exists + filename = "./output/tmp_api.png" + img.save(filename, format="PNG", pnginfo=metadata) + + print(f"Temporary image saved as: {filename}") + + return {"ui": {"images": [{"filename": filename, "type": "output"}]}} \ No newline at end of file diff --git a/screenshots/save_image_to_folder.png b/screenshots/save_image_to_folder.png new file mode 100644 index 0000000..cd12677 Binary files /dev/null and b/screenshots/save_image_to_folder.png differ