主要更改: - 重写前端 extension.js,完全自己控制图像渲染 - 去掉 nodes.py 的 image_upload,避免双图像问题 - 实现选区框绘制(橙色)+ 遮罩区域显示(红色) - 实现选区框拖动功能 - 修复 onMouseDown 参数格式: (e, pos, canvas) - 更新文档和开发日志
153 lines
4.6 KiB
Python
153 lines
4.6 KiB
Python
"""
|
||
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")
|