chore: 将仓库迁移到子目录

This commit is contained in:
DJ-PM
2025-03-03 02:13:08 +08:00
commit 5f384c69f9
7 changed files with 779 additions and 0 deletions
+321
View File
@@ -0,0 +1,321 @@
# ComfyUI 扩展开发手册
## 1. 项目结构
一个标准的ComfyUI扩展项目应包含以下文件:
```
├── README.md # 项目说明文档
├── __init__.py # 扩展入口文件
├── [主要功能文件].py # 核心功能实现
└── requirements.txt # 依赖项配置
```
## 2. 节点开发规范
### 2.1 基本结构
每个节点类需要定义以下基本属性:
```python
class YourNode:
RETURN_TYPES = ("IMAGE",) # 返回值类型
FUNCTION = "process" # 处理函数名称
CATEGORY = "image/pattern" # 节点分类
```
### 2.2 输入参数定义
使用 `INPUT_TYPES` 类方法定义节点的输入参数:
```python
@classmethod
def INPUT_TYPES(cls):
return {
"required": { # 必需参数
"param_name": ("TYPE", {
"default": default_value,
"min": min_value, # 数值类型可选
"max": max_value, # 数值类型可选
"step": step_value # 数值类型可选
})
}
}
```
### 2.3 最小化节点功能原则
为了确保节点的可维护性和复用性,应遵循以下原则:
1. 单一职责:
- 每个节点应专注于单一的功能或转换
- 避免在一个节点中实现多个不相关的功能
- 功能应该是原子的,不可再分
2. 参数精简:
- 只暴露必要的参数
- 参数应具有明确的用途和合理的默认值
- 避免冗余或相互依赖的参数
3. 输入输出清晰:
- 明确定义输入和输出的数据类型
- 保持数据流的简单性和直观性
- 避免复杂的数据转换和状态管理
4. 错误处理:
- 提供清晰的错误信息
- 对异常情况进行适当处理
- 保护节点不受无效输入的影响
### 2.4 图像处理注意事项
1. 输入图像格式处理:
```python
# 处理输入图像格式(从Tensor转为NumPy数组)
if isinstance(image, torch.Tensor):
img = (image[0] * 255).cpu().numpy().astype(np.uint8)
else:
img = (image[0] * 255).astype(np.uint8)
# 检查图像维度
if len(img.shape) != 3:
raise ValueError("输入图像必须是3通道RGB图像")
height, width, channels = img.shape
if channels != 3:
raise ValueError(f"输入图像必须是3通道RGB图像,当前通道数:{channels}")
```
2. 输出图像格式转换:
```python
# 创建新图像,确保尺寸与输入图像一致
result_img = Image.new('RGB', (width, height), 'black')
# ... 图像处理逻辑 ...
# 转换为ComfyUI标准格式
result_array = np.array(result_img) / 255.0
result_tensor = torch.from_numpy(result_array).float()
# 确保输出张量的维度与ComfyUI期望的格式一致
result_tensor = result_tensor.unsqueeze(0)
return (result_tensor,)
```
### 2.5 参数定义常见问题与解决方案
1. 参数类型不匹配问题:
- 问题描述:在定义节点参数时,如果参数类型与实际处理函数中的期望类型不匹配,可能导致运行时错误
- 解决方案:
- 在INPUT_TYPES中明确定义参数类型和范围
- 在处理函数中添加类型检查和转换
- 使用适当的默认值和范围限制
2. 参数范围设置:
- 问题描述:参数范围设置不当可能导致功能异常或性能问题
- 解决方案:
- 根据实际需求设置合理的最小值和最大值
- 考虑性能影响,避免过大的参数范围
- 添加参数验证逻辑
3. 参数依赖关系:
- 问题描述:某些参数可能相互依赖,需要确保它们的值在合理范围内
- 解决方案:
- 在处理函数中添加参数关系验证
- 使用条件判断确保参数组合有效
- 提供清晰的错误提示
示例代码:
```python
def process(self, image, min_radius, max_radius):
# 参数验证
if min_radius >= max_radius:
raise ValueError("最小半径必须小于最大半径")
# 类型转换和范围检查
min_radius = int(min_radius)
max_radius = int(max_radius)
if min_radius < 1:
raise ValueError("最小半径不能小于1")
# 处理逻辑
...
```
4. 最佳实践:
- 在INPUT_TYPES中使用描述性的参数名称
- 为每个参数提供合理的默认值
- 添加参数验证和错误处理
- 在文档中清晰说明参数的用途和限制
- 定期测试边界条件和异常情况
3. 图像格式转换最佳实践:
- 始终检查输入图像类型,确保正确处理Tensor和NumPy数组
- 在进行图像处理操作前,确保将图像数据范围调整到合适的区间(如0-255)
- 使用astype()明确指定数据类型,避免精度问题
- 在返回结果前,确保转换为ComfyUI期望的格式(通常是float32类型的Tensor)
- 保持输入输出图像尺寸的一致性,避免意外的尺寸变化
- 在创建新图像时,显式指定尺寸参数,确保与输入图像匹配
4. 常见错误处理:
- 检查图像维度是否正确(通常需要[batch, height, width, channels]格式)
- 注意数据类型转换可能带来的精度损失
- 处理图像格式转换时的内存使用问题
- 确保正确处理GPU和CPU之间的数据传输(使用.cpu()方法)
- 对图像尺寸和通道数进行明确的验证,提供清晰的错误信息
- 在进行图像变换操作时,验证坐标是否在有效范围内
## 3. 依赖管理
### 3.1 requirements.txt
明确指定依赖版本,避免兼容性问题:
```
numpy>=1.21.0
opencv-python>=4.5.0
torch>=1.7.0
Pillow>=8.0.0
```
## 4. 最佳实践
### 4.1 代码组织
1. 功能模块化:将不同功能的代码分离到不同的文件中
2. 使用类封装:将相关的功能和数据封装在类中
3. 提供清晰的文档注释
### 4.2 错误处理
1. 对输入参数进行验证
2. 使用适当的异常处理机制
3. 提供有意义的错误信息
### 4.3 性能优化
1. 避免不必要的数据转换
2. 合理使用内存资源
3. 考虑批处理操作
## 5. 调试技巧
1. 使用print语句或日志记录中间结果
2. 检查图像格式和数值范围
3. 验证参数有效性
## 6. 文档编写
### 6.1 README.md
包含以下内容:
1. 功能说明
2. 安装方法
3. 使用说明
4. 参数说明
5. 示例展示
### 6.2 代码注释
1. 函数和类的文档字符串
2. 关键算法步骤说明
3. 参数说明和返回值说明
## 7. 发布注意事项
1. 版本号管理
2. 更新日志维护
3. 许可证说明
4. 兼容性测试
## 8. 常见问题解决
1. 图像格式转换问题
2. 参数范围验证
3. 性能优化建议
4. 依赖冲突处理
## 9. 错误处理与最佳实践
### 9.1 图像处理错误
1. 图像格式转换错误:
- 确保在转换前检查图像类型(Tensor/NumPy)
- 验证图像维度和通道数
- 注意数据类型转换时的精度损失
2. 内存管理问题:
- 及时释放大型图像数据
- 避免不必要的数据复制
- 使用适当的数据类型减少内存占用
### 9.2 参数配置错误
1. 参数范围验证:
- 明确指定参数的有效范围
- 提供合理的默认值
- 添加参数类型检查
2. 配置文件错误:
- 使用标准的配置文件格式
- 提供配置模板和示例
- 添加配置验证机制
### 9.3 OpenCV函数参数处理
1. 数据类型转换:
- 确保输入数组类型正确(如uint8、float32等)
- 注意数值范围的转换(如0-255到0-1的归一化)
- 处理特殊值(如NaN、Inf)的检查
2. 参数验证:
- 检查矩阵维度和通道数
- 验证核大小的奇偶性要求
- 确保阈值范围的合法性
3. 错误处理:
- 捕获并处理OpenCV异常
- 提供详细的错误信息和建议
- 实现优雅的降级处理
4. 性能优化:
- 避免频繁的数据类型转换
- 合理使用就地操作(in-place operation)
- 注意内存管理和资源释放
### 9.3 Git操作规范
1. Commit信息规范:
- 使用引号包裹commit信息:git commit -m "feat: 添加新功能"
- 采用统一的commit前缀:
* feat: 新功能
* fix: 修复bug
* docs: 文档更新
* style: 代码格式调整
* refactor: 代码重构
* test: 测试相关
* chore: 构建过程或辅助工具的变动
2. 分支管理:
- 主分支保持稳定
- 功能开发使用feature分支
- 版本发布使用release分支
3. 代码提交前检查:
- 运行测试确保功能正常
- 检查代码格式
- 更新文档和注释
## 10. 社区贡献
1. 代码风格规范
2. Pull Request流程
3. Issue提交指南
## 10. 参考资源
1. ComfyUI官方文档
2. 相关技术文档
3. 示例项目
## 许可证
MIT License
+81
View File
@@ -0,0 +1,81 @@
# Circle Pattern Processor for ComfyUI
这是一个用于ComfyUI的自定义节点,可以检测图像中的圆形图案并生成标准化的圆形输出。
## 功能特点
- 使用霍夫圆变换检测图像中的圆形
- 可调节的预处理阈值
- 可配置的圆形检测参数(最小/最大半径)
- 输出标准大小的圆形图案
- 支持将检测到的圆形导出为SVG格式
## 安装方法
1. 将此仓库克隆到ComfyUI的`custom_nodes`目录下:
```bash
cd custom_nodes
git clone [repository_url] circle_pattern_processor
```
2. 安装依赖:
```bash
cd circle_pattern_processor
pip install -r requirements.txt
```
## 使用方法
1. 启动ComfyUI
2. 在节点选择菜单中找到"Circle Pattern Processor"和"Circle Pattern SVG Exporter"
3. 将节点添加到工作流中
4. 连接输入图像
5. 调整参数:
- preprocessing_threshold:图像预处理的阈值(0-255)
- min_radius:检测圆形的最小半径
- max_radius:检测圆形的最大半径
- output_circle_size:输出圆形的大小
## 参数说明
### CirclePatternProcessor节点
- preprocessing_threshold (默认: 127)
- 范围: 0-255
- 用于图像二值化的阈值
- min_radius (默认: 5)
- 范围: 1-100
- 检测圆形的最小半径
- max_radius (默认: 20)
- 范围: 1-100
- 检测圆形的最大半径
- output_circle_size (默认: 10)
- 范围: 1-50
- 输出图像中圆形的标准大小
### CirclePatternSVGExporter节点
- output_size (默认: 512)
- 范围: 64-2048
- 输出SVG画布的大小
- circle_radius (默认: 10)
- 范围: 1-100
- SVG中圆形的半径
- stroke_width (默认: 2)
- 范围: 0-10
- SVG中圆形边框的宽度
- stroke_color (默认: "#000000")
- SVG中圆形边框的颜色
- fill_color (默认: "none")
- SVG中圆形的填充颜色
## 许可证
MIT License
+16
View File
@@ -0,0 +1,16 @@
from .circle_detector import CirclePatternProcessor, ImageBinarizer
from .svg_exporter import CirclePatternSVGExporter
NODE_CLASS_MAPPINGS = {
"CirclePatternProcessor": CirclePatternProcessor,
"ImageBinarizer": ImageBinarizer,
"CirclePatternSVGExporter": CirclePatternSVGExporter
}
NODE_DISPLAY_NAME_MAPPINGS = {
"CirclePatternProcessor": "Circle Pattern Processor",
"ImageBinarizer": "Image Binarizer",
"CirclePatternSVGExporter": "Circle Pattern SVG Exporter"
}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
+216
View File
@@ -0,0 +1,216 @@
import numpy as np
import cv2
import torch
from PIL import Image, ImageDraw
import time
class ImageBinarizer:
RETURN_TYPES = ("IMAGE",)
FUNCTION = "binarize_image"
CATEGORY = "image/pattern"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"threshold": ("FLOAT", {
"default": 127,
"min": 0,
"max": 255,
"step": 1,
"display": "slider"
})
}
}
def binarize_image(self, image, threshold):
if isinstance(image, torch.Tensor):
img = (image[0] * 255).cpu().numpy().astype(np.uint8)
else:
img = (image[0] * 255).astype(np.uint8)
if len(img.shape) != 3:
raise ValueError("输入图像必须是3通道RGB图像")
height, width, channels = img.shape
if channels != 3:
raise ValueError(f"输入图像必须是3通道RGB图像,当前通道数:{channels}")
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
_, binary = cv2.threshold(gray, threshold, 255, cv2.THRESH_BINARY)
result_img = Image.fromarray(binary).convert('RGB')
result_array = np.array(result_img) / 255.0
result_tensor = torch.from_numpy(result_array).float()
result_tensor = result_tensor.unsqueeze(0)
return (result_tensor,)
class CirclePatternProcessor:
RETURN_TYPES = ("IMAGE", "TUPLE")
RETURN_NAMES = ("IMAGE", "CIRCLES")
FUNCTION = "process_circles"
CATEGORY = "image/pattern"
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"min_radius": ("FLOAT", {
"default": 5,
"min": 1,
"max": 100,
"step": 1,
"display": "slider"
}),
"max_radius": ("FLOAT", {
"default": 20,
"min": 1,
"max": 100,
"step": 1,
"display": "slider"
}),
"output_circle_size": ("FLOAT", {
"default": 10,
"min": 1,
"max": 50,
"step": 1,
"display": "slider"
}),
"edge_detection_sensitivity": ("FLOAT", {
"default": 50,
"min": 1,
"max": 100,
"step": 1,
"display": "slider"
}),
"circle_accumulator_threshold": ("FLOAT", {
"default": 30,
"min": 1,
"max": 100,
"step": 1,
"display": "slider"
}),
"min_center_dist": ("FLOAT", {
"default": 10,
"min": 1,
"max": 100,
"step": 1,
"display": "slider"
}),
"merge_mode": (["keep_first", "use_average"], {
"default": "keep_first"
})
}
}
def process_circles(self, image, min_radius, max_radius, output_circle_size, edge_detection_sensitivity, circle_accumulator_threshold, min_center_dist, merge_mode):
try:
# 转换图像格式:从Tensor转为NumPy数组
if isinstance(image, torch.Tensor):
img = (image[0] * 255).cpu().numpy().astype(np.uint8)
else:
img = (image[0] * 255).astype(np.uint8)
# 检查图像维度
if len(img.shape) != 3:
raise ValueError("输入图像必须是3通道RGB图像")
height, width, channels = img.shape
if channels != 3:
raise ValueError(f"输入图像必须是3通道RGB图像,当前通道数:{channels}")
# 参数类型检查和转换
min_radius_int = max(1, int(min_radius))
max_radius_int = max(min_radius_int, int(max_radius))
output_circle_size = max(1, int(output_circle_size))
edge_detection_sensitivity = float(edge_detection_sensitivity)
circle_accumulator_threshold = float(circle_accumulator_threshold)
min_center_dist = max(1, float(min_center_dist))
# 转换为灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
circles = cv2.HoughCircles(
gray,
cv2.HOUGH_GRADIENT,
dp=1,
minDist=min_radius_int,
param1=edge_detection_sensitivity,
param2=circle_accumulator_threshold,
minRadius=min_radius_int,
maxRadius=max_radius_int
)
# 创建新图像用于绘制标准化的圆
result_img = Image.new('RGB', (width, height), 'black')
draw = ImageDraw.Draw(result_img)
# 存储检测到的圆形数据
circle_data = []
if circles is not None:
circles = np.round(circles[0, :]).astype(int)
# 合并距离较近的圆心
merged_circles = []
used_indices = set()
for i in range(len(circles)):
if i in used_indices:
continue
current_circle = circles[i]
close_circles = [current_circle]
# 查找距离当前圆心较近的其他圆心
for j in range(i + 1, len(circles)):
if j in used_indices:
continue
other_circle = circles[j]
dist = np.sqrt((current_circle[0] - other_circle[0])**2 +
(current_circle[1] - other_circle[1])**2)
if dist <= min_center_dist:
close_circles.append(other_circle)
used_indices.add(j)
# 根据合并模式处理圆心
if merge_mode == "keep_first":
merged_circles.append(close_circles[0])
else: # use_average
avg_x = int(np.mean([c[0] for c in close_circles]))
avg_y = int(np.mean([c[1] for c in close_circles]))
avg_r = int(np.mean([c[2] for c in close_circles]))
merged_circles.append([avg_x, avg_y, avg_r])
used_indices.add(i)
# 绘制合并后的圆并存储圆形数据
for (x, y, r) in merged_circles:
# 确保圆的坐标在图像范围内
if 0 <= x < width and 0 <= y < height:
# 绘制标准大小的白色圆
draw.ellipse(
[(x - output_circle_size, y - output_circle_size),
(x + output_circle_size, y + output_circle_size)],
fill='white'
)
# 存储圆形数据
circle_data.append((x, y, r))
# 转换回 ComfyUI 格式
result_array = np.array(result_img) / 255.0
result_tensor = torch.from_numpy(result_array).float()
result_tensor = result_tensor.unsqueeze(0)
return (result_tensor, circle_data)
except Exception as e:
raise ValueError(f"处理圆形图案时出错:{str(e)}")
# 转换回 ComfyUI 格式
result_array = np.array(result_img) / 255.0
result_tensor = torch.from_numpy(result_array).float()
result_tensor = result_tensor.unsqueeze(0)
return (result_tensor, circle_data)
+30
View File
@@ -0,0 +1,30 @@
# 仓库迁移脚本
# 此脚本用于将Git仓库从上级目录迁移到当前目录
# 步骤1: 在当前目录初始化新的Git仓库
Write-Host "在当前目录初始化新的Git仓库..."
git init
# 步骤2: 添加所有文件到新仓库
Write-Host "添加文件到Git仓库..."
git add .
# 步骤3: 提交更改
Write-Host "提交第一次更改..."
git commit -m "chore: 将仓库迁移到子目录"
# 步骤4: 从原始仓库获取提交历史(可选)
Write-Host "如果需要保留完整的提交历史,请执行以下步骤:"
Write-Host "1. 添加原始仓库作为远程仓库:"
Write-Host " git remote add original ../../.."
Write-Host "2. 拉取原始仓库的历史:"
Write-Host " git fetch original"
Write-Host "3. 合并历史(可能需要解决冲突):"
Write-Host " git merge original/master --allow-unrelated-histories"
# 步骤5: 如果有远程仓库,设置新的远程仓库
Write-Host "如果需要设置远程仓库,请执行:"
Write-Host "git remote add origin YOUR_REMOTE_URL"
Write-Host "git push -u origin master"
Write-Host "仓库迁移完成!现在您可以在当前目录中使用Git管理代码了。"
+3
View File
@@ -0,0 +1,3 @@
numpy
opencv-python
Pillow
+112
View File
@@ -0,0 +1,112 @@
import os
import re
import torch
import numpy as np
class CirclePatternSVGExporter:
RETURN_TYPES = ("STRING",)
FUNCTION = "export_to_svg"
CATEGORY = "image/pattern"
@staticmethod
def get_next_sequence_number(base_path):
# 获取目录中现有的文件
if not os.path.exists(base_path):
os.makedirs(base_path)
return 0
files = os.listdir(base_path)
max_sequence = -1
# 查找当前最大序号
pattern = re.compile(r'\d{4}\.[^.]+$')
for file in files:
if pattern.search(file):
try:
sequence = int(file[-8:-4])
max_sequence = max(max_sequence, sequence)
except ValueError:
continue
return max_sequence + 1
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"circles": ("TUPLE",),
"image": ("IMAGE",),
"filename": ("STRING", {
"default": "pattern.svg"
}),
"output_circle_size": ("FLOAT", {
"default": 10,
"min": 1,
"max": 50,
"step": 1,
"display": "slider"
})
}
}
def export_to_svg(self, circles, image, filename, output_circle_size):
try:
print("开始保存SVG文件...")
# 确保circles是有效的元组
if not circles or not isinstance(circles, (tuple, list)):
raise ValueError("无效的圆形数据")
# 从图像获取尺寸
if not isinstance(image, torch.Tensor):
raise ValueError("无效的图像数据")
height, width = image.shape[1:3]
# 构建输出路径,使用绝对路径
current_dir = os.path.dirname(os.path.abspath(__file__))
base_dir = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(current_dir)), "output", "svg"))
# 处理子目录
if "/" in filename or "\\" in filename:
subdir = os.path.dirname(filename)
filename = os.path.basename(filename)
base_dir = os.path.join(base_dir, subdir)
# 确保目录存在
os.makedirs(base_dir, exist_ok=True)
# 获取序列号并构建完整文件名
sequence = self.get_next_sequence_number(base_dir)
name, ext = os.path.splitext(filename)
if not ext:
ext = ".svg"
final_filename = f"{name}{sequence:04d}{ext}"
output_path = os.path.join(base_dir, final_filename)
print(f"准备保存SVG文件到: {output_path}")
# 生成SVG内容
svg_content = f'<?xml version="1.0" encoding="UTF-8"?>\n'
svg_content += f'<svg width="{width}" height="{height}" xmlns="http://www.w3.org/2000/svg">\n'
# 将圆形添加到SVG中
circle_count = 0
for (x, y, _) in circles: # 忽略原始半径,使用传入的大小
if 0 <= x < width and 0 <= y < height:
svg_content += f' <circle cx="{x}" cy="{y}" r="{output_circle_size}" '
svg_content += 'fill="white" stroke="none" opacity="1" />'
circle_count += 1
svg_content += '</svg>'
print(f"已生成SVG内容,包含 {circle_count} 个圆形")
# 保存SVG文件
with open(output_path, 'w', encoding='utf-8') as f:
f.write(svg_content)
print(f"SVG文件保存成功: {output_path}")
return (f"SVG文件已保存至: {output_path}",)
except Exception as e:
error_msg = f"保存SVG文件时出错: {str(e)}"
print(error_msg)
return (error_msg,)