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:
@@ -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
@@ -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
@@ -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
@@ -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🐰",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
opencv-python
|
||||
numpy
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user