Commit ImageMaskScaleAs node

This commit is contained in:
chflame163
2024-02-01 22:09:08 +08:00
committed by GitHub
parent af621fbd84
commit f600fcbd1d
11 changed files with 465 additions and 5 deletions
+24
View File
@@ -13,6 +13,7 @@ Nodes are divided into four groups according to their functions: LayerStyle, Lay
[中文说明点这里](./README_CN.MD)
## Update
* Commit [ImageMaskScaleAs](#ImageMaskScaleAs) node to adjust the image or mask size based on the reference image.
* Commit [ImageScaleRestore](#ImageScaleRestore) node to work with CropByMask for local upscale and repair works.
* Commit [CropByMask](#CropByMask) and [RestoreCropBox](#RestoreCropBox) nodes. The combination of these two can partially crop and redraw the image before restoring it.
* Commit [ColorAdapter](#ColorAdapter) node, that can automatically adjust the color tone of the image.
@@ -188,6 +189,7 @@ Node options:
* [note](#notes)
# <a id="table1">LayerColor</a>
![image](image/layercolor_title.png)
![image](image/layercolor_nodes.png)
### <a id="table1">LUT</a> Apply
@@ -437,6 +439,28 @@ Outputs:
* mask: If have mask input, the scaled mask will be output.
* original_size: The original size data of the image is used for subsequent node recovery.
### <a id="table1">ImageMaskScaleAs</a>
Scale the image or mask to the size of the reference image (or reference mask).
![image](image/image_mask_scale_as_example.png)
Node options:
![image](image/image_mask_scale_as_node.png)
* scale_as<sup>*</sup>: Reference size. It can be an image or a mask.
* image: Image to be scaled. this option is optional input. if there is no input, a black image will be output.
* mask: Mask to be scaled. this option is optional input. if there is no input, a black mask will be output.
* fit: Scale aspect ratio mode. when the width to height ratio of the original image does not match the scaled size, there are three modes to choose from,
The _letterbox_ mode retains the complete frame and fills in the blank spaces with black;
The _crop_ mode retains the complete short edge, and any excess of the long edge will be cut off;
The _fill_ mode does not maintain frame ratio and fills the screen with width and height.
* method: Scaling sampling methods, including lanczos, bicubic, hamming, bilinear, box, and nearest.
<sup>*</sup>Only limited to input images and masks. forcing the integration of other types of inputs will result in node errors.
Outputs:
* image: If there is an image input, the scaled image will be output.
* mask: If there is a mask input, the scaled mask will be output.
* original_size: The original size data of the image is used for subsequent node recovery.
### <a id="table1">ImageBlend</a>
A simple node for composit layer image and background image, multiple blend modes are available for option, and transparency can be set.
![image](image/image_blend_example.png)
+21
View File
@@ -10,6 +10,7 @@ ComfyUI
* [LayerFilter](#LayerFilter)节点组提供图像效果滤镜。
## 更新说明
* 添加[ImageMaskScaleAs](#ImageMaskScaleAs) 节点,可根据参考图片调整图像或遮罩大小。
* 添加[ImageScaleRestore](#ImageScaleRestore) 节点,用于配合CropByMask进行局部放大修复工作。
* 添加[CropByMask](#CropByMask) 和 [RestoreCropBox](#RestoreCropBox)节点。此二者配合可将图片局部裁切重绘然后还原。
* 添加[ColorAdapter](#ColorAdapter) 节点,可自动调整图片色调。
@@ -182,6 +183,7 @@ ComfyUI
* [节点注解](#节点注解)
# <a id="table1">LayerColor</a>
![image](image/layercolor_title.png)
![image](image/layercolor_nodes.png)
### <a id="table1">LUT</a> Apply
@@ -427,6 +429,25 @@ mask
* mask: 如果有mask输入,将输出缩放后的mask。
* original_size: 图像的原始大小数据,用于后续节点进行恢复。
### <a id="table1">ImageMaskScaleAs</a>
将图像或遮罩缩放到参考图像(或遮罩)的大小。
![image](image/image_mask_scale_as_example.png)
节点选项说明:
![image](image/image_mask_scale_as_node.png)
* scale_as<sup>*</sup>: 参考大小。可以是图像image,也可以是遮罩mask。
* image: 待缩放的图像。此选项为可选输入,如果没有输入将输出纯黑图片。
* mask: 待缩放的遮罩。此选项为可选输入,如果没有输入将输出纯黑遮罩。
* fit: 缩放画幅宽高比模式。当原图与缩放尺寸画幅宽高比例不一致时,有3种模式可以选择, letterbox模式保留完整的画幅,空白处用黑色补足;crop模式保留完整的短边,长边超出部分将被切除;fill模式不保持画幅比例,宽高各自填满画面。
* method: 缩放的采样方法,包括lanczos、bicubic、hamming、bilinear、box和nearest。
<sup>*</sup>仅限输入image和mask, 如果强制接入其他类型输入,将导致节点错误。
输出:
* image: 如果有image输入,将输出缩放后的图像。
* mask: 如果有mask输入,将输出缩放后的遮罩。
* original_size: 图像的原始大小数据,用于后续节点进行恢复。
### <a id="table1">ImageBlend</a>
一个用于合成图层的简单节点,提供多种混合模式供选择,可设置透明度。
![image](image/image_blend_example.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 505 KiB

After

Width:  |  Height:  |  Size: 595 KiB

+74
View File
@@ -0,0 +1,74 @@
from .imagefunc import *
any = AnyType("*")
class ImageMaskScaleAs:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(self):
fit_mode = ['letterbox', 'crop', 'fill']
method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
return {
"required": {
"scale_as": (any, {}),
"fit": (fit_mode,),
"method": (method_mode,),
},
"optional": {
"image": ("IMAGE",), #
"mask": ("MASK",), #
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BOX",)
RETURN_NAMES = ("image", "mask", "original_size")
FUNCTION = 'image_mask_scale_as'
CATEGORY = '😺dzNodes/LayerUtility'
OUTPUT_NODE = True
def image_mask_scale_as(self, scale_as, fit, method,
image=None, mask = None,
):
_asimage = tensor2pil(scale_as)
target_width, target_height = _asimage.size
_mask = Image.new('L', size=_asimage.size, color='black')
_image = Image.new('RGB', size=_asimage.size, color='black')
orig_width = 4
orig_height = 4
if mask is not None:
_mask = tensor2pil(mask).convert('L')
orig_width, orig_height = _mask.size
if image is not None:
_image = tensor2pil(image).convert('RGB')
orig_width, orig_height = _image.size
resize_sampler = Image.LANCZOS
if method == "bicubic":
resize_sampler = Image.BICUBIC
elif method == "hamming":
resize_sampler = Image.HAMMING
elif method == "bilinear":
resize_sampler = Image.BILINEAR
elif method == "box":
resize_sampler = Image.BOX
elif method == "nearest":
resize_sampler = Image.NEAREST
if image is not None:
_image = fit_resize_image(_image, target_width, target_height, fit, resize_sampler)
if mask is not None:
_mask = fit_resize_image(_mask, target_width, target_height, fit, resize_sampler).convert('L')
return (pil2tensor(_image), image2mask(_mask), [orig_width, orig_height],)
NODE_CLASS_MAPPINGS = {
"LayerUtility: ImageMaskScaleAs": ImageMaskScaleAs
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LayerUtility: ImageMaskScaleAs": "LayerUtility: ImageMaskScaleAs"
}
+32
View File
@@ -127,6 +127,33 @@ def motion_blur(image:Image, angle:int, blur:int) -> Image:
ret_image = cv22pil(blurred)
return ret_image
def fit_resize_image(image:Image, target_width:int, target_height:int, fit:str, resize_sampler:str) -> Image:
image = image.convert('RGB')
orig_width, orig_height = image.size
if image is not None:
if fit == 'letterbox':
if orig_width / orig_height > target_width / target_height: # 更宽,上下留黑
fit_width = target_width
fit_height = int(target_width / orig_width * orig_height)
else: # 更瘦,左右留黑
fit_height = target_height
fit_width = int(target_height / orig_height * orig_width)
fit_image = image.resize((fit_width, fit_height), resize_sampler)
ret_image = Image.new('RGB', size=(target_width, target_height), color='black')
ret_image.paste(fit_image, box=((target_width - fit_width)//2, (target_height - fit_height)//2))
elif fit == 'crop':
if orig_width / orig_height > target_width / target_height: # 更宽,裁左右
fit_width = int(orig_height * target_width / target_height)
fit_image = image.crop(
((orig_width - fit_width)//2, 0, (orig_width - fit_width)//2 + fit_width, orig_height))
else: # 更瘦,裁上下
fit_height = int(orig_width * target_height / target_width)
fit_image = image.crop(
(0, (orig_height-fit_height)//2, orig_width, (orig_height-fit_height)//2 + fit_height))
ret_image = fit_image.resize((target_width, target_height), resize_sampler)
else:
ret_image = image.resize((target_width, target_height), resize_sampler)
return ret_image
def __rotate_expand(image:Image, angle:float, SSAA:int=0, method:str="lanczos") -> Image:
images = pil2tensor(image)
@@ -536,3 +563,8 @@ def has_letters(string:str) -> bool:
return True
else:
return False
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
+1 -5
View File
@@ -1,11 +1,7 @@
import json
import torch
from .imagefunc import tensor2pil, log
class AnyType(str):
"""A special class that is always equal in not equal comparisons. Credit to pythongosssss"""
def __ne__(self, __value: object) -> bool:
return False
from .imagefunc import AnyType
any = AnyType("*")
+313
View File
@@ -0,0 +1,313 @@
{
"last_node_id": 65,
"last_link_id": 146,
"nodes": [
{
"id": 42,
"type": "LoadImage",
"pos": [
133,
364
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
137
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": [],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"D04QPvc (26).jpg",
"image"
]
},
{
"id": 59,
"type": "LayerMask: MaskPreview",
"pos": [
1543,
510
],
"size": {
"0": 359.6678771972656,
"1": 408.945556640625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"link": 142
}
],
"properties": {
"Node name for S&R": "LayerMask: MaskPreview"
}
},
{
"id": 16,
"type": "Image Remove Background Rembg (mtb)",
"pos": [
485,
735
],
"size": {
"0": 294,
"1": 230
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 22
}
],
"outputs": [
{
"name": "Image (rgba)",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "Mask",
"type": "MASK",
"links": [
145
],
"shape": 3,
"slot_index": 1
},
{
"name": "Image",
"type": "IMAGE",
"links": [],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "Image Remove Background Rembg (mtb)"
},
"widgets_values": [
false,
240,
10,
10,
false,
"#000000"
]
},
{
"id": 58,
"type": "PreviewImage",
"pos": [
1143,
506
],
"size": {
"0": 372.0478820800781,
"1": 417.78558349609375
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 140
}
],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 64,
"type": "LayerUtility: ImageMaskScaleAs",
"pos": [
783,
507
],
"size": {
"0": 315,
"1": 122
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "scale_as",
"type": "*",
"link": 137
},
{
"name": "image",
"type": "IMAGE",
"link": 144
},
{
"name": "mask",
"type": "MASK",
"link": 145
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
140
],
"shape": 3,
"slot_index": 0
},
{
"name": "mask",
"type": "MASK",
"links": [
142
],
"shape": 3,
"slot_index": 1
},
{
"name": "original_size",
"type": "BOX",
"links": [],
"shape": 3,
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "LayerUtility: ImageMaskScaleAs"
},
"widgets_values": [
"letterbox",
"lanczos"
]
},
{
"id": 2,
"type": "LoadImage",
"pos": [
131,
726
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
22,
144
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"512x512with_background (15).png",
"image"
]
}
],
"links": [
[
22,
2,
0,
16,
0,
"IMAGE"
],
[
137,
42,
0,
64,
0,
"*"
],
[
140,
64,
0,
58,
0,
"IMAGE"
],
[
142,
64,
1,
59,
0,
"MASK"
],
[
144,
2,
0,
64,
1,
"IMAGE"
],
[
145,
16,
1,
64,
2,
"MASK"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}