diff --git a/README.MD b/README.MD index 4938ab8..998e56f 100644 --- a/README.MD +++ b/README.MD @@ -80,6 +80,8 @@ When this error has occurred, please check the network environment. ## Update **If the dependency package error after updating, please reinstall the relevant dependency packages.
+* [CropByMaskV2](#CropByMaskV2) add option to round the cutting size by multiples. +* Commit [CheckMask](#CheckMask) node, it detect whether the mask contains sufficient effective areas. Commit [HSVValue](#HSVValue) node, it convert color values to HSV values. * [BooleanOperatorV2](#BooleanOperatorV2), [NumberCalculatorV2](#NumberCalculatorV2), [Integer](#Integer), [Float](#Float), [Boolean](#Boolean) nodes add string output to output the value as a string for use with [SwitchCase](#SwitchCase). * Commit [SwitchCase](#SwitchCase) node, Switches the output based on the matching string. Can be used for any type of data switching. * Commit [String](#String) node, Used to output a string. It is the TextBox simplified node. @@ -564,6 +566,7 @@ The following changes have been made based on CropByMask: ![image](image/corp_by_mask_v2_node.jpg) * The input ```mask_for_crop``` reanme to ```mask```。 * Add optional inputs to the ```crop_box```. If there are inputs here, mask detection will be ignored and this data will be directly used for cropping. +* Add the option ```round_to_multiple``` to round the trimming edge length multiple. For example, setting it to 8 will force the width and height to be multiples of 8. ### RestoreCropBox Restore the cropped image to the original image by [CropByMask](#CropByMask). @@ -768,6 +771,13 @@ Output the color value as a single R, G, B three decimal values. Supports HEX an Node Options: * color_value: Supports hexadecimal (HEX) or decimal (DEC) color values and should be of string or tuple type. Forcing in other types will result in an error. +### HSVValue +Output color values as individual decimal values of H, S, and V (maximum value of 255). Supports HEX and DEC formats for ColorPicker node output. +![image](image/hsv_value_node.jpg) + +Node Options: +* color_value: Supports hexadecimal (HEX) or decimal (DEC) color values and should be of string or tuple type. Forcing in other types will result in an error. + ### GetColorTone Obtain the main color or average color from the image and output RGB values. ![image](image/get_color_tone_example.jpg) @@ -1169,6 +1179,14 @@ Node Options: * condition: Judgment conditions. ```include``` determines whether it contains a substring, and ```exclude``` determines whether it does not. * sub_string: Substring. +### CheckMask +Check if the mask contains enough valid areas and output a Boolean value. + +Node Options: +![image](image/check_mask_node.jpg) +* white_point: The white point threshold used to determine whether the mask is valid is considered valid if it exceeds this value. +* area_percent: The percentage of effective areas. If the proportion of effective areas exceeds this value, output True. + ### If ![image](image/if_example.jpg) diff --git a/README_CN.MD b/README_CN.MD index 52de01c..f114dc8 100644 --- a/README_CN.MD +++ b/README_CN.MD @@ -80,6 +80,9 @@ git clone https://github.com/chflame163/ComfyUI_LayerStyle.git ## 更新说明 **如果本插件更新后出现依赖包错误,请重新安装相关依赖包。 +* [CropByMaskV2](#CropByMaskV2) 增加裁切尺寸按倍数取整选项。 +* 添加 [CheckMask](#CheckMask) 节点, 用于检测遮罩是否包含足够的有效区域。 +* 添加 [HSVValue](#HSVValue) 节点, 用于转换色值为HSV值。 * [BooleanOperatorV2](#BooleanOperatorV2), [NumberCalculatorV2](#NumberCalculatorV2), [Integer](#Integer), [Float](#Float), [Boolean](#Boolean)节点增加string输出,将值输出为字符串,以配合[SwitchCase](#SwitchCase)使用。 * 添加 [SwitchCase](#SwitchCase) 节点, 根据匹配字符串切换输出。可用于任意类型的数据切换。 * 添加 [String](#String) 节点, 用于输出字符串。这是TextBox简化版节点。 @@ -556,6 +559,7 @@ CropByMask的V2升级版。支持crop_box输入,方便裁切相同尺寸的图 ![image](image/corp_by_mask_v2_node.jpg) * ```mask_for_crop```更名为```mask```。 * 增加```crop_box```可选输入,如果这里有输入将忽略遮罩探测,直接使用此数据裁切。 +* 增加```round_to_multiple```选项,使裁切边长倍数取整。例如设置为8,宽和高将强制设置为8的倍数。 ### RestoreCropBox @@ -758,6 +762,12 @@ ImageScaleByAspectRatio的V2升级版 节点选项说明: * color_value: 支持十六进制(HEX)或十进制(DEC)色值,应是string或tuple类型,强行接入其他类型将导致错误。 +### HSVValue +将色值输出为单独的H, S, V三个10进制数值(最大值255)。支持ColorPicker节点输出的HEX和DEC格式。 +![image](image/hsv_value_node.jpg) + +节点选项说明: +* color_value: 支持十六进制(HEX)或十进制(DEC)色值,应是string或tuple类型,强行接入其他类型将导致错误。 ### GetColorTone 从图片中获取主颜色或平均色。 @@ -1160,6 +1170,14 @@ BooleanOperator的升级版,增加了节点内数值输入,增加了大于 * sub_string: 子字符串文本。 +### CheckMask +检测遮罩是否包含足够的有效区域, 输出布尔值。 + +节点选项说明: +![image](image/check_mask_node.jpg) +* white_point: 判断遮罩是否有效的白点值,高于此值被计入有效。 +* area_percent: 有效区域所占百分比。检测有效区域占比超过此值则输出True。 + ### If ![image](image/if_example.jpg) 根据布尔值条件输入切换输出。可用于任意类型的数据切换,包括且不限于数值、字符串、图片、遮罩、模型、latent、pipe管线等。 diff --git a/image/check_mask_node.jpg b/image/check_mask_node.jpg new file mode 100644 index 0000000..368a6c0 Binary files /dev/null and b/image/check_mask_node.jpg differ diff --git a/image/corp_by_mask_v2_node.jpg b/image/corp_by_mask_v2_node.jpg index bb5ba7e..e68209f 100644 Binary files a/image/corp_by_mask_v2_node.jpg and b/image/corp_by_mask_v2_node.jpg differ diff --git a/image/hsv_value_node.jpg b/image/hsv_value_node.jpg new file mode 100644 index 0000000..c28baf9 Binary files /dev/null and b/image/hsv_value_node.jpg differ diff --git a/py/check_mask.py b/py/check_mask.py new file mode 100644 index 0000000..42669fb --- /dev/null +++ b/py/check_mask.py @@ -0,0 +1,46 @@ +from .imagefunc import * + +NODE_NAME = 'CheckMask' + +# 检查mask是否有效,如果mask面积少于指定比例则判为无效mask +class CheckMask: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + blank_mask_list = ['white', 'black'] + return { + "required": { + "mask": ("MASK",), # + "white_point": ("INT", {"default": 1, "min": 1, "max": 254, "step": 1}), # 用于判断mask是否有效的白点值,高于此值被计入有效 + "area_percent": ("INT", {"default": 1, "min": 1, "max": 99, "step": 1}), # 区域百分比,低于此则mask判定无效 + }, + "optional": { # + } + } + + RETURN_TYPES = ("BOOLEAN",) + RETURN_NAMES = ('bool',) + FUNCTION = 'check_mask' + CATEGORY = '😺dzNodes/LayerUtility' + + def check_mask(self, mask, white_point, area_percent,): + + if mask.dim() == 2: + mask = torch.unsqueeze(mask, 0) + mask = tensor2pil(mask[0]) + if mask.width * mask.height > 262144: + target_width = 512 + target_height = int(target_width * mask.height / mask.width) + mask = mask.resize((target_width, target_height), Image.LANCZOS) + return (mask_white_area(mask, white_point) * 100 > area_percent,) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: CheckMask": CheckMask +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: CheckMask": "LayerUtility: Check Mask" +} \ No newline at end of file diff --git a/py/color_to_HSVvalue.py b/py/color_to_HSVvalue.py new file mode 100644 index 0000000..c393f0d --- /dev/null +++ b/py/color_to_HSVvalue.py @@ -0,0 +1,44 @@ +from .imagefunc import * + +NODE_NAME = 'HSV Value' +any = AnyType("*") + +class ColorValuetoHSVValue: + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(self): + + return { + "required": { + "color_value": (any, {}), + }, + "optional": { + } + } + + RETURN_TYPES = ("INT", "INT", "INT") + RETURN_NAMES = ("H", "S", "V") + FUNCTION = 'color_value_to_hsv_value' + CATEGORY = '😺dzNodes/LayerUtility/Data' + + def color_value_to_hsv_value(self, color_value,): + H, S, V = 0, 0, 0 + if isinstance(color_value, str): + H, S, V = Hex_to_HSV_255level(color_value) + elif isinstance(color_value, tuple): + H, S, V = Hex_to_HSV_255level(RGB_to_Hex(color_value)) + else: + log(f"{NODE_NAME}: color_value input type must be tuple or string.", message_type="error") + + return (H, S, V,) + +NODE_CLASS_MAPPINGS = { + "LayerUtility: HSV Value": ColorValuetoHSVValue +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LayerUtility: HSV Value": "LayerUtility: HSV Value" +} \ No newline at end of file diff --git a/py/crop_by_mask_v2.py b/py/crop_by_mask_v2.py index f1986db..29c15d2 100644 --- a/py/crop_by_mask_v2.py +++ b/py/crop_by_mask_v2.py @@ -9,7 +9,8 @@ class CropByMaskV2: @classmethod def INPUT_TYPES(self): - detect_mode = ['min_bounding_rect', 'max_inscribed_rect', 'mask_area'] + detect_mode = ['mask_area', 'min_bounding_rect', 'max_inscribed_rect'] + multiple_list = ['8', '16', '32', '64', 'None'] return { "required": { "image": ("IMAGE", ), # @@ -20,6 +21,7 @@ class CropByMaskV2: "bottom_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}), "left_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}), "right_reserve": ("INT", {"default": 20, "min": -9999, "max": 9999, "step": 1}), + "round_to_multiple": (multiple_list,), }, "optional": { "crop_box": ("BOX",), @@ -33,7 +35,7 @@ class CropByMaskV2: def crop_by_mask_v2(self, image, mask, invert_mask, detect, top_reserve, bottom_reserve, - left_reserve, right_reserve, + left_reserve, right_reserve, round_to_multiple, crop_box=None ): @@ -63,24 +65,31 @@ class CropByMaskV2: width = 0 height = 0 if detect == "min_bounding_rect": - (x, y, width, height) = min_bounding_rect(bluredmask) + (x, y, w, h) = min_bounding_rect(bluredmask) elif detect == "max_inscribed_rect": - (x, y, width, height) = max_inscribed_rect(bluredmask) + (x, y, w, h) = max_inscribed_rect(bluredmask) else: - (x, y, width, height) = mask_area(_mask) + (x, y, w, h) = mask_area(_mask) - width = num_round_to_multiple(width, 8) - height = num_round_to_multiple(height, 8) - - log(f"{NODE_NAME}: Box detected. x={x},y={y},width={width},height={height}") canvas_width, canvas_height = tensor2pil(torch.unsqueeze(image[0], 0)).convert('RGB').size x1 = x - left_reserve if x - left_reserve > 0 else 0 y1 = y - top_reserve if y - top_reserve > 0 else 0 - x2 = x + width + right_reserve if x + width + right_reserve < canvas_width else canvas_width - y2 = y + height + bottom_reserve if y + height + bottom_reserve < canvas_height else canvas_height + x2 = x + w + right_reserve if x + w + right_reserve < canvas_width else canvas_width + y2 = y + h + bottom_reserve if y + h + bottom_reserve < canvas_height else canvas_height + + if round_to_multiple != 'None': + multiple = int(round_to_multiple) + width = num_round_up_to_multiple(x2 - x1, multiple) + height = num_round_up_to_multiple(y2 - y1, multiple) + x1 = x1 - (width - (x2 - x1)) // 2 + y1 = y1 - (height - (y2 - y1)) // 2 + x2 = x1 + width + y2 = y1 + height + + log(f"{NODE_NAME}: Box detected. x={x1},y={y1},width={width},height={height}") crop_box = (x1, y1, x2, y2) - preview_image = draw_rect(preview_image, x, y, width, height, line_color="#F00000", - line_width=(width + height) // 100) + preview_image = draw_rect(preview_image, x, y, w, h, line_color="#F00000", + line_width=(w + h) // 100) preview_image = draw_rect(preview_image, crop_box[0], crop_box[1], crop_box[2] - crop_box[0], crop_box[3] - crop_box[1], line_color="#00F000", diff --git a/py/imagefunc.py b/py/imagefunc.py index 0102d28..dc24e87 100644 --- a/py/imagefunc.py +++ b/py/imagefunc.py @@ -1631,6 +1631,18 @@ def gray_threshold(image:Image, thresh:int=127, otsu:bool=False) -> Image: def image_to_colormap(image:Image, index:int) -> Image: return cv22pil(cv2.applyColorMap(pil2cv2(image), index)) +# 检查mask有效区域面积比例 +def mask_white_area(mask:Image, white_point:int) -> float: + if mask.mode != 'L': + mask.convert('L') + white_pixels = 0 + for y in range(mask.height): + for x in range(mask.width): + mask.getpixel((x, y)) > 16 + if mask.getpixel((x, y)) > white_point: + white_pixels += 1 + return white_pixels / (mask.width * mask.height) + '''Color Functions''' @@ -1766,6 +1778,7 @@ def random_numbers(total:int, random_range:int, seed:int=0, sum_of_numbers:int=0 ret_list.append((sum_of_numbers - sum(ret_list)) // 2) return ret_list +# 四舍五入取整数倍 def num_round_to_multiple(number:int, multiple:int) -> int: remainder = number % multiple if remainder == 0 : @@ -1776,6 +1789,15 @@ def num_round_to_multiple(number:int, multiple:int) -> int: factor += 1 return factor * multiple +# 向上取整数倍 +def num_round_up_to_multiple(number: int, multiple: int) -> int: + remainder = number % multiple + if remainder == 0: + return number + else: + factor = (number + multiple - 1) // multiple # 向上取整的计算方式 + return factor * multiple + def calculate_side_by_ratio(orig_width:int, orig_height:int, ratio:float, longest_side:int=0) -> int: if orig_width > orig_height: diff --git a/pyproject.toml b/pyproject.toml index 0798e55..c98ab31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_layerstyle" description = "A set of nodes for ComfyUI it generate image like Adobe Photoshop's Layer Style. the Drop Shadow is first completed node, and follow-up work is in progress." -version = "1.0.8" +version = "1.0.9" license = "MIT" dependencies = ["numpy", "pillow", "torch", "matplotlib", "Scipy", "scikit_image", "opencv-contrib-python", "pymatting", "segment_anything", "timm", "addict", "yapf", "colour-science", "wget", "mediapipe", "loguru", "typer_config", "fastapi", "rich", "google-generativeai", "diffusers", "omegaconf", "tqdm", "transformers", "kornia", "image-reward", "ultralytics", "blend_modes", "blind-watermark", "qrcode", "pyzbar", "psd-tools"]