From 412bc279a821511432763b9dde265ac3e8d4b2d4 Mon Sep 17 00:00:00 2001 From: justumen Date: Wed, 28 May 2025 17:55:44 +0200 Subject: [PATCH] v1.1.6 --- README.md | 2 +- __init__.py | 20 ++- combine_background_overlay.py | 98 ++++++------ load_image_from_path.py | 66 ++++++++ masks_nodes.py | 276 ++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 6 files changed, 404 insertions(+), 60 deletions(-) create mode 100644 load_image_from_path.py diff --git a/README.md b/README.md index 43a80bb..112d40d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ๐Ÿ”— Comfyui : Bjornulf_custom_nodes v1.1.5 ๐Ÿ”— +# ๐Ÿ”— Comfyui : Bjornulf_custom_nodes v1.1.6 ๐Ÿ”— A list of 168 custom nodes for Comfyui : Display, manipulate, create and edit text, images, videos, loras, generate characters and more. You can manage looping operations, generate randomized content, trigger logical conditions, pause and manually control your workflows and even work with external AI tools, like Ollama or Text To Speech. diff --git a/__init__.py b/__init__.py index 0b793ae..29dfad7 100644 --- a/__init__.py +++ b/__init__.py @@ -127,8 +127,9 @@ from .style_selector import StyleSelector from .split_image import SplitImageGrid, ReassembleImageGrid from .API_openai import APIGenerateGPT4o -from .masks_nodes import LargestMaskOnly, BoundingRectangleMask +from .masks_nodes import LargestMaskOnly, BoundingRectangleMask, BoundingRectangleMaskBlur from .openai_nodes import OpenAIVisionNode +# MultiOpenAIVisionNode from .loop_random_seed import LoopRandomSeed # from .video_text_generator import VideoTextGenerator @@ -136,16 +137,25 @@ from .loop_random_seed import LoopRandomSeed # from .remote_nodes import RemoteVAEDecoderNodeTiled, RemoteVAEDecoderNode, LoadFromBase64, SaveTensors, LoadTensor # from .fix_face import FixFace, FaceSettings from .image_cut_and_shift import HorizontalCutAndShift +from .load_image_from_path import LoadImageWithTransparencyFromPath +# from .kofi_nodes import CivitAILoraSelectorWanVideo, CivitAILoraSelectorHunyuan +# from .json_prompt_extractor import JSONImagePromptExtractor #RemoteTextEncodingWithCLIPs NODE_CLASS_MAPPINGS = { + # "Bjornulf_LoraSelectorHunyuan": CivitAILoraSelectorHunyuan, + # "Bjornulf_LoraSelectorWanVideo": CivitAILoraSelectorWanVideo, + # "Bjornulf_JSONImagePromptExtractor": JSONImagePromptExtractor, "Bjornulf_MatchTextToInput": MatchTextToInput, "Bjornulf_LargestMaskOnly": LargestMaskOnly, "Bjornulf_BoundingRectangleMask": BoundingRectangleMask, + "Bjornulf_BoundingRectangleMaskBlur": BoundingRectangleMaskBlur, "Bjornulf_OpenAIVisionNode": OpenAIVisionNode, + # "Bjornulf_MultiOpenAIVisionNode": MultiOpenAIVisionNode, "Bjornulf_LoopRandomSeed": LoopRandomSeed, "Bjornulf_HorizontalCutAndShift": HorizontalCutAndShift, + "Bjornulf_LoadImageWithTransparencyFromPath": LoadImageWithTransparencyFromPath, # "Bjornulf_PurgeCLIPNode": PurgeCLIPNode, # "Bjornulf_RemoteTextEncodingWithCLIPs": RemoteTextEncodingWithCLIPs, @@ -327,11 +337,17 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { + #"Bjornulf_LoraSelectorHunyuan": "โ˜• Lora Selector Hunyuan", + #"Bjornulf_LoraSelectorWanVideo": "โ˜• Lora Selector WanVideo", + #"Bjornulf_JSONImagePromptExtractor": "JSONImagePromptExtractor", "Bjornulf_MatchTextToInput": "๐Ÿ”›๐Ÿ“ Match 10 Text to Input", "Bjornulf_LargestMaskOnly": "๐Ÿ‘บ๐Ÿ”ช Largest Mask Only", "Bjornulf_BoundingRectangleMask": "๐Ÿ‘บโžœโ–ข Convert mask to rectangle", + "Bjornulf_BoundingRectangleMaskBlur": "๐Ÿ‘บโžœโ–ข Convert mask to rectangle (with Blur)", "Bjornulf_OpenAIVisionNode": "๐Ÿ”ฎ OpenAI Vision Node", + #"Bjornulf_MultiOpenAIVisionNode": "๐Ÿ”ฎ OpenAI Vision Node (โš ๏ธ Multiple images accepted as input โš ๏ธ)", "Bjornulf_LoopRandomSeed": "โ™ป๐ŸŽฒ Loop Random Seed", + "Bjornulf_LoadImageWithTransparencyFromPath": "๐Ÿ“ฅ๐Ÿ–ผ Load Image with Transparency From Path", # "Bjornulf_RemoteTextEncodingWithCLIPs": "[BETA] ๐Ÿ”ฎ Remote Text Encoding with CLIPs", # "Bjornulf_ConditionalSwitch": "ConditionalSwitch", # "Bjornulf_PurgeCLIPNode": "๐Ÿงน๐Ÿ“Ž Purge CLIP", @@ -519,4 +535,4 @@ NODE_DISPLAY_NAME_MAPPINGS = { } WEB_DIRECTORY = "./web" -__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] \ No newline at end of file +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] diff --git a/combine_background_overlay.py b/combine_background_overlay.py index aec8cd7..0ab1360 100644 --- a/combine_background_overlay.py +++ b/combine_background_overlay.py @@ -9,24 +9,24 @@ class CombineBackgroundOverlay: "required": { "background": ("IMAGE",), "overlay": ("IMAGE",), - "mask": ("MASK",), "horizontal_position": ("FLOAT", {"default": 50, "min": -50, "max": 150, "step": 0.1}), "vertical_position": ("FLOAT", {"default": 50, "min": -50, "max": 150, "step": 0.1}), }, + "optional": { + "mask": ("MASK",), + }, } RETURN_TYPES = ("IMAGE",) FUNCTION = "combine_background_overlay" CATEGORY = "Bjornulf" - def combine_background_overlay(self, background, overlay, mask, horizontal_position, vertical_position): + def combine_background_overlay(self, background, overlay, horizontal_position, vertical_position, mask=None): results = [] - # Use the first background image for all overlays + # Process the first background image bg = background[0].cpu().numpy() bg = np.clip(bg * 255, 0, 255).astype(np.uint8) - - # Check if background has alpha channel (4 channels) if bg.shape[2] == 4: bg_img = Image.fromarray(bg, 'RGBA') bg_has_alpha = True @@ -34,88 +34,74 @@ class CombineBackgroundOverlay: bg_img = Image.fromarray(bg, 'RGB') bg_has_alpha = False - # Process each overlay image with the same background + # Process each overlay for i in range(overlay.shape[0]): - # Get overlay and corresponding mask ov = overlay[i].cpu().numpy() ov = np.clip(ov * 255, 0, 255).astype(np.uint8) - - # Use corresponding mask or repeat last mask if fewer masks - mask_idx = min(i, mask.shape[0] - 1) - m = mask[mask_idx].cpu().numpy() - m = np.clip(m * 255, 0, 255).astype(np.uint8) - # Ensure overlay has correct shape (height, width, 3) - if len(ov.shape) == 2: - ov = np.stack([ov, ov, ov], axis=2) - elif ov.shape[2] != 3: - ov = ov[:, :, :3] - - # Create PIL Image for overlay - ov_img = Image.fromarray(ov, 'RGB') - - # Ensure mask has correct shape and create alpha channel - if len(m.shape) == 2: - alpha = Image.fromarray(m, 'L') + # Check if overlay has an alpha channel + if ov.shape[2] == 4: + ov_img = Image.fromarray(ov, 'RGBA') else: - # If mask has multiple channels, use the first one - alpha = Image.fromarray(m[:, :, 0] if len(m.shape) > 2 else m, 'L') - - # Resize alpha to match overlay if needed - if alpha.size != ov_img.size: - alpha = alpha.resize(ov_img.size, Image.LANCZOS) - - # Combine RGB overlay with alpha mask - ov_img.putalpha(alpha) + ov_img = Image.fromarray(ov, 'RGB') - # Calculate positions + # Apply mask if provided + if mask is not None: + mask_idx = min(i, mask.shape[0] - 1) + m = mask[mask_idx].cpu().numpy() + m = np.clip(m * 255, 0, 255).astype(np.uint8) + mask_img = Image.fromarray(m, 'L') + + # Resize mask to match overlay if needed + if mask_img.size != ov_img.size: + mask_img = mask_img.resize(ov_img.size, Image.LANCZOS) + + if ov_img.mode == 'RGBA': + # Combine overlayโ€™s alpha with mask + ov_alpha = np.array(ov_img.split()[3], dtype=np.float32) / 255.0 + mask_alpha = np.array(mask_img, dtype=np.float32) / 255.0 + effective_alpha = (ov_alpha * mask_alpha * 255).astype(np.uint8) + ov_img.putalpha(Image.fromarray(effective_alpha, 'L')) + else: + # Use mask as alpha for RGB overlay + ov_img.putalpha(mask_img) + else: + if ov_img.mode == 'RGB': + # Add fully opaque alpha for RGB overlay + ov_img.putalpha(Image.new('L', ov_img.size, 255)) + # For RGBA, keep the existing alpha + + # Calculate paste position x = int((horizontal_position / 100) * bg_img.width - (horizontal_position / 100) * ov_img.width) y = int((vertical_position / 100) * bg_img.height - (vertical_position / 100) * ov_img.height) - # Start with a fresh copy of the background for each overlay + # Prepare the result image if bg_has_alpha: result = bg_img.copy() else: - # Convert to RGBA for compositing result = Image.new('RGBA', bg_img.size, (0, 0, 0, 0)) result.paste(bg_img, (0, 0)) - # Paste the overlay with alpha blending + # Paste overlay with alpha blending if x + ov_img.width > 0 and y + ov_img.height > 0 and x < result.width and y < result.height: - # Create a temporary image for positioning temp = Image.new('RGBA', result.size, (0, 0, 0, 0)) temp.paste(ov_img, (x, y), ov_img) - - # Composite the overlay onto the result result = Image.alpha_composite(result.convert('RGBA'), temp) - # Convert back to numpy array and then to torch tensor + # Convert result back to tensor result_np = np.array(result) - - # Determine output format based on background if bg_has_alpha: - # Keep RGBA format if background had alpha - if result_np.shape[2] == 4: - result_tensor = torch.from_numpy(result_np).float() / 255.0 - else: - # Add alpha channel if somehow lost - alpha_channel = np.ones((result_np.shape[0], result_np.shape[1], 1), dtype=np.uint8) * 255 - result_np = np.concatenate([result_np, alpha_channel], axis=2) - result_tensor = torch.from_numpy(result_np).float() / 255.0 + result_tensor = torch.from_numpy(result_np).float() / 255.0 else: - # Convert RGBA to RGB if background was RGB + # Convert RGBA to RGB, blending with white only where needed if result_np.shape[2] == 4: - # Alpha blend with white background alpha = result_np[:, :, 3:4] / 255.0 rgb = result_np[:, :, :3] white_bg = np.ones_like(rgb) * 255 result_np = (rgb * alpha + white_bg * (1 - alpha)).astype(np.uint8) - result_tensor = torch.from_numpy(result_np).float() / 255.0 results.append(result_tensor) - # Stack all results into a single tensor final_result = torch.stack(results) - return (final_result,) \ No newline at end of file diff --git a/load_image_from_path.py b/load_image_from_path.py new file mode 100644 index 0000000..e64352a --- /dev/null +++ b/load_image_from_path.py @@ -0,0 +1,66 @@ +import torch +import numpy as np +from PIL import Image, ImageOps, ImageSequence +import node_helpers + +class LoadImageWithTransparencyFromPath: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image_path": ("STRING", {"default": "", "multiline": False}), + }, + } + + RETURN_TYPES = ("IMAGE", "MASK", "STRING") + RETURN_NAMES = ("image", "mask", "image_path") + FUNCTION = "load_image_alpha" + CATEGORY = "Bjornulf" + + def load_image_alpha(self, image_path): + # Validate that image_path is not None or empty + if not image_path: + raise ValueError("image_path cannot be None or empty") + + # Load the image using the provided path + img = node_helpers.pillow(Image.open, image_path) + + output_images = [] + output_masks = [] + w, h = None, None + excluded_formats = ['MPO'] + + # Process each frame in the image sequence + for i in ImageSequence.Iterator(img): + i = node_helpers.pillow(ImageOps.exif_transpose, i) + + if i.mode == 'I': + i = i.point(lambda i: i * (1 / 255)) + image = i.convert("RGBA") + + if len(output_images) == 0: + w = image.size[0] + h = image.size[1] + + if image.size[0] != w or image.size[1] != h: + continue + + image = np.array(image).astype(np.float32) / 255.0 + image = torch.from_numpy(image)[None,] + if 'A' in i.getbands(): + mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0 + mask = 1. - torch.from_numpy(mask) # Invert mask as per ComfyUI convention + else: + mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu") + output_images.append(image) + output_masks.append(mask.unsqueeze(0)) + + # Handle multi-frame images + if len(output_images) > 1 and img.format not in excluded_formats: + 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] + + return (output_image, output_mask, image_path) \ No newline at end of file diff --git a/masks_nodes.py b/masks_nodes.py index edf578b..e31c88f 100644 --- a/masks_nodes.py +++ b/masks_nodes.py @@ -218,4 +218,280 @@ class BoundingRectangleMask: else: raise ValueError("Mask must be 2D (H, W) or 3D (N, H, W)") + return (torch.from_numpy(result),) + +class BoundingRectangleMaskBlur: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mask": ("MASK",), + "up": ("INT", {"default": 0, "min": -10000, "max": 10000}), + "down": ("INT", {"default": 0, "min": -10000, "max": 10000}), + "right": ("INT", {"default": 0, "min": -10000, "max": 10000}), + "left": ("INT", {"default": 0, "min": -10000, "max": 10000}), + "blur_up": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "blur_down": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "blur_left": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "blur_right": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "tapered_corners": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("MASK",) + FUNCTION = "process" + CATEGORY = "Bjornulf" + + def _get_bounding_box(self, mask_np): + """Extract bounding box coordinates from active mask pixels.""" + active = mask_np > 0.5 + if not np.any(active): + return None + + rows_with_active = np.any(active, axis=1) + cols_with_active = np.any(active, axis=0) + + min_row = np.where(rows_with_active)[0][0] + max_row = np.where(rows_with_active)[0][-1] + min_col = np.where(cols_with_active)[0][0] + max_col = np.where(cols_with_active)[0][-1] + + return min_row, max_row, min_col, max_col + + def _expand_bounding_box(self, bbox, up, down, left, right, shape): + """Expand bounding box by specified amounts, clamped to image bounds.""" + min_row, max_row, min_col, max_col = bbox + H, W = shape + + min_row_adj = max(0, min_row - up) + max_row_adj = min(H - 1, max_row + down) + min_col_adj = max(0, min_col - left) + max_col_adj = min(W - 1, max_col + right) + + # Check for invalid bounds + if min_row_adj > max_row_adj or min_col_adj > max_col_adj: + return None + + return min_row_adj, max_row_adj, min_col_adj, max_col_adj + + def _create_directional_blur_mask(self, mask, direction, blur_amount): + """Create a mask blurred in one specific direction.""" + if blur_amount <= 0: + return np.zeros_like(mask) + + H, W = mask.shape + + # Find the edge of the mask in the specified direction + mask_binary = mask > 0.5 + if not np.any(mask_binary): + return np.zeros_like(mask) + + # Create blur based on direction + if direction == 'up': + # Find top edge + top_rows = np.any(mask_binary, axis=1) + if not np.any(top_rows): + return np.zeros_like(mask) + top_edge = np.where(top_rows)[0][0] + + # Create gradient going upward from top edge + result = np.zeros_like(mask) + for row in range(top_edge + 1): + distance = top_edge - row + strength = np.exp(-(distance ** 2) / (2 * blur_amount ** 2)) + result[row, :] = strength * np.any(mask_binary[top_edge:, :], axis=0) + + elif direction == 'down': + # Find bottom edge + top_rows = np.any(mask_binary, axis=1) + if not np.any(top_rows): + return np.zeros_like(mask) + bottom_edge = np.where(top_rows)[0][-1] + + # Create gradient going downward from bottom edge + result = np.zeros_like(mask) + for row in range(bottom_edge, H): + distance = row - bottom_edge + strength = np.exp(-(distance ** 2) / (2 * blur_amount ** 2)) + result[row, :] = strength * np.any(mask_binary[:bottom_edge+1, :], axis=0) + + elif direction == 'left': + # Find left edge + left_cols = np.any(mask_binary, axis=0) + if not np.any(left_cols): + return np.zeros_like(mask) + left_edge = np.where(left_cols)[0][0] + + # Create gradient going leftward from left edge + result = np.zeros_like(mask) + for col in range(left_edge + 1): + distance = left_edge - col + strength = np.exp(-(distance ** 2) / (2 * blur_amount ** 2)) + result[:, col] = strength * np.any(mask_binary[:, left_edge:], axis=1) + + elif direction == 'right': + # Find right edge + left_cols = np.any(mask_binary, axis=0) + if not np.any(left_cols): + return np.zeros_like(mask) + right_edge = np.where(left_cols)[0][-1] + + # Create gradient going rightward from right edge + result = np.zeros_like(mask) + for col in range(right_edge, W): + distance = col - right_edge + strength = np.exp(-(distance ** 2) / (2 * blur_amount ** 2)) + result[:, col] = strength * np.any(mask_binary[:, :right_edge+1], axis=1) + + return result + + def _create_corner_blend(self, mask, blur_up, blur_down, blur_left, blur_right): + """Create smooth corner blending for diagonal blur combinations using individual blur values.""" + H, W = mask.shape + result = np.zeros_like(mask) + + # Find mask boundaries + mask_binary = mask > 0.5 + if not np.any(mask_binary): + return result + + rows_with_mask = np.any(mask_binary, axis=1) + cols_with_mask = np.any(mask_binary, axis=0) + + if not np.any(rows_with_mask) or not np.any(cols_with_mask): + return result + + top_edge = np.where(rows_with_mask)[0][0] + bottom_edge = np.where(rows_with_mask)[0][-1] + left_edge = np.where(cols_with_mask)[0][0] + right_edge = np.where(cols_with_mask)[0][-1] + + # Create coordinate grids + y_coords, x_coords = np.mgrid[0:H, 0:W] + + # Top-left corner + if blur_up > 0 and blur_left > 0: + # Calculate separate distances and strengths for each direction + dist_from_top = np.maximum(0, top_edge - y_coords) + dist_from_left = np.maximum(0, left_edge - x_coords) + + # Calculate strength based on individual blur values + strength_top = np.exp(-(dist_from_top**2) / (2 * blur_up**2)) + strength_left = np.exp(-(dist_from_left**2) / (2 * blur_left**2)) + + # Combine strengths multiplicatively for smooth corner transition + strength = strength_top * strength_left + + # Only apply in the top-left quadrant + corner_mask = (y_coords <= top_edge) & (x_coords <= left_edge) + result = np.maximum(result, strength * corner_mask) + + # Top-right corner + if blur_up > 0 and blur_right > 0: + dist_from_top = np.maximum(0, top_edge - y_coords) + dist_from_right = np.maximum(0, x_coords - right_edge) + + strength_top = np.exp(-(dist_from_top**2) / (2 * blur_up**2)) + strength_right = np.exp(-(dist_from_right**2) / (2 * blur_right**2)) + strength = strength_top * strength_right + + corner_mask = (y_coords <= top_edge) & (x_coords >= right_edge) + result = np.maximum(result, strength * corner_mask) + + # Bottom-left corner + if blur_down > 0 and blur_left > 0: + dist_from_bottom = np.maximum(0, y_coords - bottom_edge) + dist_from_left = np.maximum(0, left_edge - x_coords) + + strength_bottom = np.exp(-(dist_from_bottom**2) / (2 * blur_down**2)) + strength_left = np.exp(-(dist_from_left**2) / (2 * blur_left**2)) + strength = strength_bottom * strength_left + + corner_mask = (y_coords >= bottom_edge) & (x_coords <= left_edge) + result = np.maximum(result, strength * corner_mask) + + # Bottom-right corner + if blur_down > 0 and blur_right > 0: + dist_from_bottom = np.maximum(0, y_coords - bottom_edge) + dist_from_right = np.maximum(0, x_coords - right_edge) + + strength_bottom = np.exp(-(dist_from_bottom**2) / (2 * blur_down**2)) + strength_right = np.exp(-(dist_from_right**2) / (2 * blur_right**2)) + strength = strength_bottom * strength_right + + corner_mask = (y_coords >= bottom_edge) & (x_coords >= right_edge) + result = np.maximum(result, strength * corner_mask) + + return result + + def _apply_directional_blur(self, mask, blur_up, blur_down, blur_left, blur_right, tapered_corners): + """Apply independent directional blur with optional smooth corner blending.""" + result = mask.copy() + + # Create blur masks for each direction + blur_masks = [] + + if blur_up > 0: + blur_masks.append(self._create_directional_blur_mask(mask, 'up', blur_up)) + + if blur_down > 0: + blur_masks.append(self._create_directional_blur_mask(mask, 'down', blur_down)) + + if blur_left > 0: + blur_masks.append(self._create_directional_blur_mask(mask, 'left', blur_left)) + + if blur_right > 0: + blur_masks.append(self._create_directional_blur_mask(mask, 'right', blur_right)) + + # Combine all blur masks with the original + for blur_mask in blur_masks: + result = np.maximum(result, blur_mask) + + # Add smooth corner blending only if tapered_corners is enabled + if tapered_corners: + corner_blend = self._create_corner_blend(mask, blur_up, blur_down, blur_left, blur_right) + result = np.maximum(result, corner_blend) + + return result + + def process_single(self, mask_np, up, down, right, left, blur_up, blur_down, blur_left, blur_right, tapered_corners): + """Process a single mask with bounding box expansion and directional blur.""" + # Get bounding box of active pixels + bbox = self._get_bounding_box(mask_np) + if bbox is None: + return np.zeros_like(mask_np, dtype=np.float32) + + # Expand bounding box + expanded_bbox = self._expand_bounding_box(bbox, up, down, left, right, mask_np.shape) + if expanded_bbox is None: + return np.zeros_like(mask_np, dtype=np.float32) + + # Create base rectangular mask + min_row_adj, max_row_adj, min_col_adj, max_col_adj = expanded_bbox + new_mask = np.zeros_like(mask_np, dtype=np.float32) + new_mask[min_row_adj:max_row_adj + 1, min_col_adj:max_col_adj + 1] = 1.0 + + # Apply directional blur + new_mask = self._apply_directional_blur(new_mask, blur_up, blur_down, blur_left, blur_right, tapered_corners) + + return new_mask + + def process(self, mask, up, down, right, left, blur_up, blur_down, blur_left, blur_right, tapered_corners): + """Main processing function supporting both 2D and 3D masks.""" + mask_np = mask.cpu().numpy() + + if mask_np.ndim == 2: + result = self.process_single(mask_np, up, down, right, left, blur_up, blur_down, blur_left, blur_right, tapered_corners) + result = result[None, ...] + elif mask_np.ndim == 3: + results = [] + for i in range(mask_np.shape[0]): + single_result = self.process_single( + mask_np[i], up, down, right, left, blur_up, blur_down, blur_left, blur_right, tapered_corners + ) + results.append(single_result) + result = np.stack(results, axis=0) + else: + raise ValueError("Mask must be 2D (H, W) or 3D (N, H, W)") + return (torch.from_numpy(result),) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a36de65..2089363 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "bjornulf_custom_nodes" description = "169 ComfyUI nodes : Display, manipulate, and edit text, images, videos, loras, generate characters and more. Manage looping operations, generate randomized content, use logical conditions and work with external AI tools, like Ollama or Text To Speech, etc..." -version = "1.1.5" +version = "1.1.6" license = {file = "LICENSE"} [project.urls]