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

153 lines
4.6 KiB
Python
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 Node
增强版遮罩编辑器,支持 Photopea 集成和可拖动选区
概念说明:
- 遮罩 (Mask): 定义重绘区域,AI 在这个区域内生成新内容
- 选区 (Region): 定义参考区域,AI 参考这个区域内的原图内容
约束:选区必须 >= 遮罩
"""
import torch
import numpy as np
import folder_paths
from PIL import Image
import os
import json
class InpaintRegionEditor:
"""
增强版遮罩编辑器,支持 Photopea 集成和可拖动选区
"""
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [
f
for f in os.listdir(input_dir)
if os.path.isfile(os.path.join(input_dir, f))
]
files = folder_paths.filter_files_content_types(files, ["image"])
return {
"required": {
"image": (sorted(files), {}),
"padding": ("INT", {"default": 64, "min": 0, "max": 512, "step": 32}),
},
"hidden": {
"region_coords": "STRING",
},
}
RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT", "INT", "INT")
RETURN_NAMES = (
"image",
"mask",
"region_top",
"region_left",
"region_width",
"region_height",
)
FUNCTION = "process"
CATEGORY = "image/inpaint"
def process(self, image, padding, region_coords=None):
image_path = folder_paths.get_annotated_filepath(image)
img = Image.open(image_path)
img_width, img_height = img.size
# 提取 MASK(从 Alpha 通道)
if "A" in img.getbands():
mask = np.array(img.getchannel("A")).astype(np.float32) / 255.0
mask = 1.0 - mask # ComfyUI 约定:1=遮罩区域
else:
raise ValueError(
"请先创建蒙版(局部重绘需要蒙版)\n"
"可以通过以下方式创建:\n"
"1. 上传带 Alpha 通道的 PNG\n"
"2. 使用 Photopea 编辑后保存\n"
"3. 使用 ComfyUI 系统蒙版工具绘制"
)
# 计算蒙版边界框
mask_bounds = self._calculate_mask_bounds(mask)
# 解析前端坐标(如果用户拖动过)
region_rect = None
if region_coords:
try:
region_rect = json.loads(region_coords)
except:
pass
if not region_rect:
region_rect = self._calculate_region_rect(mask_bounds, padding)
# 验证
if (
region_rect["width"] < mask_bounds["width"]
or region_rect["height"] < mask_bounds["height"]
):
raise ValueError(
f"选区太小!必须 >= 遮罩\n"
f"选区:{region_rect['width']}×{region_rect['height']}\n"
f"遮罩:{mask_bounds['width']}×{mask_bounds['height']}"
)
region_rect = self._clamp_to_image(region_rect, img_width, img_height)
# 转换为 tensor
image_tensor = torch.from_numpy(
np.array(img.convert("RGB")).astype(np.float32) / 255.0
).unsqueeze(0)
mask_tensor = torch.from_numpy(mask).unsqueeze(0)
return (
image_tensor,
mask_tensor,
int(region_rect["y"]),
int(region_rect["x"]),
int(region_rect["width"]),
int(region_rect["height"]),
)
def _calculate_mask_bounds(self, mask):
rows = np.any(mask > 0.5, axis=1)
cols = np.any(mask > 0.5, axis=0)
if not np.any(rows) or not np.any(cols):
return {"x": 0, "y": 0, "width": 0, "height": 0}
rmin, rmax = np.where(rows)[0][[0, -1]]
cmin, cmax = np.where(cols)[0][[0, -1]]
return {
"x": int(cmin),
"y": int(rmin),
"width": int(cmax - cmin + 1),
"height": int(rmax - rmin + 1),
}
def _calculate_region_rect(self, mask_bounds, padding):
return {
"x": mask_bounds["x"] - padding,
"y": mask_bounds["y"] - padding,
"width": mask_bounds["width"] + padding * 2,
"height": mask_bounds["height"] + padding * 2,
}
def _clamp_to_image(self, rect, img_width, img_height):
x = max(0, min(rect["x"], img_width - rect["width"]))
y = max(0, min(rect["y"], img_height - rect["height"]))
return {"x": x, "y": y, "width": rect["width"], "height": rect["height"]}
@classmethod
def IS_CHANGED(s, image, padding, region_coords=None):
return float("NaN")