diff --git a/README.MD b/README.MD
index 43d8140..9cf63f5 100644
--- a/README.MD
+++ b/README.MD
@@ -102,6 +102,9 @@ When this error has occurred, please check the network environment.
## Update
**If the dependency package error after updating, please reinstall the relevant dependency packages.
+* Commit [ImageTaggerSave](#ImageTaggerSave) and [ImageAutoCropV3](#ImageAutoCropV3) nodes. Used to implement the automatic trimming and marking workflow for the training set (the workflow ```image_tagger_save.json``` is located in the workflow directory).
+* Commit [CheckMaskV2](#CheckMaskV2) node, Added the ```simple``` method to detect masks more quickly.
+* Commit [ImageReel](#ImageReel) and [ImageReelComposite](#ImageReelComposite) nodes to composite multiple images on a canvas.
* [NumberCalculatorV2](#NumberCalculatorV2) and [NumberCalculator](#NumberCalculator) add the ```min``` and ```max``` method.
* Optimize node loading speed.
* [Florence2Image2Prompt](#Florence2Image2Prompt) add support for ```thwri/CogFlorence-2-Large-Freeze``` and ```thwri/CogFlorence-2.1-Large``` models. Please download the model files from [BaiduNetdisk](https://pan.baidu.com/s/1hzw9-QiU1vB8pMbBgofZIA?pwd=mfl3) or [huggingface/CogFlorence-2-Large-Freeze](https://huggingface.co/thwri/CogFlorence-2-Large-Freeze/tree/main) and [huggingface/CogFlorence-2.1-Large](https://huggingface.co/thwri/CogFlorence-2.1-Large/tree/main) , then copy it to ```ComfyUI/models/florence2``` folder.
@@ -809,6 +812,44 @@ Node options:
* opacity: Opacity of blend.
* [note](#notes)
+
+### ImageReel
+Display multiple images in one reel. Text annotations can be added to each image in the reel. By using the [ImageReelComposite](#ImageReelComposite) node, multiple reel can be combined into one image.
+
+
+Node Options:
+
+* image1: The first image. it must be input.
+* image2: The second image. optional input.
+* image3: The third image. optional input.
+* image4: The fourth image. optional input.
+* image1_text: Text annotation for the first image.
+* image2_text: Text annotation for the second image.
+* image3_text: Text annotation for the third image.
+* image4_text: Text annotation for the fourth image.
+* reel_height: The height of reel.
+* border: The border width of the image in the reel.
+
+Output:
+* reel: The reel of [ImageReelComposite](#ImageReelComposit) node input.
+
+### ImageReelComposite
+Combine multiple reel into one image.
+
+Node Options:
+
+* reel_1: The first reel. it must be input.
+* reel_2: The second reel. optional input.
+* reel_3: The third reel. optional input.
+* reel_4: The fourth reel. optional input.
+* font_file**: Here is a list of available font files in the font folder, and the selected font files will be used to generate images.
+* border: The border width of the reel.
+* color_theme: Theme color for the output image.
+*The font folder is defined in ```resource_dir.ini```, this file is located in the root directory of the plug-in, and the default name is ```resource_dir.ini.example```. to use this file for the first time, you need to change the file suffix to ```.ini```.
+Open the text editing software and find the line starting with "FONT_dir=", after "=", enter the custom folder path name. all font files in this folder will be collected and displayed in the node list during ComfyUI initialization.
+If the folder set in ini is invalid, the font folder that comes with the plugin will be enabled.
+
+
### ImageOpacity
Adjust image opacity

@@ -1138,6 +1179,26 @@ The V2 upgrad version of ```ImageAutoCrop```, it has made the following changes
* scale_by_length: The value here is used as ```scale_by``` to specify the length of the edge.
+### ImageAutoCropV3
+Automatically crop the image to the specified size. You can input a mask to preserve the specified area of the mask. This node is designed to generate image materials for training the model.
+
+Node Options:
+
+* image: The input image.
+* mask: Optional input mask. The masking part will be preserved within the range of the cutting aspect ratio.
+* aspect_ratio: The aspect ratio of the output. Here are common frame ratios provided, with "custom" being the custom ratio and "original" being the original frame ratio.
+* proportional_width: Proportionally wide. If the aspect_ratio option is not 'custom', this setting will be ignored.
+* proportional_height: High proportion. If the aspect_ratio option is not 'custom', this setting will be ignored.
+* method: Scaling sampling methods include Lanczos, Bicubic, Hamming, Bilinear, Box, and Nearest.
+* scale_to_side: Allow scaling to be specified by long side, short side, width, height, or total pixels.
+* scale_to_length: The value here is used as the scale_to-side to specify the length of the edge or the total number of pixels (kilo pixels).
+* round_to_multiple: Multiply to the nearest whole. For example, if set to 8, the width and height will be forcibly set to multiples of 8.
+
+Outputs:
+cropped_image: The cropped image.
+box_preview: Preview of cutting position.
+
+
### HLFrequencyDetailRestore
Using low frequency filtering and retaining high frequency to recover image details. Compared to [kijai's DetailTransfer](https://github.com/kijai/ComfyUI-IC-Light), this node is better integrated with the environment while retaining details.

@@ -1255,6 +1316,12 @@ 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.
+### CheckMaskV2
+On the basis of CheckMask, the ```method``` option has been added, which allows for the selection of different detection methods. The ```area_percent``` is changed to a floating point number with an accuracy of 2 decimal places, which can detect smaller effective areas.
+
+Node Options:
+
+* method: There are two detection methods, which are ```simple``` and ```detectability```. The simple method only detects whether the mask is completely black, while the detect_percent method detects the proportion of effective areas.
### If

@@ -1321,6 +1388,23 @@ Node Options:
* Enter```%date``` for the current date (YY-mm-dd) and ```%time``` for the current time (HH-MM-SS). You can enter ```/``` for subdirectories. For example, ```%date/name_%tiem``` will output the image to the ```YY-mm-dd``` folder, with ```name_HH-MM-SS``` as the file name prefix.
+### ImageTaggerSave
+
+The node used to save the training set images and their text labels, where the image files and text label files have the same file name. Customizable directory for saving images, adding timestamps to file names, selecting save formats, and setting image compression rates.
+*The workflow image_tagger_stave.exe is located in the workflow directory.
+
+Node Options:
+
+* iamge: The input image.
+* tag_text: Text label of image.
+* custom_path*: User-defined directory, enter the directory name in the correct format. If empty, it is saved in the default output directory of ComfyUI.
+* filename_prefix*: The prefix of file name.
+* timestamp: Timestamp the file name, opting for date, time to seconds, and time to milliseconds.
+* format: The format of image save. Currently available in ```png``` and ```jpg```. Note that only png format is supported for RGBA mode pictures.
+* quality: Image quality, the value range 10-100, the higher the value, the better the picture quality, the volume of the file also correspondingly increases.
+* preview: Preview switch.
+
+* Enter```%date``` for the current date (YY-mm-dd) and ```%time``` for the current time (HH-MM-SS). You can enter ```/``` for subdirectories. For example, ```%date/name_%tiem``` will output the image to the ```YY-mm-dd``` folder, with ```name_HH-MM-SS``` as the file name prefix.
### AddBlindWaterMark
diff --git a/README_CN.MD b/README_CN.MD
index 5ef9aa7..ab70dc1 100644
--- a/README_CN.MD
+++ b/README_CN.MD
@@ -103,6 +103,9 @@ git clone https://github.com/chflame163/ComfyUI_LayerStyle.git
## 更新说明
**如果本插件更新后出现依赖包错误,请重新安装相关依赖包。
+* 添加 [ImageTaggerSave](#ImageTaggerSave) 和 [ImageAutoCropV3](#ImageAutoCropV3) 节点,用于实现训练集自动裁切打标工作流(工作流```image_tagger_save_example.json```在workflow目录中)。
+* 添加 [CheckMaskV2](#CheckMaskV2) 节点,增加了```simple```方法以更快速检测遮罩。
+* 添加 [ImageReel ](#ImageReel) 和 [ImageReelComposit](#ImageReelComposit) 节点,可将多张图片显示在一起。
* [NumberCalculatorV2](#NumberCalculatorV2) 和 [NumberCalculator](#NumberCalculator) 节点增加 ```min``` 和 ```max``` 方法。
* 优化节点加载速度。
* [Florence2Image2Prompt](#Florence2Image2Prompt) 增加thwri/CogFlorence-2-Large-Freeze 和 thwri/CogFlorence-2.1-Large 模型支持。请从[百度网盘](https://pan.baidu.com/s/1hzw9-QiU1vB8pMbBgofZIA?pwd=mfl3) 或 [huggingface/CogFlorence-2-Large-Freeze](https://huggingface.co/thwri/CogFlorence-2-Large-Freeze/tree/main) 和 [huggingface/CogFlorence-2.1-Large](https://huggingface.co/thwri/CogFlorence-2.1-Large/tree/main) 下载模型文件并复制到```ComfyUI/models/florence2```文件夹。
@@ -799,6 +802,40 @@ ImageScaleByAspectRatio的V2升级版
* opacity: 不透明度。
* [节点注解](#节点注解)
+### ImageReel
+将多张图片显示在一个卷轴中。可为卷轴中的每张图片添加文字注解。配合[ImageReelComposite](#ImageReelComposit)节点可将多个卷轴拼合为一张图片。
+
+
+节点选项说明:
+
+* image1: 第一张图片。必须输入。
+* image2: 第二张图片。可选输入图片。
+* image3: 第三张图片。可选输入图片。
+* image4: 第四张图片。可选输入图片。
+* image1_text: 第一张图片的文字注解。
+* image2_text: 第二张图片的文字注解。
+* image3_text: 第三张图片的文字注解。
+* image4_text: 第四张图片的文字注解。
+* reel_height: 卷轴高度。
+* border: 卷轴中图片的边框宽度。
+
+输出:
+* reel:卷轴,用于输入[ImageReelComposite](#ImageReelComposit)节点。
+
+### ImageReelComposite
+将多个卷轴拼合为一张图片。
+
+节点选项说明:
+
+* reel_1: 第一个卷轴。必须输入。
+* reel_2: 第二个卷轴。可选输入。
+* reel_3: 第三个卷轴。可选输入。
+* reel_4: 第四个卷轴。可选输入。
+* font_file*: 字体文件。
+* border: 卷轴的边框宽度。
+* color_theme: 主题色。
+*font文件夹在```resource_dir.ini```中定义,这个文件位于插件根目录下, 默认名字是```resource_dir.ini.example```, 初次使用这个文件需将文件后缀改为.ini。用文本编辑软件打开,找到“FONT_dir=”开头的这一行,编辑“=”之后为自定义文件夹路径名。这个文件夹里面所有的.ttf和.otf文件将在ComfyUI初始化时被收集并显示在节点的列表中。如果ini中设定的文件夹无效,将启用插件自带的font文件夹。
+
### ImageOpacity
调整图像不透明度。

@@ -1122,6 +1159,26 @@ cropped_mask: 裁切后的遮罩。
* scale_by: 允许按长边、短边、宽度或高度指定尺寸缩放。
* scale_by_length: 这里的数值作为scale_by指定边的长度。
+### ImageAutoCropV3
+自动裁切图片到指定的尺寸。可输入mask以保留遮罩指定的区域。这个节点是为生成训练模型的图片素材而设计的。
+
+
+节点选项说明:
+
+* image: 输入的图像。
+* mask: 可选输入遮罩。遮罩部分将在裁切长宽比例范围内得到保留。
+* aspect_ratio: 输出的宽高比。这里提供了常见的画幅比例, "custom"为自定义比例, "original"为原始画面比例。
+* proportional_width: 比例宽。如果aspect_ratio选项不是"custom",此处设置将被忽略。
+* proportional_height: 比例高。如果aspect_ratio选项不是"custom",此处设置将被忽略。
+* method: 缩放的采样方法,包括lanczos、bicubic、hamming、bilinear、box和nearest。
+* scale_to_side: 允许按长边、短边、宽度、高度或总像素指定尺寸缩放。
+* scale_to_length: 这里的数值作为scale_to_side指定边的长度, 或者总像素数量(kilo pixels)。
+* round_to_multiple: 倍数取整。例如设置为8,宽和高将强制设置为8的倍数。
+
+输出:
+cropped_image: 裁切后的图像。
+box_preview: 裁切位置预览。
+
### HLFrequencyDetailRestore
使用低频滤波加保留高频来恢复图像细节。相比[kijai's DetailTransfer](https://github.com/kijai/ComfyUI-IC-Light), 这个节点在保留细节的同时,与环境的融合度更好。

@@ -1242,6 +1299,13 @@ BooleanOperator的升级版,增加了节点内数值输入,增加了大于
* white_point: 判断遮罩是否有效的白点值,高于此值被计入有效。
* area_percent: 有效区域所占百分比。检测有效区域占比超过此值则输出True。
+### CheckMaskV2
+在CheckMask基础上增加了```method```选项,可以选择不同的检测方法。area_percent改为浮点数,精度为小数点后2位,可检测更小的有效区域。
+
+节点选项说明:
+
+* method: 检测方法,有```simple``` 和 ```detect_percent``` 两种。simple方法仅检测mask是否全黑,detect_percent方法检测有效区域占比。
+
### If

根据布尔值条件输入切换输出。可用于任意类型的数据切换,包括且不限于数值、字符串、图片、遮罩、模型、latent、pipe管线等。
@@ -1307,6 +1371,24 @@ BooleanOperator的升级版,增加了节点内数值输入,增加了大于
*输入```%date```表示当前日期(YY-mm-dd),```%time```表示当前时间(HH-MM-SS)。可以输入```/```表示子目录。例如```%date/name_%time``` 将输出图片到```YY-mm-dd```文件夹下,以```name_HH-MM-SS```为文件名前缀。
+### ImageTaggerSave
+
+用于保存训练集图片及其文本标签的节点,图片文件和文本标签文件具有相同的文件名。可自定义保存图片的目录,文件名增加时间戳,选择保存格式,设置图片压缩率。
+*工作流image_tagger_save_example.json在workflow目录中。
+
+节点选项说明:
+
+* iamge: 输入的图片。
+* tag_text: 文本标签。
+* custom_path*: 用户自定义目录,请按正确的格式输入目录名。如果为空则保存在ComfyUI默认的output目录。
+* filename_prefix*:文件名前缀。。
+* timestamp: 为文件名加上时间戳,可选择日期、时间到秒和时间到毫秒。
+* format:图片保存格式。目前提供png和jpg两种。
+* quality:图片质量,数值范围10-100,数值越高,图片质量越好,文件的体积也对应增大。
+* preview: 预览开关。
+
+*输入```%date```表示当前日期(YY-mm-dd),```%time```表示当前时间(HH-MM-SS)。可以输入```/```表示子目录。例如```%date/name_%time``` 将输出图片到```YY-mm-dd```文件夹下,以```name_HH-MM-SS```为文件名前缀。
+
### AddBlindWaterMark

给图片添加隐形水印。以肉眼无法觉察的方式添加水印图片,使用```ShowBlindWaterMark```节点可以解码水印。
diff --git a/image/check_mask_v2_node.jpg b/image/check_mask_v2_node.jpg
new file mode 100644
index 0000000..be3a091
Binary files /dev/null and b/image/check_mask_v2_node.jpg differ
diff --git a/image/image_auto_crop_v3_node.jpg b/image/image_auto_crop_v3_node.jpg
new file mode 100644
index 0000000..4bd1683
Binary files /dev/null and b/image/image_auto_crop_v3_node.jpg differ
diff --git a/image/image_reel_composit_node.jpg b/image/image_reel_composit_node.jpg
new file mode 100644
index 0000000..78f37ee
Binary files /dev/null and b/image/image_reel_composit_node.jpg differ
diff --git a/image/image_reel_example.jpg b/image/image_reel_example.jpg
new file mode 100644
index 0000000..a27162f
Binary files /dev/null and b/image/image_reel_example.jpg differ
diff --git a/image/image_reel_node.jpg b/image/image_reel_node.jpg
new file mode 100644
index 0000000..c96c12d
Binary files /dev/null and b/image/image_reel_node.jpg differ
diff --git a/image/image_tagger_save_example.jpg b/image/image_tagger_save_example.jpg
new file mode 100644
index 0000000..79e6bd3
Binary files /dev/null and b/image/image_tagger_save_example.jpg differ
diff --git a/image/image_tagger_save_node.jpg b/image/image_tagger_save_node.jpg
new file mode 100644
index 0000000..04ddb17
Binary files /dev/null and b/image/image_tagger_save_node.jpg differ
diff --git a/py/check_mask_v2.py b/py/check_mask_v2.py
new file mode 100644
index 0000000..cbd0e29
--- /dev/null
+++ b/py/check_mask_v2.py
@@ -0,0 +1,56 @@
+from .imagefunc import *
+
+NODE_NAME = 'CheckMaskV2'
+
+# 检查mask是否有效,如果mask面积少于指定比例则判为无效mask
+class CheckMaskV2:
+
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(self):
+ method_list = ['simple', 'detect_percent']
+ blank_mask_list = ['white', 'black']
+ return {
+ "required": {
+ "mask": ("MASK",), #
+ "method": (method_list,), #
+ "white_point": ("INT", {"default": 1, "min": 1, "max": 254, "step": 1}), # 用于判断mask是否有效的白点值,高于此值被计入有效
+ "area_percent": ("FLOAT", {"default": 0.01, "min": 0, "max": 100, "step": 0.01}), # 区域百分比,低于此则mask判定无效
+ },
+ "optional": { #
+ }
+ }
+
+ RETURN_TYPES = ("BOOLEAN",)
+ RETURN_NAMES = ('bool',)
+ FUNCTION = 'check_mask_v2'
+ CATEGORY = '😺dzNodes/LayerUtility'
+
+ def check_mask_v2(self, mask, method, white_point, area_percent,):
+
+ if mask.dim() == 2:
+ mask = torch.unsqueeze(mask, 0)
+ tensor_mask = mask[0]
+ print(f"tensor_mask={tensor_mask},shape is {tensor_mask.shape}")
+ pil_mask = tensor2pil(tensor_mask)
+ if pil_mask.width * pil_mask.height > 262144:
+ target_width = 512
+ target_height = int(target_width * pil_mask.height / pil_mask.width)
+ pil_mask = pil_mask.resize((target_width, target_height), Image.LANCZOS)
+ ret_bool = False
+ if method == 'simple':
+ ret_bool = is_valid_mask(tensor_mask)
+ else:
+ ret_bool = mask_white_area(pil_mask, white_point) * 100 > area_percent
+
+ return (ret_bool,)
+
+NODE_CLASS_MAPPINGS = {
+ "LayerUtility: CheckMaskV2": CheckMaskV2
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LayerUtility: CheckMaskV2": "LayerUtility: Check Mask V2"
+}
\ No newline at end of file
diff --git a/py/image_auto_crop_v3.py b/py/image_auto_crop_v3.py
new file mode 100644
index 0000000..99e8e8b
--- /dev/null
+++ b/py/image_auto_crop_v3.py
@@ -0,0 +1,187 @@
+from .imagefunc import *
+
+NODE_NAME = 'ImageAutoCropV3'
+
+class ImageAutoCropV3:
+
+ def __init__(self):
+ pass
+
+ @classmethod
+ def INPUT_TYPES(self):
+ ratio_list = ['1:1', '3:2', '4:3', '16:9', '2:3', '3:4', '9:16', 'custom', 'original']
+ scale_to_side_list = ['None', 'longest', 'shortest', 'width', 'height', 'total_pixel(kilo pixel)']
+ multiple_list = ['8', '16', '32', '64', '128', '256', '512', 'None']
+ method_mode = ['lanczos', 'bicubic', 'hamming', 'bilinear', 'box', 'nearest']
+ return {
+ "required": {
+ "image": ("IMAGE", ),
+ "aspect_ratio": (ratio_list,),
+ "proportional_width": ("INT", {"default": 1, "min": 1, "max": 99999999, "step": 1}),
+ "proportional_height": ("INT", {"default": 1, "min": 1, "max": 99999999, "step": 1}),
+ "method": (method_mode,),
+ "scale_to_side": (scale_to_side_list,),
+ "scale_to_length": ("INT", {"default": 1024, "min": 4, "max": 999999, "step": 1}),
+ "round_to_multiple": (multiple_list,),
+ },
+ "optional": {
+ "mask": ("MASK",),
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE", "IMAGE",)
+ RETURN_NAMES = ("cropped_image", "box_preview",)
+ FUNCTION = 'image_auto_crop_v3'
+ CATEGORY = '😺dzNodes/LayerUtility'
+
+ def image_auto_crop_v3(self, image, aspect_ratio,
+ proportional_width, proportional_height, method,
+ scale_to_side, scale_to_length, round_to_multiple,
+ mask=None,
+ ):
+
+ ret_images = []
+ ret_box_previews = []
+ ret_masks = []
+ input_images = []
+ input_masks = []
+ crop_boxs = []
+
+ for l in image:
+ input_images.append(torch.unsqueeze(l, 0))
+ m = tensor2pil(l)
+ if m.mode == 'RGBA':
+ input_masks.append(m.split()[-1])
+ 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'))
+
+ if len(input_masks) > 0 and len(input_masks) != len(input_images):
+ input_masks = []
+ log(f"Warning, {NODE_NAME} unable align alpha to image, drop it.", message_type='warning')
+
+ fit = 'crop'
+ _image = tensor2pil(input_images[0])
+ (orig_width, orig_height) = _image.size
+ if aspect_ratio == 'custom':
+ ratio = proportional_width / proportional_height
+ elif aspect_ratio == 'original':
+ ratio = orig_width / orig_height
+ else:
+ s = aspect_ratio.split(":")
+ ratio = int(s[0]) / int(s[1])
+
+ 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
+
+ # calculate target width and height
+ if ratio > 1:
+ if scale_to_side == 'longest':
+ target_width = scale_to_length
+ target_height = int(target_width / ratio)
+ elif scale_to_side == 'shortest':
+ target_height = scale_to_length
+ target_width = int(target_height * ratio)
+ elif scale_to_side == 'width':
+ target_width = scale_to_length
+ target_height = int(target_width / ratio)
+ elif scale_to_side == 'height':
+ target_height = scale_to_length
+ target_width = int(target_height * ratio)
+ elif scale_to_side == 'total_pixel(kilo pixel)':
+ target_width = math.sqrt(ratio * scale_to_length * 1000)
+ target_height = target_width / ratio
+ target_width = int(target_width)
+ target_height = int(target_height)
+ else:
+ target_width = orig_width
+ target_height = int(target_width / ratio)
+ else:
+ if scale_to_side == 'longest':
+ target_height = scale_to_length
+ target_width = int(target_height * ratio)
+ elif scale_to_side == 'shortest':
+ target_width = scale_to_length
+ target_height = int(target_width / ratio)
+ elif scale_to_side == 'width':
+ target_width = scale_to_length
+ target_height = int(target_width / ratio)
+ elif scale_to_side == 'height':
+ target_height = scale_to_length
+ target_width = int(target_height * ratio)
+ elif scale_to_side == 'total_pixel(kilo pixel)':
+ target_width = math.sqrt(ratio * scale_to_length * 1000)
+ target_height = target_width / ratio
+ target_width = int(target_width)
+ target_height = int(target_height)
+ else:
+ target_height = orig_height
+ target_width = int(target_height * ratio)
+
+ if round_to_multiple != 'None':
+ multiple = int(round_to_multiple)
+ target_width = num_round_up_to_multiple(target_width, multiple)
+ target_height = num_round_up_to_multiple(target_height, multiple)
+
+ for i in range(len(input_images)):
+ _image = tensor2pil(input_images[i]).convert('RGB')
+
+ if len(input_masks) > 0:
+ _mask = input_masks[i]
+ else:
+ _mask = Image.new('L', _image.size, color='black')
+
+ bluredmask = gaussian_blur(_mask, 20).convert('L')
+ (mask_x, mask_y, mask_w, mask_h) = mask_area(bluredmask)
+ orig_ratio = _image.width / _image.height
+ target_ratio = target_width / target_height
+ # crop image to target ratio
+ if orig_ratio > target_ratio: # crop LiftRight side
+ crop_w = int(_image.height * target_ratio)
+ crop_h = _image.height
+ else: # crop TopBottom side
+ crop_w = _image.width
+ crop_h = int(_image.width / target_ratio)
+ crop_x = mask_w // 2 + mask_x - crop_w // 2
+ if crop_x < 0:
+ crop_x = 0
+ if crop_x + crop_w > _image.width:
+ crop_x = _image.width - crop_w
+ crop_y = mask_h // 2 + mask_y - crop_h // 2
+ if crop_y < 0:
+ crop_y = 0
+ if crop_y + crop_h > _image.height:
+ crop_y = _image.height - crop_h
+ crop_image = _image.crop((crop_x, crop_y, crop_x + crop_w, crop_y + crop_h))
+ line_width = (_image.width + _image.height) // 200
+ preview_image = draw_rect(_image, crop_x, crop_y,
+ crop_w, crop_h,
+ line_color="#F00000", line_width=line_width)
+ ret_image = crop_image.resize((target_width, target_height), resize_sampler)
+ ret_images.append(pil2tensor(ret_image))
+ ret_box_previews.append(pil2tensor(preview_image))
+
+ log(f"{NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
+ return (torch.cat(ret_images, dim=0),
+ torch.cat(ret_box_previews, dim=0),
+ )
+
+NODE_CLASS_MAPPINGS = {
+ "LayerUtility: ImageAutoCrop V3": ImageAutoCropV3
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LayerUtility: ImageAutoCrop V3": "LayerUtility: ImageAutoCrop V3"
+}
\ No newline at end of file
diff --git a/py/image_reel.py b/py/image_reel.py
new file mode 100644
index 0000000..708b1e5
--- /dev/null
+++ b/py/image_reel.py
@@ -0,0 +1,214 @@
+from .imagefunc import *
+
+class ImageReelPipeline:
+ def __init__(self):
+ self.image = None
+ self.texts = {}
+ self.reel_height = 0
+ self.reel_border = 0
+
+Reel = ImageReelPipeline()
+class ImageReel:
+
+ def __init__(self):
+ self.NODE_NAME = 'ImageReel'
+
+ @classmethod
+ def INPUT_TYPES(self):
+ return {
+ "required": {
+ "image1": ("IMAGE",),
+ "image1_text": ("STRING", {"multiline": False, "default": "image1"}),
+ "image2_text": ("STRING", {"multiline": False, "default": "image2"}),
+ "image3_text": ("STRING", {"multiline": False, "default": "image3"}),
+ "image4_text": ("STRING", {"multiline": False, "default": "image4"}),
+ "reel_height": ("INT", {"default": 512, "min": 64, "max": 2048}),
+ "border": ("INT", {"default": 32, "min": 8, "max": 512}),
+ },
+ "optional": {
+ "image2": ("IMAGE",),
+ "image3": ("IMAGE",),
+ "image4": ("IMAGE",),
+ }
+ }
+
+ RETURN_TYPES = ("Reel",)
+ RETURN_NAMES = ("reel",)
+ FUNCTION = 'image_reel'
+ CATEGORY = '😺dzNodes/LayerUtility'
+
+ def image_reel(self, image1, image1_text, image2_text, image3_text, image4_text,
+ reel_height, border,
+ image2=None, image3=None, image4=None,):
+
+ image_list = []
+ texts = []
+ for img in image1:
+ i = self.resize_image_to_height(tensor2pil(img.unsqueeze(0)),reel_height)
+ image_list.append(i)
+ texts.append([image1_text,i.width])
+ if image2 is not None:
+ for img in image2:
+ i = self.resize_image_to_height(tensor2pil(img.unsqueeze(0)),reel_height)
+ image_list.append(i)
+ texts.append([image2_text,i.width])
+ if image3 is not None:
+ for img in image3:
+ i = self.resize_image_to_height(tensor2pil(img.unsqueeze(0)),reel_height)
+ image_list.append(i)
+ texts.append([image3_text,i.width])
+ if image4 is not None:
+ for img in image4:
+ i = self.resize_image_to_height(tensor2pil(img.unsqueeze(0)),reel_height)
+ image_list.append(i)
+ texts.append([image4_text,i.width])
+
+ reel = ImageReel()
+ reel.image = self.draw_reel_image(image_list, border, reel_height)
+ reel.texts = texts
+ reel.reel_height = reel_height
+ reel.reel_border = border
+ return (reel,)
+
+ def resize_image_to_height(self, image, target_height) -> Image:
+ w = int(target_height / image.height * image.width)
+ return image.resize((w, target_height), Image.LANCZOS)
+
+ def draw_reel_image(self, image_list, border, reel_height) -> Image:
+ reel_width = 0
+ for img in image_list:
+ reel_width += img.width + border
+ reel_img = Image.new('RGBA', (reel_width, reel_height + border), color=(0, 0, 0, 0))
+ #paste images
+ w = border // 2
+ for img in image_list:
+ reel_img.paste(img, (w, border // 2))
+ w += img.width + border
+ return reel_img
+
+
+class ImageReelComposit:
+
+ def __init__(self):
+ self.NODE_NAME = 'ImageReelComposit'
+
+ @classmethod
+ def INPUT_TYPES(self):
+ color_theme_list = ['light', 'dark']
+ return {
+ "required": {
+ "reel_1": ("Reel",),
+ "font_file": (FONT_LIST,),
+ "font_size": ("INT", {"default": 40, "min": 4, "max": 1024}),
+ "border": ("INT", {"default": 32, "min": 8, "max": 512}),
+ "color_theme": (color_theme_list,),
+ },
+ "optional": {
+ "reel_2": ("Reel",),
+ "reel_3": ("Reel",),
+ "reel_4": ("Reel",),
+ }
+ }
+
+ RETURN_TYPES = ("IMAGE",)
+ RETURN_NAMES = ("image1",)
+ FUNCTION = 'image_reel_composit'
+ CATEGORY = '😺dzNodes/LayerUtility'
+
+ def image_reel_composit(self, reel_1, font_file, font_size, border, color_theme, reel_2=None, reel_3=None, reel_4=None,):
+
+ ret_images = []
+
+ if color_theme == 'light':
+ bg_color = "#E5E5E5"
+ text_color = "#121212"
+ else:
+ bg_color = "#121212"
+ text_color = "#E5E5E5"
+
+
+ font_space = int(font_size * 1.5)
+ width = reel_1.image.width
+ height = reel_1.image.height + font_space + border
+ if reel_2 is not None:
+ width = max(width, reel_2.image.width)
+ height += reel_2.image.height + font_space + border
+ if reel_3 is not None:
+ width = max(width, reel_3.image.width)
+ height += reel_3.image.height + font_space + border
+ if reel_4 is not None:
+ width = max(width, reel_4.image.width)
+ height += reel_4.image.height + font_space + border
+
+ ret_image = Image.new('RGB', (width, height), color=bg_color)
+ paste_y = 0
+ reel1_text_image = self.draw_reel_text(reel_1, font_file, font_size, text_color)
+ shadow_size = reel_1.image.height // 80
+ ret_image = self.paste_drop_shadow(ret_image, reel_1.image, reel1_text_image, ((width - reel_1.image.width) // 2, paste_y),
+ shadow_size, text_color)
+
+ paste_y += reel_1.image.height + font_space + border
+ if reel_2 is not None:
+ reel2_text_image = self.draw_reel_text(reel_2, font_file, font_size, text_color)
+ shadow_size = reel_2.image.height // 80
+ ret_image = self.paste_drop_shadow(ret_image, reel_2.image, reel2_text_image, ((width - reel_2.image.width) // 2, paste_y),
+ shadow_size, text_color)
+ paste_y += reel_2.image.height + font_space + border
+ if reel_3 is not None:
+ reel3_text_image = self.draw_reel_text(reel_3, font_file, font_size, text_color)
+ shadow_size = reel_3.image.height // 80
+ ret_image = self.paste_drop_shadow(ret_image, reel_3.image, reel3_text_image,((width - reel_3.image.width) // 2, paste_y),
+ shadow_size, text_color)
+ paste_y += reel_3.image.height + font_space + border
+ if reel_4 is not None:
+ reel4_text_image = self.draw_reel_text(reel_4, font_file, font_size, text_color)
+ shadow_size = reel_4.image.height // 80
+ ret_image = self.paste_drop_shadow(ret_image, reel_4.image, reel4_text_image,((width - reel_4.image.width) // 2, paste_y),
+ shadow_size, text_color)
+
+ ret_images.append(pil2tensor(ret_image))
+
+ log(f"{self.NODE_NAME} Processed {len(ret_images)} image(s).", message_type='finish')
+ return (torch.cat(ret_images, dim=0),)
+
+ def paste_drop_shadow(self, background_image, image, text_image, box, shadow_size, text_color) -> Image:
+ # drop shadow
+ _mask = image.split()[3]
+ _blured_mask = gaussian_blur(_mask, shadow_size//1.3)
+ _blured_mask = adjust_levels(_blured_mask, 0, 255, 0.5, 0, output_white=54).convert('L')
+ background_image.paste(Image.new('RGBA', image.size, color="black"), (box[0]+shadow_size, box[1]+shadow_size), mask=_blured_mask)
+ background_image.paste(image, box, mask=_mask)
+ background_image.paste(Image.new('RGB', text_image.size, color=text_color), (box[0], box[1] + image.height), mask=text_image.split()[3])
+ return background_image
+
+ def draw_reel_text(self, reel, font_file, font_size, text_color) -> Image:
+ font_path = FONT_DICT.get(font_file)
+ font = ImageFont.truetype(font_path, font_size)
+ texts = reel.texts
+ text_image = Image.new('RGBA', (reel.image.width, reel.reel_border + int(font_size * 1.5)), color=(0, 0, 0, 0))
+ draw = ImageDraw.Draw(text_image)
+ x = reel.reel_border
+ for t in texts:
+ text = t[0]
+ width = t[1]
+ text_width = font.getbbox(text)[2]
+ draw.text(
+ xy=(x + width // 2 - text_width//2, reel.reel_border//4),
+ text=text,
+ fill=text_color,
+ font=font,
+ )
+ x += width + reel.reel_border
+ return text_image
+
+
+
+NODE_CLASS_MAPPINGS = {
+ "LayerUtility: ImageReel": ImageReel,
+ "LayerUtility: ImageReelComposit": ImageReelComposit
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LayerUtility: ImageReel": "LayerUtility: Image Reel",
+ "LayerUtility: ImageReelComposit": "LayerUtility: Image Reel Composit"
+}
\ No newline at end of file
diff --git a/py/image_scale_by_aspect_ratio_v2.py b/py/image_scale_by_aspect_ratio_v2.py
index 9da5751..0b2b5bf 100644
--- a/py/image_scale_by_aspect_ratio_v2.py
+++ b/py/image_scale_by_aspect_ratio_v2.py
@@ -63,17 +63,23 @@ class ImageScaleByAspectRatioV2:
mask = torch.unsqueeze(mask, 0)
for m in mask:
m = torch.unsqueeze(m, 0)
- orig_masks.append(m)
- _width, _height = tensor2pil(orig_masks[0]).size
- if (orig_width > 0 and orig_width != _width) or (orig_height > 0 and orig_height != _height):
- log(f"Error: {NODE_NAME} skipped, because the mask is does'nt match image.", message_type='error')
- return (None, None, None, 0, 0,)
- elif orig_width + orig_height == 0:
- orig_width = _width
- orig_height = _height
+ print(f"m.shape={m.shape}")
+ if not is_valid_mask(m) and m.shape==torch.Size([1,64,64]):
+ log(f"Warning: {NODE_NAME} input mask is empty, ignore it.", message_type='warning')
+ else:
+ orig_masks.append(m)
+
+ if len(orig_masks) > 0:
+ _width, _height = tensor2pil(orig_masks[0]).size
+ if (orig_width > 0 and orig_width != _width) or (orig_height > 0 and orig_height != _height):
+ log(f"Error: {NODE_NAME} execute failed, because the mask is does'nt match image.", message_type='error')
+ return (None, None, None, 0, 0,)
+ elif orig_width + orig_height == 0:
+ orig_width = _width
+ orig_height = _height
if orig_width + orig_height == 0:
- log(f"Error: {NODE_NAME} skipped, because the image or mask at least one must be input.", message_type='error')
+ log(f"Error: {NODE_NAME} execute failed, because the image or mask at least one must be input.", message_type='error')
return (None, None, None, 0, 0,)
if aspect_ratio == 'original':
diff --git a/py/image_tagger_save.py b/py/image_tagger_save.py
new file mode 100644
index 0000000..8c59193
--- /dev/null
+++ b/py/image_tagger_save.py
@@ -0,0 +1,135 @@
+import os.path
+import shutil
+from PIL.PngImagePlugin import PngInfo
+import datetime
+from .imagefunc import *
+
+NODE_NAME = 'ImageTaggerSave'
+
+class LSImageTaggerSave:
+ def __init__(self):
+ self.output_dir = folder_paths.get_output_directory()
+ self.type = "output"
+ self.prefix_append = ""
+ self.compress_level = 4
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required":
+ {"image": ("IMAGE", ),
+ "tag_text": ("STRING", {"default": "", "forceInput":True}),
+ "custom_path": ("STRING", {"default": ""}),
+ "filename_prefix": ("STRING", {"default": "comfyui"}),
+ "timestamp": (["None", "second", "millisecond"],),
+ "format": (["png", "jpg"],),
+ "quality": ("INT", {"default": 80, "min": 10, "max": 100, "step": 1}),
+ "preview": ("BOOLEAN", {"default": True}),
+ },
+ "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
+ }
+
+ RETURN_TYPES = ()
+ FUNCTION = "image_tagger_save"
+ OUTPUT_NODE = True
+ CATEGORY = '😺dzNodes/LayerUtility/SystemIO'
+
+ def image_tagger_save(self, image, tag_text, custom_path, filename_prefix, timestamp, format, quality,
+ preview,
+ prompt=None, extra_pnginfo=None):
+
+ now = datetime.datetime.now()
+ custom_path = custom_path.replace("%date", now.strftime("%Y-%m-%d"))
+ custom_path = custom_path.replace("%time", now.strftime("%H-%M-%S"))
+ filename_prefix = filename_prefix.replace("%date", now.strftime("%Y-%m-%d"))
+ filename_prefix = filename_prefix.replace("%time", now.strftime("%H-%M-%S"))
+ filename_prefix += self.prefix_append
+ full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, image[0].shape[1], image[0].shape[0])
+ results = list()
+ temp_sub_dir = generate_random_name('_savepreview_', '_temp', 16)
+ temp_dir = os.path.join(folder_paths.get_temp_directory(), temp_sub_dir)
+ metadata = None
+ i = 255. * image[0].cpu().numpy()
+ img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
+
+ if timestamp == "millisecond":
+ file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}'
+ elif timestamp == "second":
+ file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S")}'
+ else:
+ file = f'{filename}_{counter:08}'
+
+ preview_filename = ""
+ if custom_path != "":
+ if not os.path.exists(custom_path):
+ try:
+ os.makedirs(custom_path)
+ except Exception as e:
+ log(f"Error: {NODE_NAME} skipped, because unable to create temporary folder.",
+ message_type='warning')
+ raise FileNotFoundError(f"cannot create custom_path {custom_path}, {e}")
+
+ full_output_folder = os.path.normpath(custom_path)
+ # save preview image to temp_dir
+ if os.path.isdir(temp_dir):
+ shutil.rmtree(temp_dir)
+ try:
+ os.makedirs(temp_dir)
+ except Exception as e:
+ print(e)
+ log(f"Error: {NODE_NAME} skipped, because unable to create temporary folder.",
+ message_type='warning')
+ try:
+ preview_filename = os.path.join(generate_random_name('saveimage_preview_', '_temp', 16) + '.png')
+ img.save(os.path.join(temp_dir, preview_filename))
+ except Exception as e:
+ print(e)
+ log(f"Error: {NODE_NAME} skipped, because unable to create temporary file.", message_type='warning')
+
+ # check if file exists, change filename
+ while os.path.isfile(os.path.join(full_output_folder, f"{file}.{format}")):
+ counter += 1
+ if timestamp == "millisecond":
+ file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}_{counter:08}'
+ elif timestamp == "second":
+ file = f'{filename}_{now.strftime("%Y-%m-%d_%H-%M-%S")}_{counter:08}'
+ else:
+ file = f"{filename}_{counter:08}"
+
+ image_file_name = os.path.join(full_output_folder, f"{file}.{format}")
+ tag_file_name = os.path.join(full_output_folder, f"{file}.txt")
+
+ if format == "png":
+ img.save(image_file_name, pnginfo=metadata, compress_level= (100 - quality) // 10)
+ else:
+ if img.mode == "RGBA":
+ img = img.convert("RGB")
+ img.save(image_file_name, quality=quality)
+ with open(tag_file_name, "w", encoding="utf-8") as f:
+ f.write(remove_empty_lines(tag_text))
+ log(f"{NODE_NAME} -> Saving image to {image_file_name}")
+
+ if preview:
+ if custom_path == "":
+ results.append({
+ "filename": f"{file}.{format}",
+ "subfolder": subfolder,
+ "type": self.type
+ })
+ else:
+ results.append({
+ "filename": preview_filename,
+ "subfolder": temp_sub_dir,
+ "type": "temp"
+ })
+
+ counter += 1
+
+ return { "ui": { "images": results } }
+
+NODE_CLASS_MAPPINGS = {
+ "LayerUtility: ImageTaggerSave": LSImageTaggerSave
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LayerUtility: ImageTaggerSave": "LayerUtility: Image Tagger Save"
+}
\ No newline at end of file
diff --git a/py/imagefunc.py b/py/imagefunc.py
index d3affc6..93eb8f4 100644
--- a/py/imagefunc.py
+++ b/py/imagefunc.py
@@ -1813,6 +1813,8 @@ def HSV_255level_to_Hex(HSV: list) -> str:
return '#' + hex_r + hex_g + hex_b
'''Value Functions'''
+def is_valid_mask(tensor:torch.Tensor) -> bool:
+ return not bool(torch.all(tensor == 0).item())
def step_value(start_value, end_value, total_step, step) -> float: # 按当前步数在总步数中的位置返回比例值
factor = step / total_step
@@ -1973,6 +1975,12 @@ def tensor_info(tensor:object) -> str:
value = f"tensor_info: Not tensor, type is {type(tensor)}"
return value
+# 去除空行
+def remove_empty_lines(text):
+ lines = text.split('\n')
+ non_empty_lines = [line for line in lines if line.strip() != '']
+ return '\n'.join(non_empty_lines)
+
# 去除重复的句子
def remove_duplicate_string(text:str) -> str:
sentences = re.split(r'(?<=[:;,.!?])\s+', text)
diff --git a/pyproject.toml b/pyproject.toml
index 4087a73..e2e5200 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.27"
+version = "1.0.29"
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", "transparent-background", "huggingface_hub", "psd-tools"]
diff --git a/workflow/image_reel_example.json b/workflow/image_reel_example.json
new file mode 100644
index 0000000..e8dcb62
--- /dev/null
+++ b/workflow/image_reel_example.json
@@ -0,0 +1,477 @@
+{
+ "last_node_id": 9,
+ "last_link_id": 8,
+ "nodes": [
+ {
+ "id": 3,
+ "type": "LoadImage",
+ "pos": [
+ 290,
+ 340
+ ],
+ "size": [
+ 315,
+ 314
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 2
+ ],
+ "shape": 3
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "1280x720car.jpg",
+ "image"
+ ]
+ },
+ {
+ "id": 4,
+ "type": "LoadImage",
+ "pos": [
+ 290,
+ 710
+ ],
+ "size": [
+ 315,
+ 314.0000114440918
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 3
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "1344x768_redcar.png",
+ "image"
+ ]
+ },
+ {
+ "id": 7,
+ "type": "LoadImage",
+ "pos": [
+ 990,
+ 650
+ ],
+ "size": {
+ "0": 315,
+ "1": 314
+ },
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 5
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "1344x768_hair.png",
+ "image"
+ ]
+ },
+ {
+ "id": 8,
+ "type": "LoadImage",
+ "pos": [
+ 980,
+ 1020
+ ],
+ "size": {
+ "0": 315,
+ "1": 314
+ },
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 8
+ ],
+ "shape": 3,
+ "slot_index": 0
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "768x1344_dress.png",
+ "image"
+ ]
+ },
+ {
+ "id": 1,
+ "type": "LayerUtility: ImageReel",
+ "pos": [
+ 650,
+ 590
+ ],
+ "size": {
+ "0": 315,
+ "1": 238
+ },
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image1",
+ "type": "IMAGE",
+ "link": 2
+ },
+ {
+ "name": "image2",
+ "type": "IMAGE",
+ "link": 3
+ },
+ {
+ "name": "image3",
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "name": "image4",
+ "type": "IMAGE",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "reel",
+ "type": "Reel",
+ "links": [
+ 1
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerUtility: ImageReel"
+ },
+ "widgets_values": [
+ "image1 on reel1",
+ "image2 on reel1",
+ "image3",
+ "image4",
+ 768,
+ 32
+ ]
+ },
+ {
+ "id": 5,
+ "type": "LayerUtility: ImageReel",
+ "pos": [
+ 1350,
+ 750
+ ],
+ "size": {
+ "0": 315,
+ "1": 238
+ },
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image1",
+ "type": "IMAGE",
+ "link": 4
+ },
+ {
+ "name": "image2",
+ "type": "IMAGE",
+ "link": 5
+ },
+ {
+ "name": "image3",
+ "type": "IMAGE",
+ "link": 8
+ },
+ {
+ "name": "image4",
+ "type": "IMAGE",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "reel",
+ "type": "Reel",
+ "links": [
+ 6
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerUtility: ImageReel"
+ },
+ "widgets_values": [
+ "image1 on reel2",
+ "image2 on reel2",
+ "image3 on reel2",
+ "image4",
+ 1024,
+ 32
+ ]
+ },
+ {
+ "id": 2,
+ "type": "LayerUtility: ImageReelComposit",
+ "pos": [
+ 1690,
+ 590
+ ],
+ "size": {
+ "0": 315,
+ "1": 190
+ },
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "reel_1",
+ "type": "Reel",
+ "link": 1
+ },
+ {
+ "name": "reel_2",
+ "type": "Reel",
+ "link": 6
+ },
+ {
+ "name": "reel_3",
+ "type": "Reel",
+ "link": null
+ },
+ {
+ "name": "reel_4",
+ "type": "Reel",
+ "link": null
+ }
+ ],
+ "outputs": [
+ {
+ "name": "image1",
+ "type": "IMAGE",
+ "links": [
+ 7
+ ],
+ "shape": 3,
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerUtility: ImageReelComposit"
+ },
+ "widgets_values": [
+ "Alibaba-PuHuiTi-Heavy.ttf",
+ 40,
+ 32,
+ "light"
+ ]
+ },
+ {
+ "id": 9,
+ "type": "PreviewImage",
+ "pos": [
+ 2060,
+ 380
+ ],
+ "size": [
+ 1023.9856266119964,
+ 713.8515867095819
+ ],
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "images",
+ "type": "IMAGE",
+ "link": 7
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "PreviewImage"
+ }
+ },
+ {
+ "id": 6,
+ "type": "LoadImage",
+ "pos": [
+ 990,
+ 280
+ ],
+ "size": {
+ "0": 315,
+ "1": 314
+ },
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 4
+ ],
+ "shape": 3
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImage"
+ },
+ "widgets_values": [
+ "768x1344_beach.png",
+ "image"
+ ]
+ }
+ ],
+ "links": [
+ [
+ 1,
+ 1,
+ 0,
+ 2,
+ 0,
+ "Reel"
+ ],
+ [
+ 2,
+ 3,
+ 0,
+ 1,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 3,
+ 4,
+ 0,
+ 1,
+ 1,
+ "IMAGE"
+ ],
+ [
+ 4,
+ 6,
+ 0,
+ 5,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 5,
+ 7,
+ 0,
+ 5,
+ 1,
+ "IMAGE"
+ ],
+ [
+ 6,
+ 5,
+ 0,
+ 2,
+ 1,
+ "Reel"
+ ],
+ [
+ 7,
+ 2,
+ 0,
+ 9,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 8,
+ 8,
+ 0,
+ 5,
+ 2,
+ "IMAGE"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {
+ "ds": {
+ "scale": 0.6209213230591553,
+ "offset": [
+ 304.4138606804165,
+ 388.12270422110066
+ ]
+ }
+ },
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/workflow/image_tagger_save_example.json b/workflow/image_tagger_save_example.json
new file mode 100644
index 0000000..6aaf2c6
--- /dev/null
+++ b/workflow/image_tagger_save_example.json
@@ -0,0 +1,552 @@
+{
+ "last_node_id": 61,
+ "last_link_id": 74,
+ "nodes": [
+ {
+ "id": 55,
+ "type": "LayerMask: LoadFlorence2Model",
+ "pos": [
+ 1252.96286383667,
+ 367.9042214660644
+ ],
+ "size": {
+ "0": 315,
+ "1": 58
+ },
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "florence2_model",
+ "type": "FLORENCE2",
+ "links": [
+ 64
+ ],
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: LoadFlorence2Model"
+ },
+ "widgets_values": [
+ "CogFlorence-2-Large-Freeze"
+ ]
+ },
+ {
+ "id": 54,
+ "type": "LayerUtility: Florence2Image2Prompt",
+ "pos": [
+ 1234.96286383667,
+ 492.9042214660644
+ ],
+ "size": {
+ "0": 367.79998779296875,
+ "1": 198
+ },
+ "flags": {},
+ "order": 6,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "florence2_model",
+ "type": "FLORENCE2",
+ "link": 64
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 72
+ }
+ ],
+ "outputs": [
+ {
+ "name": "text",
+ "type": "STRING",
+ "links": [
+ 73
+ ],
+ "slot_index": 0,
+ "shape": 3
+ },
+ {
+ "name": "preview_image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerUtility: Florence2Image2Prompt"
+ },
+ "widgets_values": [
+ "more detailed caption",
+ "",
+ 1024,
+ 3,
+ false,
+ false
+ ]
+ },
+ {
+ "id": 57,
+ "type": "LayerUtility: String",
+ "pos": [
+ 177,
+ 492
+ ],
+ "size": {
+ "0": 315,
+ "1": 58
+ },
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "outputs": [
+ {
+ "name": "string",
+ "type": "STRING",
+ "links": [
+ 70
+ ],
+ "slot_index": 0,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerUtility: String"
+ },
+ "widgets_values": [
+ "E:\\tmp\\test"
+ ]
+ },
+ {
+ "id": 58,
+ "type": "Reroute",
+ "pos": [
+ 1099,
+ 594
+ ],
+ "size": [
+ 75,
+ 26
+ ],
+ "flags": {},
+ "order": 5,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 71
+ }
+ ],
+ "outputs": [
+ {
+ "name": "",
+ "type": "IMAGE",
+ "links": [
+ 72
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "showOutputText": false,
+ "horizontal": false
+ }
+ },
+ {
+ "id": 31,
+ "type": "LayerUtility: ImageTaggerSave",
+ "pos": [
+ 1872,
+ 311
+ ],
+ "size": {
+ "0": 397.0539245605469,
+ "1": 422.8654479980469
+ },
+ "flags": {},
+ "order": 8,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 68
+ },
+ {
+ "name": "tag_text",
+ "type": "STRING",
+ "link": 74,
+ "widget": {
+ "name": "tag_text"
+ }
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerUtility: ImageTaggerSave"
+ },
+ "widgets_values": [
+ "",
+ "e:\\tmp\\test111",
+ "my_training_set",
+ "None",
+ "png",
+ 80,
+ true
+ ]
+ },
+ {
+ "id": 35,
+ "type": "LayerUtility: ImageAutoCrop V3",
+ "pos": [
+ 877,
+ 816
+ ],
+ "size": {
+ "0": 315,
+ "1": 222
+ },
+ "flags": {},
+ "order": 4,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 60
+ },
+ {
+ "name": "mask",
+ "type": "MASK",
+ "link": 51
+ }
+ ],
+ "outputs": [
+ {
+ "name": "cropped_image",
+ "type": "IMAGE",
+ "links": [
+ 68,
+ 71
+ ],
+ "slot_index": 0,
+ "shape": 3
+ },
+ {
+ "name": "box_preview",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerUtility: ImageAutoCrop V3"
+ },
+ "widgets_values": [
+ "custom",
+ 768,
+ 1024,
+ "lanczos",
+ "height",
+ 1024,
+ "8"
+ ]
+ },
+ {
+ "id": 59,
+ "type": "Reroute",
+ "pos": [
+ 1704,
+ 585
+ ],
+ "size": [
+ 75,
+ 26
+ ],
+ "flags": {},
+ "order": 7,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "",
+ "type": "*",
+ "link": 73,
+ "widget": {
+ "name": "value"
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "name": "",
+ "type": "STRING",
+ "links": [
+ 74
+ ],
+ "slot_index": 0
+ }
+ ],
+ "properties": {
+ "showOutputText": false,
+ "horizontal": false
+ }
+ },
+ {
+ "id": 48,
+ "type": "LoadImageListFromDir //Inspire",
+ "pos": [
+ 394,
+ 838
+ ],
+ "size": {
+ "0": 315,
+ "1": 170
+ },
+ "flags": {},
+ "order": 2,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "directory",
+ "type": "STRING",
+ "link": 70,
+ "widget": {
+ "name": "directory"
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "name": "IMAGE",
+ "type": "IMAGE",
+ "links": [
+ 59,
+ 60
+ ],
+ "slot_index": 0,
+ "shape": 6
+ },
+ {
+ "name": "MASK",
+ "type": "MASK",
+ "links": null,
+ "shape": 6
+ },
+ {
+ "name": "FILE PATH",
+ "type": "STRING",
+ "links": null,
+ "shape": 6
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LoadImageListFromDir //Inspire"
+ },
+ "widgets_values": [
+ "E:\\tmp\\test",
+ 0,
+ 0,
+ false
+ ]
+ },
+ {
+ "id": 41,
+ "type": "LayerMask: YoloV8Detect",
+ "pos": [
+ 650,
+ 433.7733489990234
+ ],
+ "size": {
+ "0": 315,
+ "1": 122
+ },
+ "flags": {},
+ "order": 3,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": 59
+ }
+ ],
+ "outputs": [
+ {
+ "name": "mask",
+ "type": "MASK",
+ "links": [
+ 51
+ ],
+ "slot_index": 0,
+ "shape": 3
+ },
+ {
+ "name": "yolo_plot_image",
+ "type": "IMAGE",
+ "links": null,
+ "shape": 3
+ },
+ {
+ "name": "yolo_masks",
+ "type": "MASK",
+ "links": [],
+ "slot_index": 2,
+ "shape": 3
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "LayerMask: YoloV8Detect"
+ },
+ "widgets_values": [
+ "face_yolov8m.pt",
+ "1"
+ ]
+ }
+ ],
+ "links": [
+ [
+ 51,
+ 41,
+ 0,
+ 35,
+ 1,
+ "MASK"
+ ],
+ [
+ 59,
+ 48,
+ 0,
+ 41,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 60,
+ 48,
+ 0,
+ 35,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 64,
+ 55,
+ 0,
+ 54,
+ 0,
+ "FLORENCE2"
+ ],
+ [
+ 68,
+ 35,
+ 0,
+ 31,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 70,
+ 57,
+ 0,
+ 48,
+ 0,
+ "STRING"
+ ],
+ [
+ 71,
+ 35,
+ 0,
+ 58,
+ 0,
+ "*"
+ ],
+ [
+ 72,
+ 58,
+ 0,
+ 54,
+ 1,
+ "IMAGE"
+ ],
+ [
+ 73,
+ 54,
+ 0,
+ 59,
+ 0,
+ "*"
+ ],
+ [
+ 74,
+ 59,
+ 0,
+ 31,
+ 1,
+ "STRING"
+ ]
+ ],
+ "groups": [
+ {
+ "title": "Image2Prompt (can be replaced)",
+ "bounding": [
+ 1040,
+ 280,
+ 794,
+ 454
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "locked": false
+ },
+ {
+ "title": "Input original images folder path",
+ "bounding": [
+ 93,
+ 280,
+ 484,
+ 455
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "locked": false
+ },
+ {
+ "title": "Facial recognition (optional)",
+ "bounding": [
+ 598,
+ 279,
+ 420,
+ 452
+ ],
+ "color": "#3f789e",
+ "font_size": 24,
+ "locked": false
+ },
+ {
+ "title": "Automatic crop and tag workflow by chflame163",
+ "bounding": [
+ 95,
+ 31,
+ 2187,
+ 208
+ ],
+ "color": "#b58b2a",
+ "font_size": 102,
+ "locked": false
+ }
+ ],
+ "config": {},
+ "extra": {
+ "ds": {
+ "scale": 0.9090909090909094,
+ "offset": [
+ -186.56606702980383,
+ -78.17586248866625
+ ]
+ }
+ },
+ "version": 0.4
+}
\ No newline at end of file