diff --git a/README.MD b/README.MD
index 294c98c..ffceccf 100644
--- a/README.MD
+++ b/README.MD
@@ -37,6 +37,7 @@ git clone https://github.com/chflame163/ComfyUI_LayerStyle.git
## Update
**If the dependency package error after updating, please reinstall the relevant dependency packages. for details, please refer to [here](https://github.com/chflame163/ComfyUI_LayerStyle/issues/5).
+* Ultra nodes have been fully upgraded to V2 version, with the addition of VITMatte edge processing method, which is suitable for handling semi transparent areas. Include [MaskEdgeUltraDetailV2](#MaskEdgeUltraDetailV2), [SegmentAnythingUltraV2](#SegmentAnythingUltraV2), [RmBgUltraV2](#RmBgUltraV2) and [PersonMaskUltraV2](#PersonMaskUltraV2) nodes.
* Commit [Color of Shadow & Highlight](#Highlight) node, it can adjust the color of the dark and bright parts separately. Commit [Shadow & Highlight Mask](#Shadow) node, it can output mask for dark and bright areas.
* Commit [CropByMaskV2](#CropByMaskV2) node, On the basis of the original node, it supports ```crop_box``` input, making it convenient to cut layers of the same size.
* Commit [SimpleTextImage](#SimpleTextImage) node, it generate simple typesetting images and masks from text. This node references some of the functionalities and code of [ZHO-ZHO-ZHO/ComfyUI-Text_Image-Composite](https://github.com/ZHO-ZHO-ZHO/ComfyUI-Text_Image-Composite).
@@ -878,6 +879,16 @@ Node options:
* process_detail: Set to false here will skip edge processing to save runtime.
* prompt: Input for SAM's prompt.
+### SegmentAnythingUltraV2
+The V2 upgraded version of SegmentAnythingUltra has added the VITMatte edge processing method.
+
+
+On the basis of SegmentAnythingUltra, the following changes have been made:
+
+* detail_method: Edge processing methods. provides three methods: VITMatte, PyMatting and GuidedFilter.
+* detail_erode: Mask the erosion range inward from the edge. the larger the value, the larger the range of inward repair.
+* detail_dilate: The edge of the mask expands outward. the larger the value, the wider the range of outward repair.
+
### RemBgUltra
Remove background. compared to the similar background removal nodes, this node has ultra-high edge details.
@@ -896,6 +907,15 @@ Node options:
* white_point: Edge white sampling threshold.
* process_detail: Set to false here will skip edge processing to save runtime.
+### RmBgUltraV2
+The V2 upgraded version of RemBgUltra has added the VITMatte edge processing method.
+
+On the basis of RemBgUltra, the following changes have been made:
+
+* detail_method: Edge processing methods. provides three methods: VITMatte, PyMatting and GuidedFilter.
+* detail_erode: Mask the erosion range inward from the edge. the larger the value, the larger the range of inward repair.
+* detail_dilate: The edge of the mask expands outward. the larger the value, the wider the range of outward repair.
+
### PersonMaskUltra
Generate masks for portrait's face, hair, body skin, clothing, or accessories. Compared to the previous A Person Mask Generator node, this node has ultra-high edge details.
The model code for this node comes from [a-person-mask-generator](https://github.com/djbielejeski/a-person-mask-generator), edge processing code from [ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters).
@@ -916,6 +936,15 @@ Node options:
* white_point: Edge white sampling threshold.
* process_detail: Set to false here will skip edge processing to save runtime.
+### PersonMaskUltraV2
+The V2 upgraded version of PersonMaskUltra has added the VITMatte edge processing method.
+
+On the basis of PersonMaskUltra, the following changes have been made:
+
+* detail_method: Edge processing methods. provides three methods: VITMatte, PyMatting and GuidedFilter.
+* detail_erode: Mask the erosion range inward from the edge. the larger the value, the larger the range of inward repair.
+* detail_dilate: The edge of the mask expands outward. the larger the value, the wider the range of outward repair.
+
### Shadow & Highlight Mask
Generate masks for the dark and bright parts of the image.
@@ -967,6 +996,16 @@ Node options:
* black_point: Edge black sampling threshold.
* white_point: Edge white sampling threshold.
+### MaskEdgeUltraDetailV2
+The V2 upgraded version of MaskEdgeUltraDetail has added the VITMatte edge processing method.This method is suitable for handling semi transparent areas. The following figure is an example of the difference in output between three methods.
+
+
+On the basis of MaskEdgeUltraDetail, the following changes have been made:
+
+* method: Edge processing methods. provides three methods: VITMatte, PyMatting and GuidedFilter.
+* edge_erode: Mask the erosion range inward from the edge. the larger the value, the larger the range of inward repair.
+* edge_dilate: The edge of the mask expands outward. the larger the value, the wider the range of outward repair.
+
### MaskGrow
Grow and shrink edges and blur the mask
diff --git a/README_CN.MD b/README_CN.MD
index 19943ef..f9d0c8e 100644
--- a/README_CN.MD
+++ b/README_CN.MD
@@ -38,6 +38,7 @@ git clone https://github.com/chflame163/ComfyUI_LayerStyle.git
## 更新说明
**如果本插件更新后出现依赖包错误,请重新安装相关依赖包。详情见[这里](https://github.com/chflame163/ComfyUI_LayerStyle/issues/5)。
+* Ultra 节点全面升级到V2版本,增加了VITMatte边缘处理方法,此方法适合处理半透明区域。包括 [MaskEdgeUltraDetailV2](#MaskEdgeUltraDetailV2), [SegmentAnythingUltraV2](#SegmentAnythingUltraV2), [RmBgUltraV2](#RmBgUltraV2) 以及 [PersonMaskUltraV2](#PersonMaskUltraV2) 节点。
* 添加 [Color of Shadow & Highlight](#Highlight) 节点,可对暗部和亮部分别进行色彩调整。添加 [Shadow & Highlight Mask](#Shadow) 节点, 可输出暗部和亮部的遮罩。
* 添加 [CropByMaskV2](#CropByMaskV2) 节点,在原节点基础上支持```crop_box```输入,方便裁切相同尺寸的图层。
* 添加 [SimpleTextImage](#SimpleTextImage) 节点。从文字生成简单排版的图片以及遮罩。这个节点参考了[ZHO-ZHO-ZHO/ComfyUI-Text_Image-Composite](https://github.com/ZHO-ZHO-ZHO/ComfyUI-Text_Image-Composite)的部分功能和代码。
@@ -876,6 +877,17 @@ cropped_mask: 裁切后的遮罩。
* prompt: SAM的prompt输入。
+### SegmentAnythingUltraV2
+SegmentAnythingUltra的V2升级版,增加了VITMatte边缘处理方法。
+
+
+在SegmentAnythingUltra的基础上做了如下改变:
+
+* detail_method: 边缘处理方法。提供了VITMatte, PyMatting, GuidedFilter三种方法。
+* detail_erode: 遮罩边缘向内侵蚀范围。数值越大,向内修复的范围越大。
+* detail_dilate: 遮罩边缘向外扩张范围。数值越大,向外修复的范围越大。
+
+
### RemBgUltra
去除背景。与类似的背景移除节点相比,这个节点具有超高的边缘细节。
本节点结合了spacepxl的[ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters)的Alpha Matte节点,以及ZHO-ZHO-ZHO的[ComfyUI-BRIA_AI-RMBG](https://github.com/ZHO-ZHO-ZHO/ComfyUI-BRIA_AI-RMBG)的功能。
@@ -894,6 +906,16 @@ cropped_mask: 裁切后的遮罩。
* white_point: 边缘黑色采样阈值。
* process_detail: 此处设为False将跳过边缘处理以节省运行时间。
+### RmBgUltraV2
+RemBgUltra的V2升级版,增加了VITMatte边缘处理方法。
+
+在RemBgUltra的基础上做了如下改变:
+
+* detail_method: 边缘处理方法。提供了VITMatte, PyMatting, GuidedFilter三种方法。
+* detail_erode: 遮罩边缘向内侵蚀范围。数值越大,向内修复的范围越大。
+* detail_dilate: 遮罩边缘向外扩张范围。数值越大,向外修复的范围越大。
+
+
### PersonMaskUltra
为人物生成脸、头发、身体皮肤、衣服或配饰的遮罩。与之前的A Person Mask Generator节点相比,这个节点具有超高的边缘细节。
本节点的模型代码来自[a-person-mask-generator](https://github.com/djbielejeski/a-person-mask-generator),边缘处理代码来自spacepxl的[ComfyUI-Image-Filters](https://github.com/spacepxl/ComfyUI-Image-Filters)。
@@ -915,6 +937,16 @@ cropped_mask: 裁切后的遮罩。
* process_detail: 此处设为False将跳过边缘处理以节省运行时间。
+### PersonMaskUltraV2
+PersonMaskUltra的V2升级版,增加了VITMatte边缘处理方法。
+
+在PersonMaskUltra的基础上做了如下改变:
+
+* detail_method: 边缘处理方法。提供了VITMatte, PyMatting, GuidedFilter三种方法。
+* detail_erode: 遮罩边缘向内侵蚀范围。数值越大,向内修复的范围越大。
+* detail_dilate: 遮罩边缘向外扩张范围。数值越大,向外修复的范围越大。
+
+
### Shadow & Highlight Mask
生成图像暗部和亮部的遮罩。

@@ -965,6 +997,16 @@ cropped_mask: 裁切后的遮罩。
* black_point: 边缘黑色采样阈值。
* white_point: 边缘黑色采样阈值。
+### MaskEdgeUltraDetailV2
+MaskEdgeUltraDetail的V2升级版,增加了VITMatte边缘处理方法,此方法适合处理半透明区域。下图为三种方法输出区别的示例。
+
+
+在MaskEdgeUltraDetail的基础上做了如下改变:
+
+* method: 边缘处理方法。增加了VITMatte方法。
+* edge_erode: 遮罩边缘向内侵蚀范围。数值越大,向内修复的范围越大。
+* edge_dilate: 遮罩边缘向外扩张范围。数值越大,向外修复的范围越大。
+
### MaskGrow
对mask进行扩张收缩边缘和模糊处理
diff --git a/image/mask_edge_ultra_detail_v2_example.png b/image/mask_edge_ultra_detail_v2_example.png
new file mode 100644
index 0000000..1116131
Binary files /dev/null and b/image/mask_edge_ultra_detail_v2_example.png differ
diff --git a/image/mask_edge_ultra_detail_v2_node.png b/image/mask_edge_ultra_detail_v2_node.png
new file mode 100644
index 0000000..506be01
Binary files /dev/null and b/image/mask_edge_ultra_detail_v2_node.png differ
diff --git a/image/person_mask_ultra_v2_node.png b/image/person_mask_ultra_v2_node.png
new file mode 100644
index 0000000..d601db7
Binary files /dev/null and b/image/person_mask_ultra_v2_node.png differ
diff --git a/image/rmbg_ultra_v2_node.png b/image/rmbg_ultra_v2_node.png
new file mode 100644
index 0000000..1e03987
Binary files /dev/null and b/image/rmbg_ultra_v2_node.png differ
diff --git a/image/segment_anything_ultra_v2_node.png b/image/segment_anything_ultra_v2_node.png
new file mode 100644
index 0000000..bd54636
Binary files /dev/null and b/image/segment_anything_ultra_v2_node.png differ
diff --git a/image/ultra_v2_nodes_example.png b/image/ultra_v2_nodes_example.png
new file mode 100644
index 0000000..b67b294
Binary files /dev/null and b/image/ultra_v2_nodes_example.png differ
diff --git a/py/image_scale_by_aspect_ratio.py b/py/image_scale_by_aspect_ratio.py
index b65340d..ff77925 100644
--- a/py/image_scale_by_aspect_ratio.py
+++ b/py/image_scale_by_aspect_ratio.py
@@ -14,7 +14,7 @@ class ImageScaleByAspectRatio:
ratio_list = ['original', 'custom', '1:1', '3:2', '4:3', '16:9', '2:3', '3:4', '9:16']
fit_mode = ['letterbox', 'crop', 'fill']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
- multiple_list = ['8', '16', '64', 'None']
+ multiple_list = ['8', '16', '32', '64', 'None']
return {
"required": {
diff --git a/py/image_scale_by_aspect_ratio_v2.py b/py/image_scale_by_aspect_ratio_v2.py
index 14710ab..772320e 100644
--- a/py/image_scale_by_aspect_ratio_v2.py
+++ b/py/image_scale_by_aspect_ratio_v2.py
@@ -14,7 +14,7 @@ class ImageScaleByAspectRatioV2:
ratio_list = ['original', 'custom', '1:1', '3:2', '4:3', '16:9', '2:3', '3:4', '9:16']
fit_mode = ['letterbox', 'crop', 'fill']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
- multiple_list = ['8', '16', '64', 'None']
+ multiple_list = ['8', '16', '32', '64', 'None']
scale_to_list = ['None', 'longest', 'shortest']
return {
"required": {
diff --git a/py/imagefunc.py b/py/imagefunc.py
index 912c8b7..46b543c 100644
--- a/py/imagefunc.py
+++ b/py/imagefunc.py
@@ -22,6 +22,7 @@ from typing import Union, List
from PIL import Image, ImageFilter, ImageChops, ImageDraw, ImageOps, ImageEnhance, ImageFont
from skimage import img_as_float, img_as_ubyte
from pymatting import fix_trimap, estimate_alpha_cf, estimate_foreground_ml
+from transformers import VitMatteImageProcessor, VitMatteForImageMatting
import torchvision.transforms.functional as TF
import torch.nn.functional as F
import colorsys
@@ -1034,8 +1035,64 @@ def RMBG(image:Image) -> Image:
mi = torch.min(result)
result = (result - mi) / (ma - mi)
im_array = (result * 255).cpu().data.numpy().astype(np.uint8)
- _mask = Image.fromarray(np.squeeze(im_array)).convert('L')
- return _mask
+ _mask = torch.from_numpy(np.squeeze(im_array).astype(np.float32))
+ return tensor2pil(_mask)
+
+class VITMatteModel:
+ def __init__(self,model,processor):
+ self.model = model
+ self.processor = processor
+
+def load_VITMatte_model(model_name:str) -> object:
+ model = VitMatteForImageMatting.from_pretrained(model_name)
+ processor = VitMatteImageProcessor.from_pretrained(model_name)
+ vitmatte = VITMatteModel(model, processor)
+ return vitmatte
+
+def generate_VITMatte(image:Image, trimap:Image) -> Image:
+ if image.mode != 'RGB':
+ image = image.convert('RGB')
+ if trimap.mode != 'L':
+ trimap = trimap.convert('L')
+ model_name = "hustvl/vitmatte-small-composition-1k"
+ vit_matte_model = load_VITMatte_model(model_name=model_name)
+ inputs = vit_matte_model.processor(images=image, trimaps=trimap, return_tensors="pt")
+ with torch.no_grad():
+ predictions = vit_matte_model.model(**inputs).alphas
+ mask = tensor2pil(predictions).convert('L')
+ mask = mask.crop(
+ (0, 0, image.width, image.height)) # remove padding that the prediction appends (works in 32px tiles)
+ return mask
+
+def generate_VITMatte_trimap(mask:torch.Tensor, erode_kernel_size:int, dilate_kernel_size:int) -> Image:
+ mask = mask.squeeze(0).cpu().detach().numpy().astype(np.uint8) * 255
+ trimap = __generate_trimap(mask, erode_kernel_size, dilate_kernel_size).astype(np.float32)
+ trimap[trimap == 128] = 0.5
+ trimap[trimap == 255] = 1
+ trimap = torch.from_numpy(trimap).unsqueeze(0)
+ return tensor2pil(trimap).convert('L')
+
+def __generate_trimap(mask, erode_kernel_size=10, dilate_kernel_size=10):
+ erode_kernel = np.ones((erode_kernel_size, erode_kernel_size), np.uint8)
+ dilate_kernel = np.ones((dilate_kernel_size, dilate_kernel_size), np.uint8)
+ eroded = cv2.erode(mask, erode_kernel, iterations=5)
+ dilated = cv2.dilate(mask, dilate_kernel, iterations=5)
+ trimap = np.zeros_like(mask)
+ trimap[dilated == 255] = 128
+ trimap[eroded == 255] = 255
+ return trimap
+
+def get_a_person_mask_generator_model_path() -> str:
+ model_folder_name = 'mediapipe'
+ model_name = 'selfie_multiclass_256x256.tflite'
+ model_folder_path = os.path.join(folder_paths.models_dir, model_folder_name)
+ model_file_path = os.path.join(model_folder_path, model_name)
+ if not os.path.exists(model_file_path):
+ model_url = f'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_multiclass_256x256/float32/latest/{model_name}'
+ print(f"Downloading '{model_name}' model")
+ os.makedirs(model_folder_path, exist_ok=True)
+ wget.download(model_url, model_file_path)
+ return model_file_path
def mask_edge_detail(image:torch.Tensor, mask:torch.Tensor, detail_range:int=8, black_point:float=0.01, white_point:float=0.99) -> torch.Tensor:
d = detail_range * 5 + 1
diff --git a/py/mask_edge_ultrl_detail_v2.py b/py/mask_edge_ultrl_detail_v2.py
new file mode 100644
index 0000000..bb77f6c
--- /dev/null
+++ b/py/mask_edge_ultrl_detail_v2.py
@@ -0,0 +1,82 @@
+from .imagefunc import *
+
+NODE_NAME = 'MaskEdgeUltraDetail V2'
+
+class MaskEdgeUltraDetailV2:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ method_list = ['VITMatte', 'PyMatting', 'GuidedFilter']
+ return {
+ "required": {
+ "image": ("IMAGE",),
+ "mask": ("MASK",),
+ "method": (method_list,),
+ "mask_grow": ("INT", {"default": 0, "min": 0, "max": 256, "step": 1}),
+ "fix_gap": ("INT", {"default": 0, "min": 0, "max": 32, "step": 1}),
+ "fix_threshold": ("FLOAT", {"default": 0.75, "min": 0.01, "max": 0.99, "step": 0.01}),
+ "edge_erode": ("INT", {"default": 50, "min": 1, "max": 255, "step": 1}),
+ "edte_dilate": ("INT", {"default": 20, "min": 1, "max": 255, "step": 1}),
+ "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01}),
+ "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}),
+ },
+ "optional": {
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE", "MASK", )
+ RETURN_NAMES = ("image", "mask", )
+ FUNCTION = "mask_edge_ultra_detail_v2"
+ CATEGORY = '😺dzNodes/LayerMask'
+
+ def mask_edge_ultra_detail_v2(self, image, mask, method, mask_grow, fix_gap, fix_threshold,
+ edge_erode, edte_dilate, black_point, white_point,):
+ ret_images = []
+ ret_masks = []
+ l_images = []
+ l_masks = []
+ if mask.dim() == 2:
+ mask = torch.unsqueeze(mask, 0)
+ for l in image:
+ l_images.append(torch.unsqueeze(l, 0))
+ for m in mask:
+ l_masks.append(torch.unsqueeze(m, 0))
+ if len(l_images) != len(l_masks) or tensor2pil(l_images[0]).size != tensor2pil(l_masks[0]).size:
+ log(f"Error: {NODE_NAME} skipped, because mask does'nt match image.", message_type='error')
+ return (image, mask,)
+ detail_range = edge_erode + edte_dilate
+ for i in range(len(l_images)):
+ _image = l_images[i]
+ orig_image = tensor2pil(_image).convert('RGB')
+ _mask = l_masks[i]
+ if mask_grow != 0:
+ _mask = expand_mask(_mask, mask_grow, mask_grow//2)
+ if fix_gap:
+ _mask = mask_fix(_mask, 1, fix_gap, fix_threshold, fix_threshold)
+ log(f"{NODE_NAME} Processing...")
+ if method == 'GuidedFilter':
+ _mask = guided_filter_alpha(_image, _mask, detail_range//6)
+ _mask = tensor2pil(histogram_remap(_mask, black_point, white_point))
+ elif method == 'PyMatting':
+ _mask = tensor2pil(mask_edge_detail(_image, _mask, detail_range//8, black_point, white_point))
+ else:
+ _trimap = generate_VITMatte_trimap(_mask, edge_erode, edte_dilate)
+ _mask = generate_VITMatte(orig_image, _trimap)
+ _mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point))
+
+ ret_image = RGB2RGBA(orig_image, _mask.convert('L'))
+ ret_images.append(pil2tensor(ret_image))
+ ret_masks.append(image2mask(_mask))
+
+ log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
+ return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),)
+
+NODE_CLASS_MAPPINGS = {
+ "LayerMask: MaskEdgeUltraDetail V2": MaskEdgeUltraDetailV2,
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LayerMask: MaskEdgeUltraDetail V2": "LayerMask: MaskEdgeUltraDetail V2",
+}
diff --git a/py/person_mask_Ultra.py b/py/person_mask_Ultra.py
index f50d5e2..0c37074 100644
--- a/py/person_mask_Ultra.py
+++ b/py/person_mask_Ultra.py
@@ -7,21 +7,6 @@ from .segment_anything_func import *
NODE_NAME = 'PersonMaskUltra'
-def get_a_person_mask_generator_model_path() -> str:
- model_folder_name = 'mediapipe'
- model_name = 'selfie_multiclass_256x256.tflite'
-
- model_folder_path = os.path.join(folder_paths.models_dir, model_folder_name)
- model_file_path = os.path.join(model_folder_path, model_name)
-
- if not os.path.exists(model_file_path):
- model_url = f'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_multiclass_256x256/float32/latest/{model_name}'
- print(f"Downloading '{model_name}' model")
- os.makedirs(model_folder_path, exist_ok=True)
- wget.download(model_url, model_file_path)
-
- return model_file_path
-
class PersonMaskUltra:
diff --git a/py/person_mask_ultra_v2.py b/py/person_mask_ultra_v2.py
new file mode 100644
index 0000000..da7bf26
--- /dev/null
+++ b/py/person_mask_ultra_v2.py
@@ -0,0 +1,163 @@
+from .imagefunc import *
+from functools import reduce
+import wget
+import mediapipe as mp
+import folder_paths
+from .segment_anything_func import *
+
+NODE_NAME = 'PersonMaskUltra V2'
+
+class PersonMaskUltraV2:
+
+ def __init__(self):
+ # download the model if we need it
+ get_a_person_mask_generator_model_path()
+
+ @classmethod
+ def INPUT_TYPES(self):
+
+ method_list = ['VITMatte', 'PyMatting', 'GuidedFilter']
+
+ return {
+ "required":
+ {
+ "images": ("IMAGE",),
+ "face": ("BOOLEAN", {"default": True, "label_on": "enabled", "label_off": "disabled"}),
+ "hair": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
+ "body": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
+ "clothes": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
+ "accessories": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
+ "background": ("BOOLEAN", {"default": False, "label_on": "enabled", "label_off": "disabled"}),
+ "confidence": ("FLOAT", {"default": 0.4, "min": 0.05, "max": 0.95, "step": 0.01},),
+ "detail_method": (method_list,),
+ "detail_erode": ("INT", {"default": 50, "min": 1, "max": 255, "step": 1}),
+ "detail_dilate": ("INT", {"default": 20, "min": 1, "max": 255, "step": 1}),
+ "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01}),
+ "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}),
+ "process_detail": ("BOOLEAN", {"default": True}),
+ },
+ "optional":
+ {
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE", "MASK", )
+ RETURN_NAMES = ("image", "mask", )
+ FUNCTION = 'person_mask_ultra_v2'
+ CATEGORY = '😺dzNodes/LayerMask'
+ OUTPUT_NODE = True
+
+ def get_mediapipe_image(self, image: Image) -> mp.Image:
+ # Convert image to NumPy array
+ numpy_image = np.asarray(image)
+ image_format = mp.ImageFormat.SRGB
+ # Convert BGR to RGB (if necessary)
+ if numpy_image.shape[-1] == 4:
+ image_format = mp.ImageFormat.SRGBA
+ elif numpy_image.shape[-1] == 3:
+ image_format = mp.ImageFormat.SRGB
+ numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_BGR2RGB)
+ return mp.Image(image_format=image_format, data=numpy_image)
+
+ def person_mask_ultra_v2(self, images, face, hair, body, clothes,
+ accessories, background, confidence,
+ detail_method, detail_erode, detail_dilate,
+ black_point, white_point, process_detail):
+
+ a_person_mask_generator_model_path = get_a_person_mask_generator_model_path()
+ a_person_mask_generator_model_buffer = None
+ with open(a_person_mask_generator_model_path, "rb") as f:
+ a_person_mask_generator_model_buffer = f.read()
+ image_segmenter_base_options = mp.tasks.BaseOptions(model_asset_buffer=a_person_mask_generator_model_buffer)
+ options = mp.tasks.vision.ImageSegmenterOptions(
+ base_options=image_segmenter_base_options,
+ running_mode=mp.tasks.vision.RunningMode.IMAGE,
+ output_category_mask=True)
+ # Create the image segmenter
+ ret_images = []
+ ret_masks = []
+ with mp.tasks.vision.ImageSegmenter.create_from_options(options) as segmenter:
+ for image in images:
+ _image = torch.unsqueeze(image, 0)
+ orig_image = tensor2pil(_image).convert('RGB')
+ # Convert the Tensor to a PIL image
+ i = 255. * image.cpu().numpy()
+ image_pil = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
+ # create our foreground and background arrays for storing the mask results
+ mask_background_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8)
+ mask_background_array[:] = (0, 0, 0, 255)
+ mask_foreground_array = np.zeros((image_pil.size[0], image_pil.size[1], 4), dtype=np.uint8)
+ mask_foreground_array[:] = (255, 255, 255, 255)
+ # Retrieve the masks for the segmented image
+ media_pipe_image = self.get_mediapipe_image(image=image_pil)
+ segmented_masks = segmenter.segment(media_pipe_image)
+ masks = []
+ if background:
+ masks.append(segmented_masks.confidence_masks[0])
+ if hair:
+ masks.append(segmented_masks.confidence_masks[1])
+ if body:
+ masks.append(segmented_masks.confidence_masks[2])
+ if face:
+ masks.append(segmented_masks.confidence_masks[3])
+ if clothes:
+ masks.append(segmented_masks.confidence_masks[4])
+ if accessories:
+ masks.append(segmented_masks.confidence_masks[5])
+ image_data = media_pipe_image.numpy_view()
+ image_shape = image_data.shape
+ # convert the image shape from "rgb" to "rgba" aka add the alpha channel
+ if image_shape[-1] == 3:
+ image_shape = (image_shape[0], image_shape[1], 4)
+ mask_background_array = np.zeros(image_shape, dtype=np.uint8)
+ mask_background_array[:] = (0, 0, 0, 255)
+ mask_foreground_array = np.zeros(image_shape, dtype=np.uint8)
+ mask_foreground_array[:] = (255, 255, 255, 255)
+ mask_arrays = []
+ if len(masks) == 0:
+ mask_arrays.append(mask_background_array)
+ else:
+ for i, mask in enumerate(masks):
+ condition = np.stack((mask.numpy_view(),) * image_shape[-1], axis=-1) > confidence
+ mask_array = np.where(condition, mask_foreground_array, mask_background_array)
+ mask_arrays.append(mask_array)
+ # Merge our masks taking the maximum from each
+ merged_mask_arrays = reduce(np.maximum, mask_arrays)
+ # Create the image
+ mask_image = Image.fromarray(merged_mask_arrays)
+ # convert PIL image to tensor image
+ tensor_mask = mask_image.convert("RGB")
+ tensor_mask = np.array(tensor_mask).astype(np.float32) / 255.0
+ tensor_mask = torch.from_numpy(tensor_mask)[None,]
+ _mask = tensor_mask.squeeze(3)[..., 0]
+
+ detail_range = detail_erode + detail_dilate
+ if process_detail:
+ if detail_method == 'GuidedFilter':
+ _mask = guided_filter_alpha(_image, _mask, detail_range // 6)
+ _mask = tensor2pil(histogram_remap(_mask, black_point, white_point))
+ elif detail_method == 'PyMatting':
+ _mask = tensor2pil(
+ mask_edge_detail(_image, _mask,
+ detail_range // 8, black_point, white_point))
+ else:
+ _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_dilate)
+ _mask = generate_VITMatte(orig_image, _trimap)
+ _mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point))
+ else:
+ _mask = mask2image(_mask)
+
+ ret_image = RGB2RGBA(orig_image, _mask)
+ ret_images.append(pil2tensor(ret_image))
+ ret_masks.append(image2mask(_mask))
+
+ log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
+ return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),)
+
+NODE_CLASS_MAPPINGS = {
+ "LayerMask: PersonMaskUltra V2": PersonMaskUltraV2
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LayerMask: PersonMaskUltra V2": "LayerMask: PersonMaskUltra V2"
+}
\ No newline at end of file
diff --git a/py/rembg_ultra_v2.py b/py/rembg_ultra_v2.py
new file mode 100644
index 0000000..61d6b02
--- /dev/null
+++ b/py/rembg_ultra_v2.py
@@ -0,0 +1,71 @@
+from .imagefunc import *
+
+NODE_NAME = 'RmBgUltra V2'
+
+class RmBgUltraV2:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+
+ method_list = ['VITMatte', 'PyMatting', 'GuidedFilter']
+
+ return {
+ "required": {
+ "image": ("IMAGE",),
+ "detail_method": (method_list,),
+ "detail_erode": ("INT", {"default": 50, "min": 1, "max": 255, "step": 1}),
+ "detail_dilate": ("INT", {"default": 20, "min": 1, "max": 255, "step": 1}),
+ "black_point": ("FLOAT", {"default": 0.01, "min": 0.01, "max": 0.98, "step": 0.01}),
+ "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}),
+ "process_detail": ("BOOLEAN", {"default": True}),
+ },
+ "optional": {
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE", "MASK", )
+ RETURN_NAMES = ("image", "mask", )
+ FUNCTION = "rmbg_ultra_v2"
+ CATEGORY = '😺dzNodes/LayerMask'
+
+ def rmbg_ultra_v2(self, image, detail_method, detail_erode, detail_dilate,
+ black_point, white_point, process_detail):
+ ret_images = []
+ ret_masks = []
+
+ for i in image:
+ i = torch.unsqueeze(i, 0)
+ orig_image = tensor2pil(i).convert('RGB')
+ _mask = RMBG(orig_image)
+ _mask = pil2tensor(_mask)
+
+ detail_range = detail_erode + detail_dilate
+ if process_detail:
+ if detail_method == 'GuidedFilter':
+ _mask = guided_filter_alpha(i, _mask, detail_range // 6)
+ _mask = tensor2pil(histogram_remap(_mask, black_point, white_point))
+ elif detail_method == 'PyMatting':
+ _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8, black_point, white_point))
+ else:
+ _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_dilate)
+ _mask = generate_VITMatte(orig_image, _trimap)
+ _mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point))
+ else:
+ _mask = mask2image(_mask)
+
+ ret_image = RGB2RGBA(orig_image, _mask.convert('L'))
+ ret_images.append(pil2tensor(ret_image))
+ ret_masks.append(image2mask(_mask))
+
+ log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
+ return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),)
+
+NODE_CLASS_MAPPINGS = {
+ "LayerMask: RmBgUltra V2": RmBgUltraV2,
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LayerMask: RmBgUltra V2": "LayerMask: RmBgUltra V2",
+}
diff --git a/py/segment_anything_ultra_v2.py b/py/segment_anything_ultra_v2.py
new file mode 100644
index 0000000..11639eb
--- /dev/null
+++ b/py/segment_anything_ultra_v2.py
@@ -0,0 +1,92 @@
+from .imagefunc import *
+from .segment_anything_func import *
+
+NODE_NAME = 'SegmentAnythingUltra V2'
+
+SAM_MODEL = None
+DINO_MODEL = None
+
+class SegmentAnythingUltraV2:
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(cls):
+
+ method_list = ['VITMatte', 'PyMatting', 'GuidedFilter']
+
+ return {
+ "required": {
+ "image": ("IMAGE",),
+ "sam_model": (list_sam_model(), ),
+ "grounding_dino_model": (list_groundingdino_model(),),
+ "threshold": ("FLOAT", {"default": 0.3, "min": 0, "max": 1.0, "step": 0.01}),
+ "detail_method": (method_list,),
+ "detail_erode": ("INT", {"default": 50, "min": 1, "max": 255, "step": 1}),
+ "detail_dilate": ("INT", {"default": 20, "min": 1, "max": 255, "step": 1}),
+ "black_point": ("FLOAT", {"default": 0.15, "min": 0.01, "max": 0.98, "step": 0.01}),
+ "white_point": ("FLOAT", {"default": 0.99, "min": 0.02, "max": 0.99, "step": 0.01}),
+ "process_detail": ("BOOLEAN", {"default": True}),
+ "prompt": ("STRING", {"default": "subject"}),
+ },
+ "optional": {
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE", "MASK", )
+ RETURN_NAMES = ("image", "mask", )
+ FUNCTION = "segment_anything_ultra_v2"
+ CATEGORY = '😺dzNodes/LayerMask'
+
+
+ def segment_anything_ultra_v2(self, image, sam_model, grounding_dino_model, threshold,
+ detail_method, detail_erode, detail_dilate,
+ black_point, white_point, process_detail,
+ prompt, ):
+ global SAM_MODEL
+ global DINO_MODEL
+ if SAM_MODEL is None: SAM_MODEL = load_sam_model(sam_model)
+ if DINO_MODEL is None: DINO_MODEL = load_groundingdino_model(grounding_dino_model)
+ ret_images = []
+ ret_masks = []
+
+ for i in image:
+ i = torch.unsqueeze(i, 0)
+ _image = tensor2pil(i).convert('RGBA')
+ boxes = groundingdino_predict(DINO_MODEL, _image, prompt, threshold)
+ if boxes.shape[0] == 0:
+ break
+ (_, _mask) = sam_segment(SAM_MODEL, _image, boxes)
+ _mask = _mask[0]
+ detail_range = detail_erode + detail_dilate
+ if process_detail:
+ if detail_method == 'GuidedFilter':
+ _mask = guided_filter_alpha(i, _mask, detail_range // 6)
+ _mask = tensor2pil(histogram_remap(_mask, black_point, white_point))
+ elif detail_method == 'PyMatting':
+ _mask = tensor2pil(mask_edge_detail(i, _mask, detail_range // 8, black_point, white_point))
+ else:
+ _trimap = generate_VITMatte_trimap(_mask, detail_erode, detail_dilate)
+ _mask = generate_VITMatte(_image, _trimap)
+ _mask = tensor2pil(histogram_remap(pil2tensor(_mask), black_point, white_point))
+ else:
+ _mask = mask2image(_mask)
+ _image = RGB2RGBA(tensor2pil(i).convert('RGB'), _mask.convert('L'))
+
+ ret_images.append(pil2tensor(_image))
+ ret_masks.append(image2mask(_mask))
+ if len(ret_masks) == 0:
+ _, height, width, _ = image.size()
+ empty_mask = torch.zeros((1, height, width), dtype=torch.uint8, device="cpu")
+ return (empty_mask, empty_mask)
+
+ log(f"{NODE_NAME} Processed {len(ret_masks)} image(s).", message_type='finish')
+ return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),)
+
+NODE_CLASS_MAPPINGS = {
+ "LayerMask: SegmentAnythingUltra V2": SegmentAnythingUltraV2,
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LayerMask: SegmentAnythingUltra V2": "LayerMask: SegmentAnythingUltra V2",
+}
diff --git a/requirements.txt b/requirements.txt
index e218d6a..7d1a73c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -19,4 +19,6 @@ fastapi
rich
google-generativeai
diffusers
-omegaconf
\ No newline at end of file
+omegaconf
+tqdm
+transformers
\ No newline at end of file
diff --git a/workflow/768x1344_dress.png b/workflow/768x1344_dress.png
new file mode 100644
index 0000000..2714ce4
Binary files /dev/null and b/workflow/768x1344_dress.png differ
diff --git a/workflow/mask_edge_ultra_detail_v2_example.json b/workflow/mask_edge_ultra_detail_v2_example.json
new file mode 100644
index 0000000..6d34acb
--- /dev/null
+++ b/workflow/mask_edge_ultra_detail_v2_example.json
@@ -0,0 +1,588 @@
+{
+ "last_node_id": 56,
+ "last_link_id": 88,
+ "nodes": [
+ {
+ "id": 41,
+ "type": "LayerMask: MaskEdgeUltraDetail V2",
+ "pos": [
+ 1020,
+ 160
+ ],
+ "size": {
+ "0": 315,
+ "1": 246
+ },
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 55
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 87
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 72
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "links": [
+ 58
+ ],
+ "shape": 3,
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: MaskEdgeUltraDetail V2"
+ },
+ "widgets_values": [
+ "VITMatte",
+ 0,
+ 0,
+ 0.75,
+ 50,
+ 20,
+ 0.01,
+ 0.99
+ ]
+ },
+ {
+ "id": 55,
+ "type": "LayerMask: SegmentAnythingUltra",
+ "pos": [
+ 610,
+ 240
+ ],
+ "size": {
+ "0": 315,
+ "1": 246
+ },
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 84
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "links": [
+ 85,
+ 86,
+ 87,
+ 88
+ ],
+ "shape": 3,
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: SegmentAnythingUltra"
+ },
+ "widgets_values": [
+ "sam_vit_h (2.56GB)",
+ "GroundingDINO_SwinT_OGC (694MB)",
+ 0.3,
+ 16,
+ 0.15,
+ 0.99,
+ false,
+ "subject"
+ ]
+ },
+ {
+ "id": 43,
+ "type": "LayerMask: MaskEdgeUltraDetail V2",
+ "pos": [
+ 1020,
+ 450
+ ],
+ "size": {
+ "0": 315,
+ "1": 246
+ },
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 60
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 88
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 73
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "links": [
+ 62
+ ],
+ "shape": 3,
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: MaskEdgeUltraDetail V2"
+ },
+ "widgets_values": [
+ "PyMatting",
+ 0,
+ 0,
+ 0.75,
+ 50,
+ 20,
+ 0.01,
+ 0.99
+ ]
+ },
+ {
+ "id": 45,
+ "type": "LayerMask: MaskEdgeUltraDetail V2",
+ "pos": [
+ 1020,
+ 750
+ ],
+ "size": {
+ "0": 315,
+ "1": 246
+ },
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 63
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 86
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 74
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "links": [
+ 65
+ ],
+ "shape": 3,
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: MaskEdgeUltraDetail V2"
+ },
+ "widgets_values": [
+ "GuidedFilter",
+ 0,
+ 0,
+ 0.75,
+ 50,
+ 20,
+ 0.01,
+ 0.99
+ ]
+ },
+ {
+ "id": 42,
+ "type": "LayerMask: MaskPreview",
+ "pos": [
+ 1360,
+ 150
+ ],
+ "size": {
+ "0": 184.8000030517578,
+ "1": 260.4266357421875
+ },
+ "flags": {},
+ "order": 9,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 58,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: MaskPreview"
+ }
+ },
+ {
+ "id": 49,
+ "type": "PreviewImage",
+ "pos": [
+ 1570,
+ 150
+ ],
+ "size": {
+ "0": 176.3199462890625,
+ "1": 253.52662658691406
+ },
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 72
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "PreviewImage"
+ }
+ },
+ {
+ "id": 44,
+ "type": "LayerMask: MaskPreview",
+ "pos": [
+ 1360,
+ 460
+ ],
+ "size": {
+ "0": 190.71994018554688,
+ "1": 246
+ },
+ "flags": {},
+ "order": 11,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 62,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: MaskPreview"
+ }
+ },
+ {
+ "id": 50,
+ "type": "PreviewImage",
+ "pos": [
+ 1570,
+ 460
+ ],
+ "size": {
+ "0": 172.48670959472656,
+ "1": 246
+ },
+ "flags": {},
+ "order": 10,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 73
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "PreviewImage"
+ }
+ },
+ {
+ "id": 51,
+ "type": "PreviewImage",
+ "pos": [
+ 1570,
+ 750
+ ],
+ "size": {
+ "0": 170.93331909179688,
+ "1": 246
+ },
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 74
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "PreviewImage"
+ }
+ },
+ {
+ "id": 46,
+ "type": "LayerMask: MaskPreview",
+ "pos": [
+ 1360,
+ 750
+ ],
+ "size": {
+ "0": 197.17332458496094,
+ "1": 246
+ },
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 65,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: MaskPreview"
+ }
+ },
+ {
+ "id": 54,
+ "type": "LayerMask: MaskPreview",
+ "pos": [
+ 670,
+ 550
+ ],
+ "size": {
+ "0": 203.78663635253906,
+ "1": 252.93331909179688
+ },
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 85,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: MaskPreview"
+ }
+ },
+ {
+ "id": 5,
+ "type": "LoadImage",
+ "pos": [
+ 230,
+ 420
+ ],
+ "size": {
+ "0": 315,
+ "1": 314
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 55,
+ 60,
+ 63,
+ 84
+ ],
+ "shape": 3,
+ "slot_index": 0,
+ "label": "IMAGE"
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null,
+ "shape": 3,
+ "label": "MASK"
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "dress_768x1344.png",
+ "image"
+ ]
+ }
+ ],
+ "links": [
+ [
+ 55,
+ 5,
+ 0,
+ 41,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 58,
+ 41,
+ 1,
+ 42,
+ 0,
+ "MASK"
+ ],
+ [
+ 60,
+ 5,
+ 0,
+ 43,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 62,
+ 43,
+ 1,
+ 44,
+ 0,
+ "MASK"
+ ],
+ [
+ 63,
+ 5,
+ 0,
+ 45,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 65,
+ 45,
+ 1,
+ 46,
+ 0,
+ "MASK"
+ ],
+ [
+ 72,
+ 41,
+ 0,
+ 49,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 73,
+ 43,
+ 0,
+ 50,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 74,
+ 45,
+ 0,
+ 51,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 84,
+ 5,
+ 0,
+ 55,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 85,
+ 55,
+ 1,
+ 54,
+ 0,
+ "MASK"
+ ],
+ [
+ 86,
+ 55,
+ 1,
+ 45,
+ 1,
+ "MASK"
+ ],
+ [
+ 87,
+ 55,
+ 1,
+ 41,
+ 1,
+ "MASK"
+ ],
+ [
+ 88,
+ 55,
+ 1,
+ 43,
+ 1,
+ "MASK"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {
+ "0246.VERSION": [
+ 0,
+ 0,
+ 4
+ ]
+ },
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/workflow/ultra_v2_nodes_example.json b/workflow/ultra_v2_nodes_example.json
new file mode 100644
index 0000000..9b2c06f
--- /dev/null
+++ b/workflow/ultra_v2_nodes_example.json
@@ -0,0 +1,467 @@
+{
+ "last_node_id": 81,
+ "last_link_id": 127,
+ "nodes": [
+ {
+ "id": 72,
+ "type": "LoadImage",
+ "pos": [
+ 48,
+ 204
+ ],
+ "size": {
+ "0": 315,
+ "1": 314
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 119
+ ],
+ "shape": 3,
+ "slot_index": 0,
+ "label": "IMAGE"
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null,
+ "shape": 3,
+ "label": "MASK"
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "768x1344_beach.png",
+ "image"
+ ]
+ },
+ {
+ "id": 5,
+ "type": "LoadImage",
+ "pos": [
+ 47,
+ 604
+ ],
+ "size": {
+ "0": 315,
+ "1": 314
+ },
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 89,
+ 123
+ ],
+ "shape": 3,
+ "slot_index": 0,
+ "label": "IMAGE"
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null,
+ "shape": 3,
+ "label": "MASK"
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "dress_768x1344 (1).png",
+ "image"
+ ]
+ },
+ {
+ "id": 57,
+ "type": "LayerMask: SegmentAnythingUltra V2",
+ "pos": [
+ 438,
+ 225
+ ],
+ "size": {
+ "0": 315,
+ "1": 294
+ },
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 89
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 121
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "links": [
+ 122
+ ],
+ "shape": 3,
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: SegmentAnythingUltra V2"
+ },
+ "widgets_values": [
+ "sam_vit_h (2.56GB)",
+ "GroundingDINO_SwinT_OGC (694MB)",
+ 0.3,
+ "VITMatte",
+ 90,
+ 10,
+ 0.01,
+ 0.88,
+ true,
+ "subject"
+ ]
+ },
+ {
+ "id": 79,
+ "type": "LayerMask: PersonMaskUltra V2",
+ "pos": [
+ 437,
+ 584
+ ],
+ "size": {
+ "0": 315,
+ "1": 366
+ },
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 123
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 127
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "links": [
+ 126
+ ],
+ "shape": 3,
+ "slot_index": 1
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: PersonMaskUltra V2"
+ },
+ "widgets_values": [
+ false,
+ true,
+ false,
+ false,
+ false,
+ false,
+ 0.55,
+ "GuidedFilter",
+ 4,
+ 18,
+ 0.65,
+ 0.99,
+ true
+ ]
+ },
+ {
+ "id": 77,
+ "type": "LayerUtility: ImageBlend",
+ "pos": [
+ 814,
+ 228
+ ],
+ "size": {
+ "0": 315,
+ "1": 146
+ },
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "background_image",
+ "type": "IMAGE",
+ "link": 119
+ },
+ {
+ "name": "layer_image",
+ "type": "IMAGE",
+ "link": 121
+ },
+ {
+ "name": "layer_mask",
+ "type": "MASK",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 124
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerUtility: ImageBlend"
+ },
+ "widgets_values": [
+ true,
+ "normal",
+ 100
+ ]
+ },
+ {
+ "id": 78,
+ "type": "MaskPreview+",
+ "pos": [
+ 824,
+ 420
+ ],
+ "size": [
+ 194.09358354048254,
+ 227.87744542143548
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 122
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "MaskPreview+"
+ }
+ },
+ {
+ "id": 81,
+ "type": "MaskPreview+",
+ "pos": [
+ 828,
+ 698
+ ],
+ "size": [
+ 185.24506170099403,
+ 240.1805127154695
+ ],
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 126
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "MaskPreview+"
+ }
+ },
+ {
+ "id": 80,
+ "type": "LayerUtility: ImageBlend",
+ "pos": [
+ 1153,
+ 382
+ ],
+ "size": {
+ "0": 315,
+ "1": 146
+ },
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "background_image",
+ "type": "IMAGE",
+ "link": 124
+ },
+ {
+ "name": "layer_image",
+ "type": "IMAGE",
+ "link": 127
+ },
+ {
+ "name": "layer_mask",
+ "type": "MASK",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "links": [
+ 125
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerUtility: ImageBlend"
+ },
+ "widgets_values": [
+ true,
+ "normal",
+ 100
+ ]
+ },
+ {
+ "id": 73,
+ "type": "PreviewImage",
+ "pos": [
+ 1511,
+ 246
+ ],
+ "size": [
+ 357.3965465372253,
+ 654.0592602816492
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 125
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "PreviewImage"
+ }
+ }
+ ],
+ "links": [
+ [
+ 89,
+ 5,
+ 0,
+ 57,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 119,
+ 72,
+ 0,
+ 77,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 121,
+ 57,
+ 0,
+ 77,
+ 1,
+ "IMAGE"
+ ],
+ [
+ 122,
+ 57,
+ 1,
+ 78,
+ 0,
+ "MASK"
+ ],
+ [
+ 123,
+ 5,
+ 0,
+ 79,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 124,
+ 77,
+ 0,
+ 80,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 125,
+ 80,
+ 0,
+ 73,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 126,
+ 79,
+ 1,
+ 81,
+ 0,
+ "MASK"
+ ],
+ [
+ 127,
+ 79,
+ 0,
+ 80,
+ 1,
+ "IMAGE"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {
+ "0246.VERSION": [
+ 0,
+ 0,
+ 4
+ ]
+ },
+ "version": 0.4
+}
\ No newline at end of file