Commit ColorofShadow&Highlight and Shadow&HighlightMask nodes, ImageScalyByAspectRatio node add 64 multiple to support LayerDiffusion
This commit is contained in:
@@ -36,6 +36,8 @@ git clone https://github.com/chflame163/ComfyUI_LayerStyle.git
|
||||
|
||||
## Update
|
||||
<font size="4">**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). </font><br />
|
||||
|
||||
* Commit [Color of Shadow & Highlight](#Color of Shadow & Highlight) node, it can adjust the color of the dark and bright parts separately. Commit [Shadow & Highlight Mask](#Shadow & Highlight Mask) 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).
|
||||
* Commit [PromptTagger](#PromptTagger) node,Inference the prompts based on the image. and it can replace key word for the prompt(need apply for Google Studio API key). Upgrade [ColorImageV2](#ColorImageV2) and [GradientImageV2](#GradientImageV2),support user customize preset sizes and size_as input.
|
||||
@@ -276,6 +278,25 @@ Node options:
|
||||
Change the exposure of the image.
|
||||

|
||||
|
||||
### <a id="table1">Color of Shadow & Highlight</a>
|
||||
Adjust the color of the dark and bright parts of the image.
|
||||

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

|
||||
* image: The input image.
|
||||
* mask: Optional input. if there is input, only the colors within the mask range will be adjusted.
|
||||
* shadow_brightness: The brightness of the dark area.
|
||||
* shadow_saturation: The color saturation in the dark area.
|
||||
* shadow_hue: The color hue in the dark area.
|
||||
* shadow_level_offset: The offset of values in the dark area, where larger values bring more areas closer to the bright into the dark area.
|
||||
* shadow_range: The transitional range of the dark area.
|
||||
* highlight_brightness: The brightness of the highlight area.
|
||||
* highlight_saturation: The color saturation in the highlight area.
|
||||
* highlight_hue: The color hue in the highlight area.
|
||||
* highlight_level_offset: The offset of values in the highlight area, where larger values bring more areas closer to the dark into the highlight area.
|
||||
* highlight_range: The transitional range of the highlight area.
|
||||
|
||||
Node option:
|
||||
* exposure: Exposure value. Higher values indicate brighter image.
|
||||
|
||||
@@ -895,6 +916,21 @@ Node options:
|
||||
* white_point: Edge white sampling threshold.
|
||||
* process_detail: Set to false here will skip edge processing to save runtime.
|
||||
|
||||
|
||||
### <a id="table1">Shadow & Highlight Mask</a>
|
||||
Generate masks for the dark and bright parts of the image.
|
||||

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

|
||||
* image: The input image.
|
||||
* mask: Optional input. if there is input, only the colors within the mask range will be adjusted.
|
||||
* shadow_level_offset: The offset of values in the dark area, where larger values bring more areas closer to the bright into the dark area.
|
||||
* shadow_range: The transitional range of the dark area.
|
||||
* highlight_level_offset: The offset of values in the highlight area, where larger values bring more areas closer to the dark into the highlight area.
|
||||
* highlight_range: The transitional range of the highlight area.
|
||||
|
||||
|
||||
### <a id="table1">PixelSpread</a>
|
||||
Pixel expansion preprocessing on the masked edge of an image can effectively improve the edges of image composit.
|
||||

|
||||
|
||||
@@ -37,6 +37,8 @@ git clone https://github.com/chflame163/ComfyUI_LayerStyle.git
|
||||
|
||||
## 更新说明
|
||||
<font size="4">**如果本插件更新后出现依赖包错误,请重新安装相关依赖包。详情见[这里](https://github.com/chflame163/ComfyUI_LayerStyle/issues/5)。 </font><br />
|
||||
|
||||
* 添加 [Color of Shadow & Highlight](#Color of Shadow & Highlight) 节点,可对暗部和亮部分别进行色彩调整。添加 [Shadow & Highlight Mask](#Shadow & Highlight Mask) 节点, 可输出暗部和亮部的遮罩。
|
||||
* 添加 [CropByMaskV2](#CropByMaskV2) 节点,在原节点基础上支持```crop_box```输入,方便裁切相同尺寸的图层。
|
||||
* 添加 [SimpleTextImage](#SimpleTextImage) 节点。从文字生成简单排版的图片以及遮罩。这个节点参考了[ZHO-ZHO-ZHO/ComfyUI-Text_Image-Composite](https://github.com/ZHO-ZHO-ZHO/ComfyUI-Text_Image-Composite)的部分功能和代码。
|
||||
* 添加 [PromptTagger](#PromptTagger) 节点,根据图片反推提示词,可以替换关键词。需要申请Google Studio API使用。升级节点[ColorImageV2](#ColorImageV2)和[GradientImageV2](#GradientImageV2),支持用户自定义预设尺寸和size_as输入。
|
||||
@@ -274,6 +276,25 @@ git clone https://github.com/chflame163/ComfyUI_LayerStyle.git
|
||||
节点选项说明:
|
||||
* exposure: 曝光值。更高的数值表示更亮的曝光。
|
||||
|
||||
### <a id="table1">Color of Shadow & Highlight</a>
|
||||
调整图像暗部和亮部的颜色。
|
||||

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

|
||||
* image: 图像输入。
|
||||
* mask: 可选输入。如果有输入,将只调整遮罩范围内的颜色。
|
||||
* shadow_brightness: 暗部的亮度。
|
||||
* shadow_saturation: 暗部的色彩饱和度。
|
||||
* shadow_hue: 暗部的色相。
|
||||
* shadow_level_offset: 暗部取值的偏移量,更大的数值使更多靠近明亮的区域纳入暗部。
|
||||
* shadow_range: 暗部的过渡范围。
|
||||
* highlight_brightness: 亮部的亮度。
|
||||
* highlight_saturation: 亮部的色彩饱和度。
|
||||
* highlight_hue: 亮部的色相。
|
||||
* highlight_level_offset: 亮部取值的偏移量,更小的数值使更多靠近阴暗的区域纳入亮部。
|
||||
* highlight_range: 亮部的过渡范围。
|
||||
|
||||
### <a id="table1">Gamma</a>
|
||||
改变图像的Gamma值。
|
||||
|
||||
@@ -894,6 +915,20 @@ cropped_mask: 裁切后的遮罩。
|
||||
* process_detail: 此处设为False将跳过边缘处理以节省运行时间。
|
||||
|
||||
|
||||
### <a id="table1">Shadow & Highlight Mask</a>
|
||||
生成图像暗部和亮部的遮罩。
|
||||

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

|
||||
* image: 图像输入。
|
||||
* mask: 可选输入。如果有输入,将只调整遮罩范围内的颜色。
|
||||
* shadow_level_offset: 暗部取值的偏移量,更大的数值使更多靠近明亮的区域纳入暗部。
|
||||
* shadow_range: 暗部的过渡范围。
|
||||
* highlight_level_offset: 亮部取值的偏移量,更小的数值使更多靠近阴暗的区域纳入亮部。
|
||||
* highlight_range: 亮部的过渡范围。
|
||||
|
||||
|
||||
### <a id="table1">PixelSpread</a>
|
||||
对图像的遮罩边缘部分进行像素扩张预处理,可有效改善图像合成的边缘。
|
||||

|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@@ -0,0 +1,128 @@
|
||||
from .imagefunc import *
|
||||
|
||||
NODE_NAME = 'Color of Shadow & Highlight'
|
||||
|
||||
def norm_value(value):
|
||||
if value < 0.01:
|
||||
value = 0.01
|
||||
if value > 0.99:
|
||||
value = 0.99
|
||||
return value
|
||||
class ColorCorrectShadowAndHighlight:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE", ),
|
||||
"shadow_brightness": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
|
||||
"shadow_saturation": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
|
||||
"shadow_hue": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
|
||||
"shadow_level_offset": ("INT", {"default": 0, "min": -99, "max": 99, "step": 1}),
|
||||
"shadow_range": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 0.99, "step": 0.01}),
|
||||
"highlight_brightness": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
|
||||
"highlight_saturation": ("FLOAT", {"default": 1, "min": 0.0, "max": 3, "step": 0.01}),
|
||||
"highlight_hue": ("INT", {"default": 0, "min": -255, "max": 255, "step": 1}),
|
||||
"highlight_level_offset": ("INT", {"default": 0, "min": -99, "max": 99, "step": 1}),
|
||||
"highlight_range": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 0.99, "step": 0.01}),
|
||||
},
|
||||
"optional": {
|
||||
"mask": ("MASK",), #
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = 'color_shadow_and_highlight'
|
||||
CATEGORY = '😺dzNodes/LayerColor'
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def color_shadow_and_highlight(self, image,
|
||||
shadow_brightness, shadow_saturation,
|
||||
shadow_level_offset, shadow_range, shadow_hue,
|
||||
highlight_brightness, highlight_saturation, highlight_hue,
|
||||
highlight_level_offset, highlight_range,
|
||||
mask=None
|
||||
):
|
||||
|
||||
ret_images = []
|
||||
input_images = []
|
||||
input_masks = []
|
||||
|
||||
for i in image:
|
||||
input_images.append(torch.unsqueeze(i, 0))
|
||||
m = tensor2pil(i)
|
||||
if m.mode == 'RGBA':
|
||||
input_masks.append(m.split()[-1])
|
||||
else:
|
||||
input_masks.append(Image.new('L', size=m.size, color='white'))
|
||||
if mask is not None:
|
||||
if mask.dim() == 2:
|
||||
mask = torch.unsqueeze(mask, 0)
|
||||
input_masks = []
|
||||
for m in mask:
|
||||
input_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
|
||||
max_batch = max(len(input_images), len(input_masks))
|
||||
|
||||
for i in range(max_batch):
|
||||
_image = input_images[i] if i < len(input_images) else input_images[-1]
|
||||
_image = tensor2pil(_image).convert('RGB')
|
||||
_mask = input_masks[i] if i < len(input_masks) else input_masks[-1]
|
||||
|
||||
avg_gray = get_gray_average(_image, _mask)
|
||||
shadow_level, highlight_level = calculate_shadow_highlight_level(avg_gray)
|
||||
_canvas = _image.copy()
|
||||
if shadow_saturation !=1 or shadow_brightness !=1 or shadow_hue:
|
||||
shadow_low_threshold = (shadow_level + shadow_level_offset) / 100 + shadow_range / 2
|
||||
shadow_low_threshold = norm_value(shadow_low_threshold)
|
||||
shadow_high_threshold = (shadow_level + shadow_level_offset) / 100 - shadow_range / 2
|
||||
shadow_high_threshold = norm_value(shadow_high_threshold)
|
||||
_shadow_mask = luminance_keyer(_image, shadow_low_threshold, shadow_high_threshold)
|
||||
_shadow = _image.copy()
|
||||
if shadow_brightness != 1:
|
||||
brightness_image = ImageEnhance.Brightness(_shadow)
|
||||
_shadow = brightness_image.enhance(factor=shadow_brightness)
|
||||
if shadow_saturation != 1:
|
||||
color_image = ImageEnhance.Color(_shadow)
|
||||
_shadow = color_image.enhance(factor=shadow_saturation)
|
||||
if shadow_hue:
|
||||
_h, _s, _v = _shadow.convert('HSV').split()
|
||||
_h = image_hue_offset(_h, shadow_hue)
|
||||
_shadow = image_channel_merge((_h, _s, _v), 'HSV')
|
||||
_canvas.paste(_shadow, mask=gaussian_blur(_shadow_mask,(_shadow_mask.width + _shadow_mask.height)//800))
|
||||
_canvas.paste(_image, mask=ImageChops.invert(_mask))
|
||||
if highlight_saturation != 1 or highlight_brightness != 1 or highlight_hue:
|
||||
highlight_low_threshold = (highlight_level + highlight_level_offset) / 100 - highlight_range / 2
|
||||
highlight_low_threshold = norm_value(highlight_low_threshold)
|
||||
highlight_high_threshold = (highlight_level + highlight_level_offset) / 100 + highlight_range / 2
|
||||
highlight_high_threshold = norm_value(highlight_high_threshold)
|
||||
_highlight_mask = luminance_keyer(_image, highlight_low_threshold, highlight_high_threshold)
|
||||
_highlight = _image.copy()
|
||||
if highlight_brightness != 1:
|
||||
brightness_image = ImageEnhance.Brightness(_highlight)
|
||||
_highlight = brightness_image.enhance(factor=highlight_brightness)
|
||||
if highlight_saturation != 1:
|
||||
color_image = ImageEnhance.Color(_highlight)
|
||||
_highlight = color_image.enhance(factor=highlight_saturation)
|
||||
if highlight_hue:
|
||||
_h, _s, _v = _highlight.convert('HSV').split()
|
||||
_h = image_hue_offset(_h, highlight_hue)
|
||||
_highlight = image_channel_merge((_h, _s, _v), 'HSV')
|
||||
_canvas.paste(_highlight, mask=gaussian_blur(_highlight_mask, (_highlight_mask.width + _highlight_mask.height)//800))
|
||||
_canvas.paste(_image, mask=ImageChops.invert(_mask))
|
||||
ret_images.append(pil2tensor(_canvas))
|
||||
|
||||
log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
|
||||
return (torch.cat(ret_images, dim=0),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LayerColor: Color of Shadow & Highlight": ColorCorrectShadowAndHighlight
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LayerColor: Color of Shadow & Highlight": "LayerColor: Color of Shadow & Highlight"
|
||||
}
|
||||
@@ -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', 'None']
|
||||
multiple_list = ['8', '16', '64', 'None']
|
||||
|
||||
return {
|
||||
"required": {
|
||||
|
||||
@@ -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', 'None']
|
||||
multiple_list = ['8', '16', '64', 'None']
|
||||
scale_to_list = ['None', 'longest', 'shortest']
|
||||
return {
|
||||
"required": {
|
||||
|
||||
+83
-68
@@ -277,80 +277,49 @@ def blend_hard_mix(background_image:Image, layer_image:Image) -> Image:
|
||||
img = img * mask
|
||||
return cv22pil(ski2cv2(img))
|
||||
|
||||
def tuple_averge(tuples:list) -> tuple:
|
||||
values = []
|
||||
ret = []
|
||||
for i in tuples[0]:
|
||||
values.append(0)
|
||||
ret.append(0)
|
||||
for t in tuples:
|
||||
for j in range(len(t)):
|
||||
values[j] += t[j]
|
||||
for k in range(len(values)):
|
||||
ret[k] = int(values[k] / len(tuples))
|
||||
return tuple(ret)
|
||||
# def tuple_averge(tuples:list) -> tuple:
|
||||
# values = []
|
||||
# ret = []
|
||||
# for i in tuples[0]:
|
||||
# values.append(0)
|
||||
# ret.append(0)
|
||||
# for t in tuples:
|
||||
# for j in range(len(t)):
|
||||
# values[j] += t[j]
|
||||
# for k in range(len(values)):
|
||||
# ret[k] = int(values[k] / len(tuples))
|
||||
# return tuple(ret)
|
||||
|
||||
def get_pixel_from_round(image:Image, position:tuple) -> tuple:
|
||||
(x, y) = position
|
||||
width, height = image.size
|
||||
pixels = []
|
||||
if x > 0:
|
||||
pixels.append(image.getpixel((x - 1, y)))
|
||||
if y > 0:
|
||||
pixels.append(image.getpixel((x - 1, y - 1)))
|
||||
if y < height:
|
||||
pixels.append(image.getpixel((x - 1, y + 1)))
|
||||
if x < width:
|
||||
pixels.append(image.getpixel((x + 1, y)))
|
||||
if y > 0:
|
||||
pixels.append(image.getpixel((x + 1, y - 1)))
|
||||
if y < height:
|
||||
pixels.append(image.getpixel((x + 1, y + 1)))
|
||||
if y > 0:
|
||||
pixels.append(image.getpixel((x, y-1)))
|
||||
if y < height:
|
||||
pixels.append(image.getpixel((x, y + 1)))
|
||||
return tuple_averge(pixels)
|
||||
# def get_pixel_from_round(image:Image, position:tuple) -> tuple:
|
||||
# (x, y) = position
|
||||
# width, height = image.size
|
||||
# pixels = []
|
||||
# if x > 0:
|
||||
# pixels.append(image.getpixel((x - 1, y)))
|
||||
# if y > 0:
|
||||
# pixels.append(image.getpixel((x - 1, y - 1)))
|
||||
# if y < height:
|
||||
# pixels.append(image.getpixel((x - 1, y + 1)))
|
||||
# if x < width:
|
||||
# pixels.append(image.getpixel((x + 1, y)))
|
||||
# if y > 0:
|
||||
# pixels.append(image.getpixel((x + 1, y - 1)))
|
||||
# if y < height:
|
||||
# pixels.append(image.getpixel((x + 1, y + 1)))
|
||||
# if y > 0:
|
||||
# pixels.append(image.getpixel((x, y-1)))
|
||||
# if y < height:
|
||||
# pixels.append(image.getpixel((x, y + 1)))
|
||||
# return tuple_averge(pixels)
|
||||
|
||||
def displace_pixel(image:Image, source_pixel:tuple, target_pixel:tuple) -> Image:
|
||||
# ret_image = image.copy()
|
||||
image.putpixel(target_pixel, image.getpixel(source_pixel))
|
||||
return image
|
||||
|
||||
def displace_pixel_np(np_image:np.ndarray, source_pixel:tuple, target_pixel:tuple) -> np.ndarray:
|
||||
np_image[target_pixel[1], target_pixel[0], :] = np_image[source_pixel[1], source_pixel[0], :]
|
||||
return np_image
|
||||
|
||||
# def de_warp(image:Image) -> Image:
|
||||
|
||||
#
|
||||
# img = pil2cv2(image)
|
||||
# gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
# edges = cv2.Canny(gray, 50, 150, apertureSize=3)
|
||||
#
|
||||
# # 霍夫变换
|
||||
# lines = cv2.HoughLines(edges, 1, np.pi / 180, 0)
|
||||
# rotate_angle = 0
|
||||
# for rho, theta in lines[0]:
|
||||
# a = np.cos(theta)
|
||||
# b = np.sin(theta)
|
||||
# x0 = a * rho
|
||||
# y0 = b * rho
|
||||
# x1 = int(x0 + 1000 * (-b))
|
||||
# y1 = int(y0 + 1000 * (a))
|
||||
# x2 = int(x0 - 1000 * (-b))
|
||||
# y2 = int(y0 - 1000 * (a))
|
||||
# if x1 == x2 or y1 == y2:
|
||||
# continue
|
||||
# t = float(y2 - y1) / (x2 - x1)
|
||||
# rotate_angle = math.degrees(math.atan(t)) + 45
|
||||
# if rotate_angle > 45:
|
||||
# rotate_angle = -90 + rotate_angle
|
||||
# elif rotate_angle < -45:
|
||||
# rotate_angle = 90 + rotate_angle
|
||||
# rotate_img = scipy.ndimage.rotate(img, rotate_angle)
|
||||
# return cv22pil(rotate_img)
|
||||
|
||||
# def displace_pixel_np(np_image:np.ndarray, source_pixel:tuple, target_pixel:tuple) -> np.ndarray:
|
||||
# np_image[target_pixel[1], target_pixel[0], :] = np_image[source_pixel[1], source_pixel[0], :]
|
||||
# return np_image
|
||||
|
||||
def shift_image(image:Image, distance_x:int, distance_y:int, background_color:str='#000000', cyclic:bool=False) -> Image:
|
||||
width = image.width
|
||||
@@ -717,7 +686,6 @@ def gradient(start_color_inhex:str, end_color_inhex:str, width:int, height:int,
|
||||
|
||||
def draw_rect(image:Image, x:int, y:int, width:int, height:int, line_color:str, line_width:int,
|
||||
box_color:str=None) -> Image:
|
||||
# image = image.convert('RGBA')
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.rectangle((x, y, x + width, y + height), fill=box_color, outline=line_color, width=line_width, )
|
||||
return image
|
||||
@@ -762,6 +730,53 @@ def get_image_color_average(image:Image) -> str:
|
||||
ret_color = RGB_to_Hex(color)
|
||||
return ret_color
|
||||
|
||||
def get_gray_average(image:Image, mask:Image=None) -> int:
|
||||
# image.mode = 'HSV', mask.mode = 'L'
|
||||
image = image.convert('HSV')
|
||||
_, _, _v = image.convert('HSV').split()
|
||||
if mask is not None:
|
||||
if mask.mode != 'L':
|
||||
mask = mask.convert('L')
|
||||
width, height = image.size
|
||||
total_gray = 0
|
||||
valid_pixels = 0
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
if mask is not None:
|
||||
if mask.getpixel((x, y)) > 16: #mask亮度低于16的忽略不计
|
||||
gray = _v.getpixel((x, y))
|
||||
total_gray += gray
|
||||
valid_pixels += 1
|
||||
else:
|
||||
gray = _v.getpixel((x, y))
|
||||
total_gray += gray
|
||||
valid_pixels += 1
|
||||
average_gray = total_gray // valid_pixels
|
||||
return average_gray
|
||||
|
||||
def calculate_shadow_highlight_level(gray:int) -> float:
|
||||
range = 255
|
||||
shadow_exponent = 3
|
||||
highlight_exponent = 2
|
||||
shadow_ratio = gray ** shadow_exponent / range ** shadow_exponent
|
||||
highlight_ratio = gray ** highlight_exponent / range ** highlight_exponent
|
||||
shadow_level = shadow_ratio * 100 + (1 - shadow_ratio) * 32
|
||||
highlight_level = highlight_ratio * 100 + (1 - highlight_ratio) * 32
|
||||
return shadow_level, highlight_level
|
||||
|
||||
def luminance_keyer(image:Image, low:float=0, high:float=1, gamma:float=1) -> Image:
|
||||
image = pil2tensor(image)
|
||||
t = image[:, :, :, :3].detach().clone()
|
||||
alpha = 0.2126 * t[:, :, :, 0] + 0.7152 * t[:, :, :, 1] + 0.0722 * t[:, :, :, 2]
|
||||
if low == high:
|
||||
alpha = (alpha > high).to(t.dtype)
|
||||
else:
|
||||
alpha = (alpha - low) / (high - low)
|
||||
if gamma != 1.0:
|
||||
alpha = torch.pow(alpha, 1 / gamma)
|
||||
alpha = torch.clamp(alpha, min=0, max=1).unsqueeze(3).repeat(1, 1, 1, 3)
|
||||
return tensor2pil(alpha).convert('L')
|
||||
|
||||
def get_image_bright_average(image:Image) -> int:
|
||||
image = image.convert('L')
|
||||
width, height = image.size
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
from .imagefunc import *
|
||||
|
||||
NODE_NAME = 'Shadow & Highlight Mask'
|
||||
|
||||
def norm_value(value):
|
||||
if value < 0.01:
|
||||
value = 0.01
|
||||
if value > 0.99:
|
||||
value = 0.99
|
||||
return value
|
||||
|
||||
class ShadowAndHighlightMask:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE", ),
|
||||
"shadow_level_offset": ("INT", {"default": 0, "min": -99, "max": 99, "step": 1}),
|
||||
"shadow_range": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 0.99, "step": 0.01}),
|
||||
"highlight_level_offset": ("INT", {"default": 0, "min": -99, "max": 99, "step": 1}),
|
||||
"highlight_range": ("FLOAT", {"default": 0.25, "min": 0.01, "max": 0.99, "step": 0.01}),
|
||||
},
|
||||
"optional": {
|
||||
"mask": ("MASK",), #
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK", "MASK")
|
||||
RETURN_NAMES = ("shadow_mask", "highlight_mask")
|
||||
FUNCTION = 'shadow_and_highlight_mask'
|
||||
CATEGORY = '😺dzNodes/LayerMask'
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def shadow_and_highlight_mask(self, image,
|
||||
shadow_level_offset, shadow_range,
|
||||
highlight_level_offset, highlight_range,
|
||||
mask=None
|
||||
):
|
||||
|
||||
ret_shadow_masks = []
|
||||
ret_highlight_masks = []
|
||||
input_images = []
|
||||
input_masks = []
|
||||
|
||||
for i in image:
|
||||
input_images.append(torch.unsqueeze(i, 0))
|
||||
m = tensor2pil(i)
|
||||
if m.mode == 'RGBA':
|
||||
input_masks.append(m.split()[-1])
|
||||
else:
|
||||
input_masks.append(Image.new('L', size=m.size, color='white'))
|
||||
if mask is not None:
|
||||
if mask.dim() == 2:
|
||||
mask = torch.unsqueeze(mask, 0)
|
||||
input_masks = []
|
||||
for m in mask:
|
||||
input_masks.append(tensor2pil(torch.unsqueeze(m, 0)).convert('L'))
|
||||
max_batch = max(len(input_images), len(input_masks))
|
||||
|
||||
for i in range(max_batch):
|
||||
_image = input_images[i] if i < len(input_images) else input_images[-1]
|
||||
_image = tensor2pil(_image).convert('RGB')
|
||||
_mask = input_masks[i] if i < len(input_masks) else input_masks[-1]
|
||||
|
||||
|
||||
avg_gray = get_gray_average(_image, _mask)
|
||||
shadow_level, highlight_level = calculate_shadow_highlight_level(avg_gray)
|
||||
shadow_low_threshold = (shadow_level + shadow_level_offset) / 100 + shadow_range / 2
|
||||
shadow_low_threshold = norm_value(shadow_low_threshold)
|
||||
shadow_high_threshold = (shadow_level + shadow_level_offset) / 100 - shadow_range / 2
|
||||
shadow_high_threshold = norm_value(shadow_high_threshold)
|
||||
_shadow_mask = luminance_keyer(_image, shadow_low_threshold, shadow_high_threshold)
|
||||
|
||||
highlight_low_threshold = (highlight_level + highlight_level_offset) / 100 - highlight_range / 2
|
||||
highlight_low_threshold = norm_value(highlight_low_threshold)
|
||||
highlight_high_threshold = (highlight_level + highlight_level_offset) / 100 + highlight_range / 2
|
||||
highlight_high_threshold = norm_value(highlight_high_threshold)
|
||||
_highlight_mask = luminance_keyer(_image, highlight_low_threshold, highlight_high_threshold)
|
||||
|
||||
black = Image.new('L', size=_image.size, color='black')
|
||||
_mask = ImageChops.invert(_mask)
|
||||
_shadow_mask.paste(black, mask=_mask)
|
||||
_highlight_mask.paste(black, mask=_mask)
|
||||
ret_shadow_masks.append(image2mask(_shadow_mask))
|
||||
ret_highlight_masks.append(image2mask(_highlight_mask))
|
||||
|
||||
log(f"{NODE_NAME} Processed {len(ret_shadow_masks)} image(s).", message_type='finish')
|
||||
return (torch.cat(ret_shadow_masks, dim=0),torch.cat(ret_highlight_masks, dim=0),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LayerMask: Shadow & Highlight Mask": ShadowAndHighlightMask
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LayerMask: Shadow & Highlight Mask": "LayerMask: Shadow & Highlight Mask"
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"last_node_id": 25,
|
||||
"last_link_id": 45,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 15,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1190,
|
||||
158
|
||||
],
|
||||
"size": {
|
||||
"0": 649.0001220703125,
|
||||
"1": 385.00006103515625
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 42
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
194,
|
||||
149
|
||||
],
|
||||
"size": {
|
||||
"0": 504.2838134765625,
|
||||
"1": 408.71527099609375
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
41
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"image (6) (1).png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"type": "LayerColor: Color of Shadow & Highlight",
|
||||
"pos": [
|
||||
788,
|
||||
233
|
||||
],
|
||||
"size": {
|
||||
"0": 327.6000061035156,
|
||||
"1": 294
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 41
|
||||
},
|
||||
{
|
||||
"name": "mask",
|
||||
"type": "MASK",
|
||||
"link": null
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
42
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LayerColor: Color of Shadow & Highlight"
|
||||
},
|
||||
"widgets_values": [
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0.25,
|
||||
0.88,
|
||||
1.36,
|
||||
-125,
|
||||
12,
|
||||
0.28
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
41,
|
||||
12,
|
||||
0,
|
||||
22,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
42,
|
||||
22,
|
||||
0,
|
||||
15,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {},
|
||||
"version": 0.4
|
||||
}
|
||||
Reference in New Issue
Block a user