diff --git a/README.MD b/README.MD
index 6dba8f0..2d3ca21 100644
--- a/README.MD
+++ b/README.MD
@@ -147,6 +147,8 @@ When this error has occurred, please check the network environment.
**If the dependency package error after updating, please double clicking ```repair_dependency.bat``` (for Official ComfyUI Protable) or ```repair_dependency_aki.bat``` (for ComfyUI-aki-v1.x) in the plugin folder to reinstall the dependency packages.
+* Commit [LoadImagesFromPath](#LoadImagesFromPath) and [ImageTaggerSaveV2](#ImageTaggerSaveV2) nodes, used to load a list of images from a folder and save images and tagger text file with corresponding file names.
+* Commit [LoadImageFromPath](LoadImageFromPath) node, The images in a folder can be loaded and output as image list, also supporting output of a list corresponding to a file name.
* Commit [SegformerUltraV3](SegformerUltraV3), [LoadSegformerModel](LoadSegformerModel), [SegformerClothesSetting](SegformerClothesSetting) and [SegformerFashionSetting](SegformerFashionSetting) nodes, Separate the loading of models and settings to save resources when using multiple nodes.
* Add multiple languages and increase support for 5 languages: Chinese, French, Japanese, Korean and Russian. This feature producted by [ComfyUI-Globalization-Node-Translation](https://github.com/yamanacn/ComfyUI-Globalization-Node-Translation), thank you to the original author.
* Commit [HalfTone](#HalfTone) node, use for halftone processing of images.
@@ -1744,6 +1746,28 @@ 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.
+### ImageTaggerSaveV2
+The upgraded version of [ImageTaggerSave](#ImageTaggerSave) node can be used in conjunction with [LoadImagesFromPath](#LoadImagesFromPath) node to save the text label files of corresponding images in the folder, while maintaining the original file name.
+
+The following options have been added to the original node:
+
+* custom_filename: User defined file name. If there is input here, use it as the save file name; otherwise, use filename_prefix as the file name prefix.
+* remove_custom_filename_ext: Whether to remove the extension from the original file name.
+
+### LoadImagesFromPath
+Load images from the specified folder.
+
+Node Options:
+
+* path: Folder path.
+* image_load_cap: The number of output files. The default value of 0 means to read all image files in the folder.
+* select_every_nth: Loads one image every ```select_every_nth``` images, skipping others.
+
+Outputs:
+* images: Output image list.
+* masks: Output the mask list corresponding to the image.
+* file_name: Output a list of file names corresponding to the images.
+* frame_count: Output the total number of images.
# LayerMask
diff --git a/README_CN.MD b/README_CN.MD
index 3f6f9c7..36d6ead 100644
--- a/README_CN.MD
+++ b/README_CN.MD
@@ -128,6 +128,7 @@ os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
## 更新说明
**如果本插件更新后出现依赖包错误,请双击运行插件目录下的```install_requirements.bat```(官方便携包),或 ```install_requirements_aki.bat```(秋叶整合包) 重新安装依赖包。
+* 添加 [LoadImagesFromPath](#LoadImagesFromPath) 和 [ImageTaggerSaveV2](#ImageTaggerSaveV2) 节点,用于从文件夹加载图片列表并保存对应文件名的图片和标签。
* 添加 [SegformerUltraV3](SegformerUltraV3), [LoadSegformerModel](LoadSegformerModel), [SegformerClothesSetting](SegformerClothesSetting) 和 [SegformerFashionSetting](SegformerFashionSetting) 节点,将模型与设置分离加载,在使用多个节点时节省资源。
* 添加多语言,除英语外,增加支持5种语言:中文、法语、日语、韩语和俄语。该功能使用[ComfyUI-Globalization-Node-Translation](https://github.com/yamanacn/ComfyUI-Globalization-Node-Translation)制作,感谢原作者。
* 添加 [HalfTone](#HalfTone) 节点,用于对图像进行半色调处理。
@@ -159,6 +160,7 @@ LayerMask: SegmentAnythingUltra, LayerMask: SegmentAnythingUltra V2, LayerMask:
LayerUtility: UserPromptGeneratorTxt2ImgPrompt, LayerUtility: UserPromptGeneratorTxt2ImgPromptWithReference, LayerUtility: UserPromptGeneratorReplaceWord,
LayerUtility: AddBlindWaterMark, LayerUtility: ShowBlindWaterMark, LayerMask: YoloV8Detect
+* 添加 [LoadImageFromPath](LoadImageFromPath) 节点,可将一个文件夹中的图片加载为图片列表输出,支持单个文件名对应列表输出。
* 合并[alexisrolland](https://github.com/alexisrolland) 提交的分支,添加Image Blend Advanced v3 和 Drop Shadow v3 节点,支持透明背景。
* 添加[BenUltra](#BenUltra) 和 [LoadBenModel](#LoadBenModel)节点。这两个节点是[PramaLLC/BEN](https://huggingface.co/PramaLLC/BEN) 项目在ComfyUI中的实现。
从 [huggingface](https://huggingface.co/PramaLLC/BEN/tree/main) 或 [百度网盘](https://pan.baidu.com/s/17mdBxfBl_R97mtNHuiHsxQ?pwd=2jn3)下载```BEN_Base.pth``` 和 ```config.json``` 两个文件并复制到 ```ComfyUI/models/BEN```文件夹。
@@ -1555,6 +1557,28 @@ BooleanOperator的升级版,增加了节点内数值输入,增加了大于
*输入```%date```表示当前日期(YY-mm-dd),```%time```表示当前时间(HH-MM-SS)。可以输入```/```表示子目录。例如```%date/name_%time``` 将输出图片到```YY-mm-dd```文件夹下,以```name_HH-MM-SS```为文件名前缀。
+### ImageTaggerSaveV2
+[ImageTaggerSave](#ImageTaggerSave) 节点的升级版,可配合[LoadImagesFromPath](#LoadImagesFromPath) 节点保存文件夹中对应图片的文本标签文件,并保持原文件名。
+
+在原节点上增加了以下选项:
+
+* custom_filename: 用户自定义文件名。如果此处有输入则作为保存文件名,否则使用filename_prefix作为文件名前缀。
+* remove_custom_filename_ext: 是否移除原文件名中的扩展名。
+
+### LoadImagesFromPath
+从指定的文件夹中加载图片。
+
+节点选项说明:
+
+* path: 文件夹路径。
+* image_load_cap: 输出的文件数量。默认值0表示读取文件夹中的所有图片文件。
+* select_every_nth: 每隔多少张图片加载一张图片。
+
+输出:
+* images: 输出的图片列表。
+* masks: 输出图片对应的遮罩列表。
+* file_name: 输出图片对应的文件名列表。
+* frame_count: 输出图片总数。
# LayerMask
diff --git a/image/image_tagger_save_v2_node.jpg b/image/image_tagger_save_v2_node.jpg
new file mode 100644
index 0000000..84aef56
Binary files /dev/null and b/image/image_tagger_save_v2_node.jpg differ
diff --git a/image/load_images_from_path_node.jpg b/image/load_images_from_path_node.jpg
new file mode 100644
index 0000000..ba35004
Binary files /dev/null and b/image/load_images_from_path_node.jpg differ
diff --git a/py/image_tagger_save.py b/py/image_tagger_save.py
index 9dc563e..1d88647 100644
--- a/py/image_tagger_save.py
+++ b/py/image_tagger_save.py
@@ -1,4 +1,5 @@
import os.path
+from pathlib import Path
import shutil
from PIL import Image
from PIL.PngImagePlugin import PngInfo
@@ -133,10 +134,155 @@ class LSImageTaggerSave:
return { "ui": { "images": results } }
+class LSImageTaggerSave_V2:
+ def __init__(self):
+ self.output_dir = folder_paths.get_output_directory()
+ self.type = "output"
+ self.prefix_append = ""
+ self.compress_level = 4
+ self.NODE_NAME = 'ImageTaggerSaveV2'
+
+ @classmethod
+ def INPUT_TYPES(s):
+ return {"required":
+ {"image": ("IMAGE", ),
+ "tag_text": ("STRING", {"default": "", "forceInput":True}),
+ "custom_path": ("STRING", {"default": ""}),
+ "custom_filename": ("STRING", {"default": ""}),
+ "remove_custom_filename_ext": ("BOOLEAN", {"default": True}),
+ "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_v2"
+ OUTPUT_NODE = True
+ CATEGORY = '😺dzNodes/LayerUtility/SystemIO'
+
+ def image_tagger_save_v2(self, image, tag_text, custom_path, custom_filename, remove_custom_filename_ext,
+ 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])
+
+ if custom_filename != "":
+ if remove_custom_filename_ext:
+ file = Path(custom_filename).stem
+ else:
+ file = custom_filename
+
+ if timestamp == "millisecond":
+ file = f'{file}_{now.strftime("%Y-%m-%d_%H-%M-%S-%f")[:-3]}'
+ elif timestamp == "second":
+ file = f'{file}_{now.strftime("%Y-%m-%d_%H-%M-%S")}'
+
+ else:
+ 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}'
+
+ 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))
+
+ preview_filename = ""
+ if custom_path != "":
+ if not os.path.exists(custom_path):
+ try:
+ os.makedirs(custom_path)
+ except Exception as e:
+ log(f"Error: {self.NODE_NAME} skipped, because unable to create temporary folder.",
+ message_type='warning')
+ raise FileNotFoundError(f"cannot create custom_path {custom_path}, {e}")
+ else:
+ custom_path = folder_paths.get_output_directory()
+
+ 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: {self.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: {self.NODE_NAME} skipped, because unable to create temporary file.", message_type='warning')
+
+ if custom_filename == "":
+ # 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"{self.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
+ "LayerUtility: ImageTaggerSave": LSImageTaggerSave,
+ "LayerUtility: ImageTaggerSaveV2": LSImageTaggerSave_V2
}
NODE_DISPLAY_NAME_MAPPINGS = {
- "LayerUtility: ImageTaggerSave": "LayerUtility: Image Tagger Save"
+ "LayerUtility: ImageTaggerSave": "LayerUtility: Image Tagger Save",
+ "LayerUtility: ImageTaggerSaveV2": "LayerUtility: Image Tagger Save V2",
}
\ No newline at end of file
diff --git a/py/load_images_from_path.py b/py/load_images_from_path.py
new file mode 100644
index 0000000..4a7bf50
--- /dev/null
+++ b/py/load_images_from_path.py
@@ -0,0 +1,110 @@
+import os
+from PIL import Image, ImageSequence, ImageOps
+import torch
+import numpy as np
+import folder_paths
+import node_helpers
+
+class LS_LoadImagesFromPath:
+ @classmethod
+ def INPUT_TYPES(s):
+ return {
+ "required": {
+ "path": ("STRING", {"placeholder": "c:/images", "images_path": []}),
+ },
+ "optional": {
+ "image_load_cap": ("INT", {"default": 0, "min": 0, "max": 999999, "step": 1}),
+ "select_every_nth": ("INT", {"default": 1, "min": 1, "max": 999999, "step": 1}),
+ },
+ }
+
+ RETURN_TYPES = ("IMAGE", "MASK", "STRING", "INT")
+ RETURN_NAMES = ("images", "masks", "file_name", "frame_count")
+ FUNCTION = "ls_load_images"
+ CATEGORY = '😺dzNodes/LayerUtility/SystemIO'
+ OUTPUT_IS_LIST = (True, True, True, False)
+
+
+ def ls_load_images(self, path: str, image_load_cap: int, select_every_nth: int):
+ load_images = []
+ load_masks = []
+ load_file_names = []
+ load_frame_count = 0
+
+
+ if os.path.isdir(path):
+ input_dir = os.path.normpath(path)
+ files = [
+ os.path.join(input_dir, f)
+ for f in os.listdir(input_dir)
+ if os.path.isfile(os.path.join(input_dir, f))
+ ]
+
+ for i in range(len(files)):
+
+ if i % select_every_nth != 0:
+ continue
+
+ image_file = files[i]
+ image_path = folder_paths.get_annotated_filepath(image_file)
+ img = node_helpers.pillow(Image.open, image_path)
+ output_images = []
+ output_masks = []
+ w, h = None, None
+
+ excluded_formats = ['MPO']
+
+ for i in ImageSequence.Iterator(img):
+ i = node_helpers.pillow(ImageOps.exif_transpose, i)
+
+ if i.mode == 'I':
+ i = i.point(lambda i: i * (1 / 255))
+ image = i.convert("RGB")
+
+ if len(output_images) == 0:
+ w = image.size[0]
+ h = image.size[1]
+
+ if image.size[0] != w or image.size[1] != h:
+ continue
+
+ image = np.array(image).astype(np.float32) / 255.0
+ image = torch.from_numpy(image)[None,]
+ if 'A' in i.getbands():
+ mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
+ mask = 1. - torch.from_numpy(mask)
+ elif i.mode == 'P' and 'transparency' in i.info:
+ mask = np.array(i.convert('RGBA').getchannel('A')).astype(np.float32) / 255.0
+ mask = 1. - torch.from_numpy(mask)
+ else:
+ mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
+ output_images.append(image)
+ output_masks.append(mask.unsqueeze(0))
+
+ if len(output_images) > 1 and img.format not in excluded_formats:
+ output_image = torch.cat(output_images, dim=0)
+ output_mask = torch.cat(output_masks, dim=0)
+ else:
+ output_image = output_images[0]
+ output_mask = output_masks[0]
+
+ load_images.append(output_image)
+ load_masks.append(output_mask)
+ load_file_names.append(os.path.basename(image_file))
+ load_frame_count += 1
+ if image_load_cap > 0 and load_frame_count >= image_load_cap:
+ break
+
+ return (load_images, load_masks, load_file_names, load_frame_count)
+
+ else:
+ raise Exception("directory is not valid: " + directory)
+
+
+NODE_CLASS_MAPPINGS = {
+ "LayerUtility: LoadImagesFromPath": LS_LoadImagesFromPath,
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "LayerUtility: LoadImagesFromPath": "LayerUtility: Load Images From Path",
+}
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index de6583c..e538e63 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 = "2.0.21"
+version = "2.0.22"
license = {text = "MIT License"}
dependencies = ["numpy", "pillow", "torch", "matplotlib", "Scipy", "scikit_image", "scikit_learn", "opencv-contrib-python", "pymatting", "timm", "colour-science", "transformers", "blend_modes", "huggingface_hub", "loguru"]