Files
abc-lee-ComfyUI-InpaintRegi…/DEVLOG.md
T
InpaintRegionEditor Dev 23c748bcf0 feat: 自定义图像渲染 + 选区框绘制 + 拖动功能
主要更改:
- 重写前端 extension.js,完全自己控制图像渲染
- 去掉 nodes.py 的 image_upload,避免双图像问题
- 实现选区框绘制(橙色)+ 遮罩区域显示(红色)
- 实现选区框拖动功能
- 修复 onMouseDown 参数格式: (e, pos, canvas)
- 更新文档和开发日志
2026-03-02 22:31:54 +08:00

265 lines
7.6 KiB
Markdown
Raw 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 开发日志
## 2026-03-02 重大进展
### 完成的工作
1. **完全重写前端图像渲染**
- 不再依赖 ComfyUI 的 `image_upload` 和 `node.imgs`
- 自己加载图像、自己绘制
- 去掉了 `nodes.py` 中的 `image_upload: True`
2. **选区框绘制正常工作**
- 橙色选区框 + 红色遮罩区域
- 标签显示尺寸
3. **拖动功能实现**
- 关键发现:`onMouseDown(e, pos, canvas)` 的 `pos` 参数直接就是节点本地坐标
- 不需要手动转换 `e.canvasX`
- 返回 `true` 可以阻止节点被拖动
### 遇到的问题
1. **双图像问题**:ComfyUI 自动画了一个图,我又画了一个 → 去掉 `image_upload: True`
2. **黑边问题**:用黑色覆盖图像区域导致边框难看 → 直接画图像,不覆盖
3. **拖动不工作**:鼠标事件参数格式错误
- 错误:`onMouseDown(e)` 以为 `e.canvasX` 是画布坐标
- 正确:`onMouseDown(e, pos, canvas)`,`pos[0], pos[1]` 是节点本地坐标
### 待解决问题
1. 拖动体验优化(边界检测、视觉反馈)
2. 缩放/放大图像查看
3. Photopea 编辑后的图像刷新
### 代码结构
```
extension.js:
- loadImageAndDetectMask() - 加载图像并检测 Alpha 通道
- drawNode() - 绘制图像和选区框
- getImageDrawParams() - 计算图像绘制参数
- onMouseDown/Move/Up() - 拖动交互
nodes.py:
- INPUT_TYPES 去掉 image_upload
- process() 从 Alpha 提取 MASK
```
---
## 2026-02-28 研究结论(重要!续)
### 问题:发送给 Photopea 的图片带透明通道,导致遮罩混乱
**现象**:
- 发送给 Photopea 的图片有透明区域
- Photopea 保存后,LoadImage 节点的 MASK 输出变成乱码
**根因分析**:
1. LoadImage 节点处理流程(nodes.py:1732):
```python
image = i.convert("RGB") # 丢弃 Alpha,返回纯 RGB
```
→ 这时候的 IMAGE 是**纯 RGB**,没有透明!
2. 但 `/view` API(server.py:528-546)默认行为:
```python
if 'channel' not in request.rel_url.query:
channel = 'rgba' # 默认返回 RGBA!
```
→ 即使 LoadImage 输出的是纯 RGB,前端用 `/view?filename=...` 获取时,默认返回的是**原始文件**(可能带 Alpha)
3. 解决方案:加 `&channel=rgb` 参数
```
/view?filename=xxx&type=input&channel=rgb
```
→ 服务器会用 Pillow 处理:
```python
if channel == 'rgb':
with Image.open(file) as img:
if img.mode == "RGBA":
r, g, b, a = img.split()
new_img = Image.merge('RGB', (r, g, b)) # 丢弃 Alpha
else:
new_img = img.convert("RGB")
new_img.save(buffer, format='PNG')
```
**最终代码修改**(extension.js):
```javascript
// 错误的做法(默认返回 RGBA):
var url = "/view?filename=" + encodeURIComponent(filename) + "&type=" + encodeURIComponent(type);
// 正确的做法(返回纯 RGB):
var url = "/view?filename=" + encodeURIComponent(filename) + "&type=" + encodeURIComponent(type) + "&channel=rgb";
```
### 重要教训
1. **LoadImage 输出的 IMAGE 确实是纯 RGB**(Pillow convert("RGB") 处理过)
2. **但是 `/view` API 默认返回原始文件**(可能带 Alpha)
3. **必须加 `&channel=rgb` 参数** 才能获取纯 RGB 图片
---
## 2026-02-28 研究结论(重要!)
### 核心发现:ComfyUI 蒙版数据格式
## 2026-02-28 研究结论(重要!)
### 核心发现:ComfyUI 蒙版数据格式
通过分析 ComfyUI 源码,彻底理解了蒙版格式:
#### IMAGE 类型
```python
# nodes.py LoadImage.load_image()
image = i.convert("RGB") # IMAGE 不含 Alpha!
# Shape: (B, H, W, 3), 值 [0, 1]
```
#### MASK 类型
```python
# nodes.py:1743-1745
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask) # 反转!
# Shape: (B, H, W), 值 [0, 1]
# 1 = 遮罩区域, 0 = 背景
```
#### PNG Alpha → MASK 转换
```
PNG Alpha = 0 (透明) → mask = 1.0 (遮罩区域)
PNG Alpha = 255 (不透明) → mask = 0.0 (背景)
```
#### /upload/mask API (server.py:462-474)
```python
original_pil = original_pil.convert('RGBA')
mask_pil = Image.open(image.file).convert('RGBA')
new_alpha = mask_pil.getchannel('A') # 取上传图像的 Alpha
original_pil.putalpha(new_alpha) # 替换原图 Alpha
```
### MaskToImage / ImageToMask 源码 (comfy_extras/nodes_mask.py)
```python
# MaskToImage: MASK → IMAGE
result = mask.reshape((-1, 1, H, W)).movedim(1, -1).expand(-1, -1, -1, 3)
# ImageToMask: IMAGE → MASK
mask = image[:, :, :, channels.index(channel)]
```
### Photopea 关键发现
#### maskFromTransparency() 有 BUG - 会丢失透明区域的 RGB 数据
#### 正确方法:loadTransparentPixels() + addMask()
```javascript
doc.selection.loadTransparentPixels(); // 选择非透明区域
layer.addMask(true); // 创建图层蒙版
```
#### Photopea 蒙版语义 vs ComfyUI
| 系统 | 白色 | 黑色 |
|------|------|------|
| Photopea 蒙版 | 显示(非遮罩) | 隐藏(遮罩) |
| ComfyUI Mask | 遮罩区域 | 背景 |
### 完整数据流
```
传入图像 → 直接打开PNG → loadTransparentPixels + addMask → 图像完整 + 蒙版在图层右侧
导出蒙版 → 复制蒙版数据 → 反转(蒙版黑色=遮罩 → Alpha=0)→ PNG(RGB=0, Alpha)
上传蒙版 → POST /upload/mask → 服务端用蒙版Alpha替换原图Alpha
```
### 代码位置
- ComfyUI 核心: `E:\ComfyUI\nodes.py`, `E:\ComfyUI\server.py`
- 蒙版节点: `E:\ComfyUI\comfy_extras\nodes_mask.py`
- 参考实现: `E:\ComfyUI\custom_nodes\ComfyUI-Impact-Pack\js\impact-sam-editor.js`
---
## 2026-02-27
### 已完成
- 基础架构:Python 后端 + JS 前端
- Photopea iframe 集成
- 右键菜单 → Open in Photopea
### 技术参考
- Photopea Masks: https://www.photopea.com/learn/masks
### Photopea Layer Mask API (2026-02-28 更新)
#### 关键发现:layer.mask 返回 null - 已知限制
**Issue #7341** (2024年11月) 确认:layer masks 无法通过脚本 API 访问
```javascript
// 永远返回 null
var mask = layer.mask;
```
#### 但有 Workaround!
用户手动测试发现:`doc.activeChannel = layer.mask` 实际上能工作!
```javascript
// 虽然 layer.mask 返回 null,但这行代码实际能工作
doc.activeChannel = layer.mask;
```
#### 正确方案
用 try-catch 包裹,直接尝试导出:
```javascript
var exportScript =
'(function(){' +
' try {' +
' var doc = app.activeDocument;' +
' var layer = doc.activeLayer;' +
' var origChannel = doc.activeChannel;' +
' ' +
' doc.activeChannel = layer.mask;' +
' doc.selection.selectAll();' +
' doc.selection.copy();' +
' var newDoc = app.documents.add(doc.width, doc.height);' +
' app.activeDocument = newDoc;' +
' newDoc.paste();' +
' newDoc.saveToOE("png");' +
' newDoc.close(SaveOptions.DONOTSAVECHANGES);' +
' ' +
' app.activeDocument = doc;' +
' doc.activeChannel = origChannel;' +
' ' +
' app.echoToOE("mask_exported");' +
' } catch(e) {' +
' app.echoToOE("no_mask:" + e.message);' +
' }' +
'})();';
```
#### 替代方案(如果上面不行)
用 visibility 切换导出带蒙版的图层:
```javascript
// 隐藏所有图层,只显示当前层(蒙版会自动应用)
for (var i = 0; i < app.activeDocument.layers.length; i++) {
app.activeDocument.layers[i].visible = false;
}
app.activeDocument.activeLayer.visible = true;
// 导出 - 蒙版已经渲染进 PNG
app.activeDocument.saveToOE("png");
```