26 Commits
Author SHA1 Message Date
ImpactFrames c4e8ce0cfe Update pyproject.toml 2025-03-14 13:24:31 +00:00
ImpactFrames ad8b29a75e Update publish_action.yml 2025-03-14 13:24:04 +00:00
ImpactFrames 264cb3eaba Update pyproject.toml 2025-03-09 09:26:16 +00:00
ImpactFrames 9bad06bc83 Update pyproject.toml 2025-03-09 09:10:48 +00:00
ImpactFrames 0e632a7b7b Update pyproject.toml 2025-01-12 20:57:33 +00:00
ImpactFrames e277386ad5 Update requirements.txt 2025-01-12 20:56:36 +00:00
ImpactFrames c9f6b37874 Update pyproject.toml 2025-01-12 20:48:25 +00:00
ImpactFrames 11847c4a7c Update IFLoadImagesNodeS.py 2024-12-28 01:55:56 +00:00
ImpactFrames 97fddde11f Update IFLoadImagesNodeS.js
route change to avoid conflicts
2024-12-28 01:55:16 +00:00
ImpactFrames c14ca10510 Update pyproject.toml 2024-12-26 13:19:16 +00:00
ImpactFrames 59accb9b08 Update pyproject.toml 2024-12-26 13:16:01 +00:00
ImpactFrames 716be3778d Add files via upload 2024-12-26 13:15:24 +00:00
ImpactFrames f4d734a246 Update pyproject.toml 2024-11-16 21:20:19 +00:00
ImpactFrames ca24f65792 Add files via upload
fixed the fkn channel OMG
2024-11-16 21:19:44 +00:00
ImpactFrames c4dd20c870 Update pyproject.toml 2024-11-15 21:23:33 +00:00
ImpactFrames eb72274c15 Delete IFLoadImagesNodeS.js 2024-11-15 21:21:32 +00:00
ImpactFrames ee74488cb4 Add files via upload 2024-11-15 21:21:15 +00:00
ImpactFrames 04645121a9 Add files via upload 2024-11-15 21:20:43 +00:00
ImpactFrames 2ffeca1217 Update README.md 2024-11-14 10:10:14 +00:00
ImpactFrames c70df9b2e5 Update README.md 2024-11-13 14:59:29 +00:00
ImpactFrames 140b8f3866 Update README.md 2024-11-12 15:57:30 +00:00
ImpactFrames b5182dcff4 Update README.md 2024-11-12 15:56:43 +00:00
ImpactFrames 584dd5f8d4 Update IFLoadImagesNodeS.py
:D
2024-11-12 15:50:52 +00:00
ImpactFrames f1d302d6e7 Update IFLoadImagesNodeS.py
forgot the stop_index
2024-11-12 15:45:25 +00:00
ImpactFrames fadec607a8 Update pyproject.toml
git on the fkn repo name
2024-11-10 13:01:02 +00:00
ImpactFrames a9f2c45d7f Update pyproject.toml 2024-11-10 12:47:04 +00:00
7 changed files with 158 additions and 117 deletions
+7 -2
View File
@@ -7,14 +7,19 @@ on:
paths: paths:
- "pyproject.toml" - "pyproject.toml"
permissions:
issues: write
jobs: jobs:
publish-node: publish-node:
name: Publish Custom Node to registry name: Publish Custom Node to registry
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'if-ai' }}
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Publish Custom Node - name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main uses: Comfy-Org/publish-node-action@v1
with: with:
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} ## Add your own personal access token to your Github Repository secrets and reference it here. ## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+100 -72
View File
@@ -377,16 +377,17 @@ class ImageManager:
return sorted(files) return sorted(files)
class IFLoadImagess: class IFLoadImagess:
_color_channels = ["alpha", "red", "green", "blue"]
def __init__(self): def __init__(self):
self.path_cache = {} # Cache for path mapping self.path_cache = {}
@classmethod @classmethod
def INPUT_TYPES(s): def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory() input_dir = folder_paths.get_input_directory()
# Count available thumbnails
available_images = len([f for f in os.listdir(input_dir) available_images = len([f for f in os.listdir(input_dir)
if f.startswith(ImageManager.THUMBNAIL_PREFIX)]) if f.startswith(ImageManager.THUMBNAIL_PREFIX)])
available_images = max(1, available_images) # Ensure at least 1 available_images = max(1, available_images)
files = [f for f in os.listdir(input_dir) files = [f for f in os.listdir(input_dir)
if f.startswith(ImageManager.THUMBNAIL_PREFIX)] if f.startswith(ImageManager.THUMBNAIL_PREFIX)]
@@ -396,7 +397,7 @@ class IFLoadImagess:
"image": (sorted(files), {"image_upload": True}), "image": (sorted(files), {"image_upload": True}),
"input_path": ("STRING", {"default": ""}), "input_path": ("STRING", {"default": ""}),
"start_index": ("INT", {"default": 0, "min": 0, "max": 9999}), "start_index": ("INT", {"default": 0, "min": 0, "max": 9999}),
"stop_index": ("INT", {"default": 10, "min": 1, "max": 9999}), # Changed to stop_index "stop_index": ("INT", {"default": 10, "min": 1, "max": 9999}),
"load_limit": (["10", "100", "1000", "10000", "100000"], {"default": "1000"}), "load_limit": (["10", "100", "1000", "10000", "100000"], {"default": "1000"}),
"image_selected": ("BOOLEAN", {"default": False}), "image_selected": ("BOOLEAN", {"default": False}),
"available_image_count": ("INT", { "available_image_count": ("INT", {
@@ -408,19 +409,21 @@ class IFLoadImagess:
"include_subfolders": ("BOOLEAN", {"default": True}), "include_subfolders": ("BOOLEAN", {"default": True}),
"sort_method": (["alphabetical", "numerical", "date_created", "date_modified"],), "sort_method": (["alphabetical", "numerical", "date_created", "date_modified"],),
"filter_type": (["none", "png", "jpg", "jpeg", "webp", "gif", "bmp"],), "filter_type": (["none", "png", "jpg", "jpeg", "webp", "gif", "bmp"],),
"channel": (s._color_channels, {"default": "alpha"}),
} }
} }
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "STRING", "STRING", "INT") RETURN_TYPES = ("IMAGE", "MASK", "STRING", "STRING", "STRING", "INT", "IMAGE", "MASK")
RETURN_NAMES = ("images", "masks", "image_paths", "filenames", "count_str", "count_int") RETURN_NAMES = ("images", "masks", "image_paths", "filenames", "count_str", "count_int", "images_batch", "masks_batch")
OUTPUT_IS_LIST = (True, True, True, True, True, True) OUTPUT_IS_LIST = (True, True, True, True, True, True, False, False)
FUNCTION = "load_images" FUNCTION = "load_images"
CATEGORY = "ImpactFrames💥🎞️" CATEGORY = "ImpactFrames💥🎞️/images"
@classmethod @classmethod
def IS_CHANGED(cls, image, input_path="", start_index=0, max_images=1, def IS_CHANGED(cls, image, input_path="", start_index=0, stop_index=0, max_images=1,
include_subfolders=True, sort_method="numerical", include_subfolders=True, sort_method="numerical", image_selected=False,
filter_type="none", image_name="", unique_id=None): filter_type="none", image_name="", unique_id=None, load_limit="1000",
available_image_count=0, channel="alpha" ):
""" """
Properly handle all input parameters and return NaN to force updates Properly handle all input parameters and return NaN to force updates
This matches the input parameters from INPUT_TYPES This matches the input parameters from INPUT_TYPES
@@ -442,20 +445,21 @@ class IFLoadImagess:
return float("NaN") return float("NaN")
def load_images(self, image="", input_path="", start_index=0, stop_index=10, def load_images(self, image="", input_path="", start_index=0, stop_index=10,
load_limit="1000", image_selected=False, available_image_count=0, load_limit="1000", image_selected=False, available_image_count=0,
include_subfolders=True, sort_method="numerical", include_subfolders=True, sort_method="numerical",
filter_type="none", image_name="", unique_id=None): filter_type="none", channel="alpha"):
try: try:
# Process input path # Process input path
abs_path = os.path.abspath(input_path if os.path.isabs(input_path) abs_path = os.path.abspath(input_path if os.path.isabs(input_path)
else os.path.join(folder_paths.get_input_directory(), input_path)) else os.path.join(folder_paths.get_input_directory(), input_path))
# Get all valid images first # Get all valid images
all_files = ImageManager.get_image_files(abs_path, include_subfolders, filter_type) all_files = ImageManager.get_image_files(abs_path, include_subfolders, filter_type)
if not all_files: if not all_files:
logger.warning(f"No valid images found in {abs_path}") logger.warning(f"No valid images found in {abs_path}")
img_tensor, mask = self.load_placeholder() img_tensor, mask = self.load_placeholder()
return ([img_tensor], [mask], [""], [""], ["0/0"], [0]) return ([img_tensor], [mask], [""], [""], ["0/0"], [0],
img_tensor.unsqueeze(0), mask.unsqueeze(0))
# Sort files # Sort files
all_files = ImageManager.sort_files(all_files, sort_method) all_files = ImageManager.sort_files(all_files, sort_method)
@@ -478,49 +482,80 @@ class IFLoadImagess:
start_index = image_order[image] start_index = image_order[image]
num_images = 1 num_images = 1
# Create path mapping
self.path_cache = {
thumb: orig for thumb, orig in zip(all_thumbnails, all_files[start_index:start_index + num_images])
}
# Process selected range # Process selected range
selected_files = all_files[start_index:start_index + num_images] selected_files = all_files[start_index:start_index + num_images]
selected_thumbnails = all_thumbnails[:num_images]
# Lists to store outputs
# Process selected files
images = [] images = []
masks = [] masks = []
paths = [] paths = []
filenames = [] filenames = []
count_strs = [] count_strs = []
count_ints = [] count_ints = []
for idx, (file_path, thumb_name) in enumerate(zip(selected_files, selected_thumbnails)): # Track max dimensions for resizing
max_height = 0
max_width = 0
# First pass to determine max dimensions
for file_path in selected_files:
try: try:
with Image.open(file_path) as img: with Image.open(file_path) as img:
img = ImageOps.exif_transpose(img) img = ImageOps.exif_transpose(img)
max_height = max(max_height, img.height)
max_width = max(max_width, img.width)
except Exception as e:
logger.error(f"Error checking dimensions of {file_path}: {e}")
continue
# Second pass to load and resize images
for idx, file_path in enumerate(selected_files):
try:
img = Image.open(file_path)
img = ImageOps.exif_transpose(img)
if img.mode == 'I':
img = img.point(lambda i: i * (1 / 255))
# Resize to match max dimensions
image = img.convert('RGB')
if image.size != (max_width, max_height):
image = image.resize((max_width, max_height), Image.Resampling.LANCZOS)
# Convert to numpy array and normalize
image_array = np.array(image).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image_array).unsqueeze(0) # [1, H, W, 3]
images.append(image_tensor)
# Handle mask based on selected channel
if img.mode not in ('RGB', 'L'):
img = img.convert('RGBA')
if img.mode == 'I': c = channel[0].upper()
img = img.point(lambda i: i * (1 / 255)) if c in img.getbands():
image = img.convert('RGB') mask = np.array(img.getchannel(c)).astype(np.float32) / 255.0
mask = torch.from_numpy(mask)
image_array = np.array(image).astype(np.float32) / 255.0 if c == 'A':
image_tensor = torch.from_numpy(image_array)[None,] mask = 1. - mask
else:
if 'A' in img.getbands(): mask = torch.zeros((max_height, max_width),
mask = np.array(img.getchannel('A')).astype(np.float32) / 255.0 dtype=torch.float32, device="cpu")
mask = 1. - torch.from_numpy(mask)
else: # Resize mask if needed
mask = torch.zeros((image_array.shape[0], image_array.shape[1]), if mask.shape != (max_height, max_width):
dtype=torch.float32, device="cpu") mask = torch.nn.functional.interpolate(
mask.unsqueeze(0).unsqueeze(0),
images.append(image_tensor) size=(max_height, max_width),
masks.append(mask.unsqueeze(0)) mode='bilinear',
paths.append(file_path) align_corners=False
filenames.append(os.path.basename(file_path)) ).squeeze(0).squeeze(0)
count_str = f"{start_index + idx + 1}/{total_files}" # Update count to show global position
count_strs.append(count_str) masks.append(mask.unsqueeze(0)) # Add batch dimension to mask [1, H, W]
count_ints.append(start_index + idx + 1)
paths.append(file_path)
filenames.append(os.path.basename(file_path))
count_strs.append(f"{start_index + idx + 1}/{total_files}")
count_ints.append(start_index + idx + 1)
except Exception as e: except Exception as e:
logger.error(f"Error processing image {file_path}: {e}") logger.error(f"Error processing image {file_path}: {e}")
@@ -528,36 +563,29 @@ class IFLoadImagess:
if not images: if not images:
img_tensor, mask = self.load_placeholder() img_tensor, mask = self.load_placeholder()
return ([img_tensor], [mask], [""], [""], ["0/0"], [0]) return ([img_tensor], [mask], [""], [""], ["0/0"], [0],
img_tensor.unsqueeze(0), mask.unsqueeze(0))
ui_data = { # Create batched version - now all images are the same size
"images": all_thumbnails, images_batch = torch.cat(images, dim=0) # [B, H, W, 3]
"current_thumbnails": selected_thumbnails, masks_batch = torch.cat(masks, dim=0) # [B, H, W]
"total_images": total_files,
"path_mapping": self.path_cache, return (images, masks, paths, filenames, count_strs, count_ints,
"available_image_count": total_files, images_batch, masks_batch)
"image_order": image_order,
"start_index": start_index,
"stop_index": start_index + num_images
}
return {
"ui": {"values": ui_data},
"result": (images, masks, paths, filenames, count_strs, count_ints)
}
except Exception as e: except Exception as e:
logger.error(f"Error in load_images: {e}", exc_info=True) logger.error(f"Error in load_images: {e}", exc_info=True)
img_tensor, mask = self.load_placeholder() img_tensor, mask = self.load_placeholder()
return ([img_tensor], [mask], [""], [""], ["error"], [0]) return ([img_tensor], [mask], [""], [""], ["error"], [0],
img_tensor.unsqueeze(0), mask.unsqueeze(0))
def load_placeholder(self): def load_placeholder(self):
"""Creates and returns a placeholder image tensor and mask""" """Creates and returns a placeholder image tensor and mask"""
img = Image.new('RGB', (512, 512), color=(73, 109, 137)) img = Image.new('RGB', (512, 512), color=(73, 109, 137))
image_array = np.array(img).astype(np.float32) / 255.0 image_array = np.array(img).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image_array)[None,] image_tensor = torch.from_numpy(image_array) # [H, W, 3]
mask = torch.zeros((1, image_array.shape[0], image_array.shape[1]), mask = torch.zeros((image_array.shape[0], image_array.shape[1]),
dtype=torch.float32, device="cpu") dtype=torch.float32, device="cpu") # [H, W]
return image_tensor, mask return image_tensor, mask
def process_single_image(self, image_path: str): def process_single_image(self, image_path: str):
@@ -588,7 +616,7 @@ class IFLoadImagess:
img_tensor, mask = self.load_placeholder() img_tensor, mask = self.load_placeholder()
return ([img_tensor], [mask], [""], [""], ["error"], [0]) return ([img_tensor], [mask], [""], [""], ["error"], [0])
@PromptServer.instance.routes.post("/ifai/backup_input") @PromptServer.instance.routes.post("/IF_img/backup_input")
async def backup_input_folder(request): async def backup_input_folder(request):
try: try:
success, message = ImageManager.backup_input_folder() success, message = ImageManager.backup_input_folder()
@@ -603,7 +631,7 @@ async def backup_input_folder(request):
"error": str(e) "error": str(e)
}, status=500) }, status=500)
@PromptServer.instance.routes.post("/ifai/restore_input") @PromptServer.instance.routes.post("/IF_img/restore_input")
async def restore_input_folder(request): async def restore_input_folder(request):
try: try:
success, message = ImageManager.restore_input_folder() success, message = ImageManager.restore_input_folder()
@@ -618,7 +646,7 @@ async def restore_input_folder(request):
"error": str(e) "error": str(e)
}, status=500) }, status=500)
@PromptServer.instance.routes.post("/ifai/refresh_previews") @PromptServer.instance.routes.post("/IF_img/refresh_previews")
async def refresh_previews(request): async def refresh_previews(request):
try: try:
data = await request.json() data = await request.json()
@@ -691,7 +719,7 @@ async def refresh_previews(request):
}, status=500) }, status=500)
# Add route for widget refresh # Add route for widget refresh
@PromptServer.instance.routes.post("/ifai/refresh_widgets") @PromptServer.instance.routes.post("/IF_img/refresh_widgets")
async def refresh_widgets(request): async def refresh_widgets(request):
try: try:
input_dir = folder_paths.get_input_directory() input_dir = folder_paths.get_input_directory()
@@ -716,4 +744,4 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
"IF_LoadImagesS": "IF Load Images S 🖼️" "IF_LoadImagesS": "IF Load Images S 🖼️"
} }
+13 -5
View File
@@ -1,3 +1,7 @@
Here’s the revised README section with the video tutorial link added:
---
# ComfyUI_IF_AI_LoadImages # ComfyUI_IF_AI_LoadImages
This tool enables you to load images from arbitrary folder selections, display previews of the images within subfolders, and output a list of images so you can work with multiple images simultaneously. This tool enables you to load images from arbitrary folder selections, display previews of the images within subfolders, and output a list of images so you can work with multiple images simultaneously.
@@ -14,16 +18,20 @@ This tool enables you to load images from arbitrary folder selections, display p
8. To use a single selected image, switch the option to true and queue the node (execute the workflow). 8. To use a single selected image, switch the option to true and queue the node (execute the workflow).
> **Note:** This is a workaround solution, so it may feel a bit clunky but functions effectively. > **Note:** This is a workaround solution, so it may feel a bit clunky but functions effectively.
>
[![Tutorial Video](https://img.youtube.com/vi/6ylkSoJ-Tnw/0.jpg)](https://www.youtube.com/watch?v=6ylkSoJ-Tnw)
*[Watch the video tutorial here](https://www.youtube.com/watch?v=6ylkSoJ-Tnw)*
![thorium_WAUJJwgYI6](https://github.com/user-attachments/assets/c3f1a31a-9d11-4e6d-b619-bb0226760e05) ![thorium_WAUJJwgYI6](https://github.com/user-attachments/assets/c3f1a31a-9d11-4e6d-b619-bb0226760e05)
![Image Preview 1](https://github.com/user-attachments/assets/55c67132-f7f5-4755-afef-7f9d5679c1d0) ![Image Preview 1](https://github.com/user-attachments/assets/55c67132-f7f5-4755-afef-7f9d5679c1d0)
## TODO ## TODO
- [x] Fix image extension filters
- [ ] Add a counter to enable loops. - [ ] Fix Masks from mask editor
- [ ] Add support for video files. - [ ] Fix single image upload
- [ ] Add a counter to enable loops
- [ ] Add support for video files
## Support ## Support
+25 -25
View File
@@ -1,25 +1,25 @@
import os import os
import glob import glob
import shutil import shutil
import sys import sys
import folder_paths import folder_paths
from .IFLoadImagesNodeS import IFLoadImagess from .IFLoadImagesNodeS import IFLoadImagess
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"IF_LoadImagesS": IFLoadImagess, "IF_LoadImagesS": IFLoadImagess,
} }
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
"IF_LoadImagesS": "IF Load Images S 🖼️", "IF_LoadImagesS": "IF Load Images S 🖼️",
} }
WEB_DIRECTORY = "./web" WEB_DIRECTORY = "./web"
__all__ = [ __all__ = [
"NODE_CLASS_MAPPINGS", "NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS",
"WEB_DIRECTORY", "WEB_DIRECTORY",
] ]
+5 -5
View File
@@ -1,9 +1,9 @@
[project] [project]
name = "comfyui_if_ai_loadimages" name = "comfyui_if_ai_loadimages"
description = "It Load Images with subfolders form arbitrary folders previous on node outputs lists- convinient selection via file browser" description = "It Load Images with subfolders form arbitrary folders previous on node outputs lists- convinient selection via file browser"
version = "1.0.1" version = "1.0.7"
license = { file = "LICENSE.txt" } license = { file = "MIT License" }
dependencies = ["pillow"] dependencies = ["pillow", "numpy"]
[project.urls] [project.urls]
Repository = "https://github.com/if-ai/ComfyUI_IF_AI_LoadImages" Repository = "https://github.com/if-ai/ComfyUI_IF_AI_LoadImages"
@@ -11,5 +11,5 @@ Repository = "https://github.com/if-ai/ComfyUI_IF_AI_LoadImages"
[tool.comfy] [tool.comfy]
PublisherId = "impactframes" PublisherId = "impactframes"
DisplayName = "ComfyUI_IF_LoadImages" DisplayName = "IF_LoadImages"
Icon = "" Icon = "https://impactframes.ai/System/Icons/48x48/if.png"
+2 -2
View File
@@ -1,2 +1,2 @@
pillow
numpy
+6 -6
View File
@@ -170,7 +170,7 @@ app.registerExtension({
const backupBtn = this.addWidget("button", "backup_input", "Backup Input 💾", const backupBtn = this.addWidget("button", "backup_input", "Backup Input 💾",
async () => { async () => {
try { try {
const response = await api.fetchApi("/if_ai/backup_input", { const response = await api.fetchApi("/IF_img/backup_input", {
method: "POST" method: "POST"
}); });
@@ -192,7 +192,7 @@ app.registerExtension({
const restoreBtn = this.addWidget("button", "restore_input", "Restore Input ♻️", const restoreBtn = this.addWidget("button", "restore_input", "Restore Input ♻️",
async () => { async () => {
try { try {
const response = await api.fetchApi("/if_ai/restore_input", { const response = await api.fetchApi("/IF_img/restore_input", {
method: "POST" method: "POST"
}); });
@@ -245,7 +245,7 @@ app.registerExtension({
load_limit: parseInt(this.widgets.find(w => w.name === "load_limit")?.value || "1000") load_limit: parseInt(this.widgets.find(w => w.name === "load_limit")?.value || "1000")
}; };
const response = await api.fetchApi("/if_ai/refresh_previews", { const response = await api.fetchApi("/IF_img/refresh_previews", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(options) body: JSON.stringify(options)
@@ -342,7 +342,7 @@ app.registerExtension({
nodeType.prototype.backupInputFolder = async function() { nodeType.prototype.backupInputFolder = async function() {
try { try {
this.showLoader(); this.showLoader();
const response = await fetch("/if_ai/backup_input", { const response = await fetch("/IF_img/backup_input", {
method: "POST" method: "POST"
}); });
@@ -365,7 +365,7 @@ app.registerExtension({
nodeType.prototype.restoreInputFolder = async function() { nodeType.prototype.restoreInputFolder = async function() {
try { try {
this.showLoader(); this.showLoader();
const response = await fetch("/if_ai/restore_input", { const response = await fetch("/IF_img/restore_input", {
method: "POST" method: "POST"
}); });
@@ -400,7 +400,7 @@ app.registerExtension({
this.showLoader(); this.showLoader();
const response = await fetch("/if_ai/refresh_previews", { const response = await fetch("/IF_img/refresh_previews", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"