Files
InpaintRegionEditor Dev 107e992cfe docs: 更新文档,准备发布 v1.4
- README: 移除开发中标记,更新功能列表
- CHANGELOG: 添加 v1.4.0 版本记录
- HANDOFF: 更新版本历史
- gitignore: 添加 *.bak 排除临时备份
- 代码注释: 掏窟窿 -> 透明区域 RGB 数据丢失
2026-03-03 21:56:48 +08:00

581 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# InpaintRegionEditor 项目交接文档
> 版本:1.4 | 日期:2026-03-03
> 交接对象:开发团队
> 交接人:项目经理 Agent
---
## 一、项目概述
### 1.1 项目定位
**InpaintRegionEditor** 是一个 ComfyUI 自定义节点,用于解决局部重绘(inpaint)工作流中的两个核心问题:
1. **图像编辑**:ComfyUI 自身没有图像编辑功能,需要集成 Photopea 实现液化、仿制图章等 PS 级功能
2. **蒙版编辑**:支持羽化效果的蒙版编辑,使用 Photopea 双图层模式
3. **选区调整**:原有的扩散选区功能无法手动调整位置,导致边缘参考区域错误
### 1.2 核心价值
```
原工作流痛点:
├─ 蒙版编辑:ComfyUI 自带工具功能有限,无法处理复杂蒙版(羽化、透明度等)
├─ 图像编辑:完全没有,需要外部 PS 处理后再上传
└─ 选区调整:只能中心扩散,无法手动移动位置
本节点解决方案:
├─ 集成 Photopea:完整的 Photoshop 功能,浏览器内完成编辑
├─ 双模式编辑:编辑图像 / 编辑蒙版(分离操作)
├─ 羽化支持:半透明蒙版自动转换为羽化效果
├─ 可拖动选区:手动调整参考区域位置,确保参考内容准确
└─ 一体化流程:上传 → 编辑 → 选区调整 → 执行,无需切换工具
```
### 1.3 节点功能
```
输入:
- image: 从 ComfyUI input 目录选择(支持上传、Ctrl+V 粘贴)
- padding: 扩散像素数(默认 64,范围 0-512)
自动处理:
1. 读取图像 Alpha 通道作为 MASK
2. 没有 Alpha → 报错 "请先创建蒙版"
3. 有 Alpha → 计算蒙版边界框(alpha < 255 都计入)
4. 选区 = 蒙版边界 + padding
5. 在图像预览上绘制遮罩 + 选区框(可拖动、可调整大小)
用户操作:
- 右键 → "编辑图像(Photopea)":编辑图像本身
- 右键 → "编辑蒙版(Photopea)":双图层蒙版编辑(参考图像 + 蒙版层)
- 右键 → "Open in MaskEditor":使用 ComfyUI 自带蒙版编辑器
- 拖动选区框:手动调整参考区域位置
- 调整选区边缘:拖动边缘/角落调整选区大小
- 调整 padding 值:改变扩散像素数
- Ctrl+V:粘贴图片
输出:
- IMAGE: RGB 图像(不含 Alpha)
- MASK: 从 Alpha 通道提取
- region_top, region_left, region_width, region_height: 选区坐标
```
---
## 二、核心需求确认
### 2.1 蒙版来源(三种)
1. **用户上传**:带 Alpha 通道的 PNG 文件
2. **Photopea 编辑**:在 Photopea 里绘制蒙版后保存
3. **系统工具**:使用 ComfyUI 自带蒙版工具绘制后保存
### 2.2 核心逻辑
```python
if 没有蒙版(Alpha 通道):
执行时报错:"请先创建蒙版(局部重绘需要蒙版)"
else:
自动计算选区 = 蒙版边界 + padding
在预览上绘制遮罩(半透明红)+ 选区框(橙色虚线)
用户可以拖动选区框位置
输出 region_* 坐标
```
### 2.3 选区概念
| 概念 | 定义 | 作用 |
|------|------|------|
| **遮罩 (Mask)** | 需要重绘的区域(可能是不规则形状) | AI 在这个区域内生成新内容 |
| **选区 (Region)** | 参考区域(矩形框) | AI 参考这个区域内的原图内容进行重绘 |
**约束**:选区必须 >= 遮罩(选区要完全包含遮罩)
**选区计算逻辑**:
```
选区边界 = 遮罩边界框 + padding(向四周扩散)
用户可拖动选区位置,但选区必须始终框住遮罩
```
---
## 三、技术架构
### 3.1 文件结构
```
ComfyUI-InpaintRegionEditor/
├── __init__.py # 节点注册
├── nodes.py # 后端节点逻辑
├── requirements.txt # 无额外依赖
│
├── web/
│ ├── extension.js # 扩展入口 + 右键菜单
│ ├── photopea-bridge.js # Photopea 通信桥接
│ ├── photopea-modal.js # Photopea 全屏对话框
│ ├── region-box-editor.js # 选区框编辑器(Canvas + 拖动)
│ └── style.css # 样式
│
└── docs/
├── technical-validation.md # 技术验证清单
├── implementation-plan.md # 详细实现方案
└── HANDOFF.md # 本文档
```
### 3.2 整体架构
```
┌─────────────────────────────────────────────────────────────────────┐
│ ComfyUI 前端 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ InpaintRegionEditor 节点 │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ 图像预览区域 │ │ │
│ │ │ ┌───────────────────────────────────────────────┐ │ │ │
│ │ │ │ [图像] │ │ │ │
│ │ │ │ ┌──────────────┐ │ │ │ │
│ │ │ │ │ ████████████ │ ← 遮罩(半透明红) │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ 虚线框 │ │ ← 选区(橙色虚线,可拖动) │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ ████████████ │ │ │ │ │
│ │ │ │ └──────────────┘ │ │ │ │
│ │ │ └───────────────────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ 控件: │ │ │
│ │ │ - 图像:[image.png ▼] [上传] │ │ │
│ │ │ - 扩散像素:[64] ▼ │ │ │
│ │ │ - [🎨 打开 Photopea 编辑] │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ ↕ │
│ 隐藏 widget: region_coords (JSON) │
│ ↕ │
└─────────────────────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────────────────────┐
│ ComfyUI 后端 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ nodes.py: InpaintRegionEditor.process() │
│ 1. 加载图像(从 input 目录) │
│ 2. 提取 Alpha 通道 → MASK │
│ 3. 计算遮罩边界框 │
│ 4. 计算选区 = 遮罩边界 + padding │
│ 5. 验证:选区 >= 遮罩 │
│ 6. 输出:IMAGE, MASK, region_* │
│ │
└─────────────────────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────────────────────┐
│ Photopea │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ - 通过 iframe 嵌入(全屏模态框) │
│ - postMessage API 通信 │
│ - 功能:图像编辑 + 蒙版绘制 │
│ - 导出:PNG 图像 + 黑白蒙版图 │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### 3.3 数据流
```
1. 用户上传图像
└─▶ POST /upload/image → input/image.png
2. 节点加载图像
└─▶ 读取 Alpha 通道 → MASK
└─▶ 计算遮罩边界 → 选区
└─▶ 前端绘制:遮罩 + 选区框
3. 用户编辑(可选)
└─▶ 右键 → 打开 Photopea
└─▶ 编辑图像/蒙版
└─▶ 保存 → 上传新文件
└─▶ 更新节点 widget.value
4. 用户调整选区(可选)
└─▶ 拖动选区框
└─▶ 更新 region_coords (hidden widget)
5. 执行工作流
└─▶ 后端读取图像 + region_coords
└─▶ 输出:IMAGE, MASK, region_*
```
---
## 四、关键技术点
### 4.1 蒙版处理
**提取逻辑**(nodes.py):
```python
if 'A' in img.getbands():
mask = np.array(img.getchannel('A')).astype(np.float32) / 255.0
mask = 1.0 - mask # ComfyUI 约定:1=遮罩区域,0=背景
else:
raise ValueError("请先创建蒙版")
```
**格式约定**:
- PNG Alpha = 0(透明) → mask = 1.0(遮罩区域)
- PNG Alpha = 255(不透明) → mask = 0.0(背景)
### 4.2 选区计算
```python
def _calculate_region_rect(mask_bounds, padding):
"""根据遮罩边界 + padding 计算选区"""
return {
'x': mask_bounds['x'] - padding,
'y': mask_bounds['y'] - padding,
'width': mask_bounds['width'] + padding * 2,
'height': mask_bounds['height'] + padding * 2
}
```
**约束验证**:
```python
if (region_rect['width'] < mask_bounds['width'] or
region_rect['height'] < mask_bounds['height']):
raise ValueError("选区必须 >= 遮罩")
```
### 4.3 Photopea 集成
**通信机制**:
```javascript
// 发送消息到 Photopea
photopeaWindow.postMessage('app.activeDocument.saveToOE("png");', "*");
// 接收响应(验证 origin)
window.addEventListener("message", (event) => {
if (event.origin !== "https://www.photopea.com") return;
// 处理响应...
});
```
**待验证功能**:
1. 能否单独导出蒙版?(`layer.mask` 已知返回 null)
2. 能否同时加载原图 + 蒙版?
3. 能否检测用户是否画了蒙版?
**参考 workaround**(DEVLOG 中提到):
```javascript
// 虽然 layer.mask 返回 null,但这行代码可能工作
doc.activeChannel = layer.mask;
doc.selection.selectAll();
doc.selection.copy();
```
### 4.4 选区框绘制
**参考实现**:`ComfyUI-Impact-Pack/js/mask-rect-area.js`
**核心功能**:
- 自定义 Canvas Widget
- 绘制遮罩(半透明红)
- 绘制选区框(橙色虚线)
- 鼠标拖动交互
- 同步坐标到 widget
---
## 五、待验证技术点
### 5.1 高优先级(决定方案可行性)
| 编号 | 测试项 | 目的 | 状态 |
|------|-------|------|------|
| P1 | Photopea 同时加载原图 + 蒙版 | 确定工作流 | ❓ 待测试 |
| P2 | Photopea 单独导出蒙版 | 确定保存逻辑 | ❓ 待测试 |
| P3 | Photopea 检测是否有蒙版 | 确定错误提示 | ❓ 待测试 |
### 5.2 中优先级(影响用户体验)
| 编号 | 测试项 | 目的 | 状态 |
|------|-------|------|------|
| M1 | Canvas 选区框绘制 | 参考 Impact-Pack | ✅ 有参考代码 |
| M2 | 拖动交互实现 | 鼠标事件监听 | ✅ 有参考代码 |
| M3 | 坐标同步机制 | hidden widget | ✅ 已验证 |
### 5.3 低优先级(可迭代优化)
| 编号 | 测试项 | 目的 | 状态 |
|------|-------|------|------|
| L1 | 两次导出上传 | 图像 + 蒙版分开保存 | ❓ 待测试 |
| L2 | 后端合成 PNG | 两张图合成带 Alpha 的 PNG | ✅ 简单 |
| L3 | UI 样式优化 | 美观度提升 | - 后期 |
---
## 六、实现路线图
### Phase 1:后端基础(1 天)
**任务**:
- [ ] 更新 `nodes.py` 实现核心逻辑
- [ ] 测试蒙版提取(从 Alpha 通道)
- [ ] 测试选区计算(遮罩边界 + padding)
- [ ] 测试错误提示(没有蒙版时报错)
**交付物**:
- 可运行的后端节点
- 单元测试脚本
### Phase 2:选区 UI(2 天)
**任务**:
- [ ] 实现 `region-box-editor.js`
- [ ] 在节点上绘制遮罩预览
- [ ] 在节点上绘制选区框(虚线)
- [ ] 实现拖动交互
- [ ] 约束验证(选区 >= 遮罩)
**参考**:`ComfyUI-Impact-Pack/js/mask-rect-area.js`
**交付物**:
- 可在节点上显示并拖动选区框
### Phase 3:Photopea 集成(2-3 天)
**任务**:
- [ ] 实现 `photopea-modal.js`
- [ ] 实现 `photopea-bridge.js`
- [ ] 测试图像加载(`&channel=rgb` 参数)
- [ ] 测试图像导出
- [ ] **测试蒙版导出**(关键!)
- [ ] 上传回 ComfyUI
**交付物**:
- 可打开 Photopea 编辑
- 可保存回节点
### Phase 4:集成测试(1 天)
**任务**:
- [ ] 完整工作流测试
- [ ] 修复 bug
- [ ] 编写 README
**交付物**:
- 可发布的完整版本
---
## 七、已知风险与备选方案
### 7.1 Photopea API 限制
**问题**:`layer.mask` 总是返回 `null`(官方已知,Issue #7341)
**影响**:无法通过脚本访问图层蒙版
**备选方案**:
1. 用 workaround 尝试导出(`doc.activeChannel = layer.mask`)
2. 如果不行,用户只能用其他方式创建蒙版(系统工具/上传)
3. 或者在 Photopea 里手动合成到 Alpha 通道后导出
### 7.2 蒙版语义差异
**问题**:
- Photopea 蒙版:白色 = 显示(非遮罩),黑色 = 隐藏(遮罩)
- ComfyUI Mask:白色 = 遮罩区域,黑色 = 背景
**解决**:导出时反转
### 7.3 性能问题
**问题**:大图片(>2048x2048)可能导致 Photopea 卡顿
**解决**:
- 提示用户使用合适尺寸
- 可选:前端缩放后编辑
---
## 八、参考资源
### 8.1 代码参考
| 文件 | 位置 | 用途 |
|------|------|------|
| `mask-rect-area.js` | `E:\ComfyUI\custom_nodes\ComfyUI-Impact-Pack\js\` | 选区框绘制参考 |
| `nodes.py` (LoadImage) | `E:\ComfyUI\nodes.py` (line 1702) | 图像加载参考 |
| `server.py` | `E:\ComfyUI\server.py` | 上传 API 参考 |
### 8.2 文档参考
| 文档 | 说明 |
|------|------|
| [Photopea API](https://www.photopea.com/api/) | 官方 API 文档 |
| [technical-validation.md](./technical-validation.md) | 技术验证清单 |
| [implementation-plan.md](./implementation-plan.md) | 详细实现方案 |
| [DEVLOG.md](../DEVLOG.md) | 开发日志(包含历史问题记录) |
### 8.3 外部参考
| 项目 | 说明 |
|------|------|
| [sd-webui-photopea-embed](https://github.com/yankooliveira/sd-webui-photopea-embed) | Stable Diffusion WebUI 的 Photopea 集成 |
| [ComfyUI-Impact-Pack](https://github.com/ltdrdata/ComfyUI-Impact-Pack) | 选区框实现参考 |
---
## 九、团队协作建议
### 9.1 分工建议
**后端开发**(1 人):
- `nodes.py` 实现
- 蒙版提取逻辑
- 选区计算逻辑
**前端开发**(1 人):
- Canvas 选区框绘制
- 拖动交互实现
- Photopea 集成
**测试**(1 人):
- Photopea 功能验证
- 完整工作流测试
- Bug 报告
### 9.2 沟通要点
1. **Photopea 验证结果** 决定技术路线
- 如果能单独导出蒙版 → 方案 A
- 如果不能 → 方案 B(用户手动合成到 Alpha)
2. **选区 UI 复杂度** 可能被低估
- 建议先用 Impact-Pack 代码验证可行性
- 再考虑自定义需求
3. **错误提示要友好**
- 没有蒙版时告诉用户三种创建方式
- 选区太小时提示最小尺寸
---
## 十、验收标准
### 10.1 功能验收
- [ ] 用户上传带 Alpha 的 PNG → 正确显示遮罩 + 选区
- [ ] 用户上传不带 Alpha 的 PNG → 执行时报错(友好提示)
- [ ] 拖动选区框 → 坐标实时更新
- [ ] 调整 padding → 选区自动重算
- [ ] 打开 Photopea → 可以编辑图像
- [ ] Photopea 保存 → 图像同步到节点
- [ ] 输出 region_* → 下游节点可用
### 10.2 性能验收
- [ ] 加载 1024x1024 图像 < 2 秒
- [ ] 拖动选区框流畅(60fps)
- [ ] Photopea 打开 < 5 秒(首次加载缓存后)
### 10.3 兼容性验收
- [ ] Windows / Linux / macOS
- [ ] Chrome / Firefox / Edge
- [ ] ComfyUI 最新版本
---
## 十一、联系与反馈
### 11.1 问题记录
开发过程中遇到的任何问题,请记录到:
- `docs/DEVLOG.md`(开发日志)
- GitHub Issues(如果有仓库)
### 11.2 关键决策记录
任何偏离本方案的技术决策,请记录:
- 决策内容
- 决策原因
- 决策日期
---
## 十二、附录
### 12.1 术语表
| 术语 | 定义 |
|------|------|
| **Mask(遮罩)** | 定义重绘区域,AI 在这个区域内生成新内容 |
| **Region(选区)** | 定义参考区域,AI 参考这个区域内的原图内容 |
| **Padding(扩散)** | 选区相对于遮罩边界的扩散像素数 |
| **Alpha 通道** | PNG 的透明度通道,用于存储蒙版 |
| **羽化** | 蒙版边缘的半透明过渡效果(alpha = 1-254) |
### 12.2 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| 0.1 | 2026-02-27 | 初始版本(简化版,仅 Photopea 集成) |
| 1.0 | 2026-03-02 | 需求最终确认,编写 handoff 文档 |
| 1.1 | 2026-03-03 | Photopea 蒙版编辑功能,修复透明区域 RGB 数据丢失问题 |
| 1.2 | 2026-03-03 | 选区调整大小、光标变化、Ctrl+V 粘贴、MaskEditor 集成 |
| 1.3 | 2026-03-03 | MaskEditor clipspace 支持、选区坐标同步、羽化效果 |
| 1.4 | 2026-03-03 | 多语言支持(中英文)、修复编辑图像透明区域 RGB 数据丢失、修复选区坐标自动更新 |
---
## 十三、关键技术实现
### 13.1 Canvas Premultiplied Alpha 问题
**问题描述**:
当 Canvas 通过 `drawImage()` 绘制带有 Alpha < 255 的 PNG 图像时,浏览器会应用 **premultiplied alpha**,导致 Alpha = 0 的像素 RGB 数据丢失。
**解决方案**:
1. **加载时**:使用 ComfyUI `/view` API 的 `channel=rgb` 和 `channel=a` 参数分别获取 RGB 和 Alpha 数据
2. **保存时**:使用 UPNG.js 库直接编码 RGBA 数据,绑过 Canvas 的 premultiplied alpha
### 13.2 Photopea 蒙版编辑流程
```
1. 加载图像
├─ 获取 RGB 数据(channel=rgb)→ 完整 RGB,无窟窿
└─ 获取 Alpha 数据(channel=a)→ 用于创建蒙版层
2. 创建双图层文档
├─ 底层:参考图像(完整 RGB)
└─ 顶层:蒙版层(用户在此绘制黑色 = 遮罩)
3. 保存蒙版
├─ 隐藏参考图像层
├─ 只导出蒙版层
├─ 获取原图 RGB(channel=rgb)
└─ 合并蒙版到 Alpha 通道(UPNG.js 编码)
```
### 13.3 羽化逻辑
```
蒙版层(用户绘制):
- 黑色不透明(alpha=255)→ 遮罩 → 最终 alpha=0
- 黑色半透明(alpha=128)→ 羽化 → 最终 alpha=128
- 完全透明(alpha=0)→ 不遮罩 → 最终 alpha=255
公式:最终 alpha = 255 - 蒙版层alpha
```
---
*文档创建时间:2026-03-02*
*最后更新时间:2026-03-03*
*基于与用户的多次讨论整理*
*交接完成标志:开发团队确认收到并理解本文档*