diff --git a/node_list.json b/node_list.json index 7770a2b..2d416b5 100644 --- a/node_list.json +++ b/node_list.json @@ -1,46 +1,54 @@ -{ - "Animation Builder (mtb)": "Convenient way to manage basic animation maths at the core of many of my workflows", - "Any To String (mtb)": "Tries to take any input and convert it to a string", - "Bbox (mtb)": "The bounding box (BBOX) custom type used by other nodes", - "Bbox From Mask (mtb)": "From a mask extract the bounding box", - "Blur (mtb)": "Blur an image using a Gaussian filter.", - "Color Correct (mtb)": "Various color correction methods", - "Colored Image (mtb)": "Constant color image of given size", - "Concat Images (mtb)": "Add images to batch", - "Crop (mtb)": "Crops an image and an optional mask to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input\n ", - "Debug (mtb)": "Experimental node to debug any Comfy values, support for more types and widgets is planned", - "Deep Bump (mtb)": "Normal & height maps generation from single pictures", - "Export With Ffmpeg (mtb)": "Export with FFmpeg (Experimental)", - "Face Swap (mtb)": "Face swap using deepinsight/insightface models", - "Film Interpolation (mtb)": "Google Research FILM frame interpolation for large motion", - "Fit Number (mtb)": "Fit the input float using a source and target range", - "Float To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" FLOAT to a NUMBER.", - "Get Batch From History (mtb)": "Very experimental node to load images from the history of the server.\n\n Queue items without output are ignored in the count.", - "Image Compare (mtb)": "Compare two images and return a difference image", - "Image Premultiply (mtb)": "Premultiply image with mask", - "Image Remove Background Rembg (mtb)": "Removes the background from the input using Rembg.", - "Image Resize Factor (mtb)": "Extracted mostly from WAS Node Suite, with a few edits (most notably multiple image support) and less features.", - "Int To Bool (mtb)": "Basic int to bool conversion", - "Int To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" INT to a NUMBER.", - "Latent Lerp (mtb)": "Linear interpolation (blend) between two latent vectors", - "Load Face Analysis Model (mtb)": "Loads a face analysis model", - "Load Face Enhance Model (mtb)": "Loads a GFPGan or RestoreFormer model for face enhancement.", - "Load Face Swap Model (mtb)": "Loads a faceswap model", - "Load Film Model (mtb)": "Loads a FILM model", - "Load Image From Url (mtb)": "Load an image from the given URL", - "Load Image Sequence (mtb)": "Load an image sequence from a folder. The current frame is used to determine which image to load.\n\n Usually used in conjunction with the `Primitive` node set to increment to load a sequence of images from a folder.\n Use -1 to load all matching frames as a batch.\n ", - "Mask To Image (mtb)": "Converts a mask (alpha) to an RGB image with a color and background", - "Qr Code (mtb)": "Basic QR Code generator", - "Restore Face (mtb)": "Uses GFPGan to restore faces", - "Save Gif (mtb)": "Save the images from the batch as a GIF", - "Save Image Grid (mtb)": "Save all the images in the input batch as a grid of images.", - "Save Image Sequence (mtb)": "Save an image sequence to a folder. The current frame is used to determine which image to save.\n\n This is merely a wrapper around the `save_images` function with formatting for the output folder and filename.\n ", - "Save Tensors (mtb)": "Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy", - "Smart Step (mtb)": "Utils to control the steps start/stop of the KAdvancedSampler in percentage", - "String Replace (mtb)": "Basic string replacement", - "Styles Loader (mtb)": "Load csv files and populate a dropdown from the rows (\u00e0 la A111)", - "Text To Image (mtb)": "Utils to convert text to image using a font\n\n\n The tool looks for any .ttf file in the Comfy folder hierarchy.\n ", - "Transform Image (mtb)": "Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy\n\n\n it return a tensor representing the transformed images with the same shape as the input tensor\n ", - "Uncrop (mtb)": "Uncrops an image to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input", - "Unsplash Image (mtb)": "Unsplash Image given a keyword and a size" -} +{ + "Animation Builder (mtb)": "Convenient way to manage basic animation maths at the core of many of my workflows", + "Any To String (mtb)": "Tries to take any input and convert it to a string", + "Batch Float (mtb)": "Generates a batch of float values with interpolation", + "Batch Shape (mtb)": "Generates a batch of 2D shapes with optional shading (experimental)", + "Batch Transform (mtb)": "Transform a batch of images using a batch of keyframes", + "Bbox (mtb)": "The bounding box (BBOX) custom type used by other nodes", + "Bbox From Mask (mtb)": "From a mask extract the bounding box", + "Blur (mtb)": "Blur an image using a Gaussian filter.", + "Color Correct (mtb)": "Various color correction methods", + "Colored Image (mtb)": "Constant color image of given size", + "Concat Images (mtb)": "Add images to batch", + "Crop (mtb)": "Crops an image and an optional mask to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input\n ", + "Debug (mtb)": "Experimental node to debug any Comfy values, support for more types and widgets is planned", + "Deep Bump (mtb)": "Normal & height maps generation from single pictures", + "Export With Ffmpeg (mtb)": "Export with FFmpeg (Experimental)", + "Face Swap (mtb)": "Face swap using deepinsight/insightface models", + "Film Interpolation (mtb)": "Google Research FILM frame interpolation for large motion", + "Fit Number (mtb)": "Fit the input float using a source and target range", + "Float To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" FLOAT to a NUMBER.", + "Get Batch From History (mtb)": "Very experimental node to load images from the history of the server.\n\n Queue items without output are ignored in the count.", + "Image Compare (mtb)": "Compare two images and return a difference image", + "Image Premultiply (mtb)": "Premultiply image with mask", + "Image Remove Background Rembg (mtb)": "Removes the background from the input using Rembg.", + "Image Resize Factor (mtb)": "Extracted mostly from WAS Node Suite, with a few edits (most notably multiple image support) and less features.", + "Image Tile Offset (mtb)": "Mimics an old photoshop technique to check for seamless textures", + "Int To Bool (mtb)": "Basic int to bool conversion", + "Int To Number (mtb)": "Node addon for the WAS Suite. Converts a \"comfy\" INT to a NUMBER.", + "Interpolate Clip Sequential (mtb)": null, + "Latent Lerp (mtb)": "Linear interpolation (blend) between two latent vectors", + "Load Face Analysis Model (mtb)": "Loads a face analysis model", + "Load Face Enhance Model (mtb)": "Loads a GFPGan or RestoreFormer model for face enhancement.", + "Load Face Swap Model (mtb)": "Loads a faceswap model", + "Load Film Model (mtb)": "Loads a FILM model", + "Load Image From Url (mtb)": "Load an image from the given URL", + "Load Image Sequence (mtb)": "Load an image sequence from a folder. The current frame is used to determine which image to load.\n\n Usually used in conjunction with the `Primitive` node set to increment to load a sequence of images from a folder.\n Use -1 to load all matching frames as a batch.\n ", + "Mask To Image (mtb)": "Converts a mask (alpha) to an RGB image with a color and background", + "Model Patch Seamless (mtb)": "Uses the stable diffusion 'hack' to infer seamless images by setting the model layers padding mode to circular (experimental)", + "Qr Code (mtb)": "Basic QR Code generator", + "Restore Face (mtb)": "Uses GFPGan to restore faces", + "Save Gif (mtb)": "Save the images from the batch as a GIF", + "Save Image Grid (mtb)": "Save all the images in the input batch as a grid of images.", + "Save Image Sequence (mtb)": "Save an image sequence to a folder. The current frame is used to determine which image to save.\n\n This is merely a wrapper around the `save_images` function with formatting for the output folder and filename.\n ", + "Save Tensors (mtb)": "Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy", + "Smart Step (mtb)": "Utils to control the steps start/stop of the KAdvancedSampler in percentage", + "Stack Images (mtb)": "Stack the input images horizontally or vertically", + "String Replace (mtb)": "Basic string replacement", + "Styles Loader (mtb)": "Load csv files and populate a dropdown from the rows (\u00e0 la A111)", + "Text To Image (mtb)": "Utils to convert text to image using a font\n\n\n The tool looks for any .ttf file in the Comfy folder hierarchy.\n ", + "Transform Image (mtb)": "Save torch tensors (image, mask or latent) to disk, useful to debug things outside comfy\n\n\n it return a tensor representing the transformed images with the same shape as the input tensor\n ", + "Uncrop (mtb)": "Uncrops an image to a given bounding box\n\n The bounding box can be given as a tuple of (x, y, width, height) or as a BBOX type\n The BBOX input takes precedence over the tuple input", + "Unsplash Image (mtb)": "Unsplash Image given a keyword and a size", + "Vae Decode (mtb)": "Wrapper for the 2 core decoders but also adding the sd seamless hack, taken from: FlyingFireCo/tiled_ksampler" +} \ No newline at end of file diff --git a/nodes/batch.py b/nodes/batch.py new file mode 100644 index 0000000..7bbdb9e --- /dev/null +++ b/nodes/batch.py @@ -0,0 +1,260 @@ +import os +from pathlib import Path +from typing import List + +import cv2 +import folder_paths +import numpy as np +import torch + +from ..utils import apply_easing, pil2tensor +from .transform import TransformImage + + +def hex_to_rgb(hex_color, bgr=False): + hex_color = hex_color.lstrip("#") + if bgr: + return tuple(int(hex_color[i : i + 2], 16) for i in (4, 2, 0)) + + return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4)) + + +class BatchShape: + """Generates a batch of 2D shapes with optional shading (experimental)""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "count": ("INT", {"default": 1}), + "shape": ( + ["Box", "Circle", "Diamond"], + {"default": "Box"}, + ), + "image_width": ("INT", {"default": 512}), + "image_height": ("INT", {"default": 512}), + "shape_size": ("INT", {"default": 100}), + "color": ("COLOR", {"default": "#ffffff"}), + "bg_color": ("COLOR", {"default": "#000000"}), + "shade_color": ("COLOR", {"default": "#000000"}), + "shadex": ("FLOAT", {"default": 0.0}), + "shadey": ("FLOAT", {"default": 0.0}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "generate_shapes" + CATEGORY = "mtb/batch" + + def generate_shapes( + self, + count, + shape, + image_width, + image_height, + shape_size, + color, + bg_color, + shade_color, + shadex, + shadey, + ): + print(f"COLOR: {color}") + print(f"BG_COLOR: {bg_color}") + print(f"SHADE_COLOR: {shade_color}") + + # Parse color input to BGR tuple for OpenCV + color = hex_to_rgb(color) + bg_color = hex_to_rgb(bg_color) + shade_color = hex_to_rgb(shade_color) + res = [] + for x in range(count): + # Initialize an image canvas + canvas = np.full((image_height, image_width, 3), bg_color, dtype=np.uint8) + mask = np.zeros((image_height, image_width), dtype=np.uint8) + + # Compute the center point of the shape + center = (image_width // 2, image_height // 2) + + if shape == "Box": + half_size = shape_size // 2 + top_left = (center[0] - half_size, center[1] - half_size) + bottom_right = (center[0] + half_size, center[1] + half_size) + cv2.rectangle(mask, top_left, bottom_right, 255, -1) + elif shape == "Circle": + cv2.circle(mask, center, shape_size // 2, 255, -1) + elif shape == "Diamond": + pts = np.array( + [ + [center[0], center[1] - shape_size // 2], + [center[0] + shape_size // 2, center[1]], + [center[0], center[1] + shape_size // 2], + [center[0] - shape_size // 2, center[1]], + ] + ) + cv2.fillPoly(mask, [pts], 255) + + # Color the shape + canvas[mask == 255] = color + + # Apply shading effects to a separate shading canvas + shading = np.zeros_like(canvas, dtype=np.float32) + shading[:, :, 0] = shadex * np.linspace(0, 1, image_width) + shading[:, :, 1] = shadey * np.linspace(0, 1, image_height).reshape(-1, 1) + shading_canvas = cv2.addWeighted( + canvas.astype(np.float32), 1, shading, 1, 0 + ).astype(np.uint8) + + # Apply shading only to the shape area using the mask + canvas[mask == 255] = shading_canvas[mask == 255] + res.append(canvas) + + return (pil2tensor(res),) + + +class BatchFloat: + """Generates a batch of float values with interpolation""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mode": ( + ["Single", "Steps"], + {"default": "Single"}, + ), + "count": ("INT", {"default": 1}), + "min": ("FLOAT", {"default": 0.0}), + "max": ("FLOAT", {"default": 1.0}), + "easing": ( + [ + "Linear", + "Sine In", + "Sine Out", + "Sine In/Out", + "Quart In", + "Quart Out", + "Quart In/Out", + "Cubic In", + "Cubic Out", + "Cubic In/Out", + "Circ In", + "Circ Out", + "Circ In/Out", + "Back In", + "Back Out", + "Back In/Out", + "Elastic In", + "Elastic Out", + "Elastic In/Out", + "Bounce In", + "Bounce Out", + "Bounce In/Out", + ], + {"default": "Linear"}, + ), + } + } + + FUNCTION = "set_floats" + RETURN_TYPES = ("FLOATS",) + CATEGORY = "mtb/batch" + + def set_floats(self, mode, count, min, max, easing): + keyframes = [] + if mode == "Single": + keyframes = [min] * count + return (keyframes,) + + for i in range(count): + normalized_step = i / (count - 1) + eased_step = apply_easing(normalized_step, easing) + eased_value = min + (max - min) * eased_step + keyframes.append(eased_value) + + return (keyframes,) + + +class Batch2dTransform: + """Transform a batch of images using a batch of keyframes""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "border_handling": ( + ["edge", "constant", "reflect", "symmetric"], + {"default": "edge"}, + ), + "constant_color": ("COLOR", {"default": "#000000"}), + }, + "optional": { + "x": ("FLOATS",), + "y": ("FLOATS",), + "zoom": ("FLOATS",), + "angle": ("FLOATS",), + "shear": ("FLOATS",), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "transform_batch" + CATEGORY = "mtb/batch" + + def transform_batch( + self, + image: torch.Tensor, + border_handling, + constant_color, + x=None, + y=None, + zoom=None, + angle=None, + shear=None, + ): + if not any([x, y, zoom, angle]): + raise ValueError("At least one transform parameter must be provided") + + keyframes = {"x": [], "y": [], "zoom": [], "angle": [], "shear": []} + + default_vals = {"x": 0, "y": 0, "zoom": 1.0, "angle": 0, "shear": 0} + + if x: + keyframes["x"] = x + if y: + keyframes["y"] = y + if zoom: + keyframes["zoom"] = zoom + if angle: + keyframes["angle"] = angle + if shear: + keyframes["shear"] = shear + + for name, values in keyframes.items(): + count = len(values) + if count > 0 and count != image.shape[0]: + raise ValueError( + f"Length of {name} values ({count}) must match number of images ({image.shape[0]})" + ) + if count == 0: + keyframes[name] = [default_vals[name]] * image.shape[0] + + transformer = TransformImage() + res = [ + transformer.transform( + image[i].unsqueeze(0), + keyframes["x"][i], + keyframes["y"][i], + keyframes["zoom"][i], + keyframes["angle"][i], + keyframes["shear"][i], + border_handling, + constant_color, + )[0] + for i in range(image.shape[0]) + ] + return (torch.cat(res, dim=0),) + + +__nodes__ = [BatchFloat, Batch2dTransform, BatchShape] diff --git a/nodes/model.py b/nodes/model.py index 40d172a..e0193bf 100644 --- a/nodes/model.py +++ b/nodes/model.py @@ -49,6 +49,8 @@ class VaeDecode_: class ModelPatchSeamless: + """Uses the stable diffusion 'hack' to infer seamless images by setting the model layers padding mode to circular (experimental)""" + @classmethod def INPUT_TYPES(cls): return {