diff --git a/__init__.py b/__init__.py index 8d4943b..8f0451f 100644 --- a/__init__.py +++ b/__init__.py @@ -1,19 +1,20 @@ -from .meyo_node_Computational import StandardSize, CompareInt, FloatToInteger, GenerateNumbers, GetRandomIntegerInRange -from .meyo_node_String import AddPrefixSuffix, ExtractSubstring, ExtractSubstringByIndices, SplitStringByDelimiter, ProcessString, ExtractBeforeAfter, ReplaceNthOccurrence, BatchReplaceStrings, ReplaceMultiple, RandomLineFromText, CheckSubstringPresence, AddPrefixSuffixToLines, ExtractAndCombineLines, FilterLinesBySubstrings, FilterLinesByWordCount, SplitAndExtractText, CountOccurrences, ExtractLinesByIndex, ExtractSpecificLines, RemoveContentBetweenChars, ShuffleTextLines, ConditionalTextOutput, TextConditionCheck, TextConcatenation, ExtractSpecificData, FindFirstLineContent -from .meyo_node_File import FileListAndSuffix, ReadExcelData, WriteExcelData, FindExcelData, ReadExcelRowOrColumnDiff +from .meyo_node_Computational import CompareInt, FloatToInteger, GenerateNumbers, GetRandomIntegerInRange +from .meyo_node_String import SingleTextInput, MultiParamInputNode, AddPrefixSuffix, ExtractSubstring, ExtractSubstringByIndices, SplitStringByDelimiter, ProcessString, ExtractBeforeAfter, ReplaceNthOccurrence, BatchReplaceStrings, ReplaceMultiple, RandomLineFromText, CheckSubstringPresence, AddPrefixSuffixToLines, ExtractAndCombineLines, FilterLinesBySubstrings, FilterLinesByWordCount, SplitAndExtractText, CountOccurrences, ExtractLinesByIndex, ExtractSpecificLines, RemoveContentBetweenChars, ShuffleTextLines, ConditionalTextOutput, TextConditionCheck, TextConcatenation, ExtractSpecificData, FindFirstLineContent +from .meyo_node_File import LoadAndAdjustImage, ImageAdjuster, SaveImagEX, FileListAndSuffix, ReadExcelData, WriteExcelData, FindExcelData, ReadExcelRowOrColumnDiff from .meyo_node_Functional import GetCurrentTime, SelectionParameter NODE_CLASS_MAPPINGS = { #运算型节点:meyo_node_Computational - "StandardSize": StandardSize, "CompareInt": CompareInt, "FloatToInteger": FloatToInteger, "GenerateNumbers": GenerateNumbers, "GetRandomIntegerInRange": GetRandomIntegerInRange, #字符串处理:meyo_node_String + "SingleTextInput": SingleTextInput, + "MultiParamInputNode": MultiParamInputNode, "AddPrefixSuffix": AddPrefixSuffix, "ExtractSubstring": ExtractSubstring, "ExtractSubstringByIndices": ExtractSubstringByIndices, @@ -42,6 +43,9 @@ NODE_CLASS_MAPPINGS = { "FindFirstLineContent": FindFirstLineContent, #文件处理:meyo_node_File + "LoadAndAdjustImage": LoadAndAdjustImage, + "ImageAdjuster": ImageAdjuster, + "SaveImagEX": SaveImagEX, "FileListAndSuffix": FileListAndSuffix, "ReadExcelData": ReadExcelData, "WriteExcelData": WriteExcelData, @@ -58,13 +62,14 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { #运算型节点:meyo_node_Computational - "StandardSize": "重置尺寸(meeeyo.com)", "CompareInt": "比较数值(meeeyo.com)", "FloatToInteger": "规范数值(meeeyo.com)", "GenerateNumbers": "生成范围数组(meeeyo.com)", "GetRandomIntegerInRange": "范围内随机数(meeeyo.com)", #字符串处理:meyo_node_String + "SingleTextInput": "文本输入(meeeyo.com)", + "MultiParamInputNode": "多参数输入(meeeyo.com)", "AddPrefixSuffix": "添加前后缀(meeeyo.com)", "ExtractSubstring": "提取标签之间(meeeyo.com)", "ExtractSubstringByIndices": "按数字范围提取(meeeyo.com)", @@ -93,6 +98,9 @@ NODE_DISPLAY_NAME_MAPPINGS = { "FindFirstLineContent": "指定字符行参数(meeeyo.com)", #文件处理:meyo_node_File + "LoadAndAdjustImage": "加载重置图像(meeeyo.com)", + "ImageAdjuster": "重置图像(meeeyo.com)", + "SaveImagEX": "保存图像(meeeyo.com)", "FileListAndSuffix": "从路径加载(meeeyo.com)", "ReadExcelData": "读取表格数据(meeeyo.com)", "WriteExcelData": "写入表格数据(meeeyo.com)", @@ -100,7 +108,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ReadExcelRowOrColumnDiff": "读取表格数量差(meeeyo.com)", #功能型节点:meyo_node_Functional - "GetCurrentTime": "当前时间", + "GetCurrentTime": "当前时间(戳)", "SelectionParameter": "选择参数", } diff --git a/meyo_node_Computational.py b/meyo_node_Computational.py index 4af6d3f..497969e 100644 --- a/meyo_node_Computational.py +++ b/meyo_node_Computational.py @@ -2,52 +2,6 @@ import math import random -#======宽度高度调整 -class StandardSize: - def __init__(self): - pass - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "width": ("INT",), - "height": ("INT",), - "size_option": (["小尺寸", "中尺寸", "大尺寸"], {"default": "中尺寸"}), # 下拉选择框 - }, - "optional": {}, - } - - RETURN_TYPES = ("INT", "INT") - FUNCTION = "resize_image" - CATEGORY = "Meeeyo/Image" - - def resize_image(self, width, height, size_option): - # 根据选择的选项调整尺寸像尺寸 - if size_option == "小尺寸": - if width / height >= 1.23: - return (768, 512) - elif 0.82 < width / height < 1.23: - return (768, 768) - else: - return (512, 768) - elif size_option == "中尺寸": - if width / height >= 1.23: - return (1216, 832) - elif 0.82 < width / height < 1.23: - return (1216, 1216) - else: - return (832, 1216) - elif size_option == "大尺寸": - if width / height >= 1.23: - return (1600, 1120) - elif 0.82 < width / height < 1.23: - return (1600, 1600) - else: - return (1120, 1600) - - - #======比较数值 class CompareInt: def __init__(self): diff --git a/meyo_node_File.py b/meyo_node_File.py index 2e2772e..74db2f5 100644 --- a/meyo_node_File.py +++ b/meyo_node_File.py @@ -1,6 +1,251 @@ +import os import csv import openpyxl -import os +import pathlib +import folder_paths +import torch +import node_helpers +import numpy as np +from PIL import Image, ImageOps, ImageSequence + + +#======加载重置图像 +class LoadAndAdjustImage: + _color_channels = ["alpha", "red", "green", "blue"] + + @classmethod + def INPUT_TYPES(s): + input_dir = folder_paths.get_input_directory() + files = [f.name for f in pathlib.Path(input_dir).iterdir() if f.is_file()] + return { + "required": { + "image": (sorted(files), {"image_upload": True}), + "max_dimension": ("INT", {"default": 1024, "min": 0, "max": 4096, "step": 8}), + "size_option": (["Custom", "Small", "Medium", "Large"], {"default": "Custom"}), + } + } + + RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT") + RETURN_NAMES = ("image", "mask", "width", "height") + FUNCTION = "load_image" + CATEGORY = "Meeeyo/File" + + def load_image(self, image, max_dimension, size_option): + image_path = folder_paths.get_annotated_filepath(image) + img = node_helpers.pillow(Image.open, image_path) + + W, H = img.size + output_images = [] + output_masks = [] + + for frame in ImageSequence.Iterator(img): + frame = node_helpers.pillow(ImageOps.exif_transpose, frame) + + if frame.mode == 'P': + frame = frame.convert("RGBA") + elif 'A' in frame.getbands(): + frame = frame.convert("RGBA") + + if size_option != "Custom": + aspect_ratio = W / H + + if size_option == "Small": + if aspect_ratio >= 1.23: + target_width, target_height = 768, 512 + elif 0.82 < aspect_ratio < 1.23: + target_width, target_height = 768, 768 + else: + target_width, target_height = 512, 768 + elif size_option == "Medium": + if aspect_ratio >= 1.23: + target_width, target_height = 1216, 832 + elif 0.82 < aspect_ratio < 1.23: + target_width, target_height = 1216, 1216 + else: + target_width, target_height = 832, 1216 + elif size_option == "Large": + if aspect_ratio >= 1.23: + target_width, target_height = 1600, 1120 + elif 0.82 < aspect_ratio < 1.23: + target_width, target_height = 1600, 1600 + else: + target_width, target_height = 1120, 1600 + + image = frame.convert("RGB") + image = ImageOps.fit(image, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5)) + + if 'A' in frame.getbands(): + mask = np.array(frame.getchannel('A')).astype(np.float32) / 255.0 + mask = torch.from_numpy(mask) + mask_image = Image.fromarray((mask.numpy() * 255).astype(np.uint8)) + mask_image = ImageOps.fit(mask_image, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5)) + mask = np.array(mask_image).astype(np.float32) / 255.0 + mask = torch.from_numpy(mask) + else: + mask = torch.ones((target_height, target_width), dtype=torch.float32, device="cpu") + else: + ratio = min(max_dimension / W, max_dimension / H) + adjusted_width = round(W * ratio) + adjusted_height = round(H * ratio) + + image = frame.convert("RGB") + image = image.resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR) + + if 'A' in frame.getbands(): + mask = np.array(frame.getchannel('A')).astype(np.float32) / 255.0 + mask = torch.from_numpy(mask) + mask_image = Image.fromarray((mask.numpy() * 255).astype(np.uint8)) + mask_image = mask_image.resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR) + mask = np.array(mask_image).astype(np.float32) / 255.0 + mask = torch.from_numpy(mask) + else: + mask = torch.ones((adjusted_height, adjusted_width), dtype=torch.float32, device="cpu") + + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + output_images.append(image) + output_masks.append(mask.unsqueeze(0)) + + if len(output_images) > 1: + output_image = torch.cat(output_images, dim=0) + output_mask = torch.cat(output_masks, dim=0) + else: + output_image = output_images[0] + output_mask = output_masks[0] + if size_option != "Custom": + return (output_image, output_mask, target_width, target_height) + else: + return (output_image, output_mask, adjusted_width, adjusted_height) + + @classmethod + def VALIDATE_INPUTS(s, image): + if not folder_paths.exists_annotated_filepath(image): + return f"Invalid image file: {image}" + return True + + +#======重置图像尺寸 +class ImageAdjuster: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "max_dimension": ("INT", {"default": 1024, "min": 0, "max": 4096, "step": 8}), + "size_option": (["Custom", "Small", "Medium", "Large"], {"default": "Custom"}), + } + } + + RETURN_TYPES = ("IMAGE", "INT", "INT") + RETURN_NAMES = ("image", "width", "height") + FUNCTION = "process_image" + CATEGORY = "Meeeyo/File" + + def process_image(self, image, max_dimension=1024, size_option="Custom"): + input_image = Image.fromarray((image.squeeze(0).numpy() * 255).astype(np.uint8)) + W, H = input_image.size + + processed_images = [] + + for frame in [input_image]: + frame = ImageOps.exif_transpose(frame) + + if frame.mode == 'P': + frame = frame.convert("RGBA") + elif 'A' in frame.getbands(): + frame = frame.convert("RGBA") + + if size_option != "Custom": + aspect_ratio = W / H + + if size_option == "Small": + if aspect_ratio >= 1.23: + target_width, target_height = 768, 512 + elif 0.82 < aspect_ratio < 1.23: + target_width, target_height = 768, 768 + else: + target_width, target_height = 512, 768 + elif size_option == "Medium": + if aspect_ratio >= 1.23: + target_width, target_height = 1216, 832 + elif 0.82 < aspect_ratio < 1.23: + target_width, target_height = 1216, 1216 + else: + target_width, target_height = 832, 1216 + elif size_option == "Large": + if aspect_ratio >= 1.23: + target_width, target_height = 1600, 1120 + elif 0.82 < aspect_ratio < 1.23: + target_width, target_height = 1600, 1600 + else: + target_width, target_height = 1120, 1600 + processed_image = frame.convert("RGB") + processed_image = ImageOps.fit(processed_image, (target_width, target_height), method=Image.Resampling.BILINEAR, centering=(0.5, 0.5)) + else: + ratio = min(max_dimension / W, max_dimension / H) + adjusted_width = round(W * ratio) + adjusted_height = round(H * ratio) + + processed_image = frame.convert("RGB") + processed_image = processed_image.resize((adjusted_width, adjusted_height), Image.Resampling.BILINEAR) + + processed_image = np.array(processed_image).astype(np.float32) / 255.0 + processed_image = torch.from_numpy(processed_image)[None,] + processed_images.append(processed_image) + + output_image = torch.cat(processed_images, dim=0) if len(processed_images) > 1 else processed_images[0] + if size_option != "Custom": + return (output_image, target_width, target_height) + else: + return (output_image, adjusted_width, adjusted_height) + +#======保存图像 +class SaveImagEX: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "save_path": ("STRING", {"default": "./output"}), + "image_name": ("STRING", {"default": "ComfyUI"}), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "save_image" + OUTPUT_NODE = True + CATEGORY = "Meeeyo/File" + + def save_image(self, image, save_path, image_name): + if not isinstance(image, torch.Tensor): + raise ValueError("Invalid image tensor format") + if save_path == "./output": + save_path = self.output_dir + elif not os.path.isabs(save_path): + save_path = os.path.join(self.output_dir, save_path) + os.makedirs(save_path, exist_ok=True) + base_name = os.path.splitext(image_name)[0] + batch_size = image.shape[0] + channel_to_mode = {1: "L", 3: "RGB", 4: "RGBA"} + + for i in range(batch_size): + filename = f"{base_name}.png" if batch_size == 1 else f"{base_name}_{i:05d}.png" + full_path = os.path.join(save_path, filename) + single_image = image[i].cpu().numpy() + single_image = (single_image * 255).astype('uint8') + channels = single_image.shape[2] + if channels not in channel_to_mode: + raise ValueError(f"Unsupported channel number: {channels}") + mode = channel_to_mode[channels] + if channels == 1: + single_image = single_image[:, :, 0] + pil_image = Image.fromarray(single_image, mode) + pil_image.save(full_path, format="PNG", compress_level=0) + return (image,) #======文件路径和后缀统计 diff --git a/meyo_node_Functional.py b/meyo_node_Functional.py index f26f08e..5b9a6b1 100644 --- a/meyo_node_Functional.py +++ b/meyo_node_Functional.py @@ -1,5 +1,5 @@ -from datetime import datetime -import ntplib +import datetime +import time #======获取当前时间 class GetCurrentTime: @@ -17,7 +17,7 @@ class GetCurrentTime: }, } - RETURN_TYPES = ("STRING",) + RETURN_TYPES = ("STRING", "INT") # 新增 INTEGER 类型的返回值 FUNCTION = "get_current_time" CATEGORY = "Meeeyo/Functional" @@ -26,13 +26,15 @@ class GetCurrentTime: def get_current_time(self, prefix="", any=None): # Get the current system time - current_time = datetime.now() + current_time = datetime.datetime.now() # Format the time as a string formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S") # Include the prefix in the output result = f"{prefix}{formatted_time}" - # Return the formatted time with prefix as a string - return (result,) + # Get the current timestamp as an integer with milliseconds precision + timestamp = int(round(time.time() * 1000)) # 获取当前时间戳的整数部分并精确到毫秒 + # Return both the formatted time with prefix and the timestamp + return (result, timestamp) # 返回两个值:字符串和整数 #======选择性别和模板 diff --git a/meyo_node_String.py b/meyo_node_String.py index 5e38bd0..336d4d7 100644 --- a/meyo_node_String.py +++ b/meyo_node_String.py @@ -7,6 +7,47 @@ import math import re +#======多参数输入 +class SingleTextInput: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text": ("STRING", {"default": "", "multiline": True}), + } + } + + RETURN_TYPES = ("STRING",) + FUNCTION = "process_input" + CATEGORY = "Meeeyo/Text" + OUTPUT_NODE = False + + def process_input(self, text): + return (text,) + + +#======多参数输入 +class MultiParamInputNode: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "text1": ("STRING", {"default": "", "multiline": True}), # 第一个多行文本输入框 + "text2": ("STRING", {"default": "", "multiline": True}), # 第二个多行文本输入框 + "int1": ("INT", {"default": 0, "min": -1000000, "max": 1000000}), # 第一个整数输入框 + "int2": ("INT", {"default": 0, "min": -1000000, "max": 1000000}), # 第二个整数输入框 + } + } + + RETURN_TYPES = ("STRING", "STRING", "INT", "INT") # 返回两个字符串和两个整数 + FUNCTION = "process_inputs" + CATEGORY = "Meeeyo/String" + OUTPUT_NODE = False + + def process_inputs(self, text1, text2, int1, int2): + return (text1, text2, int1, int2) + + #======添加前后缀 class AddPrefixSuffix: def __init__(self):