# InpaintRegionEditor 开发日志 ## 2026-03-03 选区交互完善 ### 完成的工作 1. **选区调整大小** - 支持拖动边缘/角落调整选区大小 - 边缘检测阈值优化(不超过选区 15%) - 约束:选区必须 >= 蒙版边界框 2. **光标变化** - 中间区域:`grab`(移动) - 拖动时:`grabbing` - 边缘/角落:调整大小箭头 - 使用 `requestAnimationFrame` 延迟设置,确保在原生代码之后执行 3. **UI 优化** - 标签改为"参考区",字号缩小 - 像素数显示整数 - 图像左下角显示原图尺寸 4. **Ctrl+V 粘贴支持** - 全局粘贴事件监听(捕获阶段) - 只处理选中的 InpaintRegionEditor 节点 - 阻止系统创建新 LoadImage 节点 ### 遇到的问题 1. **光标设置被覆盖**:原生 `onMouseMove` 在我们之后执行,覆盖光标 - 解决:使用 `requestAnimationFrame` 延迟设置 2. **系统菜单 "Open in MaskEditor | Image Canvas" 出现**:设置 `previewMediaType = 'image'` 后自动添加 - 解决:不设置 `previewMediaType`,改用全局粘贴监听 3. **粘贴时创建新节点**:系统也处理了粘贴事件 - 解决:捕获阶段 + `stopImmediatePropagation()` ### 代码结构 ``` extension.js (820 行): ├── PhotopeaBridge - Photopea iframe 通信 ├── showPhotopeaModal() - Photopea 弹窗 ├── loadImageAndDetectMask() - 加载图像并检测蒙版 ├── constrainRegion() - 选区约束(框住蒙版 + 不超边界) ├── drawNode() - 绘制图像、尺寸标签、参考区框 ├── getResizeHandle() - 检测鼠标位置(中间/边缘/角落) ├── onMouseDown/Move/Up() - 拖动和调整大小交互 └── init() - 全局粘贴事件监听 ``` --- ## 2026-03-03 选区约束 + MaskEditor 集成 ### 完成的工作 1. **选区约束功能** - 选区必须框住蒙版区域 - 选区不能超出图像边界 - padding 改变时自动调整选区位置和大小 - 统一 `constrainRegion()` 函数处理所有约束逻辑 2. **MaskEditor 集成** - 右键菜单添加 "Open in MaskEditor" 选项 - 使用 `app.extensionManager.command.execute("Comfy.MaskEditor.OpenMaskEditor")` 调用系统命令 - 解决 MaskEditor 返回后双图像问题(清除 `node.imgs`) - 解决 blob URL 不被 MaskEditor 接受的问题(使用原始 ComfyUI URL) 3. **UI 优化** - 移除红色蒙版区域显示(真实蒙版已在图像上显示) - 只保留橙色选区框 ### 遇到的问题 1. **变量名冲突**:`beforeRegisterNodeDef(nodeType, nodeData)` 的参数 `nodeData` 覆盖了全局的 `nodeData` Map - 解决:全局变量改名为 `nodeImageData` 2. **`app.commands` undefined**:新版本 ComfyUI 命令系统变了 - 解决:使用 `app.extensionManager.command.execute()` 3. **MaskEditor 不接受 blob URL**: - 解决:保存原始 ComfyUI URL (`/view?filename=...&type=...`),点击菜单时用这个 URL 创建图像 4. **MaskEditor 返回后双图像**: - 解决:在 `onDrawBackground` 中检测并清除 `node.imgs` ### 关键代码 ```javascript // 统一的选区约束函数 function constrainRegion(data, padding) { // 1. 计算期望大小 // 2. 约束不超过图像大小 // 3. 约束不超出边界 // 4. 优先框住蒙版 } // 调用 MaskEditor app.extensionManager.command.execute("Comfy.MaskEditor.OpenMaskEditor"); ``` ### 待解决问题 1. 选区大小调整(目前只能拖动位置,不能调整大小) 2. 多选区支持 --- ## 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"); ```