Initial commit

Features:
- Implemented batch image processing nodes (Loop Open/Close)
- Implemented single image processing nodes
- Added Loop Index Switch for dynamic input control
- Added mask segmentation and merging functionality

Core components:
- flow_control.py: Main loop control implementation
- mask_split.py: Mask processing utilities
- web/node/dynamicnode.js: Dynamic input UI implementation

Documentation:
- Added detailed README in both English and Chinese
- Included usage instructions and node documentation
- Added acknowledgments and references"
This commit is contained in:
WainWong
2025-01-01 00:11:34 +08:00
parent 399665f2be
commit abe0004331
7 changed files with 1267 additions and 2 deletions
+361 -2
View File
@@ -1,2 +1,361 @@
# ComfyUI-Loop-image
A comfyui node that uses the loop function to process images and masks
# ComfyUI Loop Image
[English](#english) | [简体中文](#简体中文)
<a name="english"></a>
# ComfyUI Loop Image
## Introduction
ComfyUI Loop Image is a node package specifically designed for image loop processing. It provides two main processing modes: Batch Image Processing and Single Image Processing, along with supporting image segmentation and merging functions.
## Differences between Batch and Single Processing
### Batch Image Processing
- Suitable for scenarios requiring simultaneous processing of multiple different regions
- Uses Mask Segmentation node to divide images into multiple parts
- Processes one segmented region per iteration
- Merges results through Mask Merge after all regions are processed
### Single Image Processing
- Suitable for scenarios requiring multiple processing passes on the same image
- Uses the result of the previous iteration as input for the next
- Enables progressive image modification
- Ideal for iterative optimization tasks
## Node Documentation
### 1. Batch Processing Nodes
#### Mask Segmentation🐰
- **Functionality**
- Automatically segments a mask containing multiple independent regions into separate mask sequences
- Each segmented mask corresponds to an independent region in the original image
- Segmentation based on connected component analysis
- **Segmentation Rules**
- Independent regions are identified as separate parts
- Regions with holes are properly processed, maintaining hole structure
- **Sequence Rules**
- Masks are arranged from left to right, then top to bottom
- Sorting based on leftmost pixel position, then topmost pixel position
- This order determines subsequent processing sequence
- Example: In a mask with three regions, leftmost region is iteration 0, middle is 1, rightmost is 2
#### Batch Image Loop Open🐰
- **Input/Output Details**
- Inputs:
- segmented_images: Image sequence from Mask Segmentation
- segmented_masks: Mask sequence from Mask Segmentation
- Outputs:
- current_image: Currently processed image portion
- current_mask: Current iteration mask
- max_iterations: Total iteration count (equals number of segmented regions)
- iteration_count: Current iteration number (starts from 0)
- **Usage Notes**
- current_image and current_mask can be used directly for subsequent processing
- iteration_count can connect to Loop Index Switch for different processing parameters
- max_iterations used for loop control, usually doesn't need manual handling
#### Batch Image Loop Close🐰
- **Input/Output Details**
- Inputs:
- flow_control: Control signal from Loop Open
- current_image: Currently processed image
- current_mask: Current processed mask
- max_iterations: Total iteration count from Loop Open
- Outputs:
- result_images: All processed image sequences
- result_masks: All processed mask sequences
#### Mask Merge🐰
- **Functionality**
- Merges multiple processed image regions back into the original image
- Uses masks to ensure each processed region is correctly placed
- Maintains original content in unprocessed areas
- **Usage Tips**
- original_image: Use original input image
- processed_images: Connect to result_images output from Loop Close
- masks: Connect to result_masks output from Loop Close
This batch processing system allows you to apply different processing methods to different regions of an image, particularly suitable for scenarios requiring differentiated processing of various image parts.
### 2. Single Image Processing Nodes
#### Single Image Loop Open🐰
- **Functionality**
- Performs multiple iterations of processing on a single image
- Uses the result of each iteration as input for the next
- Suitable for progressive enhancement or multiple optimization scenarios
- **Input Parameters**
- **Required Inputs**:
- image: Original image to process
- max_iterations: Maximum iteration count (1-100)
- **Optional Inputs**:
- mask: Optional processing area mask
- **Output Parameters**
- current_image: Current iteration image (original image for first iteration, previous result for subsequent iterations)
- current_mask: Current mask (if provided)
- max_iterations: Set maximum iterations
- iteration_count: Current iteration number (starts from 0)
#### Single Image Loop Close🐰
- **Input Parameters**
- **Required Inputs**:
- flow_control: Control signal from Loop Open
- current_image: Currently processed image
- max_iterations: Maximum iterations from Loop Open
- **Optional Inputs**:
- current_mask: Processed mask (if using mask)
- **Output Parameters**
- final_image: Final image after all iterations
- final_mask: Final mask (if using mask)
#### Single Image Processing Features and Applications
1. **Progressive Processing**
- Each iteration builds on previous results
- Enables cumulative effects
- Suitable for scenarios requiring fine-tuning
2. **Use Case Examples**
- Progressive image enhancement
- Iterative style transfer
- Multiple denoising passes
- Gradual detail optimization
### 3. Special Function Node
- **Loop Index Switch🐰**
- Function: Select different inputs based on current iteration count
- Usage:
1. Right-click node and select "Add Loop Input"
2. Enter desired iteration number (0-99)
3. Connect corresponding inputs
4. Use "Remove Loop Input" to delete unwanted inputs
- Note: Only inputs corresponding to current iteration are computed, others are skipped for efficiency
## Usage Recommendations
1. Use batch processing for scenarios requiring different processing in different image regions
2. Use single image processing for scenarios requiring multiple optimization iterations
3. Utilize Loop Index Switch to implement different parameters for different iterations
4. Control iteration count to avoid over-processing
## Example Workflows
TODO
## Acknowledgments
This project references the following excellent open source projects:
- [ComfyUI-Easy-Use](https://github.com/yolain/ComfyUI-Easy-Use/) - Provided excellent node design ideas and implementation references
- [execution-inversion-demo-comfyui](https://github.com/BadCafeCode/execution-inversion-demo-comfyui) - Provided core implementation ideas for loop control
- [cozy_ex_dynamic](https://github.com/cozy-comfyui/cozy_ex_dynamic) - Provided implementation reference for dynamic input nodes
Special thanks to the authors of these projects for their contributions to the ComfyUI community!
## About
For more ComfyUI tutorials and updates, visit:
- Bilibili: [CyberEve](https://space.bilibili.com/16993154)
- Content includes:
- ComfyUI node development tutorials
- Workflow usage tutorials
- Latest feature updates
- AI drawing tips
If you find this project helpful, please follow the author's Bilibili account for more resources!
---
<a name="简体中文"></a>
## 简介
ComfyUI Loop Image是一个专门用于处理图像循环操作的节点包。它提供了两种主要的循环处理模式:批量图像处理(Batch)和单图像重复处理(Single),以及配套的图像分割与合并功能。
## 批量处理与单图处理的区别
### 批量图像处理
- 适用于需要同时处理多个不同区域的场景
- 通过Mask Segmentation节点将图像分割成多个部分
- 每次循环处理一个分割区域
- 所有区域处理完成后通过Mask Merge合并结果
### 单图像处理
- 适用于需要对同一图像进行多次处理的场景
- 每次循环使用上一次的处理结果作为输入
- 可以实现渐进式的图像修改
- 适合迭代优化类的任务
## 节点说明
### 1. 批量处理节点详解
#### Mask Segmentation🐰 (遮罩分割)
- **功能说明**
- 将一个包含多个独立区域的遮罩图自动分割成独立的遮罩序列
- 每个分割后的遮罩对应原图中的一个独立区域
- 分割基于连通区域分析,即相互不连接的区域会被分为不同部分
- **分割规则**
- 相互独立的区域会被识别为不同的部分
- 包含孔洞的区域会被正确处理,保持孔洞结构
- **顺序规则**
- 分割后的遮罩按照从左到右排列,若左右位置相等,再按照从上到下的顺序
- 排序依据是每个区域最左边的像素点的位置,再按照最上边的像素点的位置
- 这个顺序决定了后续循环处理的顺序
- 例如:如果遮罩中有三个区域,最左边的区域将是第0次迭代,中间的是第1次,最右边的是第2次
#### Batch Image Loop Open🐰 (批量循环开始)
- **输入输出详解**
- 输入:
- segmented_images: 来自Mask Segmentation的图像序列
- segmented_masks: 来自Mask Segmentation的遮罩序列
- 输出:
- current_image: 当前迭代处理的图像部分
- current_mask: 当前迭代的遮罩
- max_iterations: 总迭代次数(等于分割区域的数量)
- iteration_count: 当前迭代次数(从0开始)
- **使用说明**
- current_image和current_mask可以直接用于后续处理
- iteration_count可以连接到Loop Index Switch来选择不同的处理参数
- max_iterations用于循环控制,一般不需要手动使用
#### Batch Image Loop Close🐰 (批量循环结束)
- **输入输出详解**
- 输入:
- flow_control: 来自Loop Open的控制信号
- current_image: 处理后的当前图像
- current_mask: 处理后的当前遮罩
- max_iterations: 来自Loop Open的总迭代次数
- 输出:
- result_images: 所有处理完成的图像序列
- result_masks: 所有处理完成的遮罩序列
#### Mask Merge🐰 (遮罩合并)
- **功能说明**
- 将循环处理后的多个图像区域合并回原始图像
- 使用遮罩确保每个处理过的区域正确放回原位
- 保持未处理区域的原始内容不变
- **使用技巧**
- original_image: 使用原始输入图像
- processed_images: 连接Loop Close的result_images输出
- masks: 连接Loop Close的result_masks输出
这样的批量处理系统允许你对图像的不同区域应用不同的处理方法,特别适合需要对图像不同部分进行差异化处理的场景。
### 2. 单图处理节点详解
#### Single Image Loop Open🐰 (单图循环开始)
- **功能说明**
- 对同一张图像进行多次迭代处理
- 每次迭代都使用上一次的处理结果作为输入
- 适合需要渐进式改善或多次优化的场景
- **输入参数详解**
- **必需输入**:
- image: 需要处理的原始图像
- max_iterations: 最大迭代次数(1-100)
- **可选输入**:
- mask: 可选的处理区域遮罩
- **输出参数详解**
- current_image: 当前迭代的图像(第一次是原始图像,之后是上一次处理的结果)
- current_mask: 当前使用的遮罩(如果提供了遮罩)
- max_iterations: 设定的最大迭代次数
- iteration_count: 当前迭代次数(从0开始)
#### Single Image Loop Close🐰 (单图循环结束)
- **输入参数详解**
- **必需输入**:
- flow_control: 来自Loop Open的控制信号
- current_image: 当前迭代处理后的图像
- max_iterations: 来自Loop Open的最大迭代次数
- **可选输入**:
- current_mask: 处理后的遮罩(如果使用了遮罩)
- **输出参数详解**
- final_image: 所有迭代完成后的最终图像
- final_mask: 最终的遮罩(如果使用了遮罩)
#### 单图处理的特点和应用场景
1. **渐进式处理**
- 每次迭代都基于上一次的结果
- 可以实现累积效果
- 适合需要多次微调的场景
2. **使用场景示例**
- 图像渐进式增强
- 迭代式风格转换
- 多次降噪处理
- 逐步细节优化
### 与Loop Index Switch的配合使用
- 可以使用Loop Index Switch根据iteration_count选择不同的处理参数
这种单图循环处理方式特别适合需要精细调整或渐进式改善的场景,通过多次迭代可以达到更理想的处理效果。配合Loop Index Switch,还可以实现更复杂的参数控制策略。
### 3. 特殊功能节点
- **Loop Index Switch🐰**
- 功能:根据当前循环次数选择不同的输入
- 使用方法:
1. 右键点击节点选择"Add Loop Input"
2. 输入想要添加的循环序号(0-99)
3. 连接对应的输入
4. 可以通过"Remove Loop Input"删除不需要的输入
- 注意:只有当前迭代次数对应的输入会被计算,其他输入会被跳过,提高效率
## 使用建议
1. 批量处理适合需要在图像不同区域应用不同处理的场景
2. 单图处理适合需要多次迭代优化的场景
3. 合理使用Loop Index Switch节点可以实现在不同迭代次数使用不同参数
4. 注意控制循环次数,避免过度处理
## 示例工作流
TODO
## 致谢
本项目在开发过程中参考和借鉴了以下优秀的开源项目:
- [ComfyUI-Easy-Use](https://github.com/yolain/ComfyUI-Easy-Use/) - 提供了优秀的节点设计思路和实现参考
- [execution-inversion-demo-comfyui](https://github.com/BadCafeCode/execution-inversion-demo-comfyui) - 提供了循环控制的核心实现思路
- [cozy_ex_dynamic](https://github.com/cozy-comfyui/cozy_ex_dynamic) - 提供了动态输入节点的实现参考
特别感谢这些项目的作者们为ComfyUI社区做出的贡献!
## 关于作者
欢迎访问作者的B站主页,获取更多ComfyUI教程和更新:
- B站:[CyberEve](https://space.bilibili.com/16993154)
- 内容包括:
- ComfyUI节点开发教程
- 工作流使用教程
- 最新功能更新介绍
- AI绘画技巧分享
如果您觉得这个项目对您有帮助,欢迎关注作者B站账号获取更多资源!
---
*Note: 本项目遵循开源协议,欢迎提出建议和改进意见。*
+13
View File
@@ -0,0 +1,13 @@
from .flow_control import CyberEve_Loop_CLASS_MAPPINGS, CyberEve_Loop_DISPLAY_NAME_MAPPINGS
from .mask_split import Mask_CLASS_MAPPINGS, Mask_DISPLAY_NAME_MAPPINGS
WEB_DIRECTORY = "./web"
NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS.update(CyberEve_Loop_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(Mask_CLASS_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS.update(CyberEve_Loop_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(Mask_DISPLAY_NAME_MAPPINGS)
+485
View File
@@ -0,0 +1,485 @@
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 while_loop_open(self, segmented_images, segmented_masks, unique_id=None, iteration_count=0):
print(f"while_loop_open Processing iteration {iteration_count}")
# 确保输入是张量
if isinstance(segmented_images, list):
segmented_images = torch.cat(segmented_images, dim=0)
if isinstance(segmented_masks, list):
segmented_masks = torch.cat(segmented_masks, dim=0)
max_iterations = segmented_images.shape[0]
if max_iterations == 0:
raise ValueError("No images provided in segmented_images")
# 获取当前迭代的图片和蒙版
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 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}")
# 维度处理
if len(current_image.shape) == 3:
current_image = current_image.unsqueeze(0)
if len(current_mask.shape) == 2:
current_mask = current_mask.unsqueeze(0)
# 结果初始化
if result_images is None:
result_images = torch.zeros((max_iterations,) + current_image.shape[1:],
dtype=current_image.dtype,
device=current_image.device)
result_masks = torch.zeros((max_iterations,) + current_mask.shape[1:],
dtype=current_mask.dtype,
device=current_mask.device)
# 存储当前结果
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🐰",
}
+246
View File
@@ -0,0 +1,246 @@
import torch
import torch.nn.functional as F
import cv2
import numpy as np
class MaskSplit:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
},
}
RETURN_TYPES = ("IMAGE","MASK")
RETURN_NAMES = ("segmented_images","segmented_masks")
FUNCTION = "segment_mask"
CATEGORY = "CyberEveLoop🐰"
def find_top_left_point(self, mask_np):
"""找到mask中最左上角的点"""
# 找到所有非零点
y_coords, x_coords = np.nonzero(mask_np)
if len(x_coords) == 0:
return float('inf'), float('inf')
# 找到最小x值
min_x = np.min(x_coords)
# 在最小x值的点中找到最小y值
min_y = np.min(y_coords[x_coords == min_x])
return min_x, min_y
def segment_mask(self, mask, image):
"""使用OpenCV快速分割蒙版并处理图像"""
# 保存原始设备信息
device = mask.device if isinstance(mask, torch.Tensor) else torch.device('cpu')
# 确保mask是正确的形状并转换为numpy数组
if isinstance(mask, torch.Tensor):
if len(mask.shape) == 2:
mask = mask.unsqueeze(0)
mask_np = (mask[0] * 255).cpu().numpy().astype(np.uint8)
else:
mask_np = (mask * 255).astype(np.uint8)
# 使用OpenCV找到轮廓
contours, hierarchy = cv2.findContours(
mask_np,
cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE
)
mask_info = [] # 用于排序的信息列表
if hierarchy is not None and len(contours) > 0:
hierarchy = hierarchy[0]
contour_masks = {}
# 创建每个轮廓的mask
for i, contour in enumerate(contours):
mask = np.zeros_like(mask_np)
cv2.drawContours(mask, [contour], -1, 255, -1)
contour_masks[i] = mask
# 处理每个轮廓
processed_indices = set()
for i, (contour, h) in enumerate(zip(contours, hierarchy)):
if i in processed_indices:
continue
current_mask = contour_masks[i].copy()
child_idx = h[2]
if child_idx != -1:
while child_idx != -1:
current_mask = cv2.subtract(current_mask, contour_masks[child_idx])
processed_indices.add(child_idx)
child_idx = hierarchy[child_idx][0]
# 找到最左上角的点
min_x, min_y = self.find_top_left_point(current_mask)
# 转换为tensor
mask_tensor = torch.from_numpy(current_mask).float() / 255.0
mask_tensor = mask_tensor.unsqueeze(0)
mask_tensor = mask_tensor.to(device)
# 保存mask和排序信息
mask_info.append((mask_tensor, min_x, min_y))
processed_indices.add(i)
# 如果没有找到任何轮廓,使用原始mask
if not mask_info:
if isinstance(mask, torch.Tensor):
mask_info.append((mask, 0, 0))
else:
mask_tensor = torch.from_numpy(mask).float()
if len(mask_tensor.shape) == 2:
mask_tensor = mask_tensor.unsqueeze(0)
mask_tensor = mask_tensor.to(device)
mask_info.append((mask_tensor, 0, 0))
# 根据最左上角点排序
mask_info.sort(key=lambda x: (x[1], x[2]))
# 确保image是正确的形状
if len(image.shape) == 3:
image = image.unsqueeze(0)
# 处理masks和images
result_masks = None
result_images = None
for mask_tensor, _, _ in mask_info:
# 处理masks
if result_masks is None:
result_masks = mask_tensor
else:
result_masks = torch.cat([result_masks, mask_tensor], dim=0)
# 处理images
if result_images is None:
result_images = image.clone()
else:
result_images = torch.cat([result_images, image.clone()], dim=0)
return (result_images, result_masks)
class MaskMerge:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"original_image": ("IMAGE",),
},
"optional": {
"processed_images": ("IMAGE", {"forceInput": True}),
"masks": ("MASK", {"forceInput": True}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("merged_image",)
# INPUT_IS_LIST = True
FUNCTION = "merge_masked_images"
CATEGORY = "CyberEveLoop🐰"
def resize_tensor(self, x, size, mode='bilinear'):
"""调整tensor尺寸的辅助函数"""
# 确保输入是4D tensor [B,C,H,W]
orig_dim = x.dim()
if orig_dim == 3:
x = x.unsqueeze(0)
# 如果是图像 [B,H,W,C],需要转换为 [B,C,H,W]
if x.shape[-1] in [1, 3, 4]:
x = x.permute(0, 3, 1, 2)
# 执行调整
x = F.interpolate(x, size=size, mode=mode, align_corners=False if mode in ['bilinear', 'bicubic'] else None)
# 转换回原始格式
if x.shape[1] in [1, 3, 4]:
x = x.permute(0, 2, 3, 1)
# 如果原始输入是3D,去掉batch维度
if orig_dim == 3:
x = x.squeeze(0)
return x
def merge_masked_images(self, original_image, processed_images=None, masks=None):
"""合并处理后的图像"""
# 确保输入有效
if processed_images is None or masks is None:
return (original_image,)
# 确保原始图像维度正确
if len(original_image.shape) == 3:
original_image = original_image.unsqueeze(0)
# 创建结果图像的副本
result = original_image.clone()
# 确保processed_images和masks都是张量
if isinstance(processed_images, list):
processed_images = torch.cat(processed_images, dim=0)
if isinstance(masks, list):
masks = torch.cat(masks, dim=0)
# 获取目标尺寸
target_height = original_image.shape[1]
target_width = original_image.shape[2]
# 调整处理图像的尺寸(如果需要)
if processed_images.shape[1:3] != (target_height, target_width):
processed_images = self.resize_tensor(
processed_images,
(target_height, target_width),
mode='bilinear'
)
# 调整蒙版尺寸(如果需要)
if masks.shape[1:3] != (target_height, target_width):
masks = self.resize_tensor(
masks,
(target_height, target_width),
mode='bilinear'
)
# 扩展蒙版维度以匹配图像通道
masks = masks.unsqueeze(-1).expand(-1, -1, -1, 3)
# 批量处理所有图片
for i in range(processed_images.shape[0]):
current_image = processed_images[i:i+1]
current_mask = masks[i:i+1]
result = current_mask * current_image + (1 - current_mask) * result
return (result,)
Mask_CLASS_MAPPINGS = {
"CyberEve_MaskSegmentation": MaskSplit,
"CyberEve_MaskMerge": MaskMerge,
}
Mask_DISPLAY_NAME_MAPPINGS = {
"CyberEve_MaskSegmentation": "Mask Segmentation🐰",
"CyberEve_MaskMerge": "Mask Merge🐰",
}
+2
View File
@@ -0,0 +1,2 @@
opencv-python
numpy
+52
View File
@@ -0,0 +1,52 @@
def MakeSmartType(t):
if isinstance(t, str):
return SmartType(t)
return t
class SmartType(str):
def __ne__(self, other):
if self == "*" or other == "*":
return False
selfset = set(self.split(','))
otherset = set(other.split(','))
return not selfset.issubset(otherset)
def VariantSupport():
def decorator(cls):
if hasattr(cls, "INPUT_TYPES"):
old_input_types = getattr(cls, "INPUT_TYPES")
def new_input_types(*args, **kwargs):
types = old_input_types(*args, **kwargs)
for category in ["required", "optional"]:
if category not in types:
continue
for key, value in types[category].items():
if isinstance(value, tuple):
types[category][key] = (MakeSmartType(value[0]),) + value[1:]
return types
setattr(cls, "INPUT_TYPES", new_input_types)
if hasattr(cls, "RETURN_TYPES"):
old_return_types = cls.RETURN_TYPES
setattr(cls, "RETURN_TYPES", tuple(MakeSmartType(x) for x in old_return_types))
if hasattr(cls, "VALIDATE_INPUTS"):
# Reflection is used to determine what the function signature is, so we can't just change the function signature
raise NotImplementedError("VariantSupport does not support VALIDATE_INPUTS yet")
else:
def validate_inputs(input_types):
inputs = cls.INPUT_TYPES()
for key, value in input_types.items():
if isinstance(value, SmartType):
continue
if "required" in inputs and key in inputs["required"]:
expected_type = inputs["required"][key][0]
elif "optional" in inputs and key in inputs["optional"]:
expected_type = inputs["optional"][key][0]
else:
expected_type = None
if expected_type is not None and MakeSmartType(value) != expected_type:
return f"Invalid type of {key}: {value} (expected {expected_type})"
return True
setattr(cls, "VALIDATE_INPUTS", validate_inputs)
return cls
return decorator
+108
View File
@@ -0,0 +1,108 @@
import { app } from "../../../scripts/app.js"
const _ID = "CyberEve_LoopIndexSwitch";
const _PREFIX = "while_";
const _TYPE = "*";
const MAX_INPUTS = 100;
app.registerExtension({
name: 'CyberEveLoop.LoopIndexSwitch',
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// 添加调试信息
console.log("Registering extension for:", nodeData.name);
console.log("Looking for:", _ID);
if (nodeData.name !== _ID) {
return;
}
// 添加右键菜单选项
const onGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function(_, options) {
if (onGetExtraMenuOptions) {
onGetExtraMenuOptions.apply(this, arguments);
}
// 添加输入槽
options.push({
content: "Add Loop Input",
callback: () => {
// 弹出对话框让用户输入循环次数
const number = prompt("Enter loop iteration number (0-99):", "0");
if (number === null || isNaN(number)) {
return;
}
const num = parseInt(number);
if (num < 0 || num >= MAX_INPUTS) {
alert(`Please enter a number between 0 and ${MAX_INPUTS-1}`);
return;
}
const slotName = `${_PREFIX}${num}`;
// 检查是否已存在该输入槽
if (this.inputs.find(input => input.name === slotName)) {
alert(`Input for iteration ${num} already exists!`);
return;
}
// 添加新的输入槽
this.addInput(slotName, _TYPE);
this.graph.setDirtyCanvas(true);
}
});
// 删除输入槽子菜单
const valueInputs = this.inputs.filter(input =>
input.name.startsWith(_PREFIX)
);
if (valueInputs.length > 0) {
const removeOptions = valueInputs.map(input => ({
content: `Remove iteration ${input.name.substring(_PREFIX.length)}`,
callback: () => {
const index = this.inputs.findIndex(i => i.name === input.name);
if (index !== -1) {
this.removeInput(index);
this.graph.setDirtyCanvas(true);
}
}
}));
options.push({
content: "Remove Loop Input",
submenu: {
options: removeOptions
}
});
}
};
// 序列化节点时保存输入槽信息
nodeType.prototype.serialize = function() {
const data = LGraphNode.prototype.serialize.apply(this);
data.inputSlots = this.inputs.filter(input =>
input.name.startsWith(_PREFIX)
).map(input => ({
name: input.name,
type: input.type
}));
return data;
};
// 反序列化时恢复输入槽
nodeType.prototype.configure = function(data) {
LGraphNode.prototype.configure.apply(this, arguments);
if (data.inputSlots) {
// 移除所有循环相关的输入
this.inputs = this.inputs.filter(input =>
!input.name.startsWith(_PREFIX)
);
// 恢复保存的输入槽
data.inputSlots.forEach(slot => {
this.addInput(slot.name, slot.type);
});
}
};
}
});