Files
WainWong-ComfyUI-Loop-image/flow_control.py
T
2025-01-24 22:48:31 +08:00

561 lines
21 KiB
Python

from comfy_execution.graph_utils import GraphBuilder, is_link
from .tools import VariantSupport
import torch
from nodes import NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS
@VariantSupport()
class BatchImageLoopOpen:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
inputs = {
"required": {
"segmented_images": ("IMAGE", {"forceInput": True}),
"segmented_masks": ("MASK", {"forceInput": True}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"iteration_count": ("INT", {"default": 0}),
}
}
return inputs
RETURN_TYPES = tuple(["FLOW_CONTROL", "IMAGE", "MASK", "INT", "INT"])
RETURN_NAMES = tuple(["FLOW_CONTROL", "current_image", "current_mask", "max_iterations", "iteration_count"])
FUNCTION = "while_loop_open"
CATEGORY = "CyberEveLoop🐰"
def standardize_input(self, images, masks):
"""
标准化输入格式
images: 确保是4D tensor [B,H,W,C]
masks: 确保是3D tensor [B,H,W]
"""
# 处理images
if isinstance(images, list):
images = torch.cat(images, dim=0)
if len(images.shape) == 3: # [H,W,C] -> [1,H,W,C]
images = images.unsqueeze(0)
assert len(images.shape) == 4, f"Images must be 4D [B,H,W,C], got shape {images.shape}"
# 处理masks
if isinstance(masks, list):
masks = torch.cat(masks, dim=0)
if len(masks.shape) == 2: # [H,W] -> [1,H,W]
masks = masks.unsqueeze(0)
assert len(masks.shape) == 3, f"Masks must be 3D [B,H,W], got shape {masks.shape}"
# 确保batch维度相同
assert images.shape[0] == masks.shape[0], \
f"Batch size mismatch: images {images.shape[0]} vs masks {masks.shape[0]}"
return images, masks
def while_loop_open(self, segmented_images, segmented_masks, unique_id=None, iteration_count=0):
print(f"while_loop_open Processing iteration {iteration_count}")
# 标准化输入
segmented_images, segmented_masks = self.standardize_input(segmented_images, segmented_masks)
# 获取最大迭代次数
max_iterations = segmented_images.shape[0]
if max_iterations == 0:
raise ValueError("No images provided in segmented_images")
# 验证迭代计数(修改这里)
if iteration_count >= max_iterations:
raise ValueError(f"Iteration count {iteration_count} exceeds max iterations {max_iterations}")
# 获取当前迭代的图片和蒙版
current_image = segmented_images[iteration_count:iteration_count+1]
current_mask = segmented_masks[iteration_count:iteration_count+1]
return tuple(["stub", current_image, current_mask, max_iterations, iteration_count])
@VariantSupport()
class BatchImageLoopClose:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
inputs = {
"required": {
"flow_control": ("FLOW_CONTROL", {"rawLink": True}),
"current_image": ("IMAGE",),
"current_mask": ("MASK",),
"max_iterations": ("INT", {"forceInput": True}),
},
"hidden": {
"dynprompt": "DYNPROMPT",
"unique_id": "UNIQUE_ID",
"result_images": ("IMAGE",),
"result_masks": ("MASK",),
"iteration_count": ("INT", {"default": 0}),
}
}
return inputs
RETURN_TYPES = tuple(["IMAGE", "MASK"])
RETURN_NAMES = tuple(["result_images", "result_masks"])
FUNCTION = "while_loop_close"
CATEGORY = "CyberEveLoop🐰"
def explore_dependencies(self, node_id, dynprompt, upstream, parent_ids):
node_info = dynprompt.get_node(node_id)
if "inputs" not in node_info:
return
for k, v in node_info["inputs"].items():
if is_link(v):
parent_id = v[0]
display_id = dynprompt.get_display_node_id(parent_id)
display_node = dynprompt.get_node(display_id)
class_type = display_node["class_type"]
# 排除循环结束节点
if class_type not in ['BatchImageLoopClose']:
parent_ids.append(display_id)
if parent_id not in upstream:
upstream[parent_id] = []
self.explore_dependencies(parent_id, dynprompt, upstream, parent_ids)
upstream[parent_id].append(node_id)
def explore_output_nodes(self, dynprompt, upstream, output_nodes, parent_ids):
"""探索并添加输出节点的连接"""
for parent_id in upstream:
display_id = dynprompt.get_display_node_id(parent_id)
for output_id in output_nodes:
id = output_nodes[output_id][0]
if id in parent_ids and display_id == id and output_id not in upstream[parent_id]:
if '.' in parent_id:
arr = parent_id.split('.')
arr[len(arr)-1] = output_id
upstream[parent_id].append('.'.join(arr))
else:
upstream[parent_id].append(output_id)
def collect_contained(self, node_id, upstream, contained):
if node_id not in upstream:
return
for child_id in upstream[node_id]:
if child_id not in contained:
contained[child_id] = True
self.collect_contained(child_id, upstream, contained)
def standardize_input(self, image, mask):
"""
标准化输入格式
image: 确保是4D tensor [B,H,W,C]
mask: 确保是3D tensor [B,H,W]
"""
# 处理image
if len(image.shape) == 3: # [H,W,C] -> [1,H,W,C]
image = image.unsqueeze(0)
assert len(image.shape) == 4, f"Image must be 4D [B,H,W,C], got shape {image.shape}"
# 处理mask
if len(mask.shape) == 2: # [H,W] -> [1,H,W]
mask = mask.unsqueeze(0)
assert len(mask.shape) == 3, f"Mask must be 3D [B,H,W], got shape {mask.shape}"
return image, mask
def initialize_results(self, max_iterations, current_image, current_mask):
"""
初始化结果张量,确保与MaskSplit输出格式一致
"""
# 确保维度正确
assert len(current_image.shape) == 4, "Current image must be 4D [B,H,W,C]"
assert len(current_mask.shape) == 3, "Current mask must be 3D [B,H,W]"
# 创建结果张量,确保格式一致
result_images = torch.zeros(
(max_iterations, current_image.shape[1], current_image.shape[2], current_image.shape[3]),
dtype=current_image.dtype,
device=current_image.device
) # 明确指定 [B,H,W,C]
result_masks = torch.zeros(
(max_iterations, current_mask.shape[1], current_mask.shape[2]),
dtype=current_mask.dtype,
device=current_mask.device
) # 明确指定 [B,H,W]
return result_images, result_masks
def while_loop_close(self, flow_control, current_image, current_mask, max_iterations,
iteration_count=0, result_images=None, result_masks=None,
dynprompt=None, unique_id=None,):
print(f"Iteration {iteration_count} of {max_iterations}")
# 标准化输入,确保格式一致
current_image, current_mask = self.standardize_input(current_image, current_mask)
# 验证迭代计数(修改这里)
if iteration_count >= max_iterations:
raise ValueError(f"Iteration count {iteration_count} exceeds max iterations {max_iterations}")
# 结果初始化或验证
if result_images is None or result_masks is None:
result_images, result_masks = self.initialize_results(max_iterations, current_image, current_mask)
else:
# 验证现有结果的维度和格式
assert result_images.shape[0] == max_iterations and len(result_images.shape) == 4, \
f"Result images must be 4D [B,H,W,C] with batch size {max_iterations}"
assert result_masks.shape[0] == max_iterations and len(result_masks.shape) == 3, \
f"Result masks must be 3D [B,H,W] with batch size {max_iterations}"
# 存储当前结果
result_images[iteration_count:iteration_count+1] = current_image
result_masks[iteration_count:iteration_count+1] = current_mask
# 检查是否继续循环(修改这里)
if iteration_count == max_iterations - 1:
print(f"Loop finished with {iteration_count + 1} iterations")
return (result_images, result_masks)
# 准备下一次循环
this_node = dynprompt.get_node(unique_id)
upstream = {}
parent_ids = []
self.explore_dependencies(unique_id, dynprompt, upstream, parent_ids)
parent_ids = list(set(parent_ids)) # 去重
# 获取并处理输出节点
prompts = dynprompt.get_original_prompt()
output_nodes = {}
for id in prompts:
node = prompts[id]
if "inputs" not in node:
continue
class_type = node["class_type"]
if class_type in ALL_NODE_CLASS_MAPPINGS:
class_def = ALL_NODE_CLASS_MAPPINGS[class_type]
if hasattr(class_def, 'OUTPUT_NODE') and class_def.OUTPUT_NODE == True:
for k, v in node['inputs'].items():
if is_link(v):
output_nodes[id] = v
# 创建新图
graph = GraphBuilder()
self.explore_output_nodes(dynprompt, upstream, output_nodes, parent_ids)
contained = {}
open_node = flow_control[0]
self.collect_contained(open_node, upstream, contained)
contained[unique_id] = True
contained[open_node] = True
# 创建节点
for node_id in contained:
original_node = dynprompt.get_node(node_id)
node = graph.node(original_node["class_type"],
"Recurse" if node_id == unique_id else node_id)
node.set_override_display_id(node_id)
# 设置连接
for node_id in contained:
original_node = dynprompt.get_node(node_id)
node = graph.lookup_node("Recurse" if node_id == unique_id else node_id)
for k, v in original_node["inputs"].items():
if is_link(v) and v[0] in contained:
parent = graph.lookup_node(v[0])
node.set_input(k, parent.out(v[1]))
else:
node.set_input(k, v)
# 设置节点参数
my_clone = graph.lookup_node("Recurse")
my_clone.set_input("iteration_count", iteration_count + 1)
my_clone.set_input("result_images", result_images)
my_clone.set_input("result_masks", result_masks)
new_open = graph.lookup_node(open_node)
new_open.set_input("iteration_count", iteration_count + 1)
print(f"Continuing to iteration {iteration_count + 1}")
return {
"result": tuple([my_clone.out(0), my_clone.out(1)]),
"expand": graph.finalize(),
}
@VariantSupport()
class SingleImageLoopOpen:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
inputs = {
"required": {
"image": ("IMAGE",),
"max_iterations": ("INT", {"default": 5, "min": 1, "max": 100}),
},
"optional": {
"mask": ("MASK",),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"iteration_count": ("INT", {"default": 0}),
"previous_image": ("IMAGE",),
"previous_mask": ("MASK",),
}
}
return inputs
RETURN_TYPES = tuple(["FLOW_CONTROL", "IMAGE", "MASK", "INT", "INT"])
RETURN_NAMES = tuple(["FLOW_CONTROL", "current_image", "current_mask", "max_iterations", "iteration_count"])
FUNCTION = "loop_open"
CATEGORY = "CyberEveLoop🐰"
def loop_open(self, image, max_iterations, mask=None, unique_id=None,
iteration_count=0, previous_image=None, previous_mask=None):
print(f"SingleImageLoopOpen Processing iteration {iteration_count}")
# 确保维度正确
if len(image.shape) == 3:
image = image.unsqueeze(0)
if mask is not None and len(mask.shape) == 2:
mask = mask.unsqueeze(0)
# 使用上一次循环的结果(如果有)
current_image = previous_image if previous_image is not None and iteration_count > 0 else image
current_mask = previous_mask if previous_mask is not None and iteration_count > 0 else mask
return tuple(["stub", current_image, current_mask, max_iterations, iteration_count])
@VariantSupport()
class SingleImageLoopClose:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
inputs = {
"required": {
"flow_control": ("FLOW_CONTROL", {"rawLink": True}),
"current_image": ("IMAGE",),
"max_iterations": ("INT", {"forceInput": True}),
},
"optional": {
"current_mask": ("MASK",),
},
"hidden": {
"dynprompt": "DYNPROMPT",
"unique_id": "UNIQUE_ID",
"iteration_count": ("INT", {"default": 0}),
}
}
return inputs
RETURN_TYPES = tuple(["IMAGE", "MASK"])
RETURN_NAMES = tuple(["final_image", "final_mask"])
FUNCTION = "loop_close"
CATEGORY = "CyberEveLoop🐰"
def explore_dependencies(self, node_id, dynprompt, upstream, parent_ids):
node_info = dynprompt.get_node(node_id)
if "inputs" not in node_info:
return
for k, v in node_info["inputs"].items():
if is_link(v):
parent_id = v[0]
display_id = dynprompt.get_display_node_id(parent_id)
display_node = dynprompt.get_node(display_id)
class_type = display_node["class_type"]
if class_type not in ['SingleImageLoopClose']:
parent_ids.append(display_id)
if parent_id not in upstream:
upstream[parent_id] = []
self.explore_dependencies(parent_id, dynprompt, upstream, parent_ids)
upstream[parent_id].append(node_id)
def explore_output_nodes(self, dynprompt, upstream, output_nodes, parent_ids):
for parent_id in upstream:
display_id = dynprompt.get_display_node_id(parent_id)
for output_id in output_nodes:
id = output_nodes[output_id][0]
if id in parent_ids and display_id == id and output_id not in upstream[parent_id]:
if '.' in parent_id:
arr = parent_id.split('.')
arr[len(arr)-1] = output_id
upstream[parent_id].append('.'.join(arr))
else:
upstream[parent_id].append(output_id)
def collect_contained(self, node_id, upstream, contained):
if node_id not in upstream:
return
for child_id in upstream[node_id]:
if child_id not in contained:
contained[child_id] = True
self.collect_contained(child_id, upstream, contained)
def loop_close(self, flow_control, current_image, max_iterations, current_mask=None,
iteration_count=0, dynprompt=None, unique_id=None):
print(f"Iteration {iteration_count} of {max_iterations}")
# 维度处理
if len(current_image.shape) == 3:
current_image = current_image.unsqueeze(0)
if current_mask is not None and len(current_mask.shape) == 2:
current_mask = current_mask.unsqueeze(0)
# 检查是否继续循环
if iteration_count >= max_iterations - 1:
print(f"Loop finished with {iteration_count + 1} iterations")
return (current_image, current_mask if current_mask is not None else torch.zeros_like(current_image[:,:,:,0]))
# 准备下一次循环
this_node = dynprompt.get_node(unique_id)
upstream = {}
parent_ids = []
self.explore_dependencies(unique_id, dynprompt, upstream, parent_ids)
parent_ids = list(set(parent_ids))
# 获取并处理输出节点
prompts = dynprompt.get_original_prompt()
output_nodes = {}
for id in prompts:
node = prompts[id]
if "inputs" not in node:
continue
class_type = node["class_type"]
if class_type in ALL_NODE_CLASS_MAPPINGS:
class_def = ALL_NODE_CLASS_MAPPINGS[class_type]
if hasattr(class_def, 'OUTPUT_NODE') and class_def.OUTPUT_NODE == True:
for k, v in node['inputs'].items():
if is_link(v):
output_nodes[id] = v
# 创建新图
graph = GraphBuilder()
self.explore_output_nodes(dynprompt, upstream, output_nodes, parent_ids)
contained = {}
open_node = flow_control[0]
self.collect_contained(open_node, upstream, contained)
contained[unique_id] = True
contained[open_node] = True
# 创建节点
for node_id in contained:
original_node = dynprompt.get_node(node_id)
node = graph.node(original_node["class_type"],
"Recurse" if node_id == unique_id else node_id)
node.set_override_display_id(node_id)
# 设置连接
for node_id in contained:
original_node = dynprompt.get_node(node_id)
node = graph.lookup_node("Recurse" if node_id == unique_id else node_id)
for k, v in original_node["inputs"].items():
if is_link(v) and v[0] in contained:
parent = graph.lookup_node(v[0])
node.set_input(k, parent.out(v[1]))
else:
node.set_input(k, v)
# 设置节点参数
my_clone = graph.lookup_node("Recurse")
my_clone.set_input("iteration_count", iteration_count + 1)
new_open = graph.lookup_node(open_node)
new_open.set_input("iteration_count", iteration_count + 1)
new_open.set_input("previous_image", current_image)
if current_mask is not None:
new_open.set_input("previous_mask", current_mask)
print(f"Continuing to iteration {iteration_count + 1}")
return {
"result": tuple([my_clone.out(0), my_clone.out(1)]),
"expand": graph.finalize(),
}
@VariantSupport()
class LoopIndexSwitch:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
"""
预定义100个隐藏的lazy输入
"""
optional_inputs = {
"default_value": ("*", {"lazy": True}), # 默认值也设为lazy
}
# 添加100个隐藏的lazy输入
hidden_inputs = {}
for i in range(100):
hidden_inputs[f"while_{i}"] = ("*", {"lazy": True})
return {
"required": {
"iteration_count": ("INT", {"forceInput": True}), # 当前迭代次数
},
"optional": optional_inputs,
"hidden": hidden_inputs,
}
RETURN_TYPES = ("*",)
FUNCTION = "index_switch"
CATEGORY = "CyberEveLoop🐰"
def check_lazy_status(self, iteration_count, **kwargs):
"""
检查当前迭代需要的输入和默认值
"""
needed = []
current_key = f"while_{iteration_count}"
# 检查当前迭代的输入
if current_key in kwargs :
needed.append(current_key)
else:
needed.append("default_value")
print(f"Index switch needed: {needed}")
return needed
def index_switch(self, iteration_count, **kwargs):
"""
根据当前迭代次数选择对应的输入值
"""
current_key = f"while_{iteration_count}"
if current_key in kwargs and kwargs[current_key] is not None:
return (kwargs[current_key],)
return (kwargs.get("default_value"),)
CyberEve_Loop_CLASS_MAPPINGS = {
"CyberEve_BatchImageLoopOpen": BatchImageLoopOpen,
"CyberEve_BatchImageLoopClose": BatchImageLoopClose,
"CyberEve_LoopIndexSwitch": LoopIndexSwitch,
"CyberEve_SingleImageLoopOpen": SingleImageLoopOpen,
"CyberEve_SingleImageLoopClose": SingleImageLoopClose,
}
CyberEve_Loop_DISPLAY_NAME_MAPPINGS = {
"CyberEve_BatchImageLoopOpen": "Batch Image Loop Open🐰",
"CyberEve_BatchImageLoopClose": "Batch Image Loop Close🐰",
"CyberEve_LoopIndexSwitch": "Loop Index Switch🐰",
"CyberEve_SingleImageLoopOpen": "Single Image Loop Open🐰",
"CyberEve_SingleImageLoopClose": "Single Image Loop Close🐰",
}