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: +![image](image/image_tagger_save_v2_node.jpg) +* 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: +![image](image/load_images_from_path_node.jpg) +* 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) 节点保存文件夹中对应图片的文本标签文件,并保持原文件名。 + +在原节点上增加了以下选项: +![image](image/image_tagger_save_v2_node.jpg) +* custom_filename: 用户自定义文件名。如果此处有输入则作为保存文件名,否则使用filename_prefix作为文件名前缀。 +* remove_custom_filename_ext: 是否移除原文件名中的扩展名。 + +### LoadImagesFromPath +从指定的文件夹中加载图片。 + +节点选项说明: +![image](image/load_images_from_path_node.jpg) +* 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"]