v1.0.7 support auto crop and tag image in foreach

This commit is contained in:
刘雪峰
2024-10-24 17:11:55 +08:00
parent 8086ad3345
commit b428ad49cc
6 changed files with 454 additions and 65 deletions
+60 -51
View File
@@ -22,57 +22,62 @@ Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带
```
## 节点
| 名称 | 说明 |
|------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| LoadImageFromURL | 从网络地址加载图片,一行代表一个图片 |
| LoadMaskFromURL | 从网络地址加载遮罩,一行代表一个 |
| Base64ToImage | 把图片base64字符串转成图片 |
| Base64ToMask | 把遮罩图片base64字符串转成遮罩 |
| ImageToBase64Advanced | 把图片转成base64字符串, 可以选择图片类型(image, mask) ,方便接口调用判断 |
| ImageToBase64 | 把图片转成base64字符串(imageType=["image"]) |
| MaskToBase64Image | 把遮罩转成对应图片的base64字符串(imageType=["mask"]) |
| MaskImageToBase64 | 把遮罩图片转成base64字符串(imageType=["mask"]) |
| LoadImageToBase64 | 加载本地图片转成base64字符串 |
| SamAutoMaskSEGS | 得到图片所有语义分割的coco或uncompress_rle格式。<br/>配合ComfyUI-Impact-Pack的SAMLoader或comfyui_segment_anything的SAMModelLoader。<br/>但是如果使用hq模型,必须使用comfyui_segment_anything |
| InsightFaceBBOXDetect | 为图片中的人脸添加序号和区域框 |
| ColorPicker | 颜色选择器 |
| IntToNumber | 整型转数字 |
| StringToList | 字符串转列表 |
| IntToList | 整型转列表 |
| ListMerge | 列表合并 |
| JoinList | 列表根据指定分隔符连接 |
| ShowString | 显示字符串(可指定消息中key值) |
| ShowInt | 显示整型(可指定消息中key值) |
| ShowFloat | 显示浮点型(可指定消息中key值) |
| ShowNumber | 显示数字(可指定消息中key值) |
| ShowBoolean | 显示布尔值(可指定消息中key值) |
| ImageEqual | 图片是否相等(可用于通过判断遮罩图是否全黑来判定是否有遮罩) |
| SDBaseVerNumber | 判断SD大模型版本是1.5还是xl |
| ListWrapper | 包装成列表(任意类型) |
| ListUnWrapper | 转成输出列表,后面连接的节点会把每个元素执行一遍,实现类似遍历效果 |
| BboxToCropData | bbox转cropData,方便接入was节点使用 |
| BboxToBbox | bbox两种格式(x,y,w,h)和(x1,y1,x2,y2)的相互转换 |
| BboxesToBboxes | BboxToBbox节点的列表版本 |
| SelectBbox | 从Bbox列表中选择一个 |
| SelectBboxes | 从Bbox列表中选择多个 |
| CropImageByBbox | 根据Bbox区域裁剪图片 |
| MaskByBboxes | 根据Bbox列表画遮罩 |
| SplitStringToList | 根据分隔符把字符串拆分为某种数据类型(str/int/float/bool)的列表 |
| IndexOfList | 从列表中获取指定位置的元素 |
| IndexesOfList | 从列表中筛选出指定位置的元素列表 |
| StringArea | 字符串文本框(多行输入区域) |
| ForEachOpen | 循环开始节点 |
| ForEachClose | 循环结束节点 |
| LoadJsonStrToList | json字符串转换为对象列表 |
| GetValueFromJsonObj | 从对象中获取指定key的值 |
| FilterValueForList | 根据指定值过滤列表中元素 |
| SliceList | 列表切片 |
| LoadLocalFilePath | 列出给定路径下的文件列表 |
| LoadImageFromLocalPath | 根据图片全路径加载图片 |
| LoadMaskFromLocalPath | 根据遮罩全路径加载遮罩 | |
| IsNoneOrEmpty | 判断是否为空或空字符串或空列表或空字典 |
| IsNoneOrEmptyOptional | 为空时返回指定值(惰性求值),否则返回原值 |
| EmptyOutputNode | 空的输出类型节点 |
| 名称 | 说明 |
|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
| LoadImageFromURL | 从网络地址加载图片,一行代表一个图片 |
| LoadMaskFromURL | 从网络地址加载遮罩,一行代表一个 |
| Base64ToImage | 把图片base64字符串转成图片 |
| Base64ToMask | 把遮罩图片base64字符串转成遮罩 |
| ImageToBase64Advanced | 把图片转成base64字符串, 可以选择图片类型(image, mask) ,方便接口调用判断 |
| ImageToBase64 | 把图片转成base64字符串(imageType=["image"]) |
| MaskToBase64Image | 把遮罩转成对应图片的base64字符串(imageType=["mask"]) |
| MaskImageToBase64 | 把遮罩图片转成base64字符串(imageType=["mask"]) |
| LoadImageToBase64 | 加载本地图片转成base64字符串 |
| SamAutoMaskSEGS | 得到图片所有语义分割的coco或uncompress_rle格式。<br/>配合ComfyUI-Impact-Pack的SAMLoader或comfyui_segment_anything的SAMModelLoader。<br/>但是如果使用hq模型,必须使用comfyui_segment_anything |
| InsightFaceBBOXDetect | 为图片中的人脸添加序号和区域框 |
| ColorPicker | 颜色选择器 |
| IntToNumber | 整型转数字 |
| StringToList | 字符串转列表 |
| IntToList | 整型转列表 |
| ListMerge | 列表合并 |
| JoinList | 列表根据指定分隔符连接 |
| ShowString | 显示字符串(可指定消息中key值) |
| ShowInt | 显示整型(可指定消息中key值) |
| ShowFloat | 显示浮点型(可指定消息中key值) |
| ShowNumber | 显示数字(可指定消息中key值) |
| ShowBoolean | 显示布尔值(可指定消息中key值) |
| ImageEqual | 图片是否相等(可用于通过判断遮罩图是否全黑来判定是否有遮罩) |
| SDBaseVerNumber | 判断SD大模型版本是1.5还是xl |
| ListWrapper | 包装成列表(任意类型) |
| ListUnWrapper | 转成输出列表,后面连接的节点会把每个元素执行一遍,实现类似遍历效果 |
| BboxToCropData | bbox转cropData,方便接入was节点使用 |
| BboxToBbox | bbox两种格式(x,y,w,h)和(x1,y1,x2,y2)的相互转换 |
| BboxesToBboxes | BboxToBbox节点的列表版本 |
| SelectBbox | 从Bbox列表中选择一个 |
| SelectBboxes | 从Bbox列表中选择多个 |
| CropImageByBbox | 根据Bbox区域裁剪图片 |
| MaskByBboxes | 根据Bbox列表画遮罩 |
| SplitStringToList | 根据分隔符把字符串拆分为某种数据类型(str/int/float/bool)的列表 |
| IndexOfList | 从列表中获取指定位置的元素 |
| IndexesOfList | 从列表中筛选出指定位置的元素列表 |
| StringArea | 字符串文本框(多行输入区域) |
| ForEachOpen | 循环开始节点 |
| ForEachClose | 循环结束节点 |
| LoadJsonStrToList | json字符串转换为对象列表 |
| GetValueFromJsonObj | 从对象中获取指定key的值 |
| FilterValueForList | 根据指定值过滤列表中元素 |
| SliceList | 列表切片 |
| LoadLocalFilePath | 列出给定路径下的文件列表 |
| LoadImageFromLocalPath | 根据图片全路径加载图片 |
| LoadMaskFromLocalPath | 根据遮罩全路径加载遮罩 |
| IsNoneOrEmpty | 判断是否为空或空字符串或空列表或空字典 |
| IsNoneOrEmptyOptional | 为空时返回指定值(惰性求值),否则返回原值 |
| EmptyOutputNode | 空的输出类型节点 |
| SaveTextToFileByImagePath | 保存文本到图片路径,以图片名作为文件名 |
| CopyAndRenameFiles | 复制或重命名文件 |
| SaveImagesWithoutOutput | 保存图像到指定目录,不是输出类型节点,可用于循环批量跑图和作为惰性求值的前置节点 |
| SaveSingleImageWithoutOutput | 保存单个图像到指定目录,不是输出类型节点,可用于循环批量跑图和作为惰性求值的前置节点 |
| CropTargetSizeImageByBbox | 以bbox的区域中心裁剪指定大小图片 |
### 示例
![save api extended](docs/example_note.png)
@@ -81,8 +86,12 @@ Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带
![save api extended](example/example_1.png)
![save api extended](example/example_2.png)
![save api extended](example/example_3.png)
![批量裁剪打标](example/example_image_crop_tag.png)
## 更新记录
### 2024-10-24 (v1.0.7)
- 新增节点:SaveTextToFileByImagePath、 CopyAndRenameFiles、 SaveImagesWithoutOutput、 SaveSingleImageWithoutOutput、 CropTargetSizeImageByBbox
### 2024-10-18
- 新增节点:SliceList、LoadLocalFilePath、LoadImageFromLocalPath、LoadMaskFromLocalPath、IsNoneOrEmpty、IsNoneOrEmptyOptional、EmptyOutputNode
+99 -4
View File
@@ -2,6 +2,7 @@ from json import JSONDecoder
import torch
import nodes
from .util import any_type
@@ -221,12 +222,12 @@ class CropImageByBbox:
"required": {
"image": ("IMAGE",),
"bbox": ("BBOX",),
"margin": ("INT", {"default": 16}),
"margin": ("INT", {"default": 16, "tooltip": "bbox矩形区域向外扩张的像素距离"}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BBOX")
RETURN_NAMES = ("crop_image", "mask", "crop_bbox")
RETURN_TYPES = ("IMAGE", "MASK", "BBOX", "INT", "INT")
RETURN_NAMES = ("crop_image", "mask", "crop_bbox", "w", "h")
FUNCTION = "crop"
CATEGORY = "EasyApi/Bbox"
DESCRIPTION = "根据bbox区域裁剪图片。 bbox的格式是左上角和右下角坐标: [x,y,x1,y1]"
@@ -257,7 +258,99 @@ class CropImageByBbox:
mask[new_bbox[1]:new_bbox[3], new_bbox[0]:new_bbox[2]] = 1
# 如果需要转换为浮点数,并且增加一个通道维度, 形状变为 (1, height, width)
mask_tensor = mask.unsqueeze(0)
return crop_img, mask_tensor, new_bbox,
return crop_img, mask_tensor, new_bbox, to_x - x, to_y - y,
class CropTargetSizeImageByBbox:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"bbox": ("BBOX",{"forceInput": True, "tooltip": "参考区域坐标"}),
"width": ("INT", {"default": 512, "min": 1, "max": nodes.MAX_RESOLUTION, "step": 1, "tooltip": "目标宽度"}),
"height": ("INT", {"default": 512, "min": 1, "max": nodes.MAX_RESOLUTION, "step": 1, "tooltip": "目标高度"}),
"contain": ("BOOLEAN", {"default": False, "tooltip": "是否始终包含bbox完整区域"}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "BBOX", "INT", "INT")
RETURN_NAMES = ("crop_image", "mask", "crop_bbox", "w", "h")
FUNCTION = "crop"
CATEGORY = "EasyApi/Bbox"
DESCRIPTION = "根据bbox区域中心裁剪指定大小图片。 bbox的格式是左上角和右下角坐标: [x,y,x1,y1]"
def calc_area(self, image_width, image_height, rect_top_left, rect_bottom_right, w, h):
"""
以给定的矩形中心点为中心计算指定宽高的矩形框坐标
Args:
image_width: 图片高度
image_height: 图片宽度
rect_top_left: 矩形框左上角坐标
rect_bottom_right: 矩形框右下角坐标
w: 目标宽度
h: 目标高度
Returns:
"""
# 计算矩形的宽和高
x, y = rect_top_left
x1, y1 = rect_bottom_right
# 否则,计算矩形的中心(取整)
center_x = (x + x1) // 2
center_y = (y + y1) // 2
left_w = w // 2
right_w = w - left_w
top_h = h // 2
bottom_h = h - top_h
# 计算新的坐标
new_top_left_x = max(0, center_x - left_w)
new_top_left_y = max(0, center_y - top_h)
new_bottom_right_x = min(image_width, center_x + right_w)
new_bottom_right_y = min(image_height, center_y + bottom_h)
# 如果坐标越界,调整坐标
if new_top_left_x == 0:
# 左边可能超过边界了,尝试把左边超出部分加到右边
new_bottom_right_x = min(image_width, new_bottom_right_x + (left_w - center_x))
elif new_bottom_right_x == image_width:
# 右边可能超过边界了,尝试把右边超出部分加到左边
new_top_left_x = max(0, new_top_left_x - (center_x + left_w - image_width))
if new_top_left_y == 0:
# 上边可能超过边界了,尝试把上边超出部分加到下边
new_bottom_right_y = min(image_height, new_bottom_right_y + (top_h - center_y))
elif new_bottom_right_y == image_height:
# 下边可能超过边界了,尝试把下边超出部分加到上边
new_top_left_y = max(0, new_top_left_y - (center_y + top_h - image_height))
return new_top_left_x, new_top_left_y, new_bottom_right_x, new_bottom_right_y
def crop(self, image: torch.Tensor, bbox, width, height, contain):
x, y, x1, y1 = bbox
image_height = image.shape[1]
image_width = image.shape[2]
new_x, new_y, to_x, to_y = self.calc_area(image_width, image_height, (x, y), (x1, y1), width, height)
if contain:
new_x = min(new_x, x)
new_y = min(new_y, y)
to_x = max(to_x, x1)
to_y = max(to_y, y1)
# 按区域截取图片
crop_img = image[:, new_y:to_y, new_x:to_x, :]
new_bbox = (new_x, new_y, to_x, to_y)
# 创建与image相同大小的全零张量作为遮罩
mask = torch.zeros((image_height, image_width), dtype=torch.uint8) # 使用uint8类型
# 在mask上设置new_bbox区域为1
mask[new_bbox[1]:new_bbox[3], new_bbox[0]:new_bbox[2]] = 1
# 如果需要转换为浮点数,并且增加一个通道维度, 形状变为 (1, height, width)
mask_tensor = mask.unsqueeze(0)
return crop_img, mask_tensor, new_bbox, to_x - new_x, to_y - new_y,
class MaskByBboxes:
@@ -299,6 +392,7 @@ NODE_CLASS_MAPPINGS = {
"SelectBboxes": SelectBboxes,
"CropImageByBbox": CropImageByBbox,
"MaskByBboxes": MaskByBboxes,
"CropTargetSizeImageByBbox": CropTargetSizeImageByBbox,
}
@@ -310,4 +404,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"SelectBboxes": "SelectBboxes",
"CropImageByBbox": "CropImageByBbox",
"MaskByBboxes": "MaskByBboxes",
"CropTargetSizeImageByBbox": "CropTargetSizeImageByBbox",
}
+157
View File
@@ -7,6 +7,7 @@ import numpy as np
import torch
from PIL import ImageOps, Image, ImageSequence
import folder_paths
import node_helpers
from nodes import LoadImage
from comfy.cli_args import args
@@ -446,6 +447,158 @@ class LoadMaskFromLocalPath:
return (mask.unsqueeze(0),)
class SaveImagesWithoutOutput:
"""
保存图片,非输出节点
"""
def __init__(self):
self.compress_level = 4
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"images": ("IMAGE",),
"filename_prefix": ("STRING", {"default": "ComfyUI",
"tooltip": "要保存的文件的前缀。可以使用格式化信息,如%date:yyyy-MM-dd%或%Empty Latent Image.width%"}),
"output_dir": ("STRING", {"default": "", "tooltip": "若为空,存放到output目录"}),
},
"optional": {
"addMetadata": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("file_paths",)
OUTPUT_TOOLTIPS = ("保存的图片路径列表",)
FUNCTION = "save_images"
CATEGORY = "EasyApi/Image"
DESCRIPTION = "保存图像到指定目录,可根据返回的文件路径进行后续操作,此节点为非输出节点,适合批量处理和用于惰性求值的前置节点"
OUTPUT_NODE = False
def save_images(self, images, output_dir, filename_prefix="ComfyUI", addMetadata=False, prompt=None, extra_pnginfo=None):
imageList = list()
if not isinstance(images, list):
imageList.append(images)
else:
imageList = images
if output_dir is None or len(output_dir.strip()) == 0:
output_dir = folder_paths.get_output_directory()
results = list()
for (index, images) in enumerate(imageList):
for (batch_number, image) in enumerate(images):
full_output_folder, filename, counter, subfolder, curr_filename_prefix = folder_paths.get_save_image_path(
filename_prefix, output_dir, image.shape[1], image.shape[0])
img = tensor_to_pil(image)
metadata = None
if not args.disable_metadata and addMetadata:
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.png"
image_save_path = os.path.join(full_output_folder, file)
img.save(image_save_path, pnginfo=metadata, compress_level=self.compress_level)
results.append(image_save_path)
counter += 1
return (results,)
class SaveSingleImageWithoutOutput:
"""
保存图片,非输出节点
"""
def __init__(self):
self.compress_level = 4
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE",),
"filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "要保存的文件的前缀。可以使用格式化信息,如%date:yyyy-MM-dd%或%Empty Latent Image.width%"}),
"full_file_name": ("STRING", {"default": "", "tooltip": "完整的相对路径文件名,包括扩展名。若为空,则使用filename_prefix生成带序号的文件名"}),
"output_dir": ("STRING", {"default": "", "tooltip": "目标目录(绝对路径),不会自动创建。若为空,存放到output目录"}),
},
"optional": {
"addMetadata": ("BOOLEAN", {"default": False, "label_on": "True", "label_off": "False"}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("file_path",)
FUNCTION = "save_image"
CATEGORY = "EasyApi/Image"
DESCRIPTION = "保存图像到指定目录,可根据返回的文件路径进行后续操作,此节点为非输出节点,适合循环批处理和用于惰性求值的前置节点。只会处理一个"
OUTPUT_NODE = False
def save_image(self, image, full_file_name, output_dir, filename_prefix="ComfyUI", addMetadata=False, prompt=None, extra_pnginfo=None):
imageList = list()
if not isinstance(image, list):
imageList.append(image)
else:
imageList = image
if output_dir is None or len(output_dir.strip()) == 0:
output_dir = folder_paths.get_output_directory()
if not os.path.isdir(output_dir) or not os.path.isabs(output_dir):
raise RuntimeError(f"目录 {output_dir} 不存在")
if len(imageList) > 0:
image = imageList[0]
for (batch_number, image) in enumerate(image):
img = tensor_to_pil(image)
metadata = None
if not args.disable_metadata and addMetadata:
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
if full_file_name is not None and len(full_file_name.strip()) > 0:
# full_file_name是相对路径,添加校验,并自动创建子目录
full_path = os.path.join(output_dir, full_file_name)
full_normpath_name = os.path.normpath(full_path)
file_dir = os.path.dirname(full_normpath_name)
# 确保路径是out_dir 的子目录
if not os.path.isabs(file_dir) or not file_dir.startswith(output_dir):
raise RuntimeError(f"文件 {full_file_name} 不在 {output_dir} 目录下")
if not os.path.isdir(file_dir):
os.makedirs(file_dir, exist_ok=True)
image_save_path = full_normpath_name
else:
full_output_folder, filename, counter, subfolder, curr_filename_prefix = folder_paths.get_save_image_path(
filename_prefix, output_dir, image.shape[1], image.shape[0])
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.png"
image_save_path = os.path.join(full_output_folder, file)
img.save(image_save_path, pnginfo=metadata, compress_level=self.compress_level)
return image_save_path,
return (None,)
NODE_CLASS_MAPPINGS = {
"Base64ToImage": Base64ToImage,
"LoadImageFromURL": LoadImageFromURL,
@@ -459,6 +612,8 @@ NODE_CLASS_MAPPINGS = {
"LoadImageToBase64": LoadImageToBase64,
"LoadImageFromLocalPath": LoadImageFromLocalPath,
"LoadMaskFromLocalPath": LoadMaskFromLocalPath,
"SaveImagesWithoutOutput": SaveImagesWithoutOutput,
"SaveSingleImageWithoutOutput": SaveSingleImageWithoutOutput,
}
# A dictionary that contains the friendly/humanly readable titles for the nodes
@@ -475,4 +630,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"LoadImageToBase64": "Load Image To Base64",
"LoadImageFromLocalPath": "Load Image From Local Path",
"LoadMaskFromLocalPath": "Load Mask From Local Path",
"SaveImagesWithoutOutput": "Save Images Without Output",
"SaveSingleImageWithoutOutput": "Save Single Image Without Output",
}
+137 -9
View File
@@ -1,5 +1,7 @@
import mimetypes
import os
import shutil
from collections import defaultdict
import simplejson
import torch
@@ -188,6 +190,8 @@ class ListMerge:
OUTPUT_NODE = False
CATEGORY = "EasyApi/List"
DESCRIPTION = "合并两个列表。如 [1,2] 和 [3,4] => [1,2,3,4]"
def convert(self, list_a, list_b=None):
list = [] + list_a
if list_b:
@@ -679,9 +683,9 @@ class LoadLocalFilePath:
}
}
RETURN_TYPES = ("LIST", "INT",)
RETURN_NAMES = ("paths", "count",)
OUTPUT_TOOLTIPS = ("文件路径列表,若过滤不到文件返回空列表", "文件个数",)
RETURN_TYPES = ("LIST", "INT", "LIST", "STRING", )
RETURN_NAMES = ("paths", "count", "relative_path", "base_dir", )
OUTPUT_TOOLTIPS = ("文件路径列表,若过滤不到文件返回空列表", "文件个数", "文件相对路径列表", "保存的根目录", )
FUNCTION = "get_paths"
@@ -696,23 +700,31 @@ class LoadLocalFilePath:
}
@classmethod
def recursive_file_paths(cls, directory, max_depth, file_type, file_suffix, current_depth=1):
def recursive_file_paths(cls, directory, max_depth, file_type, file_suffix, base_directory=None, current_depth=1):
"""
获取指定目录及其子目录中的图片文件路径(深度优先遍历)
参数:
directory (str): 要遍历的目录路径
max_depth (int): 最大遍历层级
file_type (str): 文件类型(如 'image')
file_suffix (str): 文件后缀(多个后缀用 '|' 分隔)
base_directory (str): 根目录路径(用于计算相对路径,默认为 None)
current_depth (int): 当前遍历层级(默认值为1)
返回:
List[str]: 图片文件路径列表
List[str]: 图片文件相对路径列表
"""
image_paths = []
relative_image_paths = []
if base_directory is None:
base_directory = directory
if current_depth > max_depth:
return image_paths
return image_paths, relative_image_paths
with os.scandir(directory) as it:
for item in it:
@@ -721,20 +733,26 @@ class LoadLocalFilePath:
suffixes = [s.strip().lower() for s in file_suffix.split('|')]
if any(item.name.lower().endswith(suffix) for suffix in suffixes):
image_paths.append(item.path)
relative_image_paths.append(os.path.relpath(item.path, base_directory))
elif file_type:
mime_type, _ = mimetypes.guess_type(item.path)
if mime_type in cls.mime_types_dict.get(file_type, set()):
image_paths.append(item.path)
relative_image_paths.append(os.path.relpath(item.path, base_directory))
elif item.is_dir():
image_paths.extend(cls.recursive_file_paths(item.path, max_depth, file_type, file_suffix, current_depth + 1))
return image_paths
sub_image_paths, sub_relative_image_paths = cls.recursive_file_paths(
item.path, max_depth, file_type, file_suffix, base_directory, current_depth + 1
)
image_paths.extend(sub_image_paths)
relative_image_paths.extend(sub_relative_image_paths)
return image_paths, relative_image_paths
def get_paths(self, directory, max_depth, file_type, file_suffix):
if directory is None or len(directory.strip()) == 0:
directory = folder_paths.get_input_directory()
image_paths = self.recursive_file_paths(directory, max_depth, file_type, file_suffix)
image_paths, relative_image_paths = self.recursive_file_paths(directory, max_depth, file_type, file_suffix)
return image_paths, len(image_paths),
return image_paths, len(image_paths), relative_image_paths, directory,
class IsNoneOrEmpty:
@@ -822,6 +840,112 @@ class EmptyOutputNode:
return ()
class SaveTextToFileByImagePath:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image_path": ("STRING", {"forceInput": False}),
"text": ("STRING", {"forceInput": False, "dynamicPrompts": False, "multiline": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text_path",)
FUNCTION = "execute"
CATEGORY = "EasyApi/Utils"
DESCRIPTION = "把文本内容保存到图片路径同名的txt文件中"
def execute(self, image_path, text):
# 校验图片路径是否存在
if not os.path.isfile(image_path):
raise FileNotFoundError(f"Image file not found: {image_path}")
# 校验文本内容是否为空
if not text:
raise ValueError("The text cannot be empty")
# 获取图片文件名,不包括扩展名
base_name = os.path.splitext(os.path.basename(image_path))[0]
# 创建txt文件路径
dir_name = os.path.dirname(image_path)
# 创建txt文件路径
txt_path = os.path.join(dir_name, f"{base_name}.txt")
# 写入文本内容到txt文件
with open(txt_path, 'w', encoding='utf-8') as file:
file.write(text)
return (txt_path,)
class CopyAndRenameFiles:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"directory": ("STRING", {"forceInput": False, "tooltip": "源目录"}),
"save_directory": ("STRING", {"default": "", "tooltip": "目标目录,为空时重命名原文件"}),
"prefix": ("STRING", {"default": "", "tooltip": "新文件名前缀"}),
"name_to_num": ("BOOLEAN", {"default": True, "tooltip": "后缀是否使用在对应目录的序号"}),
}
}
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("save_directory", )
FUNCTION = "execute"
CATEGORY = "EasyApi/Utils"
DESCRIPTION = "把给定目录下的文件复制到指定目录"
def execute(self, directory, save_directory, prefix, name_to_num):
"""
递归遍历目录及其子目录中的文件,重命名或复制并重命名文件,添加自定义前缀。
如果 save_directory 为空,重命名原文件;否则,复制并重命名到目标目录,并保持层级结构,文件名为所在目录的计数编号。
:param directory: 需要重命名文件的目录路径
:param prefix: 自定义前缀
:param save_directory: 保存重命名文件的目录路径(可以为空)
:param name_to_num: 重命名文件名为数字
"""
count_dict = defaultdict(int) # 用于存储每个目录的计数器
for root, _, files in os.walk(directory):
for filename in files:
old_path = os.path.join(root, filename)
if os.path.isfile(old_path):
count_dict[root] += 1 # 增加当前目录的计数器
ext = os.path.splitext(filename)[1]
if prefix and len(prefix.strip()) > 0:
if name_to_num:
new_filename = f"{prefix.strip()}_{count_dict[root]}"
else:
new_filename = f"{prefix.strip()}_{filename}"
else:
if name_to_num:
new_filename = f"{count_dict[root]}{ext}"
else:
new_filename = filename
# 如果 save_directory 不为空,复制并重命名到目标目录,并保持层级结构
if save_directory and len(save_directory.strip()) > 0:
# 计算保存文件的目标目录
relative_path = os.path.relpath(root, directory)
target_dir = os.path.join(save_directory, relative_path)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
new_path = os.path.join(target_dir, new_filename)
shutil.copyfile(old_path, new_path)
print(f"复制并重命名: {old_path} -> {new_path}")
else:
# 否则重命名原文件
new_path = os.path.join(root, new_filename)
os.rename(old_path, new_path)
print(f"重命名: {old_path} -> {new_path}")
return (save_directory, )
NODE_CLASS_MAPPINGS = {
"GetImageBatchSize": GetImageBatchSize,
"JoinList": JoinList,
@@ -852,6 +976,8 @@ NODE_CLASS_MAPPINGS = {
"IsNoneOrEmpty": IsNoneOrEmpty,
"IsNoneOrEmptyOptional": IsNoneOrEmptyOptional,
"EmptyOutputNode": EmptyOutputNode,
"SaveTextToFileByImagePath": SaveTextToFileByImagePath,
"CopyAndRenameFiles": CopyAndRenameFiles,
}
# A dictionary that contains the friendly/humanly readable titles for the nodes
@@ -885,4 +1011,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"IsNoneOrEmpty": "IsNoneOrEmpty",
"IsNoneOrEmptyOptional": "IsNoneOrEmptyOptional",
"EmptyOutputNode": "EmptyOutputNode",
"SaveTextToFileByImagePath": "SaveTextToFileByImagePath",
"CopyAndRenameFiles": "CopyAndRenameFiles",
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-easyapi-nodes"
description = "Provides some features and nodes related to API calls."
version = "1.0.6"
version = "1.0.7"
license = { file = "LICENSE" }
dependencies = ["segment_anything", "simple_lama_inpainting", "insightface"]