Files
AbyssBadger0-ComfyUI_Badger…/__init__.py
T

919 lines
24 KiB
Python

import math
import os
from PIL import Image
import numpy as np
import torch
import comfy.utils
from .videoCut import getCutList, videoToPng, cutToDir
from .seg import get_masks
from .thick_lines_from_canny import fill_white_segments, find_largest_white_component
def getImageSize(IMAGE) -> tuple[int, int]:
samples = IMAGE.movedim(-1, 1)
size = samples.shape[3], samples.shape[2]
return size
def tensorToImg(imageTensor):
imaget = imageTensor[0]
i = 255. * imaget.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
return img
def imgToTensor(img):
image = np.array(img).astype(np.float32) / 255.0
imaget = torch.from_numpy(image)[None,]
return imaget
def img_to_mask(mask):
mask = mask.convert("RGBA")
mask = np.array(mask.getchannel('R')).astype(np.float32) / 255.0
mask = torch.from_numpy(mask)
return mask
def img_to_np(img):
if img.mode == "RGBA":
img = img.convert("RGB")
img = np.array(img)
return img
def np_to_img(numpy):
return Image.fromarray(numpy.astype(np.uint8))
def maskimg_to_mask(mask_img):
mask = np_to_img(mask_img)
mask = img_to_mask(mask)
mask = mask.unsqueeze(0)
return mask
class ImageOverlap:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"base_image": ("IMAGE",),
"additional_image": ("IMAGE",),
"x": ("INT", {
"default": 0,
"min": 0,
"max": 4096,
"step": 1,
"display": "number"
}),
"y": ("INT", {
"default": 0,
"min": 0,
"max": 4096,
"step": 1,
"display": "number"
}),
},
}
RETURN_TYPES = ("IMAGE",)
# RETURN_NAMES = ("image_output_name",)
FUNCTION = "overlap"
# OUTPUT_NODE = False
CATEGORY = "badger"
def overlap(self, base_image, additional_image, x, y):
b_image = tensorToImg(base_image)
a_image = tensorToImg(additional_image)
b_image.paste(a_image, (x, y))
o_image = imgToTensor(b_image)
return (o_image,)
class FloatToInt:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"float": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 4096.0,
"step": 0.01,
"round": 0.01,
"display": "number"})
},
}
RETURN_TYPES = ("INT",)
# RETURN_NAMES = ("image_output_name",)
FUNCTION = "floatToInt"
# OUTPUT_NODE = False
CATEGORY = "badger"
def floatToInt(self, float):
return (round(float),)
class IntToString:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"int": ("INT", {
"default": 0,
"min": 0,
"max": 4096,
"step": 1,
"display": "number"
})
},
}
RETURN_TYPES = ("STRING",)
# RETURN_NAMES = ("image_output_name",)
FUNCTION = "intToString"
# OUTPUT_NODE = False
CATEGORY = "badger"
def intToString(self, int):
return (str(int),)
class FloatToString:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"float": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 10.0,
"step": 0.00001,
"round": False,
"display": "number"})
},
}
RETURN_TYPES = ("STRING",)
# RETURN_NAMES = ("image_output_name",)
FUNCTION = "floatToString"
# OUTPUT_NODE = False
CATEGORY = "badger"
def floatToString(self, float):
return (str(float),)
class ImageNormalization:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"width": ("INT", {
"default": 1.0,
"min": 0.0,
"max": 4096.0,
"step": 0.01,
"round": 0.01,
"display": "number"}),
"height": ("INT", {
"default": 1.0,
"min": 0.0,
"max": 4096.0,
"step": 0.01,
"round": 0.01,
"display": "number"}),
"target_width": ("INT", {
"default": 1.0,
"min": 0.0,
"max": 4096.0,
"step": 0.01,
"round": 0.01,
"display": "number"}),
"target_height": ("INT", {
"default": 1.0,
"min": 0.0,
"max": 4096.0,
"step": 0.01,
"round": 0.01,
"display": "number"})
},
}
RETURN_TYPES = ("INT", "INT", "INT", "INT", "INT", "INT",)
RETURN_NAMES = ("new_width", "new_height", "top", "left", "bottom", "right")
FUNCTION = "imageNormalization"
# OUTPUT_NODE = False
CATEGORY = "badger"
def imageNormalization(self, width, height, target_width, target_height):
o_ratio = width / height
ratio = target_width / target_height
top = 0
left = 0
bottom = 0
right = 0
nw = 0
nh = 0
# 原图比期望尺寸更扁,对齐宽,计算高,补上下
if (o_ratio >= ratio):
upratio = target_width / width
nw = target_width
nh = round(height * upratio)
hdiff = target_height - nh
top = math.floor(hdiff / 2)
bottom = math.ceil(hdiff / 2)
else:
upratio = target_height / height
nw = round(width * upratio)
nh = target_height
wdiff = target_width - nw
left = math.floor(wdiff / 2)
right = math.ceil(wdiff / 2)
return (nw, nh, top, left, bottom, right,)
class ImageScaleToSide:
upscale_methods = ["nearest-exact", "bilinear", "area"]
crop_methods = ["disabled", "center"]
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"side_length": ("INT", {
"default": 1,
"min": 1,
"max": 4096,
"step": 1,
"display": "number"
}),
"side": (["Longest", "Shortest", "Width", "Height"],),
"upscale_method": (cls.upscale_methods,),
"crop": (cls.crop_methods,)}}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "imageUpscaleToSide"
CATEGORY = "badger"
def imageUpscaleToSide(self, image, upscale_method, side_length: int, side: str, crop):
samples = image.movedim(-1, 1)
size = getImageSize(image)
width_B = int(size[0])
height_B = int(size[1])
width = width_B
height = height_B
def determineSide(_side: str) -> tuple[int, int]:
width, height = 0, 0
if _side == "Width":
heigh_ratio = height_B / width_B
width = side_length
height = heigh_ratio * width
elif _side == "Height":
width_ratio = width_B / height_B
height = side_length
width = width_ratio * height
return width, height
if side == "Longest":
if width > height:
width, height = determineSide("Width")
else:
width, height = determineSide("Height")
elif side == "Shortest":
if width < height:
width, height = determineSide("Width")
else:
width, height = determineSide("Height")
else:
width, height = determineSide(side)
width = math.ceil(width)
height = math.ceil(height)
cls = comfy.utils.common_upscale(samples, width, height, upscale_method, crop)
cls = cls.movedim(1, -1)
return (cls,)
class StringToFizz:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {"required": {"text": ("STRING", {"multiline": True})}}
RETURN_TYPES = ("STRING", "INT",)
FUNCTION = "stringToFizz"
CATEGORY = "badger"
def stringToFizz(self, text):
textA = text.split("\n")
lines = 0
outText = ""
for line in textA:
if (len(line) > 0):
line = "\"" + str(lines) + "\":\"" + line + "\",\n"
lines = lines + 1
outText = outText + line
outText = outText[:-2]
return (outText, lines,)
class TextListToString:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {"required": {"texts": ("STRING", {"multiline": True})}}
RETURN_TYPES = ("STRING",)
INPUT_IS_LIST = True
FUNCTION = "textListToString"
CATEGORY = "badger"
def textListToString(self, texts):
fullString = ""
if len(texts) <= 1:
return (texts,)
else:
for text in texts:
fullString += text + "\n"
return (fullString,)
class getImageSide:
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"side_choose": (["short", "long"],)}}
RETURN_TYPES = ("INT",)
FUNCTION = "getImageSide"
CATEGORY = "badger"
def getImageSide(self, image, side_choose):
size = getImageSize(image)
width = int(size[0])
height = int(size[1])
side = 0
if width > height:
if side_choose == "short":
side = height
else:
side = width
else:
if side_choose == "short":
side = width
else:
side = height
return (side,)
class videoCut:
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video_path": ("STRING", {"default": "Temp"}),
"save_name": ("STRING", {"default": ""}),
"frame_rate": ("INT", {
"default": 1,
"min": 1,
"max": 4096,
"step": 1,
"display": "number"
}),
"min_frame": ("INT", {
"default": 1,
"min": 1,
"max": 4096,
"step": 1,
"display": "number"
}),
"max_frame": ("INT", {
"default": 240,
"min": 1,
"max": 4096,
"step": 1,
"display": "number"
})
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "videoCut"
CATEGORY = "badger"
def videoCut(self, video_path, save_name, frame_rate, min_frame, max_frame):
videoPath = os.path.abspath(video_path)
imagePath = videoToPng(videoPath, frame_rate, save_name)
cutList = getCutList(imagePath, min_frame, max_frame)
dirPathString = cutToDir(imagePath, cutList)
return (dirPathString,)
class getParentDir:
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"dir_path": ("STRING", {"default": ""}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "getParentdir"
CATEGORY = "badger"
def getParentdir(self, dir_path):
dir_path = os.path.abspath(dir_path)
parent_path = os.path.dirname(dir_path)
return (parent_path,)
class mkdir:
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"dir_path": ("STRING", {"default": ""}),
"new_dir": ("STRING", {"default": "newdir"}),
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "mkdir"
CATEGORY = "badger"
def mkdir(self, dir_path, new_dir):
dir_path = os.path.abspath(dir_path)
new_dir_path = os.path.join(dir_path, new_dir)
if not os.path.exists(new_dir_path):
os.mkdir(new_dir_path)
return (new_dir_path,)
class findCenterOfMask:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mask": ("MASK",),
}
}
CATEGORY = "badger"
RETURN_TYPES = ("FLOAT", "FLOAT",)
RETURN_NAMES = ("X", "Y",)
FUNCTION = "find_center_of_mask"
def find_center_of_mask(self, mask):
if mask.dim() == 3:
mask = mask.squeeze(0) # Remove the channel dimension if it exists
assert mask.dim() == 2, "Mask must be 2D"
# Create grids for x and y coordinates
h, w = mask.size()
x_coords = torch.arange(w).float().to(mask.device)
y_coords = torch.arange(h).float().to(mask.device)
# Compute the center of mass (centroid) of the mask
total_mass = mask.sum()
if total_mass > 0:
x_center = (mask.sum(dim=0) * x_coords).sum() / total_mass
y_center = (mask.sum(dim=1) * y_coords).sum() / total_mass
else:
x_center, y_center = torch.tensor(0), torch.tensor(0)
# Convert to int
X = float(x_center.item())
Y = float(y_center.item())
return (X, Y,)
class SegmentToMaskByPoint:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"img": ("IMAGE",),
"X": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 4096.0,
"step": 0.1,
"display": "number"
}),
"Y": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 4096.0,
"step": 0.1,
"display": "number"
}),
"dilate": ("INT", {
"default": 15,
"min": 0,
"max": 4096.0,
"step": 1,
"display": "number"
}),
"sam_ckpt": ("SAM_MODEL",),
}
}
CATEGORY = "badger"
RETURN_TYPES = ("MASK", "MASK", "MASK",)
RETURN_NAMES = ("mask0", "mask1", "mask2",)
FUNCTION = "seg_to_mask_by_point"
def seg_to_mask_by_point(self, img, X, Y, dilate, sam_ckpt):
img = tensorToImg(img)
img = img_to_np(img)
latest_coords = [X, Y]
masks = get_masks(img, latest_coords, dilate, sam_ckpt)
mask0 = maskimg_to_mask(masks[0])
mask1 = maskimg_to_mask(masks[1])
mask2 = maskimg_to_mask(masks[2])
return (mask0, mask1, mask2,)
class CropImageByMask:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
}
}
CATEGORY = "badger"
RETURN_TYPES = ("IMAGE", "INT", "INT",)
RETURN_NAMES = ("cropped_img", "X", "Y",)
FUNCTION = "crop_image_by_mask"
def crop_image_by_mask(self, image, mask):
# Ensure the mask is binary
mask = (mask > 0.5).float()
# Find the bounding box of the mask
if mask.sum() == 0:
raise ValueError("The mask is empty, cannot determine bounding box for cropping.")
# Find indices where the mask is nonzero
nonzero_indices = torch.nonzero(mask.squeeze(0), as_tuple=True)
topmost = torch.min(nonzero_indices[0])
leftmost = torch.min(nonzero_indices[1])
bottommost = torch.max(nonzero_indices[0])
rightmost = torch.max(nonzero_indices[1])
# Crop the image using the bounding box
cropped_image = image[:, topmost:bottommost + 1, leftmost:rightmost + 1]
# Return the cropped image and the top-left coordinates of the bounding box
X = int(leftmost)
Y = int(topmost)
return (cropped_image, X, Y,)
class ApplyMaskToImage:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
}
}
CATEGORY = "badger"
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("rgba_image",)
FUNCTION = "apply_mask_to_image"
def apply_mask_to_image(self, image, mask):
image = tensorToImg(image)
mask = tensorToImg(mask)
mask = mask.convert("L")
# 将图片转换为RGBA,以便添加透明度通道
image = image.convert("RGBA")
# 分离图片的通道
r, g, b, a = image.split()
# 将蒙版应用为alpha通道
new_a = Image.composite(a, Image.new('L', mask.size, 0), mask)
# 合并图像通道和新的alpha通道
result_image = Image.merge('RGBA', (r, g, b, new_a))
return (imgToTensor(result_image),)
class deleteDir:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"start": ("VHS_FILENAMES",),
"dir_path": ("STRING", {"default": ""}),
}
}
CATEGORY = "badger"
OUTPUT_NODE = True
RETURN_TYPES = ("INT", "STRING",)
RETURN_NAMES = ("result",)
FUNCTION = "delete_dir"
def delete_dir(self, start, dir_path):
e_info = ""
abs_dir_path = os.path.abspath(dir_path)
if not os.path.exists(abs_dir_path):
e_info = "路径不存在"
return (0, e_info,)
else:
try:
# 遍历文件夹中的每个文件或子文件夹
for root, dirs, files in os.walk(abs_dir_path):
for file in files:
file_path = os.path.join(root, file)
os.remove(file_path) # 删除文件
for folder in dirs:
folder_path = os.path.join(root, folder)
os.rmdir(folder_path) # 删除空文件夹
os.rmdir(abs_dir_path) # 最后删除根目录
e_info = "成功删除"
return (1, e_info,)
except Exception as e:
e_info = str(e)
return (0, e_info,)
class FindThickLinesFromCanny:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"low_threshold": ("FLOAT", {
"default": 0.01,
"min": 0.0,
"max": 1.0,
"step": 0.001,
"display": "number"
}),
"high_threshold": ("FLOAT", {
"default": 0.02,
"min": 0.0,
"max": 1.0,
"step": 0.001,
"display": "number"
}),
}
}
CATEGORY = "badger"
RETURN_TYPES = ("IMAGE",)
FUNCTION = "find_thick_lines_from_canny"
def find_thick_lines_from_canny(self, image, low_threshold, high_threshold):
img = tensorToImg(image)
result = fill_white_segments(img, low_threshold, high_threshold)
result = find_largest_white_component(result)
result = result.convert("RGB")
result_tensor = imgToTensor(result)
return (result_tensor,)
class TrimTransparentEdges:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
}
}
CATEGORY = "badger"
RETURN_TYPES = ("IMAGE",)
FUNCTION = "trim_transparent_edges"
def trim_transparent_edges(self, image):
img = tensorToImg(image)
img = img.convert("RGBA")
# 获取图片数据
datas = img.getdata()
# 获取非透明像素的边界
non_transparent_pixels = [
(i % img.width, i // img.width)
for i, pix in enumerate(datas)
if pix[3] != 0
]
if not non_transparent_pixels:
raise ValueError("Image is fully transparent")
# 获取非透明像素的最小和最大坐标
x_min = min(x for x, _ in non_transparent_pixels)
y_min = min(y for _, y in non_transparent_pixels)
x_max = max(x for x, _ in non_transparent_pixels)
y_max = max(y for _, y in non_transparent_pixels)
# 裁剪图片
cropped_img = img.crop((x_min, y_min, x_max + 1, y_max + 1))
cropped_img = imgToTensor(cropped_img)
return (cropped_img,)
class ExpandImageWithColor:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"top": ("INT", {
"default": 0,
"min": 0,
"max": 1024,
"step": 1,
"display": "number"
}),
"bottom": ("INT", {
"default": 0,
"min": 0,
"max": 1024,
"step": 1,
"display": "number"
}),
"left": ("INT", {
"default": 0,
"min": 0,
"max": 1024,
"step": 1,
"display": "number"
}),
"right": ("INT", {
"default": 0,
"min": 0,
"max": 1024,
"step": 1,
"display": "number"
}),
},
"optional": {
"color": ("STRING", {"default": None}),
}
}
CATEGORY = "badger"
RETURN_TYPES = ("IMAGE",)
FUNCTION = "expand_image_with_color"
def expand_image_with_color(self, image, top, bottom, left, right, color=None):
img = tensorToImg(image)
# Determine the new size of the image
new_width = img.width + left + right
new_height = img.height + top + bottom
# Create a new image with the new size and the given background color
if color:
new_img = Image.new("RGBA", (new_width, new_height), color)
else:
# Use transparency if no color was provided
new_img = Image.new("RGBA", (new_width, new_height), (0, 0, 0, 0))
# Paste the original image onto the new image
new_img.paste(img, (left, top), img)
result = imgToTensor(new_img)
return (result,)
NODE_CLASS_MAPPINGS = {
"ImageOverlap-badger": ImageOverlap,
"FloatToInt-badger": FloatToInt,
"IntToString-badger": IntToString,
"FloatToString-badger": FloatToString,
"ImageNormalization-badger": ImageNormalization,
"ImageScaleToSide-badger": ImageScaleToSide,
"StringToFizz-badger": StringToFizz,
"TextListToString-badger": TextListToString,
"getImageSide-badger": getImageSide,
"VideoCut-badger": videoCut,
"getParentDir-badger": getParentDir,
"mkdir-badger": mkdir,
"findCenterOfMask-badger": findCenterOfMask,
"SegmentToMaskByPoint-badger": SegmentToMaskByPoint,
"CropImageByMask-badger": CropImageByMask,
"ApplyMaskToImage-badger": ApplyMaskToImage,
"deleteDir-badger": deleteDir,
"FindThickLinesFromCanny-badger": FindThickLinesFromCanny,
"TrimTransparentEdges-badger": TrimTransparentEdges,
"ExpandImageWithColor-badger": ExpandImageWithColor,
}
NODE_DISPLAY_NAME_MAPPINGS = {
}