Skip to content

Commit

Permalink
v1.0.7 support auto crop and tag image in foreach
Browse files Browse the repository at this point in the history
  • Loading branch information
lldacing committed Oct 24, 2024
1 parent 8086ad3 commit b428ad4
Show file tree
Hide file tree
Showing 6 changed files with 454 additions and 65 deletions.
111 changes: 60 additions & 51 deletions README.md

Large diffs are not rendered by default.

103 changes: 99 additions & 4 deletions easyapi/BboxNode.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import torch

import nodes
from .util import any_type


Expand Down Expand Up @@ -221,12 +222,12 @@ def INPUT_TYPES(cls):
"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]"
Expand Down Expand Up @@ -257,7 +258,99 @@ def crop(self, image: torch.Tensor, bbox, margin):
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:
Expand Down Expand Up @@ -299,6 +392,7 @@ def crop(self, image: torch.Tensor, bboxes):
"SelectBboxes": SelectBboxes,
"CropImageByBbox": CropImageByBbox,
"MaskByBboxes": MaskByBboxes,
"CropTargetSizeImageByBbox": CropTargetSizeImageByBbox,
}


Expand All @@ -310,4 +404,5 @@ def crop(self, image: torch.Tensor, bboxes):
"SelectBboxes": "SelectBboxes",
"CropImageByBbox": "CropImageByBbox",
"MaskByBboxes": "MaskByBboxes",
"CropTargetSizeImageByBbox": "CropTargetSizeImageByBbox",
}
157 changes: 157 additions & 0 deletions easyapi/ImageNode.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import torch
from PIL import ImageOps, Image, ImageSequence

import folder_paths
import node_helpers
from nodes import LoadImage
from comfy.cli_args import args
Expand Down Expand Up @@ -446,6 +447,158 @@ def load_mask(self, image_path, channel):
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,
Expand All @@ -459,6 +612,8 @@ def load_mask(self, image_path, channel):
"LoadImageToBase64": LoadImageToBase64,
"LoadImageFromLocalPath": LoadImageFromLocalPath,
"LoadMaskFromLocalPath": LoadMaskFromLocalPath,
"SaveImagesWithoutOutput": SaveImagesWithoutOutput,
"SaveSingleImageWithoutOutput": SaveSingleImageWithoutOutput,
}

# A dictionary that contains the friendly/humanly readable titles for the nodes
Expand All @@ -475,4 +630,6 @@ def load_mask(self, image_path, channel):
"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",
}
Loading

0 comments on commit b428ad4

Please sign in to comment.