Added Iterators and counters for images from folders or frames from videos

This commit is contained in:
Level Pixel Dev
2025-07-31 07:01:10 +06:00
parent c0976f4dc2
commit 02d278fe6a
6 changed files with 260 additions and 6 deletions
+1
View File
@@ -7,6 +7,7 @@ node_list = [
"convert.convert_LP",
"image.image_utils_LP",
"image.inpaint_crop_stitch_LP",
"io.iterators_LP",
"io.folder_workers_LP",
"io.image_loaders_LP",
"io.image_outputs_LP",
+10 -1
View File
@@ -11,6 +11,8 @@
"Convert Int To Float [LP]":"Convert Int To Float",
"Convert Int To Bool [LP]":"Convert Int To Bool",
"Convert Bool To Int [LP]":"Convert Bool To Int",
"Convert Combo To Text [LP]":"Convert Combo To Text",
"Convert Any To Text [LP]":"Convert Any To Text",
"Image Overlay [LP]":"Image Overlay",
"Fast Checker Pattern [LP]":"Fast Checker Pattern",
"File Counter [LP]":"Counts the number of files in the specified folder",
@@ -67,5 +69,12 @@
"Cropped Aspect Size Parameters [LP]":"Cropped Aspect Size Parameters for Inpaint Crop",
"Cropped Forsed Size Parameters [LP]":"Cropped Forsed Size Parameters for Inpaint Crop",
"Cropped Ranged Size Parameters [LP]":"Cropped Ranged Size Parameters for Inpaint Crop",
"Cropped Free Size Parameters [LP]":"Cropped Free Size Parameter for Inpaint Crops"
"Cropped Free Size Parameters [LP]":"Cropped Free Size Parameter for Inpaint Crops",
"Get Iterator Data From Videos [LP]": "Get Iterator Data From Videos",
"Get Iterator Data From Image Folders [LP]": "Get Iterator Data From Image Folders",
"Image Data Iterator [LP]": "Image Data Iterator",
"Iterator [LP]": "Iterator",
"Get ComfyUI Folder Path [LP]": "Get ComfyUI Folder Path",
"Get ComfyUI HTTP Folder Path [LP]": "Get ComfyUI HTTP Folder Path",
"Get Filename By Index In Folder [LP]": "Get Filename By Index In Folder"
}
+65
View File
@@ -274,6 +274,67 @@ class BoolToInt:
return (int_out,)
class ComboToText:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"combo_list": ("COMBO",),
"delimiter": ("STRING", {"default": ", ", "multiline": False}),
"as_json": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "combo_to_text"
CATEGORY = "LevelPixel/Conversion"
def combo_to_text(self, combo_list, delimiter, as_json):
import json
if as_json:
try:
text = json.dumps(combo_list, ensure_ascii=False, indent=2)
except Exception as e:
text = f"Error: {e}"
else:
lines = []
for item in combo_list:
if isinstance(item, dict):
line = delimiter.join(f"{k}: {v}" for k, v in item.items())
else:
line = str(item)
lines.append(line)
text = "\n".join(lines)
return (text,)
class AnyToText:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"any_value": (any, {"defaultInput": True}),
"as_json": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "any_to_text"
CATEGORY = "LevelPixel/Conversion"
def any_to_text(self, any_value, as_json):
import json
if as_json:
try:
text = json.dumps(any_value, ensure_ascii=False, indent=2)
except Exception as e:
text = f"Error: {e}"
else:
text = str(any_value)
return (text,)
NODE_CLASS_MAPPINGS = {
"StringToInt|LP": StringToInt,
"StringToFloat|LP": StringToFloat,
@@ -287,6 +348,8 @@ NODE_CLASS_MAPPINGS = {
"IntToFloat|LP": IntToFloat,
"IntToBool|LP": IntToBool,
"BoolToInt|LP": BoolToInt,
"ComboToText|LP": ComboToText,
"AnyToText|LP": AnyToText,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -302,4 +365,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"IntToFloat|LP": "Convert Int To Float [LP]",
"IntToBool|LP": "Convert Int To Bool [LP]",
"BoolToInt|LP": "Convert Bool To Int [LP]",
"ComboToText|LP": "Convert Combo To Text [LP]",
"AnyToText|LP": "Convert Any To Text [LP]",
}
+7 -3
View File
@@ -9,6 +9,7 @@ class FileCounter:
"required": {
"directory_path": ("STRING", {"default": '', "multiline": False}),
"patterns": ("STRING", {"default": '*.jpg|*.png|*.jpeg', "multiline": False}),
"rescan_each_queue": ("BOOLEAN", {"default": True}),
},
}
@@ -20,10 +21,13 @@ class FileCounter:
CATEGORY = "LevelPixel/IO"
@classmethod
def IS_CHANGED(cls, *v):
return ALWAYS_CHANGED_FLAG
def IS_CHANGED(cls, directory_path, patterns, rescan_each_queue, *v):
if rescan_each_queue == True:
return float("NaN")
else:
return False
def file_counter(self, directory_path, patterns):
def file_counter(self, directory_path, patterns, rescan_each_queue):
if not os.path.isdir(directory_path):
return (0,)
total_int = 0
+175
View File
@@ -0,0 +1,175 @@
import os
import glob
import cv2
class GetIteratorDataVideos:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"directory_path": ("STRING", {"default": '', "multiline": False}),
"patterns": ("STRING", {"default": '*.mp4|*.avi|*.mov|*.mkv', "multiline": False}),
"rescan_each_queue": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("ITERATOR_DATA", "INT", "INT")
RETURN_NAMES = ("iterator_data", "video_count", "frame_count")
FUNCTION = "prepare_iterator_data_from_videos"
CATEGORY = "LevelPixel/Iterators"
@classmethod
def IS_CHANGED(cls, directory_path, patterns, rescan_each_queue, *v):
if rescan_each_queue == True:
return float("NaN")
else:
return False
def prepare_iterator_data_from_videos(self, directory_path, patterns, rescan_each_queue):
if not os.path.isdir(directory_path):
return ([], 0, 0)
iterator_data = []
video_files = []
for pattern in patterns.split("|"):
video_files.extend(glob.glob(os.path.join(directory_path, pattern)))
video_files = sorted(video_files)
total_frames = 0
for idx, video_path in enumerate(video_files):
name = os.path.basename(video_path)
try:
cap = cv2.VideoCapture(video_path)
count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
cap.release()
except Exception:
count = 0
iterator_data.append({"index": idx, "name": name, "count": count})
total_frames += count
return (iterator_data, len(video_files), total_frames)
class GetIteratorDataImageFolders:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"directory_path": ("STRING", {"default": '', "multiline": False}),
"patterns": ("STRING", {"default": '*.jpg|*.png|*.jpeg', "multiline": False}),
"subfolder": ("STRING", {"default": '', "multiline": False}),
"rescan_each_queue": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("ITERATOR_DATA", "INT", "INT")
RETURN_NAMES = ("iterator_data", "folder_count", "image_count")
FUNCTION = "prepare_iterator_data_from_image_folders"
CATEGORY = "LevelPixel/Iterators"
@classmethod
def IS_CHANGED(cls, directory_path, patterns, subfolder, rescan_each_queue, *v):
if rescan_each_queue == True:
return float("NaN")
else:
return False
def prepare_iterator_data_from_image_folders(self, directory_path, patterns, subfolder, rescan_each_queue):
if not os.path.isdir(directory_path):
return ([], 0, 0)
iterator_data = []
folders = [f for f in os.listdir(directory_path) if os.path.isdir(os.path.join(directory_path, f))]
total_images = 0
for idx, folder in enumerate(sorted(folders)):
if subfolder:
search_path = os.path.join(directory_path, folder, subfolder)
else:
search_path = os.path.join(directory_path, folder)
count = 0
if os.path.isdir(search_path):
for pattern in patterns.split("|"):
count += len(glob.glob(os.path.join(search_path, pattern)))
iterator_data.append({"index": idx, "name": folder, "count": count})
total_images += count
return (iterator_data, len(folders), total_images)
class ImageDataIterator:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"iterator_data": ("ITERATOR_DATA",),
"global_index": ("INT", {"default": 0, "min": 0, "control_after_generate": True}),
}
}
RETURN_TYPES = ("INT", "INT", "STRING", "INT")
RETURN_NAMES = ("item_index", "set_index", "set_name", "global_index")
FUNCTION = "iterate"
CATEGORY = "LevelPixel/Iterators"
def iterate(self, iterator_data, global_index):
total = 0
total_frames = sum(item["count"] for item in iterator_data)
if global_index >= total_frames:
raise RuntimeError("Image iteration finished")
for i, item in enumerate(iterator_data):
count = item["count"]
if global_index < total + count:
item_index = global_index - total
set_index = item["index"]
set_name = item["name"]
return (item_index, set_index, set_name, global_index)
total += count
return (0, 0, '', global_index)
class Iterator:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"index": ("INT", {"default": 0, "min": 0, "control_after_generate": True}),
"limit": ("INT", {"default": 0, "min": 0}),
"mode": ([
"Greater (index > limit)",
"Less (index < limit)",
"Equal (index == limit)",
"Greater or Equal (index >= limit)",
"Less or Equal (index <= limit)"
], {"default": "Greater (index > limit)"}),
},
}
RETURN_TYPES = ("INT",)
RETURN_NAMES = ("index",)
FUNCTION = "indexate"
CATEGORY = "LevelPixel/Iterators"
def indexate(self, index, limit, mode):
if mode == "Greater (index > limit)":
if index > limit:
raise RuntimeError("Iteration finished (index > limit)")
elif mode == "Less (index < limit)":
if index < limit:
raise RuntimeError("Iteration finished (index < limit)")
elif mode == "Equal (index == limit)":
if index == limit:
raise RuntimeError("Iteration finished (index == limit)")
elif mode == "Greater or Equal (index >= limit)":
if index >= limit:
raise RuntimeError("Iteration finished (index >= limit)")
elif mode == "Less or Equal (index <= limit)":
if index <= limit:
raise RuntimeError("Iteration finished (index <= limit)")
return (index,)
NODE_CLASS_MAPPINGS = {
"GetIteratorDataVideos|LP": GetIteratorDataVideos,
"GetIteratorDataImageFolders|LP": GetIteratorDataImageFolders,
"ImageDataIterator|LP": ImageDataIterator,
"Iterator|LP": Iterator,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"GetIteratorDataVideos|LP": "Get Iterator Data From Videos [LP]",
"GetIteratorDataImageFolders|LP": "Get Iterator Data From Image Folders [LP]",
"ImageDataIterator|LP": "Image Data Iterator [LP]",
"Iterator|LP": "Iterator [LP]",
}
+2 -2
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-levelpixel"
description = "Main nodes of the Level Pixel company (levelpixel, LP). In this package you will find easy-to-use atomic nodes that can help automate various processes - memory cleaning automation (RAM free, VRAM free), Lora loading like in Automatic1111, powerful inpaint crop and stitch, pipeline, declaring standard variables, converting variables, google translate, filtering tags by categories, working with file system, counting files in file system, counting objects, counting images. The names of the key nodes in this package are: Inpaint-CropAndStitch, Inpaint Crop, Inpaint Stitch, Resize Image, Resize Mask, Model Unloader, Free memory, Tag Filters, Tag Category Filters, Tag Choice Parser, File counter, Image Loader From Path (with counters), Google Translator, Language Translator, Override Device, Delay, Lora Tag Loader, Pipeline, Resize Masks, Resize Image. Licensed under GNU GPLv3. Repository: https://github.com/LevelPixel/ComfyUI-LevelPixel"
version = "1.2.9"
description = "Main nodes of the Level Pixel company (levelpixel, LP). In this package you will find easy-to-use atomic nodes that can help automate various processes - memory cleaning automation (RAM free, VRAM free), Lora loading like in Automatic1111, powerful inpaint crop and stitch, pipeline, declaring standard variables, converting variables, google translate, filtering tags by categories, working with file system, counting files in file system, counting objects, counting images. The names of the key nodes in this package are: Inpaint-CropAndStitch, Inpaint Crop, Inpaint Stitch, Resize Image, Resize Mask, Model Unloader, Free memory, Tag Filters, Tag Category Filters, Tag Choice Parser, File counter, Image Loader From Path (with counters), Google Translator, Language Translator, Override Device, Delay, Lora Tag Loader, Pipeline, Resize Masks, Resize Image, Iterators. Licensed under GNU GPLv3. Repository: https://github.com/LevelPixel/ComfyUI-LevelPixel"
version = "1.3.0"
license = { file = "LICENSE.txt" }
dependencies = [
"torch>=2.0.1",