v1.0.7 support auto crop and tag image in foreach
This commit is contained in:
@@ -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的区域中心裁剪指定大小图片 |
|
||||
|
||||
### 示例
|
||||

|
||||
@@ -81,8 +86,12 @@ Tips: base64格式字符串比较长,会导致界面卡顿,接口请求带
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 更新记录
|
||||
### 2024-10-24 (v1.0.7)
|
||||
- 新增节点:SaveTextToFileByImagePath、 CopyAndRenameFiles、 SaveImagesWithoutOutput、 SaveSingleImageWithoutOutput、 CropTargetSizeImageByBbox
|
||||
|
||||
### 2024-10-18
|
||||
- 新增节点:SliceList、LoadLocalFilePath、LoadImageFromLocalPath、LoadMaskFromLocalPath、IsNoneOrEmpty、IsNoneOrEmptyOptional、EmptyOutputNode
|
||||
|
||||
|
||||
+99
-4
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user