15 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
6 changed files with 143 additions and 110 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 }}
+98 -70
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, stop_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, load_limit="1000"): 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()
+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,15 +1,15 @@
[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.2" version = "1.0.7"
license = { file = "LICENSE.txt" } license = { file = "MIT License" }
dependencies = ["pillow", "numpy"] dependencies = ["pillow", "numpy"]
[project.urls] [project.urls]
Repository = "https://github.com/if-ai/ComfyUI_IF_AI_LoadImages.git" Repository = "https://github.com/if-ai/ComfyUI_IF_AI_LoadImages"
# Used by Comfy Registry https://comfyregistry.org # Used by Comfy Registry https://comfyregistry.org
[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("/ifai/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("/ifai/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("/ifai/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("/ifai/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("/ifai/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("/ifai/refresh_previews", { const response = await fetch("/IF_img/refresh_previews", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"