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:

* 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.
+
+
+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.

@@ -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:
+
+* 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

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输入,方便裁切相同尺寸的图

* ```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格式。
+
+
+节点选项说明:
+* color_value: 支持十六进制(HEX)或十进制(DEC)色值,应是string或tuple类型,强行接入其他类型将导致错误。
### GetColorTone
从图片中获取主颜色或平均色。
@@ -1160,6 +1170,14 @@ BooleanOperator的升级版,增加了节点内数值输入,增加了大于
* sub_string: 子字符串文本。
+### CheckMask
+检测遮罩是否包含足够的有效区域, 输出布尔值。
+
+节点选项说明:
+
+* white_point: 判断遮罩是否有效的白点值,高于此值被计入有效。
+* area_percent: 有效区域所占百分比。检测有效区域占比超过此值则输出True。
+
### If

根据布尔值条件输入切换输出。可用于任意类型的数据切换,包括且不限于数值、字符串、图片、遮罩、模型、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"]