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:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'if-ai' }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
uses: Comfy-Org/publish-node-action@v1
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)
class IFLoadImagess:
_color_channels = ["alpha", "red", "green", "blue"]
def __init__(self):
self.path_cache = {} # Cache for path mapping
self.path_cache = {}
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
# Count available thumbnails
available_images = len([f for f in os.listdir(input_dir)
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)
if f.startswith(ImageManager.THUMBNAIL_PREFIX)]
@@ -396,7 +397,7 @@ class IFLoadImagess:
"image": (sorted(files), {"image_upload": True}),
"input_path": ("STRING", {"default": ""}),
"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"}),
"image_selected": ("BOOLEAN", {"default": False}),
"available_image_count": ("INT", {
@@ -408,19 +409,21 @@ class IFLoadImagess:
"include_subfolders": ("BOOLEAN", {"default": True}),
"sort_method": (["alphabetical", "numerical", "date_created", "date_modified"],),
"filter_type": (["none", "png", "jpg", "jpeg", "webp", "gif", "bmp"],),
"channel": (s._color_channels, {"default": "alpha"}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "STRING", "STRING", "INT")
RETURN_NAMES = ("images", "masks", "image_paths", "filenames", "count_str", "count_int")
OUTPUT_IS_LIST = (True, True, True, True, True, True)
RETURN_TYPES = ("IMAGE", "MASK", "STRING", "STRING", "STRING", "INT", "IMAGE", "MASK")
RETURN_NAMES = ("images", "masks", "image_paths", "filenames", "count_str", "count_int", "images_batch", "masks_batch")
OUTPUT_IS_LIST = (True, True, True, True, True, True, False, False)
FUNCTION = "load_images"
CATEGORY = "ImpactFrames💥🎞️"
CATEGORY = "ImpactFrames💥🎞️/images"
@classmethod
def IS_CHANGED(cls, image, input_path="", start_index=0, max_images=1,
include_subfolders=True, sort_method="numerical",
filter_type="none", image_name="", unique_id=None):
def IS_CHANGED(cls, image, input_path="", start_index=0, stop_index=0, max_images=1,
include_subfolders=True, sort_method="numerical", image_selected=False,
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
This matches the input parameters from INPUT_TYPES
@@ -442,20 +445,21 @@ class IFLoadImagess:
return float("NaN")
def load_images(self, image="", input_path="", start_index=0, stop_index=10,
load_limit="1000", image_selected=False, available_image_count=0,
include_subfolders=True, sort_method="numerical",
filter_type="none", image_name="", unique_id=None):
load_limit="1000", image_selected=False, available_image_count=0,
include_subfolders=True, sort_method="numerical",
filter_type="none", channel="alpha"):
try:
# Process 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))
# Get all valid images first
# Get all valid images
all_files = ImageManager.get_image_files(abs_path, include_subfolders, filter_type)
if not all_files:
logger.warning(f"No valid images found in {abs_path}")
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
all_files = ImageManager.sort_files(all_files, sort_method)
@@ -478,49 +482,80 @@ class IFLoadImagess:
start_index = image_order[image]
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
selected_files = all_files[start_index:start_index + num_images]
selected_thumbnails = all_thumbnails[:num_images]
# Process selected files
# Lists to store outputs
images = []
masks = []
paths = []
filenames = []
count_strs = []
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:
with Image.open(file_path) as 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':
img = img.point(lambda i: i * (1 / 255))
image = img.convert('RGB')
image_array = np.array(image).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image_array)[None,]
if 'A' in img.getbands():
mask = np.array(img.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((image_array.shape[0], image_array.shape[1]),
dtype=torch.float32, device="cpu")
images.append(image_tensor)
masks.append(mask.unsqueeze(0))
paths.append(file_path)
filenames.append(os.path.basename(file_path))
count_str = f"{start_index + idx + 1}/{total_files}" # Update count to show global position
count_strs.append(count_str)
count_ints.append(start_index + idx + 1)
c = channel[0].upper()
if c in img.getbands():
mask = np.array(img.getchannel(c)).astype(np.float32) / 255.0
mask = torch.from_numpy(mask)
if c == 'A':
mask = 1. - mask
else:
mask = torch.zeros((max_height, max_width),
dtype=torch.float32, device="cpu")
# Resize mask if needed
if mask.shape != (max_height, max_width):
mask = torch.nn.functional.interpolate(
mask.unsqueeze(0).unsqueeze(0),
size=(max_height, max_width),
mode='bilinear',
align_corners=False
).squeeze(0).squeeze(0)
masks.append(mask.unsqueeze(0)) # Add batch dimension to mask [1, H, W]
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:
logger.error(f"Error processing image {file_path}: {e}")
@@ -528,36 +563,29 @@ class IFLoadImagess:
if not images:
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 = {
"images": all_thumbnails,
"current_thumbnails": selected_thumbnails,
"total_images": total_files,
"path_mapping": self.path_cache,
"available_image_count": total_files,
"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)
}
# Create batched version - now all images are the same size
images_batch = torch.cat(images, dim=0) # [B, H, W, 3]
masks_batch = torch.cat(masks, dim=0) # [B, H, W]
return (images, masks, paths, filenames, count_strs, count_ints,
images_batch, masks_batch)
except Exception as e:
logger.error(f"Error in load_images: {e}", exc_info=True)
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):
"""Creates and returns a placeholder image tensor and mask"""
img = Image.new('RGB', (512, 512), color=(73, 109, 137))
image_array = np.array(img).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image_array)[None,]
mask = torch.zeros((1, image_array.shape[0], image_array.shape[1]),
dtype=torch.float32, device="cpu")
image_tensor = torch.from_numpy(image_array) # [H, W, 3]
mask = torch.zeros((image_array.shape[0], image_array.shape[1]),
dtype=torch.float32, device="cpu") # [H, W]
return image_tensor, mask
def process_single_image(self, image_path: str):
@@ -588,7 +616,7 @@ class IFLoadImagess:
img_tensor, mask = self.load_placeholder()
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):
try:
success, message = ImageManager.backup_input_folder()
@@ -603,7 +631,7 @@ async def backup_input_folder(request):
"error": str(e)
}, status=500)
@PromptServer.instance.routes.post("/ifai/restore_input")
@PromptServer.instance.routes.post("/IF_img/restore_input")
async def restore_input_folder(request):
try:
success, message = ImageManager.restore_input_folder()
@@ -618,7 +646,7 @@ async def restore_input_folder(request):
"error": str(e)
}, status=500)
@PromptServer.instance.routes.post("/ifai/refresh_previews")
@PromptServer.instance.routes.post("/IF_img/refresh_previews")
async def refresh_previews(request):
try:
data = await request.json()
@@ -691,7 +719,7 @@ async def refresh_previews(request):
}, status=500)
# 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):
try:
input_dir = folder_paths.get_input_directory()
@@ -716,4 +744,4 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = {
"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
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).
> **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)
![Image Preview 1](https://github.com/user-attachments/assets/55c67132-f7f5-4755-afef-7f9d5679c1d0)
## TODO
- [ ] Add a counter to enable loops.
- [ ] Add support for video files.
- [x] Fix image extension filters
- [ ] Fix Masks from mask editor
- [ ] Fix single image upload
- [ ] Add a counter to enable loops
- [ ] Add support for video files
## Support
+25 -25
View File
@@ -1,25 +1,25 @@
import os
import glob
import shutil
import sys
import folder_paths
from .IFLoadImagesNodeS import IFLoadImagess
NODE_CLASS_MAPPINGS = {
"IF_LoadImagesS": IFLoadImagess,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"IF_LoadImagesS": "IF Load Images S 🖼️",
}
WEB_DIRECTORY = "./web"
__all__ = [
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
"WEB_DIRECTORY",
]
import os
import glob
import shutil
import sys
import folder_paths
from .IFLoadImagesNodeS import IFLoadImagess
NODE_CLASS_MAPPINGS = {
"IF_LoadImagesS": IFLoadImagess,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"IF_LoadImagesS": "IF Load Images S 🖼️",
}
WEB_DIRECTORY = "./web"
__all__ = [
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS",
"WEB_DIRECTORY",
]
+5 -5
View File
@@ -1,9 +1,9 @@
[project]
name = "comfyui_if_ai_loadimages"
description = "It Load Images with subfolders form arbitrary folders previous on node outputs lists- convinient selection via file browser"
version = "1.0.1"
license = { file = "LICENSE.txt" }
dependencies = ["pillow"]
version = "1.0.7"
license = { file = "MIT License" }
dependencies = ["pillow", "numpy"]
[project.urls]
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]
PublisherId = "impactframes"
DisplayName = "ComfyUI_IF_LoadImages"
Icon = ""
DisplayName = "IF_LoadImages"
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 💾",
async () => {
try {
const response = await api.fetchApi("/if_ai/backup_input", {
const response = await api.fetchApi("/IF_img/backup_input", {
method: "POST"
});
@@ -192,7 +192,7 @@ app.registerExtension({
const restoreBtn = this.addWidget("button", "restore_input", "Restore Input ♻️",
async () => {
try {
const response = await api.fetchApi("/if_ai/restore_input", {
const response = await api.fetchApi("/IF_img/restore_input", {
method: "POST"
});
@@ -245,7 +245,7 @@ app.registerExtension({
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",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(options)
@@ -342,7 +342,7 @@ app.registerExtension({
nodeType.prototype.backupInputFolder = async function() {
try {
this.showLoader();
const response = await fetch("/if_ai/backup_input", {
const response = await fetch("/IF_img/backup_input", {
method: "POST"
});
@@ -365,7 +365,7 @@ app.registerExtension({
nodeType.prototype.restoreInputFolder = async function() {
try {
this.showLoader();
const response = await fetch("/if_ai/restore_input", {
const response = await fetch("/IF_img/restore_input", {
method: "POST"
});
@@ -400,7 +400,7 @@ app.registerExtension({
this.showLoader();
const response = await fetch("/if_ai/refresh_previews", {
const response = await fetch("/IF_img/refresh_previews", {
method: "POST",
headers: {
"Content-Type": "application/json"