added iteration feats. modules resturctured.

This commit is contained in:
Vincent at Jupiter
2025-12-26 23:57:45 +08:00
parent 9a34deae25
commit bc0c69fe6b
8 changed files with 542 additions and 227 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
// below implementation is from https://github.com/pythongosssss/ComfyUI-Custom-Scripts
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
import { app } from "/scripts/app.js";
import { ComfyWidgets } from "/scripts/widgets.js";
// Displays input text on a node
+23 -89
View File
@@ -1,5 +1,6 @@
from .py.log import log
import importlib
from .node_registry import REGISTERED_NODES
# import importlib.util # for auto import
NODE_CLASS_MAPPINGS = {}
@@ -15,96 +16,29 @@ def AddMapping(node_class):
else:
NODE_DISPLAY_NAME_MAPPINGS["AG "+node_class.NAME] = node_class.NAME+" 🐦"
#--------------------------------------------------------------------------------#
# Load Image Nodes
#--------------------------------------------------------------------------------#
node_modules = [
".py.nodes_photoedit",
".py.nodes_utils",
".py.nodes_opencv",
".py.nodes_xfeat",
# New categories can be added here
]
# import modules
try:
nodes_utils = importlib.import_module(".py.nodes_utils", package=__package__)
for name in (
"JsonSave",
"JsonLoad",
"NparraySave",
"NparrayLoad",
"SaveNparrayImage",
"PreviewNparrayImage",
"Printf",
"SliceList",
"SliceSublist",
"ImageResizeTo",
"Image2BW",
):
if hasattr(nodes_utils, name):
AddMapping(getattr(nodes_utils, name))
for module_path in node_modules:
importlib.import_module(module_path, package=__package__)
except ImportError as e:
log("Failed to load util nodes", msg_color="BRIGHT_GREEN")
log(f"Failed to load nodes from {module_path}", msg_color="BRIGHT_GREEN")
log(e, msg_color="BRIGHT_GREEN")
#--------------------------------------------------------------------------------#
# Load OpenCV Nodes
#--------------------------------------------------------------------------------#
try:
nodes_opencv = importlib.import_module(".py.nodes_opencv", package=__package__)
for name in (
"Image2Nparray",
"Nparray2Image",
"Bitwise_not",
"Threshold",
"GaussianBlur",
"MedianBlur",
"Blur",
"Sobel",
"Canny",
"HoughLines",
"HoughLinesP",
"FindContours",
"DrawContours",
"Circles",
"ConnectedComponentsWithStats",
):
if hasattr(nodes_opencv, name):
AddMapping(getattr(nodes_opencv, name))
except ImportError as e:
log("Failed to load opencv nodes", msg_color="BRIGHT_GREEN")
log(e, msg_color="BRIGHT_GREEN")
#--------------------------------------------------------------------------------#
# Load XFeat Nodes
#--------------------------------------------------------------------------------#
try:
# use a dynamic (string) import to avoid static analysis warnings when the
# premium module is not present in free distributions
nodes_xfeat = importlib.import_module(".py.nodes_xfeat", package=__package__)
for name in (
"ColormapHotGray",
"HoughLinesAccumulator",
"HoughLinesPlotter",
"KLargestPoints",
"PointToLineParams",
"IntersectImageWithMask",
"MorphologyByRegions",
"MorphologyByRegionsWithStats",
"DrawQuiver",
"LabelsToContours",
"ContoursToLabels",
"LabelsPreview",
"CreateGVF",
"GVFSnake",
):
if hasattr(nodes_xfeat, name):
AddMapping(getattr(nodes_xfeat, name))
# from .py.nodes_xfeat import MorphCloseByRegions
# AddMapping(MorphCloseByRegions)
except ImportError as e:
log("Premium nodes ignored", msg_color="BRIGHT_GREEN")
# register all nodes
for node_class in REGISTERED_NODES:
try:
AddMapping(node_class)
except Exception as e:
if hasattr(node_class, "NAME"):
log(f"Failed to register nodes {node_class.NAME}", msg_color="BRIGHT_GREEN")
else:
log("Failed to register nodes", msg_color="BRIGHT_GREEN")
log(e, msg_color="BRIGHT_GREEN")
+9
View File
@@ -0,0 +1,9 @@
#--------------------------------------------------------------------------------#
# Global list to hold registered nodes
#--------------------------------------------------------------------------------#
REGISTERED_NODES = []
def comfy_node(cls):
"""ComfyUI decorator"""
REGISTERED_NODES.append(cls)
return cls
-1
View File
@@ -1 +0,0 @@
CURRENT_CATEGORY = "🐦‍🔥 ArchiGraph/📊 Matplot" # organized by py file
+16
View File
@@ -7,10 +7,12 @@ import comfy.model_management as model_management
from .anyType import ANY
from .enums import ColorEnum, ThreshEnum, BorderEnum, ContourModeEnum, \
ContourMethodEnum, DepthEnum, ColormapEnum
from ..node_registry import comfy_node
#---------------------------------------------------------------------------------------------------------------------#
# Helpers
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class Image2Nparray:
@classmethod
def INPUT_TYPES(cls):
@@ -43,6 +45,7 @@ class Image2Nparray:
return (ret,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class Nparray2Image:
@classmethod
def INPUT_TYPES(cls):
@@ -74,6 +77,7 @@ class Nparray2Image:
#---------------------------------------------------------------------------------------------------------------------#
# OpenCV Image Processing Nodes
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class Bitwise_not:
@classmethod
def INPUT_TYPES(cls):
@@ -100,6 +104,7 @@ class Bitwise_not:
return (result,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class Threshold:
@classmethod
def INPUT_TYPES(cls):
@@ -144,6 +149,7 @@ class Threshold:
return (result,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class GaussianBlur:
@classmethod
def INPUT_TYPES(cls):
@@ -200,6 +206,7 @@ class GaussianBlur:
return (result,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class MedianBlur:
@classmethod
def INPUT_TYPES(cls):
@@ -233,6 +240,7 @@ class MedianBlur:
return (result,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class Blur:
@classmethod
def INPUT_TYPES(cls):
@@ -281,6 +289,7 @@ class Blur:
return (result,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class Sobel:
@classmethod
def INPUT_TYPES(cls):
@@ -345,6 +354,7 @@ class Sobel:
return (result,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class Canny:
@classmethod
def INPUT_TYPES(cls):
@@ -412,6 +422,7 @@ class Canny:
return (result,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class HoughLines:
@classmethod
def INPUT_TYPES(cls):
@@ -475,6 +486,7 @@ class HoughLines:
return (list_lines,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class HoughLinesP:
@classmethod
def INPUT_TYPES(cls):
@@ -535,6 +547,7 @@ class HoughLinesP:
return (list_lines,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class FindContours:
@classmethod
def INPUT_TYPES(cls):
@@ -579,6 +592,7 @@ class FindContours:
return (contours, hierarchy, )
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class DrawContours:
@classmethod
def INPUT_TYPES(cls):
@@ -635,6 +649,7 @@ class DrawContours:
return (result[np.newaxis, :],)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class Circles:
@classmethod
def INPUT_TYPES(cls):
@@ -692,6 +707,7 @@ class Circles:
return (result,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class ConnectedComponentsWithStats:
@classmethod
def INPUT_TYPES(cls):
+194
View File
@@ -0,0 +1,194 @@
CURRENT_CATEGORY = "🐦‍🔥 ArchiGraph/📷 PhotoEdit" # organized by py file
import torch
from comfy.utils import common_upscale
from ..node_registry import comfy_node
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class ImageResizeTo:
upscale_methods = ["lanczos", "nearest-exact", "bilinear", "area", "bicubic"]
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
"""
Return a dictionary which contains config for all input fields.
Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT".
Input types "INT", "STRING" or "FLOAT" are special values for fields on the node.
The type can be a list for selection.
Returns: `dict`:
- Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required`
- Value input_fields (`dict`): Contains input fields config:
* Key field_name (`string`): Name of a entry-point method's argument
* Value field_config (`tuple`):
+ First value is a string indicate the type of field or a list for selection.
+ Second value is a config for type "INT", "STRING" or "FLOAT".
"""
return {
"required": {
"image": ("IMAGE",{
"lazy": False # Will only be evaluated if check_lazy_status requires it
}),
"target_size": (["720P", "1080P", "2K", "4K", "custom"], {
"default": "720P", # Default value
"tooltip": "Preset target sizes. Select 'custom' to specify edge size manually.",
}),
"bbox_edge1": ("INT", {
"default": 1024,
"min": 16, #Minimum value
"max": 8192, #Maximum value
"step": 2, #Slider's step
"display": "number", # Cosmetic only: display as "number" or "slider"
"tooltip": "Only valid for custom target type. Edge size for the boundingbox. Orientation will be calculated automatically.",
}),
"bbox_edge2": ("INT", {
"default": 1024,
"min": 16, #Minimum value
"max": 8192, #Maximum value
"step": 2, #Slider's step
"display": "number", # Cosmetic only: display as "number" or "slider"
"tooltip": "Only valid for custom target type. Edge size for the boundingbox. Orientation will be calculated automatically.",
}),
"upscale_method": (cls.upscale_methods,),
"onlyEnlarge": ("BOOLEAN", {
"default": True,
"tooltip": "If true, only enlarge images. Large images will be kept at original size.",
}),
},
}
RETURN_TYPES = ("IMAGE", "INT", "INT", "STRING")
RETURN_NAMES = ("IMAGE", "prev_width", "prev_height", "log")
FUNCTION = "resize"
NAME = "Image Resize To"
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "Resizes an image by specify one edge size. Proportion is kept by default."
def resize(self, image: torch.Tensor, target_size: str, bbox_edge1: int, bbox_edge2: int, upscale_method: str, onlyEnlarge: bool):
# get original image size
B, H, W, C = image.shape
prev_width = W
prev_height = H
image = image.movedim(-1, 1)
ratio = max(W, H) * 1.0 / min(W, H)
short_edge = 0
long_edge = 0
if target_size == "custom":
bbox_short = min(bbox_edge1, bbox_edge2)
bbox_long = max(bbox_edge1, bbox_edge2)
new_ratio = bbox_long * 1.0 / bbox_short
if ratio > new_ratio:
short_edge = bbox_short
long_edge = int(bbox_short * ratio)
else:
long_edge = bbox_long
short_edge = int(bbox_long / ratio)
else:
new_ratio = 16.0 / 9.0
if ratio > new_ratio:
short_edge = 720
long_edge = int(720 * ratio)
else:
long_edge = 1280
short_edge = int(1280 / ratio)
if target_size == "720P":
pass
elif target_size == "1080P":
short_edge *= 1.5
long_edge *= 1.5
elif target_size == "2K":
short_edge *= 2
long_edge *= 2
elif target_size == "4K":
short_edge *= 3
long_edge *= 3
if W >= H:
new_width = long_edge
new_height = short_edge
else:
new_width = short_edge
new_height = long_edge
if onlyEnlarge and new_width <= W:
log = f"Original image is larger, not resized."
else:
image = common_upscale(image, new_width, new_height, upscale_method, "disabled")
log = f"Resized from W{prev_width}xH{prev_height} to W{new_width}xH{new_height}."
image = image.movedim(1, -1)
return (image, prev_width, prev_height, log,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class Image2BW:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",{
"lazy": False
}),
"reds":("INT", {"default": 40, "min": -200, "max": 300, "display": "slider"}),
"yellows":("INT", {"default": 60, "min": -200, "max": 300, "display": "slider"}),
"greens":("INT", {"default": 40, "min": -200, "max": 300, "display": "slider"}),
"cyans":("INT", {"default": 60, "min": -200, "max": 300, "display": "slider"}),
"blues":("INT", {"default": 20, "min": -200, "max": 300, "display": "slider"}),
"magentas":("INT", {"default": 80, "min": -200, "max": 300, "display": "slider"}),
},
}
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("IMAGE", )
FUNCTION = "bw"
NAME = "Image to BW"
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "Converts an image to black & white like the adjustments in Photoshop."
def bw(self, image, reds, yellows, greens, cyans, blues, magentas):
datatype = torch.float32
# get original image size
B, H, W, C = image.shape
color_ratios = torch.tensor([reds, greens, blues, cyans, magentas, yellows], dtype=datatype)
color_ratios = (color_ratios + 200) / 500
sorted_vals, sorted_idx = torch.sort(image, dim=3, descending=True) # BxHxWxC
# Get the color ratio value of the maximum channel
max_idx = sorted_idx[:, :, :, 0].unsqueeze(-1) # BxHxWx1
ratios_reshaped = color_ratios.unsqueeze(0).unsqueeze(0).unsqueeze(0).expand(B,H,W,-1) # BxHxWx6
ratio_max = torch.gather(ratios_reshaped, 3, max_idx) # BxHxWx1
combo_idx = (sorted_idx[:, :, :, -1] + 3).unsqueeze(-1) # BxHxWx1
ratio_combo = torch.gather(ratios_reshaped, 3, combo_idx) # BxHxWx1
# duplicate sorted_vals to a new tensor
sorted_vals_2 = sorted_vals.clone()
sorted_vals_2[:, :, :, 0] = 0
# move the first value to the last in sorted_vals_2
sorted_vals_2 = torch.cat((sorted_vals_2[:, :, :, 1:], sorted_vals_2[:, :, :, 0].unsqueeze(-1)), dim=-1) # BxHxWxC
diff = sorted_vals - sorted_vals_2 # BxHxWxC
ratio = torch.cat((ratio_max, ratio_combo, torch.ones_like(ratio_max, dtype=datatype)), dim=-1) # BxHxWx3
# calculate the sum product of diff and ratio
final = torch.sum(diff * ratio,dim=-1) # BxHxWx1
return (final, )
#---------------------------------------------------------------------------------------------------------------------#
+297 -134
View File
@@ -1,21 +1,36 @@
CURRENT_CATEGORY = "🐦‍🔥 ArchiGraph/🛠️ Utils" # organized by py file
import os
import random
import random, uuid, hashlib, time
import torch
from comfy.utils import common_upscale
import numpy as np
from PIL import Image
from .anyType import ANY
from .nodes_opencv import Nparray2Image
from ..node_registry import comfy_node
# import comfyui core node
from server import PromptServer # import comfyui server module to manupulate prompt queue
from nodes import SaveImage
import folder_paths
NPARRAY_DIR = os.path.join(folder_paths.get_output_directory(), "nparrays")
JSON_DIR = os.path.join(folder_paths.get_output_directory(), "jsons")
#---------------------------------------------------------------------------------------------------------------------#
# Helpers
#---------------------------------------------------------------------------------------------------------------------#
def resolve_path(path: str) -> str:
# 1. 展开环境变量(如 %USERPROFILE%)
path = os.path.expandvars(path)
# 2. 展开用户目录(如 ~ 表示用户主目录)
path = os.path.expanduser(path)
# 3. 转换为绝对路径并规范化
path = os.path.abspath(path)
return path
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class JsonSave:
@classmethod
def INPUT_TYPES(cls):
@@ -59,6 +74,7 @@ class JsonSave:
return (file_path,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class JsonLoad:
@classmethod
def INPUT_TYPES(cls):
@@ -108,6 +124,7 @@ class JsonLoad:
return (data,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class NparraySave:
@classmethod
def INPUT_TYPES(cls):
@@ -140,6 +157,7 @@ class NparraySave:
return (file_path,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class NparrayLoad:
@classmethod
def INPUT_TYPES(cls):
@@ -186,6 +204,7 @@ class NparrayLoad:
return (nparray,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class SaveNparrayImage(SaveImage):
@classmethod
def INPUT_TYPES(cls):
@@ -211,6 +230,7 @@ class SaveNparrayImage(SaveImage):
return super().save_images(images, filename_prefix, prompt, extra_pnginfo)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class PreviewNparrayImage(SaveNparrayImage):
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
@@ -229,9 +249,10 @@ class PreviewNparrayImage(SaveNparrayImage):
DESCRIPTION = "Previews the input nparray images by saving them to a temporary directory."
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class Printf:
'''
Later can be used to unravel list and show specified elements only.
TODO: Can be used to unravel list and show specified elements only.
'''
@classmethod
def INPUT_TYPES(cls):
@@ -249,7 +270,7 @@ class Printf:
}
OUTPUT_NODE = True
# INPUT_IS_LIST = True # if the input is a list, workflow will pass each element separately
INPUT_IS_LIST = False # if the input is a list, workflow will pass each element separately
OUTPUT_IS_LIST = (True,)
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("string",)
@@ -280,27 +301,10 @@ class Printf:
else:
text.append(f"Type: {type(any)}, Value:\n{any}")
# save to workflow pnginfo
if unique_id is not None and extra_pnginfo is not None:
if not isinstance(extra_pnginfo, list):
print("Error: extra_pnginfo is not a list")
elif (
not isinstance(extra_pnginfo[0], dict)
or "workflow" not in extra_pnginfo[0]
):
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
else:
workflow = extra_pnginfo[0]["workflow"]
node = next(
(x for x in workflow["nodes"] if str(x["id"]) == str(unique_id[0])),
None,
)
if node:
node["widgets_values"] = [text]
return {"ui": {"text": text}, "result": (text,)}
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class SliceList:
@classmethod
def INPUT_TYPES(cls):
@@ -338,6 +342,7 @@ class SliceList:
return (sliced,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class SliceSublist:
@classmethod
def INPUT_TYPES(cls):
@@ -381,148 +386,306 @@ class SliceSublist:
return (sliced,)
#---------------------------------------------------------------------------------------------------------------------#
class ImageResizeTo:
upscale_methods = ["lanczos", "nearest-exact", "bilinear", "area", "bicubic"]
@comfy_node
class SaveImageToPath:
def __init__(self):
pass
self.compress_level = 4
@classmethod
def INPUT_TYPES(cls):
"""
Return a dictionary which contains config for all input fields.
Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT".
Input types "INT", "STRING" or "FLOAT" are special values for fields on the node.
The type can be a list for selection.
Returns: `dict`:
- Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required`
- Value input_fields (`dict`): Contains input fields config:
* Key field_name (`string`): Name of a entry-point method's argument
* Value field_config (`tuple`):
+ First value is a string indicate the type of field or a list for selection.
+ Second value is a config for type "INT", "STRING" or "FLOAT".
"""
return {
"required": {
"image": ("IMAGE",{
"lazy": False # Will only be evaluated if check_lazy_status requires it
"images": ("IMAGE", {"tooltip": "The images to save."}),
"target_folder": ("STRING", {
"default": "%USERPROFILE%",
"tooltip": "Target size for resizing."
}),
"edge_size": ("INT", {
"default": 1024,
"min": 16, #Minimum value
"max": 8192, #Maximum value
"step": 2, #Slider's step
"display": "number", # Cosmetic only: display as "number" or "slider"
"filename": ("STRING", {
"default": "output",
"tooltip": "Filename without extension (extension .png will be added automatically).",
}),
"edge_type": ("BOOLEAN", {
"default": True, # Default value
"label_on": "By Short Edge", # Label for the first option
"label_off": "By Long Edge", # Label for the second option
}),
# "is_by_short": ("BOOL", {
# }),
"upscale_method": (cls.upscale_methods,),
},
}
RETURN_TYPES = ("IMAGE", "INT", "INT", "STRING")
RETURN_NAMES = ("IMAGE", "prev_width", "prev_height", "log")
FUNCTION = "resize"
NAME = "Image Resize To"
OUTPUT_NODE = True
RETURN_TYPES = ()
FUNCTION = "execute"
NAME = "Save Image To Path"
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "Resizes an image by specify one edge size. Proportion is kept by default."
DESCRIPTION = "TODO."
def resize(self, image, edge_size, edge_type, upscale_method):
def execute(self, images, target_folder, filename):
# get original image size
B, H, W, C = image.shape
prev_width = W
prev_height = H
target_folder = resolve_path(target_folder)
image = image.movedim(-1, 1)
if not os.path.exists(target_folder):
try:
os.makedirs(target_folder, exist_ok=True)
except Exception as e:
print(f"AG {self.NAME}: Failed to create target folder: {target_folder}")
return ()
file_path = os.path.join(target_folder, f"{filename}.png")
if edge_type:
if H < W:
new_height = edge_size
new_width = int(W * edge_size / H)
else:
new_width = edge_size
new_height = int(H * edge_size / W)
else:
if H > W:
new_height = edge_size
new_width = int(W * edge_size / H)
else:
new_width = edge_size
new_height = int(H * edge_size / W)
image = common_upscale(image, new_width, new_height, upscale_method, "disabled")
image = image.movedim(1, -1)
log = f"Resized from W{prev_width}xH{prev_height} to W{new_width}xH{new_height}."
return (image, prev_width, prev_height, log,)
i = 255. * images[0].cpu().numpy()
image = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
image.save(file_path, compress_level=4)
print(f"AG {self.NAME}: {file_path} created.")
return () # need to return something to avoid error in comfyui
#---------------------------------------------------------------------------------------------------------------------#
class Image2BW:
@comfy_node
class FilenameList:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"num_files": ("INT", {
"default": 10,
"min": 1,
"tooltip": "Number of filenames to generate."
}),
"filename_prefix": ("STRING", {
"default": "AG",
"tooltip": "Prefix for the filenames."
}),
"num_digit": ("INT", {
"default": 4,
"tooltip": "Number of digits in the filename."
}),
"num_start": ("INT", {
"default": 0,
"min": 0,
"tooltip": "Starting number for the filename."
}),
},
"optional": {
"optional_idx": ("INT", {
"default": -1,
"min": -1,
"tooltip": "If specified (not -1), generate only the filename for this index."
}),
},
}
RETURN_TYPES = (ANY,)
RETURN_NAMES = ("filenames",)
FUNCTION = "execute"
NAME = "Filename List"
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "Generates a list of filenames based on the given prefix and number format."
def execute(self, num_files: int, filename_prefix: str, num_digit: int, num_start: int, optional_idx: int = -1):
if optional_idx == -1:
filenames = [f"{filename_prefix}_{i:0{num_digit}}.png" for i in range(num_start, num_start + num_files)]
else:
filenames = f"{filename_prefix}_{optional_idx:0{num_digit}}.png"
return (filenames,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class FilepathGeneratorMaker:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"target_folder": ("STRING", {
"default": "%USERPROFILE%",
"tooltip": "Target folder to enumerate files from. Subfolders are not included."
}),
"file_extension": ("STRING", {
"default": "",
"tooltip": "File extension to filter by. If empty, all 'png/jpg/jpeg' are included."
}),
},
}
RETURN_TYPES = (ANY,)
RETURN_NAMES = ("enumerator",)
FUNCTION = "execute"
NAME = "Filepath Generator Maker"
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "Returns a generator for filepaths from the specified target folder with the given file extension."
def execute(self, target_folder, file_extension):
target_folder = resolve_path(target_folder)
if not os.path.exists(target_folder):
print(f"AG {self.NAME}: Target folder does not exist: {target_folder}")
return (None,)
from pathlib import Path
def maker():
nonlocal target_folder
nonlocal file_extension
def file_generator(folder, extension):
path = Path(folder)
if extension != "":
yield from path.rglob(f"*.{extension.lstrip('.')}") # ensure extension starts with '.'
else:
for ext in ['*.png', '*.jpg', '*.jpeg']:
yield from path.rglob(ext)
return file_generator(target_folder, file_extension)
return (maker,)
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class IterateItems:
def __init__(self):
pass
self.index = 0
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",{
"lazy": False
"items": (ANY, {
"tooltip": "Input a list to iterate over its items."
}),
"iteration_on": ("BOOLEAN", {
"default": True,
"tooltip": "Always re-run this node, even if the input list is unchanged."
}),
"reds":("INT", {"default": 40, "min": -200, "max": 300, "display": "slider"}),
"yellows":("INT", {"default": 60, "min": -200, "max": 300, "display": "slider"}),
"greens":("INT", {"default": 40, "min": -200, "max": 300, "display": "slider"}),
"cyans":("INT", {"default": 60, "min": -200, "max": 300, "display": "slider"}),
"blues":("INT", {"default": 20, "min": -200, "max": 300, "display": "slider"}),
"magentas":("INT", {"default": 80, "min": -200, "max": 300, "display": "slider"}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("IMAGE", )
FUNCTION = "bw"
NAME = "Image to BW"
RETURN_TYPES = (ANY, "INT", )
RETURN_NAMES = ("item", "index", )
FUNCTION = "execute"
NAME = "Iterate Items"
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "Converts an image to black & white like the adjustments in Photoshop."
DESCRIPTION = "Iterate over each item in the input list. Outputs one item at a time."
def bw(self, image, reds, yellows, greens, cyans, blues, magentas):
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False, False, )
datatype = torch.float32
# get original image size
B, H, W, C = image.shape
color_ratios = torch.tensor([reds, greens, blues, cyans, magentas, yellows], dtype=datatype)
color_ratios = (color_ratios + 200) / 500
sorted_vals, sorted_idx = torch.sort(image, dim=3, descending=True) # BxHxWxC
# Get the color ratio value of the maximum channel
max_idx = sorted_idx[:, :, :, 0].unsqueeze(-1) # BxHxWx1
ratios_reshaped = color_ratios.unsqueeze(0).unsqueeze(0).unsqueeze(0).expand(B,H,W,-1) # BxHxWx6
ratio_max = torch.gather(ratios_reshaped, 3, max_idx) # BxHxWx1
combo_idx = (sorted_idx[:, :, :, -1] + 3).unsqueeze(-1) # BxHxWx1
ratio_combo = torch.gather(ratios_reshaped, 3, combo_idx) # BxHxWx1
# duplicate sorted_vals to a new tensor
sorted_vals_2 = sorted_vals.clone()
sorted_vals_2[:, :, :, 0] = 0
# move the first value to the last in sorted_vals_2
sorted_vals_2 = torch.cat((sorted_vals_2[:, :, :, 1:], sorted_vals_2[:, :, :, 0].unsqueeze(-1)), dim=-1) # BxHxWxC
def execute(self, items, iteration_on, unique_id=None):
# sanity check
if not (isinstance(items, list) or isinstance(items, tuple)):
print("Input is not a list or tuple, return as is.")
return (items, 0, )
diff = sorted_vals - sorted_vals_2 # BxHxWxC
ratio = torch.cat((ratio_max, ratio_combo, torch.ones_like(ratio_max, dtype=datatype)), dim=-1) # BxHxWx3
# get list item
item = items[self.index]
print(f"AG {self.NAME}: iterated {self.index + 1} times.")
# calculate the sum product of diff and ratio
final = torch.sum(diff * ratio,dim=-1) # BxHxWx1
# import json
# print(json.dumps(current_item)) # for debugging
return (final, )
# -----------------------------------------------------
# hack the prompt queue to add a new item to the queue
# -----------------------------------------------------
if iteration_on:
queue = PromptServer.instance.prompt_queue
current_item = queue.currently_running[queue.task_counter - 1]
# item structure: (number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive)
new_item = list(current_item) # copy current item
new_item[0] = queue.task_counter # update index
new_item[1] = str(uuid.uuid4()) # generate new prompt_id
new_item[3]["create_time"] = int(time.time() * 1000) # timestamp in milliseconds
# add new item to queue
if self.index < len(items) - 1:
queue.put(new_item)
current_idx = self.index
self.index = (self.index + 1) % len(items) # will return to 0 when finished
return (item, current_idx, )
return (item, 0, )
@classmethod
def IS_CHANGED(cls, items, iteration_on, unique_id=None):
if iteration_on:
return float("nan") # for faster update
input_str = f"{items}-{iteration_on}"
return hashlib.sha256(input_str.encode("utf-8")).hexdigest()
#---------------------------------------------------------------------------------------------------------------------#
@comfy_node
class IterateGenerator:
def __init__(self):
self.index = 0
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"generatorMaker": (ANY, {
"tooltip": "Input a generator to iterate"
}),
"enabled": ("BOOLEAN", {
"default": True,
"tooltip": "Only run when enabled."
}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = (ANY, "INT")
RETURN_NAMES = ("item", "index")
FUNCTION = "execute"
NAME = "Iterate Generator"
CATEGORY = CURRENT_CATEGORY
DESCRIPTION = "Iterate over each item from the generator. Outputs one item at a time."
INPUT_IS_LIST = False
OUTPUT_IS_LIST = (False,False)
def execute(self, generatorMaker, enabled, unique_id=None):
# when not enabled, exit
if not enabled:
print(f"AG {self.NAME}: Iterator not enabled.")
return (None, 0, )
# store generator at start
if self.index == 0:
self.generator = generatorMaker()
# iterate
try:
item = next(self.generator)
except StopIteration:
self.index = 0
print(f"AG {self.NAME}: StopIteration encountered.")
return (None, 0, )
# get list item
print(f"AG {self.NAME}: iterated {self.index + 1} times.")
# -----------------------------------------------------
# hack the prompt queue to add a new item to the queue
# -----------------------------------------------------
queue = PromptServer.instance.prompt_queue
current_item = queue.currently_running[queue.task_counter - 1]
# item structure: (number, prompt_id, prompt, extra_data, outputs_to_execute, sensitive)
new_item = list(current_item) # copy current item
new_item[0] = queue.task_counter # update index
new_item[1] = str(uuid.uuid4()) # generate new prompt_id
new_item[3]["create_time"] = int(time.time() * 1000) # timestamp in milliseconds
# add new item to queue
queue.put(new_item)
# return index
current_idx = self.index
self.index += 1
return (item, current_idx, )
@classmethod
def IS_CHANGED(cls, generatorMaker, enabled, unique_id=None):
if enabled:
return float("nan") # enforce update
return "unchanged" # don't update
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "archigraph"
description = "Implementation of architectural related graph algorithm in ComfyUI."
version = "0.0.6"
version = "1.0.6"
license = {file = "LICENSE"}
[project.urls]