Files

605 lines
21 KiB
Python

from .md import *
CATEGORY_TYPE = "🎈LAOGOU/Canvas"
def get_canvas_storage():
"""获取FastCanvas节点的数据存储"""
if not hasattr(PromptServer.instance, '_fast_canvas_node_data'):
PromptServer.instance._fast_canvas_node_data = {}
return PromptServer.instance._fast_canvas_node_data
def get_refresh_signals():
"""获取FastCanvas节点的刷新信号缓存"""
if not hasattr(PromptServer.instance, '_fast_canvas_refresh_signals'):
PromptServer.instance._fast_canvas_refresh_signals = {}
return PromptServer.instance._fast_canvas_refresh_signals
class FastCanvasTool:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"bg_img": ("IMAGE",),
},
"optional": {
"img_1": ("IMAGE",),
}
}
RETURN_NAMES = ("fc_data",)
RETURN_TYPES = ("FC_DATA",)
FUNCTION = "process_images"
CATEGORY = CATEGORY_TYPE
def process_images(self, bg_img, **kwargs):
canvas_data = {
"background": None,
"layers": []
}
canvas_data["background"] = {
"id": 0,
"image": tensor_to_base64(bg_img),
"is_background": True,
"size": {
"height": int(bg_img.shape[1]),
"width": int(bg_img.shape[2])
}
}
for key, value in kwargs.items():
if value is not None and key.startswith("img_"):
layer_id = int(key.split('_')[1])
layer_data = {
"id": layer_id,
"image": tensor_to_base64(value),
"is_background": False,
"size": {
"height": int(value.shape[1]),
"width": int(value.shape[2])
}
}
canvas_data["layers"].append(layer_data)
canvas_data["layers"].sort(key=lambda x: x["id"])
return (canvas_data,)
def base64_to_tensor(base64_string):
"""将 base64 图像数据转换为 tensor"""
try:
if ',' in base64_string:
base64_string = base64_string.split(',')[1]
image_data = base64.b64decode(base64_string)
with BytesIO(image_data) as bio:
with Image.open(bio) as image:
if image.mode != 'RGB':
image = image.convert('RGB')
# 转换为numpy数组并归一化
image_np = np.array(image).astype(np.float32) / 255.0
# 处理灰度图像
if image_np.ndim == 2:
image_np = np.stack([image_np] * 3, axis=-1)
# 处理RGBA图像
elif image_np.shape[2] == 4:
image_np = image_np[:, :, :3]
# 确保图像格式正确 [B, H, W, C]
image_np = np.expand_dims(image_np, axis=0)
tensor = torch.from_numpy(image_np).float()
print(f"[Tensor Debug] Converted image to tensor: {tensor.shape}")
return tensor
except Exception as e:
print(f"[Tensor Error] Failed to convert base64 to tensor: {str(e)}")
raise
def toBase64ImgUrl(img):
bytesIO = BytesIO()
img.save(bytesIO, format="png")
img_types = bytesIO.getvalue()
img_base64 = base64.b64encode(img_types)
return f"data:image/png;base64,{img_base64.decode('utf-8')}"
def tensor_to_base64(tensor):
if len(tensor.shape) == 3:
tensor = tensor.unsqueeze(0)
array = np.clip(tensor[0].cpu().numpy() * 255, 0, 255).astype(np.uint8)
if array.shape[-1] == 1:
array = np.repeat(array, 3, axis=-1)
elif array.shape[-1] == 4:
# RGBA -> BGRA
array = array[..., [2,1,0,3]]
else:
# RGB -> BGR
array = array[..., ::-1]
array = np.ascontiguousarray(array)
try:
success, buffer = cv2.imencode('.png', array)
if success:
return f"data:image/png;base64,{base64.b64encode(buffer).decode('utf-8')}"
except Exception as e:
print(f"Error encoding image: {e}")
print(f"Array shape: {array.shape}, dtype: {array.dtype}")
return None
@routes.post("/fast_canvas_all")
async def handle_canvas_data(request):
data = await request.json()
node_id = data.get('node_id')
canvas_storage = get_canvas_storage()
if node_id not in canvas_storage:
print(f"[FastCanvas] 没有找到等待响应的节点")
return web.Response(status=200)
print(f"[FastCanvas] 成功等待节点,准备处理数据")
transform_data = data.get('layer_transforms', {})
main_image = array_to_tensor(data.get('main_image'), "image")
main_mask = array_to_tensor(data.get('main_mask'), "mask")
processed_data = {
'image': main_image,
'mask': main_mask,
'transform_data': transform_data
}
node_info = canvas_storage[node_id]
node_info["processed_data"] = processed_data
node_info["event"].set()
return web.json_response({"status": "success"})
@routes.post("/fast_canvas_refresh_signal")
async def handle_refresh_signal(request):
data = await request.json()
node_id = data.get('node_id')
refresh_signals = get_refresh_signals()
refresh_signals[node_id] = True
print(f"[FastCanvas] 节点{node_id}设置need_update=True")
return web.json_response({"status": "success"})
class FastCanvas:
def __init__(self):
self.node_id = None
self.last_fc_data = None
self.need_update = False # 添加need_update变量
@classmethod
def clean_nodes(cls):
"""清理过期节点"""
canvas_storage = get_canvas_storage()
expired_nodes = []
for node_id, node_info in canvas_storage.items():
if not node_info["waiting_for_response"]:
expired_nodes.append(node_id)
for node_id in expired_nodes:
del canvas_storage[node_id]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"hidden": {"unique_id": "UNIQUE_ID"},
"optional": {
"fc_data": ("FC_DATA",),
"transform_data": ("TRANSFORM_DATA",)
}
}
RETURN_TYPES = ("IMAGE", "MASK", "TRANSFORM_DATA")
RETURN_NAMES = ("image", "mask", "transform_data")
FUNCTION = "canvas_execute"
CATEGORY = CATEGORY_TYPE
OUTPUT_NODE = True
def canvas_execute(self, unique_id, fc_data=None, transform_data=None):
try:
self.node_id = unique_id
# 检查刷新信号
refresh_signals = get_refresh_signals()
need_update = refresh_signals.pop(unique_id, False)
canvas_storage = get_canvas_storage()
event = Event()
canvas_storage[unique_id] = {
"event": event,
"processed_data": None,
"waiting_for_response": True
}
# 根据刷新信号或fc_data决定发送哪种事件
if need_update or (fc_data is not None and (not hasattr(self, 'last_fc_data') or self.last_fc_data != fc_data)):
PromptServer.instance.send_sync(
"fast_canvas_update", {
"node_id": unique_id,
"canvas_data": fc_data,
"transform_data": transform_data # 传递transform_data到前端
}
)
self.last_fc_data = fc_data
else:
PromptServer.instance.send_sync(
"fast_canvas_get_state", {
"node_id": unique_id
}
)
if not event.wait(timeout=30):
if unique_id in canvas_storage:
canvas_storage[unique_id]["waiting_for_response"] = False
FastCanvas.clean_nodes()
return None, None, None
node_info = canvas_storage.get(unique_id, {})
processed_data = node_info.get("processed_data")
if unique_id in canvas_storage:
canvas_storage[unique_id]["waiting_for_response"] = False
FastCanvas.clean_nodes()
if processed_data:
image = processed_data.get('image')
mask = processed_data.get('mask')
transform_data = processed_data.get('transform_data', {})
if image is not None:
bg_height, bg_width = image.shape[1:3]
transform_data['background'] = {
'width': bg_width,
'height': bg_height
}
return image, mask, transform_data
return None, None, None
except Exception as e:
print(f"[FastCanvas] 处理过程发生异常: {str(e)}")
canvas_storage = get_canvas_storage()
if unique_id in canvas_storage:
canvas_storage[unique_id]["waiting_for_response"] = False
FastCanvas.clean_nodes()
return None, None, None
def __del__(self):
# 确保从存储中删除节点数据
canvas_storage = get_canvas_storage()
if self.node_id and self.node_id in canvas_storage:
del canvas_storage[self.node_id]
def array_to_tensor(array_data, data_type):
try:
if array_data is None:
return None
byte_data = bytes(array_data)
image = Image.open(BytesIO(byte_data))
if data_type == "mask":
if 'A' in image.getbands():
mask = np.array(image.getchannel('A')).astype(np.float32) / 255.0
mask = torch.from_numpy(mask)
else:
mask = torch.zeros((image.height, image.width), dtype=torch.float32)
return mask.unsqueeze(0)
elif data_type == "image":
if image.mode != 'RGB':
image = image.convert('RGB')
image = np.array(image).astype(np.float32) / 255.0
return torch.from_numpy(image)[None,]
return None
except Exception as e:
print(f"[FastCanvas] Error in array_to_tensor: {str(e)}")
return None
class FastCanvasComposite:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"bg_img": ("IMAGE",),
"image": ("IMAGE",),
"mask": ("MASK",),
"transform_data": ("TRANSFORM_DATA",),
"invert_mask": ("BOOLEAN", {"default": False}),
"mode": ("BOOLEAN", {
"default": False,
"label_on": "HD Restore",
"label_off": "Inherit Mode"
}),
"offset_x": ("INT", {"default": 0, "min": -1000, "max": 1000, "step": 1}), # 添加X轴偏移
"offset_y": ("INT", {"default": 0, "min": -1000, "max": 1000, "step": 1}), # 添加Y轴偏移
}
}
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("image", "mask")
FUNCTION = "composite"
CATEGORY = CATEGORY_TYPE
def tensor2pil(self, tensor):
# 确保张量是 [H, W, C] 格式
if len(tensor.shape) == 4:
tensor = tensor[0] # 移除批次维度
# 转换到 0-255 范围
tensor = (torch.clamp(tensor, 0, 1) * 255).byte()
# 转换为PIL图像
return Image.fromarray(tensor.cpu().numpy())
def pil2tensor(self, image):
# 转换为numpy数组
np_image = np.array(image).astype(np.float32) / 255.0
# 转换为tensor并添加批次维度
return torch.from_numpy(np_image).unsqueeze(0)
def calculate_hd_scale(self, transform_data, fg_width, fg_height):
"""计算高清还原模式的缩放比例"""
# 获取图层数据
layer_data = next(trans for key, trans in transform_data.items() if key != 'background')
# 计算变换后的尺寸
transformed_width = layer_data['width'] * layer_data['scaleX']
transformed_height = layer_data['height'] * layer_data['scaleY']
# 计算放大系数
scale_x = fg_width / transformed_width
scale_y = fg_height / transformed_height
return min(scale_x, scale_y)
def scale_hd_transform(self, transform_data, scale):
"""调整高清还原模式的变换数据"""
new_data = {'background': transform_data['background']}
# 获取并处理图层数据(排除background后只剩一个)
layer_id = next(key for key in transform_data.keys() if key != 'background')
layer_data = transform_data[layer_id]
new_data[layer_id] = {
'centerX': layer_data['centerX'] * scale,
'centerY': layer_data['centerY'] * scale,
'scaleX': layer_data['scaleX'] * scale,
'scaleY': layer_data['scaleY'] * scale,
'angle': layer_data['angle'],
'width': layer_data['width'],
'height': layer_data['height'],
'flipX': layer_data['flipX'],
'flipY': layer_data['flipY']
}
return new_data
def composite(self, bg_img, image, mask, transform_data, mode=False, invert_mask=False, offset_x=0, offset_y=0):
try:
# 确保所有输入都是批次格式 [B, H, W, C] 或 [B, H, W]
if bg_img.dim() == 3:
bg_img = bg_img.unsqueeze(0)
if image.dim() == 3:
image = image.unsqueeze(0)
if mask.dim() == 2:
mask = mask.unsqueeze(0)
# 如果输入是 [B, C, H, W] 格式,转换为 [B, H, W, C]
if bg_img.shape[1] == 3 or bg_img.shape[1] == 4:
bg_img = bg_img.permute(0, 2, 3, 1)
if image.shape[1] == 3 or image.shape[1] == 4:
image = image.permute(0, 2, 3, 1)
# 获取批次大小
batch_size = bg_img.shape[0]
# 创建结果列表
result_tensors = []
mask_tensors = []
# 对每个批次进行处理
for i in range(batch_size):
# 转换当前批次的图像到PIL格式
bg_pil = self.tensor2pil(bg_img[i:i+1])
fg_pil = self.tensor2pil(image[i:i+1])
# 获取当前批次的transform_data
current_transform = transform_data[i] if isinstance(transform_data, list) else transform_data
# 获取原始目标尺寸
target_width = current_transform['background']['width']
target_height = current_transform['background']['height']
# 处理高清还原模式
if mode:
scale = self.calculate_hd_scale(current_transform, fg_pil.width, fg_pil.height)
target_width = round(target_width * scale)
target_height = round(target_height * scale)
current_transform = self.scale_hd_transform(current_transform, scale)
# 将背景图片缩放到目标尺寸
bg_pil = bg_pil.resize((target_width, target_height), Image.LANCZOS)
# 处理遮罩
current_mask = mask[i] if mask.dim() == 3 else mask[i:i+1]
mask_pil = Image.fromarray(np.clip(current_mask.cpu().numpy() * 255, 0, 255).astype(np.uint8), 'L')
if invert_mask:
mask_pil = ImageOps.invert(mask_pil)
# 创建结果画布
result = bg_pil.copy()
result_mask = Image.new('L', bg_pil.size, 0)
# 处理每个变换数据
for layer_id, trans in current_transform.items():
if layer_id == 'background':
continue
# 获取原始尺寸
orig_width = trans.get('width', fg_pil.width)
orig_height = trans.get('height', fg_pil.height)
# 获取变换参数
scale_x = trans.get('scaleX', 1)
scale_y = trans.get('scaleY', 1)
angle = trans.get('angle', 0)
center_x = trans.get('centerX', 0)
center_y = trans.get('centerY', 0)
flip_x = trans.get('flipX', False)
flip_y = trans.get('flipY', False)
# 计算实际尺寸
new_width = int(orig_width * scale_x)
new_height = int(orig_height * scale_y)
# 缩放图像和遮罩
transformed_fg = fg_pil.resize((new_width, new_height), Image.LANCZOS)
transformed_mask = mask_pil.resize((new_width, new_height), Image.LANCZOS)
# 处理翻转
if flip_x:
transformed_fg = transformed_fg.transpose(Image.FLIP_LEFT_RIGHT)
transformed_mask = transformed_mask.transpose(Image.FLIP_LEFT_RIGHT)
if flip_y:
transformed_fg = transformed_fg.transpose(Image.FLIP_TOP_BOTTOM)
transformed_mask = transformed_mask.transpose(Image.FLIP_TOP_BOTTOM)
# 处理旋转
if angle != 0:
transformed_fg = transformed_fg.rotate(-angle, expand=True, resample=Image.BICUBIC)
transformed_mask = transformed_mask.rotate(-angle, expand=True, resample=Image.BICUBIC)
# 获取最终尺寸
current_width = transformed_fg.width
current_height = transformed_fg.height
# 计算粘贴位置(添加偏移量)
paste_x = int(center_x - current_width / 2) + offset_x
paste_y = int(center_y - current_height / 2) + offset_y
# 合成图像
result.paste(transformed_fg, (paste_x, paste_y), transformed_mask)
result_mask.paste(transformed_mask, (paste_x, paste_y))
# 将处理后的结果添加到列表中
result_tensors.append(self.pil2tensor(result))
mask_tensor = torch.from_numpy(np.array(result_mask)).float() / 255.0
mask_tensors.append(mask_tensor.unsqueeze(0))
# 合并所有批次的结果
final_result = torch.cat(result_tensors, dim=0)
final_mask = torch.cat(mask_tensors, dim=0)
return (final_result, final_mask)
except Exception as e:
print(f"合成失败: {str(e)}")
print(f"背景图像形状: {bg_img.shape}")
print(f"前景图像形状: {image.shape}")
print(f"遮罩形状: {mask.shape}")
import traceback
traceback.print_exc()
return (bg_img, torch.ones_like(mask[0]))
class TransformDataFromString:
"""从字符串创建 transform_data"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json_string": ("STRING", {
"multiline": True,
"default": """{
"background": {
"width": 1024,
"height": 768
},
"1": {
"centerX": 512,
"centerY": 384,
"scaleX": 1.0,
"scaleY": 1.0,
"angle": 0,
"flipX": false,
"flipY": false,
"width": 256,
"height": 256
}
}"""
}),
}
}
RETURN_TYPES = ("TRANSFORM_DATA",)
RETURN_NAMES = ("transform_data",)
FUNCTION = "create_transform_data"
CATEGORY = CATEGORY_TYPE
def create_transform_data(self, json_string):
try:
import json
# 解析 JSON 字符串
transform_data = json.loads(json_string)
# 验证基本结构
if not isinstance(transform_data, dict):
raise ValueError("transform_data 必须是一个字典对象")
print(f"[TransformDataFromString] 成功解析 transform_data: {transform_data}")
return (transform_data,)
except json.JSONDecodeError as e:
print(f"[TransformDataFromString] JSON 解析错误: {str(e)}")
# 返回一个默认的空 transform_data
return ({},)
except Exception as e:
print(f"[TransformDataFromString] 错误: {str(e)}")
return ({},)
NODE_CLASS_MAPPINGS = {
"FastCanvasTool": FastCanvasTool,
"FastCanvas": FastCanvas,
"FastCanvasComposite": FastCanvasComposite,
"TransformDataFromString": TransformDataFromString,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"FastCanvasTool": "🎈FastCanvasTool",
"FastCanvas": "🎈FastCanvas",
"FastCanvasComposite": "🎈FastCanvasComposite",
"TransformDataFromString": "🎈TransformData From String",
}