Files
2024-06-30 11:43:51 +08:00

753 lines
26 KiB
Python

import os
import cv2
import torch
import shutil
import hashlib
from datetime import datetime
import time
import folder_paths
import node_helpers
import numpy as np
from pathlib import Path
from comfy import model_management
from PIL import Image, ImageOps, ImageSequence, ImageFile
from .utils import *
from .resolution_utils import *
class BridgingDiffusersProgress:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"pipeline":('PIPELINE', {}),
},
}
RETURN_TYPES = ("PIPELINE",)
RETURN_NAMES = ("pipeline", )
FUNCTION = "bridging"
CATEGORY = "model"
def bridging(self, pipeline):
return (pipeline,)
class ModelChangeDeviceRepeaters:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"device": (["default", "cpu", "cuda", "cuda:0", "cuda:1"],),
},
"optional": {
"sam_model": ('SAM_MODEL', {}),
"grounding_dino_model": ('GROUNDING_DINO_MODEL', {}),
"model":('MODEL', {}),
"clip":('CLIP', {}),
"vae":('VAE', {}),
"pipeline":('PIPELINE', {}),
"control_net":('CONTROL_NET', {}),
"trigger_condition_image":("IMAGE",),
"trigger_condition_mask": ("MASK",),
"trigger_condition_latent": ("LATENT",),
"trigger_condition_text": ("STRING",{"default": ""}),
"trigger_condition_seed": ("INT", {"default": 0}),
}
}
RETURN_TYPES = ("IMAGE","MASK","LATENT","STRING","INT",)
RETURN_NAMES = ("image","mask","latent","text","seed", )
FUNCTION = "changeDevice"
CATEGORY = "model"
def changeDevice(self, device = "default",sam_model=None,grounding_dino_model=None,model=None,clip=None,vae=None,pipeline=None,control_net=None,trigger_condition_image=None,trigger_condition_mask=None,trigger_condition_latent=None,trigger_condition_text=None,trigger_condition_seed=None):
#获取当前时间
current_time = time.time()
seleted_device = cast_torch_device(device)
change_obj_device(seleted_device,sam_model,grounding_dino_model,model,clip,vae,pipeline,control_net)
use_time = (time.time() - current_time)/1000
print(f"ModelChangeDeviceRepeaters 耗时:{use_time}s")
return (trigger_condition_image,trigger_condition_mask,trigger_condition_latent,trigger_condition_text,trigger_condition_seed,)
class ModelChangeDevice:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"device": (["default", "cpu", "cuda", "cuda:0", "cuda:1"],),
},
"optional": {
"sam_model": ('SAM_MODEL', {}),
"grounding_dino_model": ('GROUNDING_DINO_MODEL', {}),
"model":('MODEL', {}),
"clip":('CLIP', {}),
"vae":('VAE', {}),
"pipeline":('PIPELINE', {}),
"control_net":('CONTROL_NET', {}),
"trigger_condition_image":("IMAGE",),
"trigger_condition_mask": ("MASK",),
"trigger_condition_latent": ("LATENT",),
"trigger_condition_text": ("STRING",{"default": ""}),
"trigger_condition_seed": ("INT", {"default": 0}),
}
}
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "changeDevice"
CATEGORY = "model"
def changeDevice(self, device = "default",sam_model=None,grounding_dino_model=None,model=None,clip=None,vae=None,pipeline=None,control_net=None,trigger_condition_image=None,trigger_condition_mask=None,trigger_condition_latent=None,trigger_condition_text=None,trigger_condition_seed=None):
#获取当前时间
current_time = time.time()
seleted_device = cast_torch_device(device)
change_obj_device(seleted_device,sam_model,grounding_dino_model,model,clip,vae,pipeline,control_net)
use_time = (time.time() - current_time)/1000
print(f"ModelChangeDevice 耗时:{use_time}s")
return { "ui": { "text": f"耗时:{use_time}s" } }
class OutputImageToInput:
@classmethod
def INPUT_TYPES(s):
def load_children(dir):
childrens = []
for f in os.listdir(dir):
item = os.path.join(dir, f)
if os.path.isdir(item):
item_childrens = load_children(item)
if len(item_childrens)>0:
childrens.extend(item_childrens)
else:
childrens.append(item)
return childrens
files = load_children(folder_paths.get_output_directory())
file_mod_times = [(datetime.fromtimestamp(os.path.getmtime(file)), file) for file in files]
sorted_files = sorted(file_mod_times, key=lambda x: x[0], reverse=True)
#去除所有文件的folder_paths.get_output_directory()前缀
pre_len = len(folder_paths.get_output_directory())+1
files = [file[pre_len:] for _,file in sorted_files]
return {"required":
{"image": (files,)},
}
CATEGORY = "image"
RETURN_TYPES = ("STRING",)
FUNCTION = "copy_image"
def take_un_existed_name(self, image,no=0):
if no>0:
image_name = Path(image)
real_image = image_name.stem+f"_{str(no)}"+image_name.suffix
else:
real_image = image
target_image_path = os.path.join(folder_paths.get_input_directory(), real_image)
if os.path.exists(target_image_path):
#需要在后缀前加上_copy
return self.take_un_existed_name(image,no+1)
return (target_image_path,real_image)
def copy_image(self, image):
image_path = os.path.join(folder_paths.get_output_directory(), image)
target_image_path,target_image = self.take_un_existed_name(image)
shutil.copy(image_path, target_image_path)
return (target_image,)
class LoadImageWithName:
# copy from comfyui
@classmethod
def INPUT_TYPES(s):
return {"required":
{"image": ("STRING", {
"dynamicPrompts": False,
"multiline": False,
"default": ""
}), }
}
CATEGORY = "image"
RETURN_TYPES = ("IMAGE", "MASK")
FUNCTION = "load_image"
def load_image(self, image):
image_path = os.path.join(folder_paths.get_input_directory(), image)
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))
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
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")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1 and img.format not in excluded_formats:
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 ImageChangeDType:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"dtype": (["default", "float32", "float16", "bfloat16"],),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image", )
FUNCTION = "changeDType"
CATEGORY = "image"
def changeDType(self, image, dtype = "default"):
seleted_type = cast_torch_dtype(dtype)
image = image.to(dtype=seleted_type)
return (image,)
class MaskChangeDType:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"dtype": (["default", "float32", "float16", "bfloat16"],),
},
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask", )
FUNCTION = "changeDType"
CATEGORY = "mask"
def changeDType(self, mask, dtype = "default"):
seleted_type = cast_torch_dtype(dtype)
mask = mask.to(dtype=seleted_type)
return (mask,)
class ImageChangeDevice:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"device": (["default", "cpu", "cuda", "cuda:0", "cuda:1"],),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image", )
FUNCTION = "changeDevice"
CATEGORY = "image"
def changeDevice(self, image, device = "default"):
seleted_device = cast_torch_device(device)
if image.device != seleted_device:
image = image.to(seleted_device)
return (image,)
class MaskChangeDevice:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"device": (["default", "cpu", "cuda", "cuda:0", "cuda:1"],),
},
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask", )
FUNCTION = "changeDevice"
CATEGORY = "mask"
def changeDevice(self, mask, device = "default"):
seleted_device = cast_torch_device(device)
if mask.device != seleted_device:
mask = mask.to(seleted_device)
return (mask,)
class SplitMasks:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"ignore_threshold": ("INT", {"default": 0, "min": 0, "max": 2048, "step": 1}),
},
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("masks", )
FUNCTION = "separate"
CATEGORY = "mask"
def separate(self, mask, ignore_threshold=100):
# 将pythorch的tensor转换为opencv需要的图像格式
opencv_gray_image = tensorMask2cv2img(mask)
# 阈值处理,将图像二值化
_, binary_mask = cv2.threshold(
opencv_gray_image, 1, 255, cv2.THRESH_BINARY)
# 寻找轮廓
contours, _ = cv2.findContours(
binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 创建一个空白图像,用于绘制分割结果
segmented_masks = []
for contour in contours:
# 计算轮廓的面积
area = cv2.contourArea(contour)
# 如果面积小于忽略阈值,则跳过
if area < ignore_threshold:
continue
# 创建一个空白图像,用于绘制分割结果
segmented_mask = np.zeros_like(binary_mask)
# 绘制轮廓
cv2.drawContours(segmented_mask, [
contour], 0, (255, 255, 255), thickness=cv2.FILLED)
# 将当前分割好的遮罩添加到列表中
segmented_masks.append(segmented_mask)
output_masks = []
for segmented_mask in segmented_masks:
numpy_mask = np.array(segmented_mask).astype(np.float32) / 255.0
i_mask = torch.from_numpy(numpy_mask)
# 将图像和遮罩添加到输出列表
output_masks.append(i_mask.unsqueeze(0))
return (output_masks,)
class MaskselectionOfMasks:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"index": ("INT", {"default": 0, "min": 0, "max": 99, "step": 1}),
},
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask", )
FUNCTION = "selection"
CATEGORY = "mask"
def selection(self, mask, index=0):
# 判断 mask 是否是数组
if isinstance(mask, list):
# 如果是数组,则直接返回数组中的第一个元素
result = mask[index]
else:
# 如果不是数组,则直接返回 mask
result = mask
if result is None:
result = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
return (result,)
class ImageAdaptiveCrop:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"crop_mask": ("MASK",),
"expand": ("INT", {"default": 0, "min": 0, "max": 1024, "step": 1}),
"is_xl": (["yes", "no"],),
},
}
RETURN_TYPES = ("IMAGE", "INT", "INT", "INT", "INT", "INT", "INT",
"INT", "INT", "INT", "INT", "INT",)
# expand预期计算出来的外扩值,通过这个在生成图像中扣除新区域
# crop是目标区域从原图从哪个位置抠出来的,需要把根据expand找出来的区域填充到crop区域
RETURN_NAMES = ("adaptive_croped_image", "resolution_w", "resolution_h" "image_multiple", "expand_top", "expand_buttom", "expand_left",
"expand_right", "crop_area_x", "crop_area_y", "crop_area_width", "crop_area_height",)
FUNCTION = "adaptiveCrop"
CATEGORY = "image"
def adaptiveCrop(self, image, crop_mask, expand, is_xl):
# 将pythorch的tensor转换为opencv需要的图像格式
crop_mask_cv2 = crop_mask.cpu().squeeze(0).numpy()
crop_mask_cv2 = (crop_mask_cv2 * 255).astype(np.uint8)
# 寻找轮廓
contours, _ = cv2.findContours(
crop_mask_cv2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours or len(contours) == 0:
return (image, 1, 0, 0, 0, 0, 0, 0, 0, 0,)
image_cv2 = image.cpu().squeeze(0).numpy()
image_cv2 = (image_cv2 * 255).astype(np.uint8)
image_w = image_cv2.shape[1]
image_h = image_cv2.shape[0]
image_left = 0
image_top = 0
image_right = image_w
image_buttom = image_h
all_contours = np.vstack(contours)
all_contour_x, all_contour_y, all_contour_w, all_contour_h = cv2.boundingRect(
all_contours)
box_left = all_contour_x
box_top = all_contour_y
box_right = all_contour_x + all_contour_w
box_bottom = all_contour_y + all_contour_h
box_left = max(image_left, box_left - expand)
box_top = max(image_top, box_top - expand)
box_right = min(image_right, box_right + expand)
box_bottom = min(image_buttom, box_bottom + expand)
box_w = box_right - box_left
box_h = box_bottom - box_top
if is_xl == "yes":
resolution_w, resolution_h, resolution_scale = resolution_auto_cel_from_sdxl(
box_w, box_h)
else:
resolution_w, resolution_h, resolution_scale = resolution_auto_cel_from_sd15(
box_w, box_h)
if resolution_scale == 0:
return (image, 1, 0, 0, 0, 0, 0, 0, 0, 0,)
resolution_wt = resolution_w / resolution_scale
resolution_ht = resolution_h / resolution_scale
if resolution_wt > image_w: # 预期的画图比原图还大
rw_offset = (resolution_wt - image_w)//2
# 因为生成的比原图大,所以不需要再使用原图抠出来了
crop_area_x = 0
crop_area_width = image_w
# 需要外扩的宽度
expand_left = rw_offset
expand_right = (resolution_wt - image_w) - rw_offset
elif resolution_wt > box_w: # 预期的画图比原图小但比期望的空间大
rw_offset = (image_w - resolution_wt)//2
crop_area_x = max(0, box_left-rw_offset)
crop_area_width = resolution_wt
# 因为比图像小,不需要外扩
expand_left = 0
expand_right = 0
else:
crop_area_x = box_left
crop_area_width = box_w
# 因为比图像小,不需要外扩
expand_left = 0
expand_right = 0
if resolution_ht > image_h: # 预期的画图比原图还大
rh_offset = (resolution_ht - image_h)//2
# 因为生成的比原图大,所以不需要再使用原图抠出来了
crop_area_y = 0
crop_area_height = image_h
# 需要外扩的高度
expand_top = rh_offset
expand_buttom = (resolution_ht - image_h) - rh_offset
elif resolution_ht > box_h: # 预期的画图比原图小但比期望的空间大
rh_offset = (image_h - resolution_ht)//2
crop_area_y = max(0, box_top-rh_offset)
crop_area_height = resolution_ht
# 因为比图像小,不需要外扩
expand_top = 0
expand_buttom = 0
else:
crop_area_y = box_top
crop_area_height = box_h
# 因为比图像小,不需要外扩
expand_top = 0
expand_buttom = 0
# 使用numpy切片裁剪图像
if crop_area_y > 0:
image_cv2 = image_cv2[crop_area_y:, :]
if image_h-(crop_area_y+crop_area_height) > 0:
image_cv2 = image_cv2[:-(image_h-(crop_area_y+crop_area_height)), :]
if crop_area_x > 0:
image_cv2 = image_cv2[:, crop_area_x:]
if image_w-(crop_area_x+crop_area_width) > 0:
image_cv2 = image_cv2[:, :-(image_w-(crop_area_x+crop_area_width))]
if expand_top>0 or expand_buttom>0 or expand_left>0 or expand_top>0:
# 使用cv2.copyMakeBorder函数扩展图像
image_cv2 = cv2.copyMakeBorder(image_cv2, expand_top, expand_buttom, expand_left, expand_right, cv2.BORDER_REPLICATE)
image_cv2 = np.array(image_cv2).astype(np.float32) / 255.0
image_cv2 = torch.from_numpy(image_cv2)
return (image_cv2, resolution_w, resolution_h, resolution_scale, expand_top, expand_buttom, expand_left, expand_right, crop_area_x, crop_area_y, crop_area_width, crop_area_height,)
class ImageResolutionLimitWith8K:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"limit": ("INT", {"default": 8192, "min": 512, "max": 8192, "step": 128}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image", )
FUNCTION = "limit8K"
CATEGORY = "image"
def limit8K(self, image, limit=8192):
if limit == 0:
return (image,)
if image.shape[1] <= limit and image.shape[2] <= limit:
return (image,)
# 获取图像的宽度和高度
opencv_image = image.cpu().squeeze(0).numpy()
opencv_image = (opencv_image * 255).astype(np.uint8)
height = opencv_image.shape[0]
width = opencv_image.shape[1]
aspect_ratio = width / height
# 计算新的宽和高,确保不超过max_length
if width > height:
new_width = limit
new_height = int(limit / aspect_ratio)
else:
new_height = limit
new_width = int(limit * aspect_ratio)
# 使用cv2.resize进行缩小,同时保持宽高比
resized_img = cv2.resize(opencv_image, (new_width, new_height))
numpy_image = np.array(resized_img).astype(np.float32) / 255.0
numpy_image = torch.from_numpy(numpy_image)
return (numpy_image.unsqueeze(0),)
class ImageResolutionAdaptiveWithX:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mode": (["crop", "expand"],),
"base": ("INT", {"default": 8, "min": 0, "max": 2048, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image", )
FUNCTION = "adaptive"
CATEGORY = "image"
def adaptive(self, image, mode="crop", base=8):
if base == 0:
return (image,)
# 获取图像的宽度和高度
opencv_image = image.cpu().squeeze(0).numpy()
opencv_image = (opencv_image * 255).astype(np.uint8)
height = opencv_image.shape[0]
width = opencv_image.shape[1]
height_offset = height % base
width_offset = width % base
if mode == "expand":
height_offset = base - height_offset
width_offset = base - width_offset
else:
height_offset = height_offset
width_offset = width_offset
if mode == "expand":
top = height_offset//2
bottom = height_offset - top
left = width_offset//2
right = width_offset - left
else:
top = height_offset//2
bottom = height_offset - top
left = width_offset//2
right = width_offset - left
if mode == "expand":
# 使用cv2.copyMakeBorder函数扩展图像
opencv_image = cv2.copyMakeBorder(
opencv_image, top, bottom, left, right, cv2.BORDER_REPLICATE)
else:
# 使用numpy切片裁剪图像
if top > 0:
opencv_image = opencv_image[top:, :]
if bottom > 0:
opencv_image = opencv_image[:-bottom, :]
if left > 0:
opencv_image = opencv_image[:, left:]
if right > 0:
opencv_image = opencv_image[:, :-right]
numpy_image = np.array(opencv_image).astype(np.float32) / 255.0
numpy_image = torch.from_numpy(numpy_image)
return (numpy_image.unsqueeze(0),)
class ImageCropWithMask:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"width": ("INT", {"default": 512, "min": 0, "max": 8096, "step": 1}),
"height": ("INT", {"default": 512, "min": 0, "max": 8096, "step": 1}),
"padding": ("INT", {"default": 0, "min": 0, "max": 1024, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image", )
FUNCTION = "adaptive"
CATEGORY = "image"
def adaptive(self, image,mask,width,height, mode="zoom",padding=0):
# 将pythorch的tensor转换为opencv需要的图像格式
mask_cv2 = mask.cpu().squeeze(0).numpy()
mask_cv2 = (mask_cv2 * 255).astype(np.uint8)
_, mask_cv2 = cv2.threshold(mask_cv2, 1, 255, cv2.THRESH_BINARY)
# 寻找轮廓
contours, _ = cv2.findContours(mask_cv2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours or len(contours) == 0:
return (image)
image_cv2 = image.cpu().squeeze(0).numpy()
image_cv2 = (image_cv2 * 255).astype(np.uint8)
mask_bool = mask_cv2 > 0
image_cv2[~mask_bool]=0
all_contours = np.vstack(contours)
all_contour_x, all_contour_y, all_contour_w, all_contour_h = cv2.boundingRect(all_contours)
box_left = all_contour_x
box_top = all_contour_y
box_right = all_contour_x + all_contour_w
box_bottom = all_contour_y + all_contour_h
box_left = max(0, box_left - padding)
box_top = max(0, box_top - padding)
box_right = min(image_cv2.shape[1], box_right + padding)
box_bottom = min(image_cv2.shape[0], box_bottom + padding)
image_cv2 = image_cv2[box_top:box_bottom,box_left:box_right]
scale_w,scale_h = resize_to(image_cv2.shape[1],image_cv2.shape[0],width,height)
image_cv2 = cv2.resize(image_cv2, (scale_w,scale_h))
offset_w = width - image_cv2.shape[1]
offset_h = height - image_cv2.shape[0]
offset_top = offset_h//2
offset_bottom = offset_h - offset_top
offset_left = offset_w//2
offset_right = offset_w - offset_left
image_cv2 = cv2.copyMakeBorder(image_cv2, offset_top, offset_bottom, offset_left, offset_right, cv2.BORDER_CONSTANT,0)
image_cv2 = np.array(image_cv2).astype(np.float32) / 255.0
image_cv2 = torch.from_numpy(image_cv2)
return (image_cv2.unsqueeze(0),)
NODE_CLASS_MAPPINGS = {
"Model Change Device Repeaters": ModelChangeDeviceRepeaters,
"Model Change Device": ModelChangeDevice,
"Load Image With Name": LoadImageWithName,
"Output Image To Input": OutputImageToInput,
"Mask Change DType": MaskChangeDType,
"Image Change DType": ImageChangeDType,
"Mask Change Device": MaskChangeDevice,
"Image Change Device": ImageChangeDevice,
"Split Masks": SplitMasks,
"Mask Selection Of Masks": MaskselectionOfMasks,
"Image Adaptive Crop M&R": ImageAdaptiveCrop,
"Image Adaptive Crop With Mask": ImageCropWithMask,
"Image Resolution Limit With 8K": ImageResolutionLimitWith8K,
"Image Resolution Adaptive With X": ImageResolutionAdaptiveWithX,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Model Change Device Repeaters": "Model Change Device &Repeaters",
"Model Change Device": "Model Change Device",
"Load Image With Name": "Load Image With Name",
"Output Image To Input": "Output Image To Input",
"Mask Change DType": "Mask Change DType",
"Image Change DType": "Image Change DType",
"Mask Change Device": "Mask Change Device",
"Image Change Device": "Image Change Device",
"Split Masks": "Split Masks",
"Mask Selection Of Masks": "Mask Selection Of Masks",
"Image Adaptive Crop M&R": "Image Adaptive Crop MASK&Resolution",
"Image Adaptive Crop With Mask": "Image Adaptive Crop With Mask",
"Image Resolution Limit With 8K": "Image Resolution Limit With 8K",
"Image Resolution Adaptive With X": "Image Resolution Adaptive With X",
}