commit 5f384c69f9682e0977cfe66264d7446db75a04ed Author: DJ-PM Date: Mon Mar 3 02:13:08 2025 +0800 chore: 将仓库迁移到子目录 diff --git a/DEVELOPMENT_GUIDE.md b/DEVELOPMENT_GUIDE.md new file mode 100644 index 0000000..95e2e89 --- /dev/null +++ b/DEVELOPMENT_GUIDE.md @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba5693b --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..dd3241c --- /dev/null +++ b/__init__.py @@ -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'] \ No newline at end of file diff --git a/circle_detector.py b/circle_detector.py new file mode 100644 index 0000000..43c960d --- /dev/null +++ b/circle_detector.py @@ -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) diff --git a/move-repo.ps1 b/move-repo.ps1 new file mode 100644 index 0000000..3103a3a --- /dev/null +++ b/move-repo.ps1 @@ -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管理代码了。" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0ce334d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy +opencv-python +Pillow \ No newline at end of file diff --git a/svg_exporter.py b/svg_exporter.py new file mode 100644 index 0000000..6c7a99f --- /dev/null +++ b/svg_exporter.py @@ -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'\n' + svg_content += f'\n' + + # 将圆形添加到SVG中 + circle_count = 0 + for (x, y, _) in circles: # 忽略原始半径,使用传入的大小 + if 0 <= x < width and 0 <= y < height: + svg_content += f'