Commit CreateGradientMask, LayerImageTransform and LayerMaskTransform nodes
@@ -17,6 +17,7 @@ Nodes are divided into 5 groups according to their functions: LayerStyle, LayerC
|
||||
|
||||
## 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).
|
||||
* Commit [CreateGradientMask](#CreateGradientMask) node. Commit [LayerImageTransform](#LayerImageTransform) and [LayerMaskTransform](#LayerMaskTransform) nodes.
|
||||
* Commit [MaskEdgeUltraDetail](#MaskEdgeUltraDetail) node, it process rough masks to ultra fine edges.Commit [Exposure](#Exposure) node.
|
||||
* Commit [Sharp & Soft](#Sharp) node, it can enhance or smooth out image details. Commit [MaskByDifferent](#MaskByDifferent) node, it compare two images and output a Mask. Commit [SegmentAnythingUltra](#SegmentAnythingUltra) node, Improve the quality of mask edges. *If SegmentAnything is not installed, you will need to manually download the model.
|
||||
* All nodes have fully supported batch images, providing convenience for video creation.
|
||||
@@ -473,8 +474,39 @@ Convert absolute coordinates to percentage coordinates.
|
||||
|
||||

|
||||
Node options:
|
||||
* x: Value of X
|
||||
* y: Value of Y
|
||||
* x: Value of X.
|
||||
* y: Value of Y.
|
||||
|
||||
### <a id="table1">LayerImageTransform</a>
|
||||

|
||||
This node is used to transform layer_image separately, which can change size, rotation, aspect ratio, and mirror flip without changing the image size.
|
||||
|
||||

|
||||
Node options:
|
||||
* x: Value of X.
|
||||
* y: Value of Y.
|
||||
* mirror: Mirror flipping. Provide two flipping modes, horizontal flipping and vertical flipping.
|
||||
* scale: Layer magnification, 1.0 represents the original size.
|
||||
* aspect_ratio: Layer aspect ratio. 1.0 is the original ratio, a value greater than this indicates elongation, and a value less than this indicates flattening.
|
||||
* rotate: Layer rotation degree.
|
||||
* Sampling methods for layer enlargement and rotation, including lanczos, bicubic, hamming, bilinear, box and nearest. Different sampling methods can affect the image quality and processing time of the synthesized image.
|
||||
* anti_aliasing: Anti aliasing, ranging from 0 to 16, the larger the value, the less obvious the aliasing. An excessively high value will significantly reduce the processing speed of the node.
|
||||
|
||||
|
||||
### <a id="table1">LayerMaskTransform</a>
|
||||
Similar to LayerImageTransform node, this node is used to transform the layer_mask separately, which can scale, rotate, change aspect ratio, and mirror flip without changing the mask size.
|
||||
|
||||

|
||||
Node options:
|
||||
* x: Value of X.
|
||||
* y: Value of Y.
|
||||
* mirror: Mirror flipping. Provide two flipping modes, horizontal flipping and vertical flipping.
|
||||
* scale: Layer magnification, 1.0 represents the original size.
|
||||
* aspect_ratio: Layer aspect ratio. 1.0 is the original ratio, a value greater than this indicates elongation, and a value less than this indicates flattening.
|
||||
* rotate: Layer rotation degree.
|
||||
* Sampling methods for layer enlargement and rotation, including lanczos, bicubic, hamming, bilinear, box and nearest. Different sampling methods can affect the image quality and processing time of the synthesized image.
|
||||
* anti_aliasing: Anti aliasing, ranging from 0 to 16, the larger the value, the less obvious the aliasing. An excessively high value will significantly reduce the processing speed of the node.
|
||||
|
||||
|
||||
### <a id="table1">ColorImage</a>
|
||||

|
||||
@@ -514,8 +546,8 @@ Node options:
|
||||
* variation_range: The range of random changes in characters. when this value is greater than 0, the character will undergo random changes in size and position, and the larger the value, the greater the magnitude of the change.
|
||||
* variation_seed: The seed for randomly. fix this value to individual characters changes generated each time will not change.
|
||||
* layout: Text layout. there are horizontal and vertical options to choose from.
|
||||
* width; Width of the image. If there is a size_as input, this setting will be ignored.
|
||||
* height; Height of the image. If there is a size_as input, this setting will be ignored.
|
||||
* width: Width of the image. If there is a size_as input, this setting will be ignored.
|
||||
* height: Height of the image. If there is a size_as input, this setting will be ignored.
|
||||
* text_color: The color of text.
|
||||
* background_color: The color of background.
|
||||
|
||||
@@ -697,7 +729,7 @@ Node options:
|
||||
* angle: The angle of blur.
|
||||
|
||||
### <a id="table1">MaskGradient</a>
|
||||
Create a gradient for the mask from one side.
|
||||
Create a gradient for the mask from one side. please note the difference between this node and the CreateGradientMask node.
|
||||

|
||||
|
||||
Node options:
|
||||
@@ -708,6 +740,23 @@ Node options:
|
||||
* gradient_offset: Gradient position offset.
|
||||
* opacity: The opacity of the gradient.
|
||||
|
||||
### <a id="table1">CreateGradientMask</a>
|
||||
Create a gradient mask. please note the difference between this node and the MaskGradient node.
|
||||

|
||||
|
||||
Node options:
|
||||

|
||||
* size_as<sup>*</sup>: The input image or mask here will generate the output image and mask according to their size. this input takes priority over the width and height below.
|
||||
* width: Width of the image. If there is a size_as input, this setting will be ignored.
|
||||
* height: Height of the image. If there is a size_as input, this setting will be ignored.
|
||||
* gradient_side: Generate gradient from which edge. There are four directions: top, bottom, left and right.
|
||||
* gradient_scale: Gradient distance. The default value of 100 indicates that one side of the gradient is completely transparent and the other side is completely opaque. The smaller the value, the shorter the distance from transparent to opaque.
|
||||
* gradient_offset: Gradient position offset.
|
||||
* opacity: The opacity of the gradient.
|
||||
|
||||
<sup>*</sup>Only limited to input image and mask. forcing the integration of other types of inputs will result in node errors.
|
||||
|
||||
|
||||
### <a id="table1">MaskStroke</a>
|
||||
Generate mask contour strokes.
|
||||

|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
## 更新说明
|
||||
**如果本插件更新后出现依赖包错误,请重新安装相关依赖包。详情见[这里](https://github.com/chflame163/ComfyUI_LayerStyle/issues/5)。
|
||||
* 添加[CreateGradientMask](#CreateGradientMask) 节点。添加 [LayerImageTransform](#LayerImageTransform) 和 [LayerMaskTransform](#LayerMaskTransform) 节点。
|
||||
* 添加[MaskEdgeUltraDetail](#MaskEdgeUltraDetail) 节点,给粗糙的遮罩进行处理获得超精细的边缘。添加 [Exposure](#Exposure) 节点,调整图像曝光。
|
||||
* 添加[Sharp & Soft](#Sharp) 节点,可提升或抹平图像细节。新增[MaskByDifferent](#MaskByDifferent)节点,比较两张图片并输出Mask。新增[SegmentAnythingUltra](#SegmentAnythingUltra)节点,提升遮罩边缘质量。*如果没有安装SegmentAnything, 需要手动下载模型。
|
||||
* 所有节点已全面支持批量图片,为创作视频提供方便。( CropByMask 节点仅支持相同尺寸的切除, 如果输入批量mask_for_crop,将使用第一张的数据。)
|
||||
@@ -462,6 +463,36 @@
|
||||
* x: 坐标x值。
|
||||
* y: 坐标y值。
|
||||
|
||||
### <a id="table1">LayerImageTransform</a>
|
||||

|
||||
这个节点用于单独对layer_image进行变换,可改变大小,旋转,改变长宽比以及镜像翻转。
|
||||
|
||||

|
||||
节点选项说明:
|
||||
* x: 坐标x值。
|
||||
* y: 坐标y值。
|
||||
* mirror: 镜像翻转。提供2种翻转模式, 水平翻转和垂直翻转。
|
||||
* scale: 图层放大倍数,1.0 表示原大。
|
||||
* aspect_ratio: 图层长宽比。1.0 是原始比例,大于此值表示拉长,小于此值表示压扁。
|
||||
* rotate: 图层旋转度数。
|
||||
* transform_method: 用于图层放大和旋转的采样方法,包括lanczos、bicubic、hamming、bilinear、box和nearest。不同的采样方法会影响合成的画质和画面处理时间。
|
||||
* anti_aliasing: 抗锯齿,范围从0-16,数值越大,锯齿越不明显。过高的数值将显著降低节点的处理速度。
|
||||
|
||||
### <a id="table1">LayerMaskTransform</a>
|
||||
与LayerImageTransform类似,这个节点用于单独对layer_mask进行变换,可改变大小,旋转,改变长宽比以及镜像翻转。
|
||||
|
||||

|
||||
节点选项说明:
|
||||
* x: 坐标x值。
|
||||
* y: 坐标y值。
|
||||
* mirror: 镜像翻转。提供2种翻转模式, 水平翻转和垂直翻转。
|
||||
* scale: 图层放大倍数,1.0 表示原大。
|
||||
* aspect_ratio: 图层长宽比。1.0 是原始比例,大于此值表示拉长,小于此值表示压扁。
|
||||
* rotate: 图层旋转度数。
|
||||
* transform_method: 用于图层放大和旋转的采样方法,包括lanczos、bicubic、hamming、bilinear、box和nearest。不同的采样方法会影响合成的画质和画面处理时间。
|
||||
* anti_aliasing: 抗锯齿,范围从0-16,数值越大,锯齿越不明显。过高的数值将显著降低节点的处理速度。
|
||||
|
||||
|
||||
### <a id="table1">ColorImage</a>
|
||||

|
||||
生成一张指定颜色和大小的图片。
|
||||
@@ -500,8 +531,8 @@
|
||||
* variation_range: 字符随机变化范围。此数值大于0时,字符将产生大小和位置的随机变化,数值越大,变化幅度越大。
|
||||
* variation_seed: 随机变化的种子。固定此数值,每次产生的单个文字的变化不会改变。
|
||||
* layout: 文字排版。有横排和竖排可选择。
|
||||
* width; 画面的宽度。如果有size_as输入,此设置将被忽略。
|
||||
* height; 画面的高度。如果有size_as输入,此设置将被忽略。
|
||||
* width: 画面的宽度。如果有size_as输入,此设置将被忽略。
|
||||
* height: 画面的高度。如果有size_as输入,此设置将被忽略。
|
||||
* text_color: 文字颜色。
|
||||
* background_color: 背景颜色。
|
||||
|
||||
@@ -681,7 +712,7 @@ MaskGrow
|
||||
* angle: 模糊角度。
|
||||
|
||||
### <a id="table1">MaskGradient</a>
|
||||
使mask从一侧产生渐变。
|
||||
使mask从一侧产生渐变。请注意此节点与CreateGradientMask的区别。
|
||||

|
||||
|
||||
节点选项说明:
|
||||
@@ -692,6 +723,23 @@ MaskGrow
|
||||
* gradient_offset: 渐变位置偏移。
|
||||
* opacity: 渐变的不透明度。
|
||||
|
||||
### <a id="table1">CreateGradientMask</a>
|
||||
创建一个渐变的遮罩。请注意此节点与MaskGradient的区别。
|
||||

|
||||
|
||||
节点选项说明:
|
||||

|
||||
* size_as<sup>*</sup>: 此处输入图像或遮罩,将按照其尺寸生成输出图像和遮罩。注意,此输入优先级高于下面的width和height。
|
||||
* width: 画面的宽度。如果有size_as输入,此设置将被忽略。
|
||||
* height: 画面的高度。如果有size_as输入,此设置将被忽略。
|
||||
* gradient_side: 从哪个边产生渐变。有四个方向:顶侧top、底侧bottom、左侧left、右侧right。
|
||||
* gradient_scale: 渐变距离。默认值100表示渐变产生一侧完全透明,另一侧完全不透明。数值越小,从透明到不透明的距离越短。
|
||||
* gradient_offset: 渐变位置偏移。
|
||||
* opacity: 渐变的不透明度。
|
||||
|
||||
<sup>*</sup>仅限输入image和mask, 如果强制接入其他类型输入,将导致节点错误。
|
||||
|
||||
|
||||
### <a id="table1">MaskStroke</a>
|
||||
产生mask轮廓描边。
|
||||

|
||||
|
||||
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 521 KiB After Width: | Height: | Size: 407 KiB |
|
Before Width: | Height: | Size: 602 KiB After Width: | Height: | Size: 657 KiB |
@@ -0,0 +1,98 @@
|
||||
from .imagefunc import *
|
||||
|
||||
NODE_NAME = 'CreateGradientMask'
|
||||
|
||||
any = AnyType("*")
|
||||
class CreateGradientMask:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
side = ['bottom', 'top', 'left', 'right']
|
||||
return {
|
||||
"required": {
|
||||
"width": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
|
||||
"height": ("INT", {"default": 512, "min": 4, "max": 99999, "step": 1}),
|
||||
"gradient_side": (side,),
|
||||
"gradient_scale": ("INT", {"default": 100, "min": 1, "max": 9999, "step": 1}),
|
||||
"gradient_offset": ("INT", {"default": 0, "min": -9999, "max": 9999, "step": 1}),
|
||||
"opacity": ("INT", {"default": 100, "min": 0, "max": 100, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"size_as": (any, {}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK", )
|
||||
RETURN_NAMES = ("mask", )
|
||||
FUNCTION = 'create_gradient_mask'
|
||||
CATEGORY = '😺dzNodes/LayerMask'
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def create_gradient_mask(self, width, height, gradient_side, gradient_scale, gradient_offset, opacity, size_as=None):
|
||||
|
||||
if size_as is not None:
|
||||
if size_as.shape[0] > 0:
|
||||
_asimage = tensor2pil(size_as[0])
|
||||
else:
|
||||
_asimage = tensor2pil(size_as)
|
||||
width, height = _asimage.size
|
||||
|
||||
_black = Image.new('L', size=(width, height), color='black')
|
||||
_white = Image.new('L', size=(width, height), color='white')
|
||||
_canvas = copy.deepcopy(_black)
|
||||
start_color = '#FFFFFF'
|
||||
end_color = '#000000'
|
||||
if gradient_side == 'bottom':
|
||||
_gradient = create_gradient(start_color, end_color, width, height, direction='bottom')
|
||||
if gradient_scale != 100:
|
||||
_gradient = _gradient.resize((width, int(height * gradient_scale / 100)))
|
||||
_canvas.paste(_gradient.convert('L'), box=(0, gradient_offset))
|
||||
if gradient_offset > height:
|
||||
_canvas = _white
|
||||
elif gradient_offset > 0:
|
||||
_canvas.paste(_white, box=(0, gradient_offset - height))
|
||||
elif gradient_side == 'top':
|
||||
_gradient = create_gradient(start_color, end_color, width, height, direction='top')
|
||||
if gradient_scale != 100:
|
||||
_gradient = _gradient.resize((width, int(height * gradient_scale / 100)))
|
||||
_canvas.paste(_gradient.convert('L'), box=(0, height - int(height * gradient_scale / 100) + gradient_offset))
|
||||
if gradient_offset < -height:
|
||||
_canvas = _white
|
||||
elif gradient_offset < 0:
|
||||
_canvas.paste(_white, box=(0, height + gradient_offset))
|
||||
elif gradient_side == 'left':
|
||||
_gradient = create_gradient(start_color, end_color, width, height, direction='left')
|
||||
if gradient_scale != 100:
|
||||
_gradient = _gradient.resize((int(width * gradient_scale / 100), height))
|
||||
_canvas.paste(_gradient.convert('L'), box=(width - int(width * gradient_scale / 100) + gradient_offset, 0))
|
||||
if gradient_offset < -width:
|
||||
_canvas = _white
|
||||
elif gradient_offset < 0:
|
||||
_canvas.paste(_white, box=(width + gradient_offset, 0))
|
||||
elif gradient_side == 'right':
|
||||
_gradient = create_gradient(start_color, end_color, width, height, direction='right')
|
||||
if gradient_scale != 100:
|
||||
_gradient = _gradient.resize((int(width * gradient_scale / 100), height))
|
||||
_canvas.paste(_gradient.convert('L'), box=(gradient_offset, 0))
|
||||
if gradient_offset > width:
|
||||
_canvas = _white
|
||||
elif gradient_offset > 0:
|
||||
_canvas.paste(_white, box=(gradient_offset - width, 0))
|
||||
|
||||
# opacity
|
||||
if opacity < 100:
|
||||
_canvas = chop_image(_black, _canvas, 'normal', opacity)
|
||||
|
||||
log(f"{NODE_NAME} Processed.", message_type='finish')
|
||||
return (image2mask(_canvas),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LayerMask: CreateGradientMask": CreateGradientMask
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LayerMask: CreateGradientMask": "LayerMask: CreateGradientMask"
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class ImageBlendAdvance:
|
||||
"aspect_ratio": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}),
|
||||
"rotate": ("FLOAT", {"default": 0, "min": -999999, "max": 999999, "step": 0.01}),
|
||||
"transform_method": (method_mode,),
|
||||
"anti_aliasing": ("INT", {"default": 0, "min": 0, "max": 16, "step": 0.01}),
|
||||
"anti_aliasing": ("INT", {"default": 0, "min": 0, "max": 16, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"layer_mask": ("MASK",), #
|
||||
|
||||
@@ -414,17 +414,45 @@ def image_rotate_extend_with_alpha(image:Image, angle:float, alpha:Image=None, m
|
||||
return (_image, _alpha, ret_image)
|
||||
|
||||
|
||||
def create_gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int) -> Image:
|
||||
def create_gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int, direction:str='bottom') -> Image:
|
||||
# direction = one of top, bottom, left, right
|
||||
start_color = Hex_to_RGB(start_color_inhex)
|
||||
end_color = Hex_to_RGB(end_color_inhex)
|
||||
ret_image = Image.new("RGB", (width, height), start_color)
|
||||
draw = ImageDraw.Draw(ret_image)
|
||||
for i in range(height):
|
||||
R = int(start_color[0] * (height - i) / height + end_color[0] * i / height)
|
||||
G = int(start_color[1] * (height - i) / height + end_color[1] * i / height)
|
||||
B = int(start_color[2] * (height - i) / height + end_color[2] * i / height)
|
||||
color = (R, G, B)
|
||||
draw.line((0, i, width, i), fill=color)
|
||||
if direction == 'bottom':
|
||||
for i in range(height):
|
||||
R = int(start_color[0] * (height - i) / height + end_color[0] * i / height)
|
||||
G = int(start_color[1] * (height - i) / height + end_color[1] * i / height)
|
||||
B = int(start_color[2] * (height - i) / height + end_color[2] * i / height)
|
||||
color = (R, G, B)
|
||||
draw.line((0, i, width, i), fill=color)
|
||||
elif direction == 'top':
|
||||
for i in range(height):
|
||||
R = int(end_color[0] * (height - i) / height + start_color[0] * i / height)
|
||||
G = int(end_color[1] * (height - i) / height + start_color[1] * i / height)
|
||||
B = int(end_color[2] * (height - i) / height + start_color[2] * i / height)
|
||||
color = (R, G, B)
|
||||
draw.line((0, i, width, i), fill=color)
|
||||
elif direction == 'right':
|
||||
for i in range(width):
|
||||
R = int(start_color[0] * (width - i) / width + end_color[0] * i / width)
|
||||
G = int(start_color[1] * (width - i) / width + end_color[1] * i / width)
|
||||
B = int(start_color[2] * (width - i) / width + end_color[2] * i / width)
|
||||
color = (R, G, B)
|
||||
draw.line((i, 0, i, height), fill=color)
|
||||
elif direction == 'left':
|
||||
for i in range(width):
|
||||
R = int(end_color[0] * (width - i) / width + start_color[0] * i / width)
|
||||
G = int(end_color[1] * (width - i) / width + start_color[1] * i / width)
|
||||
B = int(end_color[2] * (width - i) / width + start_color[2] * i / width)
|
||||
color = (R, G, B)
|
||||
draw.line((i, 0, i, height), fill=color)
|
||||
else:
|
||||
log(f'A argument error of imagefunc.create_gradient(), '
|
||||
f'"direction=" must one of "top, bottom, left, right".',
|
||||
message_type='error')
|
||||
|
||||
return ret_image
|
||||
|
||||
def gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int, angle:float, ) -> Image:
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
from .imagefunc import *
|
||||
|
||||
NODE_NAME = 'LayerImageTransform'
|
||||
|
||||
class LayerImageTransform:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
mirror_mode = ['None', 'horizontal', 'vertical']
|
||||
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",), #
|
||||
"x": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
|
||||
"y": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
|
||||
"mirror": (mirror_mode,), # 镜像翻转
|
||||
"scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}),
|
||||
"aspect_ratio": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}),
|
||||
"rotate": ("FLOAT", {"default": 0, "min": -999999, "max": 999999, "step": 0.01}),
|
||||
"transform_method": (method_mode,),
|
||||
"anti_aliasing": ("INT", {"default": 0, "min": 0, "max": 16, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = 'layer_image_transform'
|
||||
CATEGORY = '😺dzNodes/LayerUtility'
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def layer_image_transform(self, image, x, y, mirror, scale, aspect_ratio, rotate,
|
||||
transform_method, anti_aliasing,
|
||||
):
|
||||
|
||||
l_images = []
|
||||
l_masks = []
|
||||
ret_images = []
|
||||
|
||||
for l in image:
|
||||
l_images.append(torch.unsqueeze(l, 0))
|
||||
m = tensor2pil(l)
|
||||
if m.mode == 'RGBA':
|
||||
l_masks.append(m.split()[-1])
|
||||
|
||||
for i in range(len(l_images)):
|
||||
layer_image = l_images[i] if i < len(l_images) else l_images[-1]
|
||||
_image = tensor2pil(layer_image).convert('RGB')
|
||||
if i < len(l_masks):
|
||||
_mask = l_masks[i]
|
||||
else:
|
||||
_mask = Image.new('L', size=_image.size, color='white')
|
||||
_image_canvas = Image.new('RGB', size=_image.size, color='black')
|
||||
_mask_canvas = Image.new('L', size=_mask.size, color='black')
|
||||
orig_layer_width = _image.width
|
||||
orig_layer_height = _image.height
|
||||
target_layer_width = int(orig_layer_width * scale)
|
||||
target_layer_height = int(orig_layer_height * scale * aspect_ratio)
|
||||
# mirror
|
||||
if mirror == 'horizontal':
|
||||
_image = _image.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
_mask = _mask.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
elif mirror == 'vertical':
|
||||
_image = _image.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
_mask = _mask.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
# scale
|
||||
_image = _image.resize((target_layer_width, target_layer_height))
|
||||
_mask = _mask.resize((target_layer_width, target_layer_height))
|
||||
# rotate
|
||||
_image, _mask, _ = image_rotate_extend_with_alpha(_image, rotate, _mask, transform_method, anti_aliasing)
|
||||
# composit layer
|
||||
_image_canvas.paste(_image, (x, y))
|
||||
_mask_canvas.paste(_mask, (x, y))
|
||||
if tensor2pil(layer_image).mode == 'RGBA':
|
||||
_image_canvas = RGB2RGBA(_image_canvas, _mask_canvas)
|
||||
|
||||
ret_images.append(pil2tensor(_image_canvas))
|
||||
|
||||
log(f"{NODE_NAME} Processed {len(l_images)} image(s).", message_type='finish')
|
||||
return (torch.cat(ret_images, dim=0),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LayerUtility: LayerImageTransform": LayerImageTransform
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LayerUtility: LayerImageTransform": "LayerUtility: LayerImageTransform"
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
from .imagefunc import *
|
||||
|
||||
NODE_NAME = 'LayerMaskTransform'
|
||||
|
||||
class LayerMaskTransform:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
mirror_mode = ['None', 'horizontal', 'vertical']
|
||||
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
|
||||
return {
|
||||
"required": {
|
||||
"mask": ("MASK",), #
|
||||
"x": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
|
||||
"y": ("INT", {"default": 0, "min": -99999, "max": 99999, "step": 1}),
|
||||
"mirror": (mirror_mode,), # 镜像翻转
|
||||
"scale": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}),
|
||||
"aspect_ratio": ("FLOAT", {"default": 1, "min": 0.01, "max": 100, "step": 0.01}),
|
||||
"rotate": ("FLOAT", {"default": 0, "min": -999999, "max": 999999, "step": 0.01}),
|
||||
"transform_method": (method_mode,),
|
||||
"anti_aliasing": ("INT", {"default": 0, "min": 0, "max": 16, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK",)
|
||||
RETURN_NAMES = ("mask",)
|
||||
FUNCTION = 'layer_mask_transform'
|
||||
CATEGORY = '😺dzNodes/LayerUtility'
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def layer_mask_transform(self, mask, x, y, mirror, scale, aspect_ratio, rotate,
|
||||
transform_method, anti_aliasing,
|
||||
):
|
||||
|
||||
l_masks = []
|
||||
ret_masks = []
|
||||
|
||||
if mask.dim() == 2:
|
||||
mask = torch.unsqueeze(mask, 0)
|
||||
for m in mask:
|
||||
l_masks.append(torch.unsqueeze(m, 0))
|
||||
for i in range(len(l_masks)):
|
||||
_mask = l_masks[i] if i < len(l_masks) else l_masks[-1]
|
||||
_mask = tensor2pil(_mask).convert('L')
|
||||
_mask_canvas = Image.new('L', size=_mask.size, color='black')
|
||||
orig_width = _mask.width
|
||||
orig_height = _mask.height
|
||||
target_layer_width = int(orig_width * scale)
|
||||
target_layer_height = int(orig_height * scale * aspect_ratio)
|
||||
# mirror
|
||||
if mirror == 'horizontal':
|
||||
_mask = _mask.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
elif mirror == 'vertical':
|
||||
_mask = _mask.transpose(Image.FLIP_TOP_BOTTOM)
|
||||
# scale
|
||||
_mask = _mask.resize((target_layer_width, target_layer_height))
|
||||
# rotate
|
||||
_, _mask, _ = image_rotate_extend_with_alpha(_mask.convert('RGB'), rotate, _mask, transform_method, anti_aliasing)
|
||||
# composit layer
|
||||
_mask_canvas.paste(_mask, (x, y))
|
||||
|
||||
ret_masks.append(image2mask(_mask_canvas))
|
||||
|
||||
log(f"{NODE_NAME} Processed {len(l_masks)} mask(s).", message_type='finish')
|
||||
return (torch.cat(ret_masks, dim=0),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LayerUtility: LayerMaskTransform": LayerMaskTransform
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LayerUtility: LayerMaskTransform": "LayerUtility: LayerMaskTransform"
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class SegmentAnythingUltra:
|
||||
"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", {}),
|
||||
"prompt": ("STRING", {"default": "subject"}),
|
||||
},
|
||||
"optional": {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
{
|
||||
"last_node_id": 18,
|
||||
"last_link_id": 35,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
210,
|
||||
670
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 314
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
32,
|
||||
35
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"1344x768_girl2 (3).png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
214,
|
||||
277
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 314
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
27
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"1344x768_beach (17).png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1396,
|
||||
376
|
||||
],
|
||||
"size": [
|
||||
676.9506879602059,
|
||||
454.54347972656274
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 28
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "LayerMask: MaskPreview",
|
||||
"pos": [
|
||||
986,
|
||||
716
|
||||
],
|
||||
"size": [
|
||||
305.28704833984375,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 33
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LayerMask: MaskPreview"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "LayerMask: CreateGradientMask",
|
||||
"pos": [
|
||||
617,
|
||||
713
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 178
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "size_as",
|
||||
"type": "*",
|
||||
"link": 32
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
33,
|
||||
34
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LayerMask: CreateGradientMask"
|
||||
},
|
||||
"widgets_values": [
|
||||
512,
|
||||
512,
|
||||
"right",
|
||||
60,
|
||||
248,
|
||||
100
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "LayerUtility: ImageBlend",
|
||||
"pos": [
|
||||
996,
|
||||
404
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 146
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "background_image",
|
||||
"type": "IMAGE",
|
||||
"link": 27
|
||||
},
|
||||
{
|
||||
"name": "layer_image",
|
||||
"type": "IMAGE",
|
||||
"link": 35
|
||||
},
|
||||
{
|
||||
"name": "layer_mask",
|
||||
"type": "MASK",
|
||||
"link": 34
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
28
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LayerUtility: ImageBlend"
|
||||
},
|
||||
"widgets_values": [
|
||||
false,
|
||||
"normal",
|
||||
100
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
27,
|
||||
7,
|
||||
0,
|
||||
17,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
28,
|
||||
17,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
32,
|
||||
2,
|
||||
0,
|
||||
18,
|
||||
0,
|
||||
"*"
|
||||
],
|
||||
[
|
||||
33,
|
||||
18,
|
||||
0,
|
||||
13,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
34,
|
||||
18,
|
||||
0,
|
||||
17,
|
||||
2,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
35,
|
||||
2,
|
||||
0,
|
||||
17,
|
||||
1,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
{
|
||||
"last_node_id": 17,
|
||||
"last_link_id": 30,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
210,
|
||||
670
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 314
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
22,
|
||||
26
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"1344x768_girl2 (3).png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "LayerMask: SegmentAnythingUltra",
|
||||
"pos": [
|
||||
600,
|
||||
670
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 246
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 22
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
24
|
||||
],
|
||||
"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,
|
||||
true,
|
||||
"subject"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "LayerUtility: LayerMaskTransform",
|
||||
"pos": [
|
||||
960,
|
||||
620
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 226
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 24
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"links": [
|
||||
16,
|
||||
30
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LayerUtility: LayerMaskTransform"
|
||||
},
|
||||
"widgets_values": [
|
||||
-638,
|
||||
-286,
|
||||
"None",
|
||||
1.75,
|
||||
1,
|
||||
12.85,
|
||||
"lanczos",
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "LayerUtility: LayerImageTransform",
|
||||
"pos": [
|
||||
970,
|
||||
310
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 226
|
||||
},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 26
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
19,
|
||||
29
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LayerUtility: LayerImageTransform"
|
||||
},
|
||||
"widgets_values": [
|
||||
-82,
|
||||
-191,
|
||||
"horizontal",
|
||||
1.45,
|
||||
1.1400000000000001,
|
||||
-7.69,
|
||||
"lanczos",
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "LayerMask: MaskPreview",
|
||||
"pos": [
|
||||
1340,
|
||||
730
|
||||
],
|
||||
"size": [
|
||||
305.2870341796877,
|
||||
194.06607275390638
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": 16
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LayerMask: MaskPreview"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1330,
|
||||
460
|
||||
],
|
||||
"size": [
|
||||
303.8503133544925,
|
||||
210.96479357910164
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 19
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "LayerUtility: ImageBlend",
|
||||
"pos": [
|
||||
1380,
|
||||
230
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 146
|
||||
},
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "background_image",
|
||||
"type": "IMAGE",
|
||||
"link": 27
|
||||
},
|
||||
{
|
||||
"name": "layer_image",
|
||||
"type": "IMAGE",
|
||||
"link": 29
|
||||
},
|
||||
{
|
||||
"name": "layer_mask",
|
||||
"type": "MASK",
|
||||
"link": 30
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
28
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LayerUtility: ImageBlend"
|
||||
},
|
||||
"widgets_values": [
|
||||
false,
|
||||
"normal",
|
||||
100
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1750,
|
||||
280
|
||||
],
|
||||
"size": {
|
||||
"0": 914.7281494140625,
|
||||
"1": 579.1661987304688
|
||||
},
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 28
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
210,
|
||||
220
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 314
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
27
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"1344x768_beach (17).png",
|
||||
"image"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
16,
|
||||
12,
|
||||
0,
|
||||
13,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
19,
|
||||
11,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
22,
|
||||
2,
|
||||
0,
|
||||
16,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
24,
|
||||
16,
|
||||
1,
|
||||
12,
|
||||
0,
|
||||
"MASK"
|
||||
],
|
||||
[
|
||||
26,
|
||||
2,
|
||||
0,
|
||||
11,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
27,
|
||||
7,
|
||||
0,
|
||||
17,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
28,
|
||||
17,
|
||||
0,
|
||||
9,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
29,
|
||||
11,
|
||||
0,
|
||||
17,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
30,
|
||||
12,
|
||||
0,
|
||||
17,
|
||||
2,
|
||||
"MASK"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||