From ac8c6eb58db28103080b180a8939695626b38b66 Mon Sep 17 00:00:00 2001 From: rui40000 Date: Wed, 8 Apr 2026 14:11:17 +0800 Subject: [PATCH] feat: Add LoadImageWithName node to output filename without extension --- README.md | 23 ++++++++++ __init__.py | 4 ++ load_image_with_name_node.py | 84 ++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 load_image_with_name_node.py diff --git a/README.md b/README.md index a7a111f..3ca386a 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Rui-Node🐶 是一个功能丰富的 ComfyUI 节点集合,提供图像处理 ### 📁 文件存储与加载类 - [按路径加载图像 / Load Image By Path](#3-按路径加载图像--load-image-by-path) +- [加载图像(带文件名) / Load Image With Name](#16-加载图像带文件名--load-image-with-name) ### 🤖 AI模型类 - [千问编辑图像生成 / Qwen Edit Image Generation](#4-千问编辑图像生成--qwen-edit-image-generation) @@ -494,6 +495,28 @@ Rui-Node🐶 是一个功能丰富的 ComfyUI 节点集合,提供图像处理 --- +### 16. 加载图像(带文件名) / Load Image With Name + +**分类**: `Rui-Node🐶/文件存储与加载📁` + +**功能描述**: +基础功能与 ComfyUI 原生的 "Load Image" 节点完全一致,支持从 ComfyUI 的 `input` 目录中选择图像,并支持拖拽上传。区别在于本节点额外提供了一个字符串输出端口,用于输出图像的文件名。 + +**输入参数**: +- `image` (下拉选择): 从 `input` 目录中选择图像文件,或通过按钮上传 + +**输出**: +- `IMAGE`: 图像数据 +- `MASK`: 图像的 Alpha 通道遮罩 +- `filename` (STRING): 图像的文件名(不包含后缀,例如上传了 `test_image.png`,则输出 `test_image`) + +**使用场景**: +- 批量处理图像时,希望以原文件名保存处理后的结果 +- 需要将当前图像的文件名作为提示词或其他参数传递给下游节点 +- 建立更规范的自动化工作流 + +--- + ## 🔧 依赖库 主要依赖库包括: diff --git a/__init__.py b/__init__.py index 4342828..0f85df9 100644 --- a/__init__.py +++ b/__init__.py @@ -5,6 +5,8 @@ from .flip_node import NODE_CLASS_MAPPINGS as FLIP_NODE_CLASS_MAPPINGS from .flip_node import NODE_DISPLAY_NAME_MAPPINGS as FLIP_NODE_DISPLAY_NAME_MAPPINGS from .load_image_node import NODE_CLASS_MAPPINGS as LOAD_NODE_CLASS_MAPPINGS from .load_image_node import NODE_DISPLAY_NAME_MAPPINGS as LOAD_NODE_DISPLAY_NAME_MAPPINGS +from .load_image_with_name_node import NODE_CLASS_MAPPINGS as LOAD_NAME_NODE_CLASS_MAPPINGS +from .load_image_with_name_node import NODE_DISPLAY_NAME_MAPPINGS as LOAD_NAME_NODE_DISPLAY_NAME_MAPPINGS from .qwenedit_node import NODE_CLASS_MAPPINGS as QWEN_NODE_CLASS_MAPPINGS from .qwenedit_node import NODE_DISPLAY_NAME_MAPPINGS as QWEN_NODE_DISPLAY_NAME_MAPPINGS from .shot_splitter_node import NODE_CLASS_MAPPINGS as SHOT_NODE_CLASS_MAPPINGS @@ -36,6 +38,7 @@ NODE_CLASS_MAPPINGS = {} NODE_CLASS_MAPPINGS.update(SAT_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(FLIP_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(LOAD_NODE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(LOAD_NAME_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(QWEN_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(SHOT_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(DIALOGUE_NODE_CLASS_MAPPINGS) @@ -54,6 +57,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS.update(SAT_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(FLIP_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(LOAD_NODE_DISPLAY_NAME_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(LOAD_NAME_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(QWEN_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(SHOT_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(DIALOGUE_NODE_DISPLAY_NAME_MAPPINGS) diff --git a/load_image_with_name_node.py b/load_image_with_name_node.py new file mode 100644 index 0000000..7a5e814 --- /dev/null +++ b/load_image_with_name_node.py @@ -0,0 +1,84 @@ +import os +import torch +import numpy as np +import hashlib +from PIL import Image, ImageOps, ImageSequence +import folder_paths + +class LoadImageWithNameNode: + """ + 基础功能与 ComfyUI 的通用图像加载节点相同,但额外输出图像的文件名(不带后缀)。 + """ + @classmethod + def INPUT_TYPES(s): + input_dir = folder_paths.get_input_directory() + files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))] + return {"required": + {"image": (sorted(files), {"image_upload": True})}, + } + + CATEGORY = "Rui-Node🐶/文件存储与加载📁" + + RETURN_TYPES = ("IMAGE", "MASK", "STRING") + RETURN_NAMES = ("IMAGE", "MASK", "filename") + FUNCTION = "load_image" + + def load_image(self, image): + image_path = folder_paths.get_annotated_filepath(image) + + img = Image.open(image_path) + + # 提取不带后缀的文件名 + filename = os.path.splitext(os.path.basename(image_path))[0] + + output_images = [] + output_masks = [] + for i in ImageSequence.Iterator(img): + i = ImageOps.exif_transpose(i) + + if i.mode == 'I': + i = i.point(lambda i: i * (1 / 255)) + + image_rgba = i.convert("RGBA") + image_rgb = image_rgba.convert("RGB") + + image_np = np.array(image_rgb).astype(np.float32) / 255.0 + output_images.append(image_np) + + if 'A' in i.getbands(): + mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 + mask = 1. - mask + else: + mask = np.zeros((image_np.shape[0], image_np.shape[1]), dtype=np.float32) + output_masks.append(mask) + + if len(output_images) > 1: + output_image = torch.from_numpy(np.stack(output_images)) + output_mask = torch.from_numpy(np.stack(output_masks)) + else: + output_image = torch.from_numpy(output_images[0])[None,] + output_mask = torch.from_numpy(output_masks[0])[None,] + + return (output_image, output_mask, filename) + + @classmethod + def IS_CHANGED(s, image): + image_path = folder_paths.get_annotated_filepath(image) + m = hashlib.sha256() + with open(image_path, 'rb') as f: + m.update(f.read()) + return m.digest().hex() + + @classmethod + def VALIDATE_INPUTS(s, image): + if not folder_paths.exists_annotated_filepath(image): + return "Invalid image file: {}".format(image) + return True + +NODE_CLASS_MAPPINGS = { + "LoadImageWithName": LoadImageWithNameNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LoadImageWithName": "加载图像(带文件名) / Load Image With Name" +}