636 lines
24 KiB
Python
636 lines
24 KiB
Python
import base64
|
||
import copy
|
||
import io
|
||
import os
|
||
|
||
import numpy as np
|
||
import torch
|
||
from PIL import ImageOps, Image, ImageSequence
|
||
|
||
import folder_paths
|
||
import node_helpers
|
||
from nodes import LoadImage
|
||
from comfy.cli_args import args
|
||
from PIL.PngImagePlugin import PngInfo
|
||
import json
|
||
from json import JSONEncoder, JSONDecoder
|
||
from .util import tensor_to_pil, pil_to_tensor, base64_to_image, image_to_base64, read_image_from_url
|
||
|
||
|
||
class LoadImageFromURL:
|
||
"""
|
||
从远程地址读取图片
|
||
"""
|
||
@classmethod
|
||
def INPUT_TYPES(self):
|
||
return {"required": {
|
||
"urls": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": False}),
|
||
},
|
||
}
|
||
|
||
RETURN_TYPES = ("IMAGE", "MASK")
|
||
RETURN_NAMES = ("images", "masks")
|
||
|
||
FUNCTION = "convert"
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
# INPUT_IS_LIST = False
|
||
OUTPUT_IS_LIST = (True, True,)
|
||
|
||
def convert(self, urls):
|
||
urls = urls.splitlines()
|
||
images = []
|
||
masks = []
|
||
for url in urls:
|
||
if not url.strip().isspace():
|
||
i = read_image_from_url(url.strip())
|
||
i = ImageOps.exif_transpose(i)
|
||
if i.mode == 'I':
|
||
i = i.point(lambda i: i * (1 / 255))
|
||
image = i.convert("RGB")
|
||
image = pil_to_tensor(image)
|
||
images.append(image)
|
||
if 'A' in i.getbands():
|
||
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
|
||
mask = 1. - torch.from_numpy(mask)
|
||
else:
|
||
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
|
||
masks.append(mask.unsqueeze(0))
|
||
|
||
return (images, masks, )
|
||
|
||
|
||
class LoadMaskFromURL:
|
||
"""
|
||
从远程地址读取图片
|
||
"""
|
||
_color_channels = ["red", "green", "blue", "alpha"]
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(self):
|
||
return {
|
||
"required": {
|
||
"urls": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": False}),
|
||
"channel": (self._color_channels, {"default": self._color_channels[0]}),
|
||
},
|
||
}
|
||
|
||
RETURN_TYPES = ("MASK", )
|
||
RETURN_NAMES = ("masks", )
|
||
|
||
FUNCTION = "convert"
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
# INPUT_IS_LIST = False
|
||
OUTPUT_IS_LIST = (True, True,)
|
||
|
||
def convert(self, urls, channel=_color_channels[0]):
|
||
urls = urls.splitlines()
|
||
masks = []
|
||
for url in urls:
|
||
if not url.strip().isspace():
|
||
i = read_image_from_url(url.strip())
|
||
# 下面代码参考LoadImage
|
||
i = ImageOps.exif_transpose(i)
|
||
if i.getbands() != ("R", "G", "B", "A"):
|
||
i = i.convert("RGBA")
|
||
c = channel[0].upper()
|
||
if c in i.getbands():
|
||
mask = np.array(i.getchannel(c)).astype(np.float32) / 255.0
|
||
mask = torch.from_numpy(mask)
|
||
if c == 'A':
|
||
mask = 1. - mask
|
||
else:
|
||
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
|
||
masks.append(mask.unsqueeze(0))
|
||
return (masks,)
|
||
|
||
|
||
class Base64ToImage:
|
||
"""
|
||
图片的base64格式还原成图片的张量
|
||
"""
|
||
@classmethod
|
||
def INPUT_TYPES(self):
|
||
return {"required": {
|
||
"base64Images": ("STRING", {"multiline": True, "default": "[\"\"]", "dynamicPrompts": False}),
|
||
},
|
||
}
|
||
|
||
RETURN_TYPES = ("IMAGE", "MASK")
|
||
# RETURN_NAMES = ("image", "mask")
|
||
|
||
FUNCTION = "convert"
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
# INPUT_IS_LIST = False
|
||
OUTPUT_IS_LIST = (True, True)
|
||
|
||
def convert(self, base64Images):
|
||
# print(base64Image)
|
||
base64ImageJson = JSONDecoder().decode(s=base64Images)
|
||
images = []
|
||
masks = []
|
||
for base64Image in base64ImageJson:
|
||
i = base64_to_image(base64Image)
|
||
# 下面代码参考LoadImage
|
||
i = ImageOps.exif_transpose(i)
|
||
image = i.convert("RGB")
|
||
image = np.array(image).astype(np.float32) / 255.0
|
||
image = torch.from_numpy(image)[None, ]
|
||
if 'A' in i.getbands():
|
||
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
|
||
mask = 1. - torch.from_numpy(mask)
|
||
else:
|
||
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
|
||
images.append(image)
|
||
masks.append(mask.unsqueeze(0))
|
||
|
||
return (images, masks,)
|
||
|
||
|
||
class ImageToBase64Advanced:
|
||
def __init__(self):
|
||
self.imageType = "image"
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(self):
|
||
return {"required": {
|
||
"images": ("IMAGE",),
|
||
"imageType": (["image", "mask"], {"default": "image"}),
|
||
},
|
||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||
}
|
||
|
||
RETURN_TYPES = ("STRING",)
|
||
RETURN_NAMES = ("base64Images",)
|
||
|
||
FUNCTION = "convert"
|
||
# 作为输出节点,返回数据格式是{"ui": {output_name:value}, "result": (value,)}
|
||
# ui中是websocket返回给前端的内容,result是py执行传给下个节点用的
|
||
OUTPUT_NODE = True
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
# INPUT_IS_LIST = False
|
||
# OUTPUT_IS_LIST = (False,False,)
|
||
|
||
def convert(self, images, imageType=None, prompt=None, extra_pnginfo=None):
|
||
if imageType is None:
|
||
imageType = self.imageType
|
||
|
||
result = list()
|
||
for i in images:
|
||
img = tensor_to_pil(i)
|
||
metadata = None
|
||
if not args.disable_metadata:
|
||
metadata = PngInfo()
|
||
if prompt is not None:
|
||
newPrompt = copy.deepcopy(prompt)
|
||
for idx in newPrompt:
|
||
node = newPrompt[idx]
|
||
if node['class_type'] == 'Base64ToImage' or node['class_type'] == 'Base64ToMask':
|
||
node['inputs']['base64Images'] = ""
|
||
metadata.add_text("prompt", json.dumps(newPrompt))
|
||
if extra_pnginfo is not None:
|
||
for x in extra_pnginfo:
|
||
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
||
|
||
# 将图像数据编码为Base64字符串
|
||
encoded_image = image_to_base64(img, pnginfo=metadata)
|
||
result.append(encoded_image)
|
||
base64Images = JSONEncoder().encode(result)
|
||
# print(images)
|
||
return {"ui": {"base64Images": result, "imageType": [imageType]}, "result": (base64Images,)}
|
||
|
||
|
||
class ImageToBase64(ImageToBase64Advanced):
|
||
def __init__(self):
|
||
self.imageType = "image"
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(self):
|
||
return {"required": {
|
||
"images": ("IMAGE",),
|
||
},
|
||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||
}
|
||
|
||
|
||
class MaskImageToBase64(ImageToBase64):
|
||
def __init__(self):
|
||
self.imageType = "mask"
|
||
|
||
|
||
class MaskToBase64Image(MaskImageToBase64):
|
||
@classmethod
|
||
def INPUT_TYPES(s):
|
||
return {
|
||
"required": {
|
||
"mask": ("MASK",),
|
||
}
|
||
}
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
RETURN_TYPES = ("STRING",)
|
||
FUNCTION = "mask_to_base64image"
|
||
|
||
def mask_to_base64image(self, mask):
|
||
"""将一个二维的掩码张量扩展为一个四维的彩色图像张量。具体的步骤如下:
|
||
|
||
第一行,使用 torch.reshape 函数,将掩码张量的形状改变为(-1, 1, mask.shape[-2], mask.shape[-1]),
|
||
其中 - 1 表示自动推断该维度的大小,1 表示增加一个新的维度,mask.shape[-2] 和 mask.shape[-1] 表示保持原来的最后两个维度不变。
|
||
这样,掩码张量就变成了一个四维的张量,其中第二个维度只有一个通道。
|
||
|
||
第二行,使用 torch.movedim 函数,将掩码张量的第二个维度(通道维度)移动到最后一个维度的位置,即将形状为(-1, 1, mask.shape[-2], mask.shape[-1])
|
||
的张量变为(-1, mask.shape[-2], mask.shape[-1], 1) 的张量。这样,掩码张量就变成了一个符合图像格式的张量,其中最后一个维度表示通道数。
|
||
|
||
第三行,使用 torch.Tensor.expand 函数,将掩码张量的最后一个维度(通道维度)扩展为 3,即将形状为(-1, mask.shape[-2], mask.shape[-1], 1) 的张量变为(-1, mask.shape[-2], mask.shape[-1], 3) 的张量。这样,掩码张量就变成了一个彩色图像张量,其中最后一个维度表示红、绿、蓝三个通道。
|
||
|
||
这段代码的结果是一个与原来的掩码张量相同元素的彩色图像张量,表示掩码的颜色
|
||
"""
|
||
images = mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])).movedim(1, -1).expand(-1, -1, -1, 3)
|
||
return super().convert(images)
|
||
|
||
|
||
class MaskToBase64(MaskImageToBase64):
|
||
@classmethod
|
||
def INPUT_TYPES(s):
|
||
return {
|
||
"required": {
|
||
"mask": ("MASK",),
|
||
}
|
||
}
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
RETURN_TYPES = ("STRING",)
|
||
FUNCTION = "mask_to_base64image"
|
||
|
||
def mask_to_base64image(self, mask):
|
||
return super().convert(mask)
|
||
|
||
|
||
class Base64ToMask:
|
||
"""
|
||
mask的base64图片还原成mask的张量
|
||
"""
|
||
_color_channels = ["red", "green", "blue", "alpha"]
|
||
@classmethod
|
||
def INPUT_TYPES(s):
|
||
return {
|
||
"required": {
|
||
# "base64Images": ("STRING", {"forceInput": True}),
|
||
"base64Images": ("STRING", {"multiline": True, "default": "[\"\"]", "dynamicPrompts": False}),
|
||
"channel": (s._color_channels, {"default": s._color_channels[0]}), }
|
||
}
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
RETURN_TYPES = ("MASK",)
|
||
FUNCTION = "base64image_to_mask"
|
||
|
||
def base64image_to_mask(self, base64Images, channel=_color_channels[0]):
|
||
base64ImageJson = JSONDecoder().decode(s=base64Images)
|
||
for base64Image in base64ImageJson:
|
||
i = base64_to_image(base64Image)
|
||
# 下面代码参考LoadImage
|
||
i = ImageOps.exif_transpose(i)
|
||
if i.getbands() != ("R", "G", "B", "A"):
|
||
i = i.convert("RGBA")
|
||
mask = None
|
||
c = channel[0].upper()
|
||
if c in i.getbands():
|
||
mask = np.array(i.getchannel(c)).astype(np.float32) / 255.0
|
||
mask = torch.from_numpy(mask)
|
||
if c == 'A':
|
||
mask = 1. - mask
|
||
else:
|
||
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
|
||
|
||
return (mask.unsqueeze(0),)
|
||
|
||
|
||
class LoadImageToBase64(LoadImage):
|
||
RETURN_TYPES = ("STRING", "IMAGE", "MASK", )
|
||
RETURN_NAMES = ("base64Images", "IMAGE", "MASK", )
|
||
|
||
FUNCTION = "convert"
|
||
OUTPUT_NODE = True
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
# INPUT_IS_LIST = False
|
||
# OUTPUT_IS_LIST = (False,False,)
|
||
|
||
def convert(self, image):
|
||
img, mask = self.load_image(image)
|
||
|
||
i = tensor_to_pil(img)
|
||
# 创建一个BytesIO对象,用于临时存储图像数据
|
||
image_data = io.BytesIO()
|
||
|
||
# 将图像保存到BytesIO对象中,格式为PNG
|
||
i.save(image_data, format='PNG')
|
||
|
||
# 将BytesIO对象的内容转换为字节串
|
||
image_data_bytes = image_data.getvalue()
|
||
|
||
# 将图像数据编码为Base64字符串
|
||
encoded_image = "[\"data:image/png;base64," + base64.b64encode(image_data_bytes).decode('utf-8') + "\"]"
|
||
return encoded_image, img, mask
|
||
|
||
|
||
class LoadImageFromLocalPath:
|
||
@classmethod
|
||
def INPUT_TYPES(s):
|
||
return {"required":
|
||
{
|
||
"image_path": ("STRING", {"default": ""},)
|
||
},
|
||
}
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
RETURN_TYPES = ("IMAGE", "MASK")
|
||
FUNCTION = "load_image"
|
||
def load_image(self, image_path):
|
||
|
||
img = node_helpers.pillow(Image.open, image_path)
|
||
|
||
output_images = []
|
||
output_masks = []
|
||
w, h = None, None
|
||
|
||
excluded_formats = ['MPO']
|
||
# 遍历图像的每一帧
|
||
for i in ImageSequence.Iterator(img):
|
||
# 旋转图像
|
||
i = node_helpers.pillow(ImageOps.exif_transpose, i)
|
||
|
||
if i.mode == 'I':
|
||
i = i.point(lambda i: i * (1 / 255))
|
||
# 将图像转换为RGB格式
|
||
image = i.convert("RGB")
|
||
|
||
if len(output_images) == 0:
|
||
w = image.size[0]
|
||
h = image.size[1]
|
||
|
||
if image.size[0] != w or image.size[1] != h:
|
||
continue
|
||
|
||
# 将图像转换为浮点数组 (H,W,Channel)
|
||
image = np.array(image).astype(np.float32) / 255.0
|
||
# 先把图片转成3维张量,并再在最前面添加一个维度,变成4维(1, H, W,Channel)
|
||
image = torch.from_numpy(image)[None,]
|
||
# 如果图像包含alpha通道,则将其转换为掩码
|
||
if 'A' in i.getbands():
|
||
# 计算后结果数组中透明像素会是0
|
||
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
|
||
# 把数组中透明像素设为1
|
||
mask = 1. - torch.from_numpy(mask)
|
||
else:
|
||
# 否则,创建一个64x64的零张量作为掩码
|
||
mask = torch.zeros((64, 64,), dtype=torch.float32, device="cpu")
|
||
# 将图像和掩码添加到输出列表中
|
||
output_images.append(image)
|
||
output_masks.append(mask.unsqueeze(0))
|
||
|
||
if len(output_images) > 1 and img.format not in excluded_formats:
|
||
# 如果有多个图像,则将它们按维度0拼接在一起
|
||
output_image = torch.cat(output_images, dim=0)
|
||
output_mask = torch.cat(output_masks, dim=0)
|
||
# 否则,返回单个图像和掩码
|
||
else:
|
||
output_image = output_images[0]
|
||
output_mask = output_masks[0]
|
||
# 返回输出图像和掩码
|
||
return (output_image, output_mask)
|
||
|
||
|
||
class LoadMaskFromLocalPath:
|
||
_color_channels = ["alpha", "red", "green", "blue"]
|
||
@classmethod
|
||
def INPUT_TYPES(s):
|
||
return {"required":
|
||
{
|
||
"image_path": ("STRING", {"default": ""}),
|
||
"channel": (s._color_channels, ),
|
||
}
|
||
}
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
RETURN_TYPES = ("MASK",)
|
||
FUNCTION = "load_mask"
|
||
def load_mask(self, image_path, channel):
|
||
i = node_helpers.pillow(Image.open, image_path)
|
||
i = node_helpers.pillow(ImageOps.exif_transpose, i)
|
||
if i.getbands() != ("R", "G", "B", "A"):
|
||
if i.mode == 'I':
|
||
i = i.point(lambda i: i * (1 / 255))
|
||
i = i.convert("RGBA")
|
||
mask = None
|
||
c = channel[0].upper()
|
||
if c in i.getbands():
|
||
mask = np.array(i.getchannel(c)).astype(np.float32) / 255.0
|
||
mask = torch.from_numpy(mask)
|
||
if c == 'A':
|
||
mask = 1. - mask
|
||
else:
|
||
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
|
||
return (mask.unsqueeze(0),)
|
||
|
||
|
||
class SaveImagesWithoutOutput:
|
||
"""
|
||
保存图片,非输出节点
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.compress_level = 4
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(self):
|
||
return {
|
||
"required": {
|
||
"images": ("IMAGE",),
|
||
"filename_prefix": ("STRING", {"default": "ComfyUI",
|
||
"tooltip": "要保存的文件的前缀。支持的占位符:%width% %height% %year% %month% %day% %hour% %minute% %second%"}),
|
||
"output_dir": ("STRING", {"default": "", "tooltip": "若为空,存放到output目录"}),
|
||
},
|
||
"optional": {
|
||
"addMetadata": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
|
||
},
|
||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||
}
|
||
|
||
RETURN_TYPES = ("STRING", )
|
||
RETURN_NAMES = ("file_paths",)
|
||
OUTPUT_TOOLTIPS = ("保存的图片路径列表",)
|
||
|
||
FUNCTION = "save_images"
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
DESCRIPTION = "保存图像到指定目录,可根据返回的文件路径进行后续操作,此节点为非输出节点,适合批量处理和用于惰性求值的前置节点"
|
||
OUTPUT_NODE = False
|
||
|
||
def save_images(self, images, output_dir, filename_prefix="ComfyUI", addMetadata=False, prompt=None, extra_pnginfo=None):
|
||
imageList = list()
|
||
if not isinstance(images, list):
|
||
imageList.append(images)
|
||
else:
|
||
imageList = images
|
||
|
||
if output_dir is None or len(output_dir.strip()) == 0:
|
||
output_dir = folder_paths.get_output_directory()
|
||
|
||
results = list()
|
||
for (index, images) in enumerate(imageList):
|
||
for (batch_number, image) in enumerate(images):
|
||
full_output_folder, filename, counter, subfolder, curr_filename_prefix = folder_paths.get_save_image_path(
|
||
filename_prefix, output_dir, image.shape[1], image.shape[0])
|
||
img = tensor_to_pil(image)
|
||
metadata = None
|
||
if not args.disable_metadata and addMetadata:
|
||
metadata = PngInfo()
|
||
if prompt is not None:
|
||
metadata.add_text("prompt", json.dumps(prompt))
|
||
if extra_pnginfo is not None:
|
||
for x in extra_pnginfo:
|
||
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
||
|
||
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
|
||
file = f"{filename_with_batch_num}_{counter:05}_.png"
|
||
image_save_path = os.path.join(full_output_folder, file)
|
||
img.save(image_save_path, pnginfo=metadata, compress_level=self.compress_level)
|
||
results.append(image_save_path)
|
||
counter += 1
|
||
|
||
return (results,)
|
||
|
||
|
||
class SaveSingleImageWithoutOutput:
|
||
"""
|
||
保存图片,非输出节点
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.compress_level = 4
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(self):
|
||
return {
|
||
"required": {
|
||
"image": ("IMAGE",),
|
||
"filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "要保存的文件的前缀。可以使用格式化信息,如%date:yyyy-MM-dd%或%Empty Latent Image.width%"}),
|
||
"full_file_name": ("STRING", {"default": "", "tooltip": "完整的相对路径文件名,包括扩展名。若为空,则使用filename_prefix生成带序号的文件名"}),
|
||
"output_dir": ("STRING", {"default": "", "tooltip": "目标目录(绝对路径),不会自动创建。若为空,存放到output目录"}),
|
||
},
|
||
"optional": {
|
||
"addMetadata": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
|
||
},
|
||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||
}
|
||
|
||
RETURN_TYPES = ("STRING", )
|
||
RETURN_NAMES = ("file_path",)
|
||
|
||
FUNCTION = "save_image"
|
||
|
||
CATEGORY = "EasyApi/Image"
|
||
|
||
DESCRIPTION = "保存图像到指定目录,可根据返回的文件路径进行后续操作,此节点为非输出节点,适合循环批处理和用于惰性求值的前置节点。只会处理一个"
|
||
OUTPUT_NODE = False
|
||
|
||
def save_image(self, image, full_file_name, output_dir, filename_prefix="ComfyUI", addMetadata=False, prompt=None, extra_pnginfo=None):
|
||
imageList = list()
|
||
if not isinstance(image, list):
|
||
imageList.append(image)
|
||
else:
|
||
imageList = image
|
||
|
||
if output_dir is None or len(output_dir.strip()) == 0:
|
||
output_dir = folder_paths.get_output_directory()
|
||
|
||
if not os.path.isdir(output_dir) or not os.path.isabs(output_dir):
|
||
raise RuntimeError(f"目录 {output_dir} 不存在")
|
||
|
||
if len(imageList) > 0:
|
||
image = imageList[0]
|
||
for (batch_number, image) in enumerate(image):
|
||
img = tensor_to_pil(image)
|
||
metadata = None
|
||
if not args.disable_metadata and addMetadata:
|
||
metadata = PngInfo()
|
||
if prompt is not None:
|
||
metadata.add_text("prompt", json.dumps(prompt))
|
||
if extra_pnginfo is not None:
|
||
for x in extra_pnginfo:
|
||
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
|
||
|
||
if full_file_name is not None and len(full_file_name.strip()) > 0:
|
||
# full_file_name是相对路径,添加校验,并自动创建子目录
|
||
full_path = os.path.join(output_dir, full_file_name)
|
||
full_normpath_name = os.path.normpath(full_path)
|
||
file_dir = os.path.dirname(full_normpath_name)
|
||
# 确保路径是out_dir 的子目录
|
||
if not os.path.isabs(file_dir) or not file_dir.startswith(output_dir):
|
||
raise RuntimeError(f"文件 {full_file_name} 不在 {output_dir} 目录下")
|
||
if not os.path.isdir(file_dir):
|
||
os.makedirs(file_dir, exist_ok=True)
|
||
image_save_path = full_normpath_name
|
||
else:
|
||
full_output_folder, filename, counter, subfolder, curr_filename_prefix = folder_paths.get_save_image_path(
|
||
filename_prefix, output_dir, image.shape[1], image.shape[0])
|
||
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
|
||
file = f"{filename_with_batch_num}_{counter:05}_.png"
|
||
image_save_path = os.path.join(full_output_folder, file)
|
||
|
||
img.save(image_save_path, pnginfo=metadata, compress_level=self.compress_level)
|
||
return image_save_path,
|
||
|
||
return (None,)
|
||
|
||
|
||
NODE_CLASS_MAPPINGS = {
|
||
"Base64ToImage": Base64ToImage,
|
||
"LoadImageFromURL": LoadImageFromURL,
|
||
"LoadMaskFromURL": LoadMaskFromURL,
|
||
"ImageToBase64": ImageToBase64,
|
||
# "MaskToBase64": MaskToBase64,
|
||
"Base64ToMask": Base64ToMask,
|
||
"ImageToBase64Advanced": ImageToBase64Advanced,
|
||
"MaskToBase64Image": MaskToBase64Image,
|
||
"MaskImageToBase64": MaskImageToBase64,
|
||
"LoadImageToBase64": LoadImageToBase64,
|
||
"LoadImageFromLocalPath": LoadImageFromLocalPath,
|
||
"LoadMaskFromLocalPath": LoadMaskFromLocalPath,
|
||
"SaveImagesWithoutOutput": SaveImagesWithoutOutput,
|
||
"SaveSingleImageWithoutOutput": SaveSingleImageWithoutOutput,
|
||
}
|
||
|
||
# A dictionary that contains the friendly/humanly readable titles for the nodes
|
||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||
"Base64ToImage": "Base64 To Image",
|
||
"LoadImageFromURL": "Load Image From Url",
|
||
"LoadMaskFromURL": "Load Image From Url (As Mask)",
|
||
"ImageToBase64": "Image To Base64",
|
||
# "MaskToBase64": "Mask To Base64",
|
||
"Base64ToMask": "Base64 To Mask",
|
||
"ImageToBase64Advanced": "Image To Base64 (Advanced)",
|
||
"MaskToBase64Image": "Mask To Base64 Image",
|
||
"MaskImageToBase64": "Mask Image To Base64",
|
||
"LoadImageToBase64": "Load Image To Base64",
|
||
"LoadImageFromLocalPath": "Load Image From Local Path",
|
||
"LoadMaskFromLocalPath": "Load Mask From Local Path",
|
||
"SaveImagesWithoutOutput": "Save Images Without Output",
|
||
"SaveSingleImageWithoutOutput": "Save Single Image Without Output",
|
||
}
|