Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4e8ce0cfe | ||
|
|
ad8b29a75e | ||
|
|
264cb3eaba | ||
|
|
9bad06bc83 | ||
|
|
0e632a7b7b | ||
|
|
e277386ad5 | ||
|
|
c9f6b37874 | ||
|
|
11847c4a7c | ||
|
|
97fddde11f | ||
|
|
c14ca10510 | ||
|
|
59accb9b08 | ||
|
|
716be3778d | ||
|
|
f4d734a246 | ||
|
|
ca24f65792 | ||
|
|
c4dd20c870 |
@@ -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 }}
|
||||
|
||||
+98
-70
@@ -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, stop_index=0, max_images=1,
|
||||
include_subfolders=True, sort_method="numerical",
|
||||
filter_type="none", image_name="", unique_id=None):
|
||||
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"):
|
||||
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()
|
||||
|
||||
+25
-25
@@ -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
@@ -1,15 +1,15 @@
|
||||
[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.2"
|
||||
license = { file = "LICENSE.txt" }
|
||||
version = "1.0.7"
|
||||
license = { file = "MIT License" }
|
||||
dependencies = ["pillow", "numpy"]
|
||||
|
||||
[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
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "impactframes"
|
||||
DisplayName = "ComfyUI_IF_LoadImages"
|
||||
Icon = ""
|
||||
DisplayName = "IF_LoadImages"
|
||||
Icon = "https://impactframes.ai/System/Icons/48x48/if.png"
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
|
||||
|
||||
pillow
|
||||
numpy
|
||||
|
||||
@@ -170,7 +170,7 @@ app.registerExtension({
|
||||
const backupBtn = this.addWidget("button", "backup_input", "Backup Input 💾",
|
||||
async () => {
|
||||
try {
|
||||
const response = await api.fetchApi("/ifai/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("/ifai/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("/ifai/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("/ifai/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("/ifai/restore_input", {
|
||||
const response = await fetch("/IF_img/restore_input", {
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
@@ -400,7 +400,7 @@ app.registerExtension({
|
||||
|
||||
this.showLoader();
|
||||
|
||||
const response = await fetch("/ifai/refresh_previews", {
|
||||
const response = await fetch("/IF_img/refresh_previews", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
|
||||
Reference in New Issue
Block a user