From 8fe9f6ed87ecbcb2386832900e7a9d2375c76624 Mon Sep 17 00:00:00 2001 From: Level Pixel Dev Date: Thu, 5 Jun 2025 08:37:57 +0600 Subject: [PATCH] Inpaint Crop Improvement --- __init__.py | 2 +- nodes/image/image_utils_LP.py | 203 ++++++- .../inpaint_crop_stitch_LP.py | 512 ++++++++---------- web/js/showcontrol.js | 90 +++ 4 files changed, 523 insertions(+), 284 deletions(-) rename nodes/{inpaint => image}/inpaint_crop_stitch_LP.py (76%) create mode 100644 web/js/showcontrol.js diff --git a/__init__.py b/__init__.py index c85a080..565a952 100644 --- a/__init__.py +++ b/__init__.py @@ -30,7 +30,7 @@ init() node_list = [ "convert.convert_LP", "image.image_utils_LP", - "inpaint.inpaint_crop_stitch_LP", + "image.inpaint_crop_stitch_LP", "io.folder_workers_LP", "io.image_loaders_LP", "io.image_outputs_LP", diff --git a/nodes/image/image_utils_LP.py b/nodes/image/image_utils_LP.py index f26c783..4752545 100644 --- a/nodes/image/image_utils_LP.py +++ b/nodes/image/image_utils_LP.py @@ -1,11 +1,12 @@ +import comfy.utils import numpy as np import io import torch -from PIL import Image, ImageOps +import nodes import matplotlib.pyplot as plt -import comfy.utils +import torchvision.transforms.functional as F +from PIL import Image, ImageOps - color_mapping = { "white": (255, 255, 255), "black": (0, 0, 0), @@ -54,6 +55,158 @@ def hex_to_rgb(hex_color): b = int(hex_color[4:6], 16) return (r, g, b) +def tensor2pil(image): + return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + +def rescale_i(samples, width, height, algorithm: str): + samples = samples.movedim(-1, 1) + algorithm = getattr(Image, algorithm.upper()) # i.e. Image.BICUBIC + samples_pil: Image.Image = F.to_pil_image(samples[0].cpu()).resize((width, height), algorithm) + samples = F.to_tensor(samples_pil).unsqueeze(0) + samples = samples.movedim(1, -1) + return samples + + +def rescale_m(samples, width, height, algorithm: str): + samples = samples.unsqueeze(1) + algorithm = getattr(Image, algorithm.upper()) # i.e. Image.BICUBIC + samples_pil: Image.Image = F.to_pil_image(samples[0].cpu()).resize((width, height), algorithm) + samples = F.to_tensor(samples_pil).unsqueeze(0) + samples = samples.squeeze(1) + return samples + + +def preresize_imm(image, mask, optional_context_mask, downscale_algorithm, upscale_algorithm, preresize_mode, preresize_min_width, preresize_min_height, preresize_max_width, preresize_max_height): + current_width, current_height = image.shape[2], image.shape[1] # Image size [batch, height, width, channels] + + if preresize_mode == "ensure minimum resolution": + if current_width >= preresize_min_width and current_height >= preresize_min_height: + return image, mask, optional_context_mask + + scale_factor_min_width = preresize_min_width / current_width + scale_factor_min_height = preresize_min_height / current_height + + scale_factor = max(scale_factor_min_width, scale_factor_min_height) + + target_width = int(current_width * scale_factor) + target_height = int(current_height * scale_factor) + + image = rescale_i(image, target_width, target_height, upscale_algorithm) + mask = rescale_m(mask, target_width, target_height, 'bilinear') + optional_context_mask = rescale_m(optional_context_mask, target_width, target_height, 'bilinear') + + assert target_width >= preresize_min_width and target_height >= preresize_min_height, \ + f"Internal error: After resizing, target size {target_width}x{target_height} is smaller than min size {preresize_min_width}x{preresize_min_height}" + + elif preresize_mode == "ensure minimum and maximum resolution": + if preresize_min_width <= current_width <= preresize_max_width and preresize_min_height <= current_height <= preresize_max_height: + return image, mask, optional_context_mask + + scale_factor_min_width = preresize_min_width / current_width + scale_factor_min_height = preresize_min_height / current_height + scale_factor_min = max(scale_factor_min_width, scale_factor_min_height) + + scale_factor_max_width = preresize_max_width / current_width + scale_factor_max_height = preresize_max_height / current_height + scale_factor_max = min(scale_factor_max_width, scale_factor_max_height) + + if scale_factor_min > 1 and scale_factor_max < 1: + assert False, "Cannot meet both minimum and maximum resolution requirements with aspect ratio preservation." + + if scale_factor_min > 1: # We're upscaling to meet min resolution + scale_factor = scale_factor_min + rescale_algorithm = upscale_algorithm # Use upscale algorithm for min resolution + else: # We're downscaling to meet max resolution + scale_factor = scale_factor_max + rescale_algorithm = downscale_algorithm # Use downscale algorithm for max resolution + + target_width = int(current_width * scale_factor) + target_height = int(current_height * scale_factor) + + image = rescale_i(image, target_width, target_height, rescale_algorithm) + mask = rescale_m(mask, target_width, target_height, 'nearest') # Always nearest for efficiency + optional_context_mask = rescale_m(optional_context_mask, target_width, target_height, 'nearest') # Always nearest for efficiency + + assert preresize_min_width <= target_width <= preresize_max_width, \ + f"Internal error: Target width {target_width} is outside the range {preresize_min_width} - {preresize_max_width}" + assert preresize_min_height <= target_height <= preresize_max_height, \ + f"Internal error: Target height {target_height} is outside the range {preresize_min_height} - {preresize_max_height}" + + elif preresize_mode == "ensure maximum resolution": + if current_width <= preresize_max_width and current_height <= preresize_max_height: + return image, mask, optional_context_mask + + scale_factor_max_width = preresize_max_width / current_width + scale_factor_max_height = preresize_max_height / current_height + scale_factor_max = min(scale_factor_max_width, scale_factor_max_height) + + target_width = int(current_width * scale_factor_max) + target_height = int(current_height * scale_factor_max) + + image = rescale_i(image, target_width, target_height, downscale_algorithm) + mask = rescale_m(mask, target_width, target_height, 'nearest') # Always nearest for efficiency + optional_context_mask = rescale_m(optional_context_mask, target_width, target_height, 'nearest') # Always nearest for efficiency + + assert target_width <= preresize_max_width and target_height <= preresize_max_height, \ + f"Internal error: Target size {target_width}x{target_height} is greater than max size {preresize_max_width}x{preresize_max_height}" + + return image, mask, optional_context_mask + +def compute_target_size(width, height, target_resolution, aspect_ratio_limit=2): + ratio = max(1 / aspect_ratio_limit, min(width / height, aspect_ratio_limit)) + height_new = target_resolution * 2 / (ratio + 1) + width_new = ratio * height_new + target_size = { + "target_height": int(round(height_new)), + "target_width": int(round(width_new)), + } + + return target_size + +def calculate_target_size(mask, target_resolution, aspect_ratio_limit=2): + B, H, W = mask.shape + mask = mask.round() + + for b in range(B): + rows = torch.any(mask[min(b, mask.shape[0]-1)] > 0, dim=1) + cols = torch.any(mask[min(b, mask.shape[0]-1)] > 0, dim=0) + + row_indices = torch.where(rows)[0] + col_indices = torch.where(cols)[0] + + if row_indices.numel() == 0 or col_indices.numel() == 0: + width, height = W, H + else: + y_min, y_max = row_indices[[0, -1]] + x_min, x_max = col_indices[[0, -1]] + width = (x_max - x_min + 1).item() + height = (y_max - y_min + 1).item() + + return compute_target_size(width, height, target_resolution, aspect_ratio_limit) + +class CalculateTargetSizeByMask: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE", ), + "mask": ("MASK", ), + "target_size": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "aspect_ratio_limit": ("FLOAT", {"default": 2, "min": 0, "max": 100, "step": 0.01}), + }, + } + + RETURN_TYPES = ("INT", "INT",) + RETURN_NAMES = ("height", "width",) + FUNCTION = "calculate_target_size" + CATEGORY = "LevelPixel/Image" + def calculate_target_size(self, image, mask, target_size=1024, aspect_ratio_limit=2): + target_size = calculate_target_size(image, mask, target_size, aspect_ratio_limit) + target_height, target_width = (target_size["target_height"], target_size["target_width"]) + + return (target_height, target_width, ) + class FastCheckerPattern: @classmethod @@ -117,10 +270,6 @@ class FastCheckerPattern: image_out = pil2tensor(img.convert("RGB")) return (image_out,) - - -def tensor2pil(image): - return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) class ImageOverlay: @@ -331,14 +480,50 @@ class ResizeImageToTargetSize: return (pil2tensor(img),) +class ResizeImageAndMasks: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "downscale_algorithm": (["nearest", "bilinear", "bicubic", "lanczos", "box", "hamming"], {"default": "lanczos"}), + "upscale_algorithm": (["nearest", "bilinear", "bicubic", "lanczos", "box", "hamming"], {"default": "lanczos"}), + "preresize_mode": (["ensure minimum resolution", "ensure maximum resolution", "ensure minimum and maximum resolution"], {"default": "ensure minimum resolution"}), + "preresize_min_width": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "preresize_min_height": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "preresize_max_width": ("INT", {"default": nodes.MAX_RESOLUTION, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "preresize_max_height": ("INT", {"default": nodes.MAX_RESOLUTION, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + }, + "optional": { + "mask": ("MASK",), + "optional_context_mask": ("MASK",), + } + } + + FUNCTION = "resize_image_and_masks" + CATEGORY = "LevelPixel/Image" + DESCRIPTION = "Crops an image around a mask for inpainting, the optional context mask defines an extra area to keep for the context." + + RETURN_TYPES = ("IMAGE", "MASK", "MASK") + RETURN_NAMES = ("image", "mask", "optional_context_mask") + + def resize_image_and_masks(self, image, downscale_algorithm, upscale_algorithm, preresize_mode, preresize_min_width, preresize_min_height, preresize_max_width, preresize_max_height, mask=None, optional_context_mask=None): + image, mask, optional_context_mask = preresize_imm(image, mask, optional_context_mask, downscale_algorithm, upscale_algorithm, preresize_mode, preresize_min_width, preresize_min_height, preresize_max_width, preresize_max_height) + + return (image, mask, optional_context_mask) + NODE_CLASS_MAPPINGS = { "ImageOverlay|LP": ImageOverlay, "FastCheckerPattern|LP": FastCheckerPattern, - "ResizeImageToTargetSize|LP": ResizeImageToTargetSize + "ResizeImageToTargetSize|LP": ResizeImageToTargetSize, + "CalculateTargetSizeByMask|LP": CalculateTargetSizeByMask, + "ResizeImageAndMasks|LP": ResizeImageAndMasks } NODE_DISPLAY_NAME_MAPPINGS = { "ImageOverlay|LP": "Image Overlay [LP]", "FastCheckerPattern|LP": "Fast Checker Pattern [LP]", - "ResizeImageToTargetSize|LP": "Resize Image To Target Size [LP]" + "ResizeImageToTargetSize|LP": "Resize Image To Target Size [LP]", + "CalculateTargetSizeByMask|LP": "Calculate Target Size By Mask [LP]", + "ResizeImageAndMasks|LP": "Resize Image and Masks [LP]" } diff --git a/nodes/inpaint/inpaint_crop_stitch_LP.py b/nodes/image/inpaint_crop_stitch_LP.py similarity index 76% rename from nodes/inpaint/inpaint_crop_stitch_LP.py rename to nodes/image/inpaint_crop_stitch_LP.py index 42584fd..3ea2b35 100644 --- a/nodes/inpaint/inpaint_crop_stitch_LP.py +++ b/nodes/image/inpaint_crop_stitch_LP.py @@ -3,10 +3,9 @@ import math import nodes import numpy as np import torch +from scipy.ndimage import gaussian_filter, grey_dilation, binary_closing, binary_fill_holes import torchvision.transforms.functional as F from PIL import Image -from scipy.ndimage import gaussian_filter, grey_dilation, binary_closing, binary_fill_holes - def rescale_i(samples, width, height, algorithm: str): samples = samples.movedim(-1, 1) @@ -25,83 +24,37 @@ def rescale_m(samples, width, height, algorithm: str): samples = samples.squeeze(1) return samples +def compute_target_size(width, height, target_resolution, aspect_ratio_limit=2): + ratio = max(1 / aspect_ratio_limit, min(width / height, aspect_ratio_limit)) + height_new = target_resolution * 2 / (ratio + 1) + width_new = ratio * height_new + target_size = { + "target_height": int(round(height_new)), + "target_width": int(round(width_new)), + } -def preresize_imm(image, mask, optional_context_mask, downscale_algorithm, upscale_algorithm, preresize_mode, preresize_min_width, preresize_min_height, preresize_max_width, preresize_max_height): - current_width, current_height = image.shape[2], image.shape[1] # Image size [batch, height, width, channels] - - if preresize_mode == "ensure minimum resolution": - if current_width >= preresize_min_width and current_height >= preresize_min_height: - return image, mask, optional_context_mask + return target_size - scale_factor_min_width = preresize_min_width / current_width - scale_factor_min_height = preresize_min_height / current_height +def calculate_target_size(mask, target_resolution, aspect_ratio_limit=2): + B, H, W = mask.shape + mask = mask.round() - scale_factor = max(scale_factor_min_width, scale_factor_min_height) + for b in range(B): + rows = torch.any(mask[min(b, mask.shape[0]-1)] > 0, dim=1) + cols = torch.any(mask[min(b, mask.shape[0]-1)] > 0, dim=0) - target_width = int(current_width * scale_factor) - target_height = int(current_height * scale_factor) + row_indices = torch.where(rows)[0] + col_indices = torch.where(cols)[0] - image = rescale_i(image, target_width, target_height, upscale_algorithm) - mask = rescale_m(mask, target_width, target_height, 'bilinear') - optional_context_mask = rescale_m(optional_context_mask, target_width, target_height, 'bilinear') - - assert target_width >= preresize_min_width and target_height >= preresize_min_height, \ - f"Internal error: After resizing, target size {target_width}x{target_height} is smaller than min size {preresize_min_width}x{preresize_min_height}" - - elif preresize_mode == "ensure minimum and maximum resolution": - if preresize_min_width <= current_width <= preresize_max_width and preresize_min_height <= current_height <= preresize_max_height: - return image, mask, optional_context_mask - - scale_factor_min_width = preresize_min_width / current_width - scale_factor_min_height = preresize_min_height / current_height - scale_factor_min = max(scale_factor_min_width, scale_factor_min_height) - - scale_factor_max_width = preresize_max_width / current_width - scale_factor_max_height = preresize_max_height / current_height - scale_factor_max = min(scale_factor_max_width, scale_factor_max_height) - - if scale_factor_min > 1 and scale_factor_max < 1: - assert False, "Cannot meet both minimum and maximum resolution requirements with aspect ratio preservation." - - if scale_factor_min > 1: # We're upscaling to meet min resolution - scale_factor = scale_factor_min - rescale_algorithm = upscale_algorithm # Use upscale algorithm for min resolution - else: # We're downscaling to meet max resolution - scale_factor = scale_factor_max - rescale_algorithm = downscale_algorithm # Use downscale algorithm for max resolution - - target_width = int(current_width * scale_factor) - target_height = int(current_height * scale_factor) - - image = rescale_i(image, target_width, target_height, rescale_algorithm) - mask = rescale_m(mask, target_width, target_height, 'nearest') # Always nearest for efficiency - optional_context_mask = rescale_m(optional_context_mask, target_width, target_height, 'nearest') # Always nearest for efficiency - - assert preresize_min_width <= target_width <= preresize_max_width, \ - f"Internal error: Target width {target_width} is outside the range {preresize_min_width} - {preresize_max_width}" - assert preresize_min_height <= target_height <= preresize_max_height, \ - f"Internal error: Target height {target_height} is outside the range {preresize_min_height} - {preresize_max_height}" - - elif preresize_mode == "ensure maximum resolution": - if current_width <= preresize_max_width and current_height <= preresize_max_height: - return image, mask, optional_context_mask - - scale_factor_max_width = preresize_max_width / current_width - scale_factor_max_height = preresize_max_height / current_height - scale_factor_max = min(scale_factor_max_width, scale_factor_max_height) - - target_width = int(current_width * scale_factor_max) - target_height = int(current_height * scale_factor_max) - - image = rescale_i(image, target_width, target_height, downscale_algorithm) - mask = rescale_m(mask, target_width, target_height, 'nearest') # Always nearest for efficiency - optional_context_mask = rescale_m(optional_context_mask, target_width, target_height, 'nearest') # Always nearest for efficiency - - assert target_width <= preresize_max_width and target_height <= preresize_max_height, \ - f"Internal error: Target size {target_width}x{target_height} is greater than max size {preresize_max_width}x{preresize_max_height}" - - return image, mask, optional_context_mask + if row_indices.numel() == 0 or col_indices.numel() == 0: + width, height = W, H + else: + y_min, y_max = row_indices[[0, -1]] + x_min, x_max = col_indices[[0, -1]] + width = (x_max - x_min + 1).item() + height = (y_max - y_min + 1).item() + return compute_target_size(width, height, target_resolution, aspect_ratio_limit) def fillholes_iterative_hipass_fill_m(samples): thresholds = [1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1] @@ -484,10 +437,8 @@ class InpaintCrop: ComfyUI-Inpaint-CropAndStitch https://github.com/lquesada/ComfyUI-Inpaint-CropAndStitch - - This node stitches the inpainted image without altering unmasked areas. """ - + @classmethod def INPUT_TYPES(cls): return { @@ -509,6 +460,13 @@ class InpaintCrop: # Context "context_from_mask_extend_factor": ("FLOAT", {"default": 1.2, "min": 1.0, "max": 100.0, "step": 0.01, "tooltip": "Grow the context area from the mask by a certain factor in every direction. For example, 1.5 grabs extra 50% up, down, left, and right as context."}), "output_padding": (["0", "8", "16", "32", "64", "128", "256", "512"], {"default": "32"}), + + # Output cropped image size + "mode": (["none", "input size parameters", "forced size", "aspect size"], {"default": "aspect size"}), + "target_size": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "aspect_ratio_limit": ("FLOAT", {"default": 2, "min": 1, "max": 100, "step": 0.01}), + "force_width": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "force_height": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}) }, "optional": { # Optional inputs @@ -516,7 +474,6 @@ class InpaintCrop: "optional_context_mask": ("MASK",), # Custom Parameters - "preresize_parameters": ("PRERESIZEDATA", ), "extend_factor_parameters": ("EXTENDFACTORDATA", ), "cropped_size_parameters": ("CROPPEDSIZEDATA", ), } @@ -528,27 +485,45 @@ class InpaintCrop: RETURN_TYPES = ("CROPDATA", "IMAGE", "MASK") RETURN_NAMES = ("crop_data", "cropped_image", "cropped_mask") - - def inpaint_crop(self, image, downscale_algorithm, upscale_algorithm, mask_hipass_filter, mask_fill_holes, mask_expand_pixels, mask_invert, mask_blend_pixels, context_from_mask_extend_factor, output_padding, mask=None, optional_context_mask=None, preresize_parameters=None, extend_factor_parameters=None, cropped_size_parameters=None): - - if preresize_parameters is not None: - preresize_mode, preresize_min_width, preresize_min_height, preresize_max_width, preresize_max_height = ( - preresize_parameters["preresize_mode"], - preresize_parameters["preresize_min_width"], - preresize_parameters["preresize_min_height"], - preresize_parameters["preresize_max_width"], - preresize_parameters["preresize_max_height"] - ) - preresize = True + def inpaint_crop(self, image, downscale_algorithm, upscale_algorithm, mask_hipass_filter, mask_fill_holes, mask_expand_pixels, mask_invert, mask_blend_pixels, context_from_mask_extend_factor, output_padding, mode, target_size, aspect_ratio_limit, force_width, force_height, mask=None, optional_context_mask=None, extend_factor_parameters=None, cropped_size_parameters=None): + if mode == "input size parameters": + if cropped_size_parameters is not None: + target_width, target_height, batch_mode = ( + cropped_size_parameters["target_width"], + cropped_size_parameters["target_height"], + cropped_size_parameters["batch_mode"] + ) + output_resize_to_target_size = True + else: + output_resize_to_target_size = False + target_width = None + target_height = None + batch_mode = False + elif mode == "forced size": + output_resize_to_target_size = True + target_width = force_width + target_height = force_height + batch_mode = True + elif mode == "aspect size": + output_resize_to_target_size = True + new_mask = mask + if optional_context_mask is not None: + assert optional_context_mask.shape[1:] == new_mask.shape[1:3], f"optional_context_mask dimensions do not match mask dimensions. Expected {new_mask.shape[1:3]}, got {optional_context_mask.shape[1:]}" + context_mask = optional_context_mask.squeeze(-1) + new_mask = ((new_mask > 0) | (context_mask > 0)).float() + aspect_target_size = calculate_target_size(new_mask, target_size, aspect_ratio_limit) + target_width, target_height = ( + aspect_target_size["target_width"], + aspect_target_size["target_height"] + ) + batch_mode = False else: - preresize = False - preresize_mode = None - preresize_min_width = None - preresize_min_height = None - preresize_max_width = None - preresize_max_height = None - + output_resize_to_target_size = False + target_width = None + target_height = None + batch_mode = False + if extend_factor_parameters is not None: extend_up_factor, extend_down_factor, extend_left_factor, extend_right_factor = ( extend_factor_parameters["extend_up_factor"], @@ -564,19 +539,6 @@ class InpaintCrop: extend_left_factor = 1.0 extend_right_factor = 1.0 - if cropped_size_parameters is not None: - target_width, target_height, aspect_ratio_limit = ( - cropped_size_parameters["target_width"], - cropped_size_parameters["target_height"], - cropped_size_parameters["aspect_ratio_limit"] - ) - output_resize_to_target_size = True - else: - output_resize_to_target_size = False - target_width = None - target_height = None - aspect_ratio_limit = None - image = image.clone() if mask is not None: mask = mask.clone() @@ -584,15 +546,12 @@ class InpaintCrop: optional_context_mask = optional_context_mask.clone() output_padding = int(output_padding) - - # Check that some parameters make sense - if preresize and preresize_mode == "ensure minimum and maximum resolution": - assert preresize_max_width >= preresize_min_width, "Preresize maximum width must be greater than or equal to minimum width" - assert preresize_max_height >= preresize_min_height, "Preresize maximum height must be greater than or equal to minimum height" + + print("target_width: ", target_width) + print("target_height: ", target_height) if image.shape[0] > 1: - assert output_resize_to_target_size, "cropped_image_target_size_parameters must be enabled when input is a batch of images, given all images in the batch output have to be the same size" - assert aspect_ratio_limit == 1, "aspect_ratio_limit must be equal to 1 when processing a batch of images" + assert batch_mode, "for batch of images use Inpaint Crop only with Cropped Forsed Size Parameters node or with Forsed mode, given all images in the batch output have to be the same size" # When a LoadImage node passes a mask without user editing, it may be the wrong shape. # Detect and fix that to avoid shape mismatch errors. @@ -665,10 +624,9 @@ class InpaintCrop: one_optional_context_mask = optional_context_mask[b].unsqueeze(0) outputs = self.inpaint_crop_single_image( - one_image, downscale_algorithm, upscale_algorithm, preresize, preresize_mode, - preresize_min_width, preresize_min_height, preresize_max_width, preresize_max_height, - extend_for_outpainting, extend_up_factor, extend_down_factor, extend_left_factor, extend_right_factor, - mask_hipass_filter, mask_fill_holes, mask_expand_pixels, mask_invert, mask_blend_pixels, + one_image, downscale_algorithm, upscale_algorithm, extend_for_outpainting, extend_up_factor, + extend_down_factor, extend_left_factor, extend_right_factor,mask_hipass_filter, + mask_fill_holes, mask_expand_pixels, mask_invert, mask_blend_pixels, context_from_mask_extend_factor, output_resize_to_target_size, target_width, target_height, output_padding, one_mask, one_optional_context_mask) @@ -687,10 +645,7 @@ class InpaintCrop: return result_stitcher, result_image, result_mask, - def inpaint_crop_single_image(self, image, downscale_algorithm, upscale_algorithm, preresize, preresize_mode, preresize_min_width, preresize_min_height, preresize_max_width, preresize_max_height, extend_for_outpainting, extend_up_factor, extend_down_factor, extend_left_factor, extend_right_factor, mask_hipass_filter, mask_fill_holes, mask_expand_pixels, mask_invert, mask_blend_pixels, context_from_mask_extend_factor, output_resize_to_target_size, output_target_width, output_target_height, output_padding, mask, optional_context_mask): - if preresize: - image, mask, optional_context_mask = preresize_imm(image, mask, optional_context_mask, downscale_algorithm, upscale_algorithm, preresize_mode, preresize_min_width, preresize_min_height, preresize_max_width, preresize_max_height) - + def inpaint_crop_single_image(self, image, downscale_algorithm, upscale_algorithm, extend_for_outpainting, extend_up_factor, extend_down_factor, extend_left_factor, extend_right_factor, mask_hipass_filter, mask_fill_holes, mask_expand_pixels, mask_invert, mask_blend_pixels, context_from_mask_extend_factor, output_resize_to_target_size, output_target_width, output_target_height, output_padding, mask, optional_context_mask): if mask_fill_holes: mask = fillholes_iterative_hipass_fill_m(mask) @@ -756,7 +711,6 @@ class InpaintCrop: return stitcher, cropped_image, cropped_mask - class InpaintStitch: """ ComfyUI-LevelPixel @@ -784,7 +738,6 @@ class InpaintStitch: FUNCTION = "inpaint_stitch" - def inpaint_stitch(self, crop_data, inpainted_image): inpainted_image = inpainted_image.clone() results = [] @@ -800,7 +753,7 @@ class InpaintStitch: for key in ['downscale_algorithm', 'upscale_algorithm', 'blend_pixels']: one_stitcher[key] = crop_data[key] for key in ['canvas_to_orig_x', 'canvas_to_orig_y', 'canvas_to_orig_w', 'canvas_to_orig_h', 'canvas_image', 'cropped_to_canvas_x', 'cropped_to_canvas_y', 'cropped_to_canvas_w', 'cropped_to_canvas_h', 'cropped_mask_for_blend']: - if override: # One stitcher for many images, always read 0. + if override: one_stitcher[key] = crop_data[key][0] else: one_stitcher[key] = crop_data[key][b] @@ -829,151 +782,13 @@ class InpaintStitch: cto_w = stitcher['canvas_to_orig_w'] cto_h = stitcher['canvas_to_orig_h'] - mask = stitcher['cropped_mask_for_blend'] # shape: [1, H, W] + mask = stitcher['cropped_mask_for_blend'] output_image = stitch_magic_im(canvas_image, inpainted_image, mask, ctc_x, ctc_y, ctc_w, ctc_h, cto_x, cto_y, cto_w, cto_h, downscale_algorithm, upscale_algorithm) return (output_image,) -def compute_target_size(width, height, target_resolution, aspect_ratio_limit=2): - ratio = max(1 / aspect_ratio_limit, min(width / height, aspect_ratio_limit)) - height_new = target_resolution * 2 / (ratio + 1) - width_new = ratio * height_new - target_size = { - "target_height": int(round(height_new)), - "target_width": int(round(width_new)), - } - - return target_size - -def calculate_target_size(mask, target_resolution, aspect_ratio_limit=2): - B, H, W = mask.shape - mask = mask.round() - - for b in range(B): - rows = torch.any(mask[min(b, mask.shape[0]-1)] > 0, dim=1) - cols = torch.any(mask[min(b, mask.shape[0]-1)] > 0, dim=0) - - row_indices = torch.where(rows)[0] - col_indices = torch.where(cols)[0] - - if row_indices.numel() == 0 or col_indices.numel() == 0: - width, height = W, H - else: - y_min, y_max = row_indices[[0, -1]] - x_min, x_max = col_indices[[0, -1]] - width = (x_max - x_min + 1).item() - height = (y_max - y_min + 1).item() - - return compute_target_size(width, height, target_resolution, aspect_ratio_limit) - -class CalculateTargetSizeByMask: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "image": ("IMAGE", ), - "mask": ("MASK", ), - "target_size": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), - "aspect_ratio_limit": ("FLOAT", {"default": 2, "min": 0, "max": 100, "step": 0.01}), - }, - } - - RETURN_TYPES = ("INT", "INT",) - RETURN_NAMES = ("height", "width",) - FUNCTION = "calculate_target_size" - CATEGORY = "LevelPixel/Inpaint" - def calculate_target_size(self, image, mask, target_size=1024, aspect_ratio_limit=2): - target_size = calculate_target_size(image, mask, target_size, aspect_ratio_limit) - target_height, target_width = (target_size["target_height"], target_size["target_width"]) - - return (target_height, target_width, ) - -class CroppedSizeAspectParameters: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "mask": ("MASK", ), - "target_size": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), - "aspect_ratio_limit": ("FLOAT", {"default": 2, "min": 1, "max": 100, "step": 0.01}), - }, - "optional": { - "optional_context_mask": ("MASK", ), - } - } - - RETURN_TYPES = ("CROPPEDSIZEDATA",) - RETURN_NAMES = ("target_size",) - FUNCTION = "cropped_size_parameters" - CATEGORY = "LevelPixel/Inpaint" - def cropped_size_parameters(self, mask, target_size=1024, aspect_ratio_limit=2, optional_context_mask=None): - if optional_context_mask is not None: - assert optional_context_mask.shape[1:] == mask.shape[1:3], f"optional_context_mask dimensions do not match mask dimensions. Expected {mask.shape[1:3]}, got {optional_context_mask.shape[1:]}" - context_mask = optional_context_mask.squeeze(-1) - mask = ((mask > 0) | (context_mask > 0)).float() - target_size = calculate_target_size(mask, target_size, aspect_ratio_limit) - target_size["aspect_ratio_limit"] = aspect_ratio_limit - - return (target_size, ) - -class CroppedSizeFixedParameters: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "cropped_height": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), - "cropped_width": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), - }, - } - - RETURN_TYPES = ("CROPPEDSIZEDATA",) - RETURN_NAMES = ("target_size",) - FUNCTION = "cropped_size_fixed_parameters" - CATEGORY = "LevelPixel/Inpaint" - def cropped_size_fixed_parameters(self, cropped_height, cropped_width): - target_height = cropped_height - target_width = cropped_width - aspect_ratio_limit = 1.0 - target_size = { - "target_height": target_height, - "target_width": target_width, - "aspect_ratio_limit": aspect_ratio_limit - } - return (target_size, ) - -class PreresizeParameters: - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "preresize_mode": (["ensure minimum resolution", "ensure maximum resolution", "ensure minimum and maximum resolution"], {"default": "ensure minimum resolution"}), - "preresize_min_width": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), - "preresize_min_height": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), - "preresize_max_width": ("INT", {"default": nodes.MAX_RESOLUTION, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), - "preresize_max_height": ("INT", {"default": nodes.MAX_RESOLUTION, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), - }, - } - - RETURN_TYPES = ("PRERESIZEDATA",) - RETURN_NAMES = ("preresize_data",) - FUNCTION = "preresize_parameters" - CATEGORY = "LevelPixel/Inpaint" - def preresize_parameters(self, preresize_mode, preresize_min_width, preresize_min_height, preresize_max_width, preresize_max_height): - preresize_data = { - "preresize_mode": preresize_mode, - "preresize_min_width": preresize_min_width, - "preresize_min_height": preresize_min_height, - "preresize_max_width": preresize_max_width, - "preresize_max_height": preresize_max_height - } - return (preresize_data, ) - class ExtendFactorParameters: @classmethod @@ -1001,22 +816,171 @@ class ExtendFactorParameters: return (extend_factor_data, ) +class CroppedAspectSizeParameters: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mask": ("MASK", ), + "target_size": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "aspect_ratio_limit": ("FLOAT", {"default": 2, "min": 1, "max": 100, "step": 0.01}), + }, + "optional": { + "optional_context_mask": ("MASK", ), + } + } + + RETURN_TYPES = ("CROPPEDSIZEDATA",) + RETURN_NAMES = ("target_size",) + FUNCTION = "cropped_aspect_size_parameters" + CATEGORY = "LevelPixel/Inpaint" + def cropped_aspect_size_parameters(self, mask, target_size=1024, aspect_ratio_limit=2, optional_context_mask=None): + if optional_context_mask is not None: + assert optional_context_mask.shape[1:] == mask.shape[1:3], f"optional_context_mask dimensions do not match mask dimensions. Expected {mask.shape[1:3]}, got {optional_context_mask.shape[1:]}" + context_mask = optional_context_mask.squeeze(-1) + mask = ((mask > 0) | (context_mask > 0)).float() + target_size = calculate_target_size(mask, target_size, aspect_ratio_limit) + target_size["batch_mode"] = False + + return (target_size, ) + + +class CroppedForsedSizeParameters: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "cropped_height": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "cropped_width": ("INT", {"default": 1024, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + }, + } + + RETURN_TYPES = ("CROPPEDSIZEDATA",) + RETURN_NAMES = ("target_size",) + FUNCTION = "cropped_forsed_size_parameters" + CATEGORY = "LevelPixel/Inpaint" + def cropped_forsed_size_parameters(self, cropped_height, cropped_width): + target_height = cropped_height + target_width = cropped_width + target_size = { + "target_height": target_height, + "target_width": target_width, + "batch_mode": True + } + return (target_size, ) + + +class CroppedRangedSizeParameters: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mask": ("MASK", ), + "min_width": ("INT", {"default": 832, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "min_height": ("INT", {"default": 832, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "max_width": ("INT", {"default": 1216, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "max_height": ("INT", {"default": 1216, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}) + }, + "optional": { + "optional_context_mask": ("MASK", ), + } + } + + RETURN_TYPES = ("CROPPEDSIZEDATA",) + RETURN_NAMES = ("target_size",) + FUNCTION = "cropped_ranged_size_parameters" + CATEGORY = "LevelPixel/Inpaint" + def cropped_ranged_size_parameters(self, mask, min_width, min_height, max_width, max_height, optional_context_mask=None): + assert max_width >= min_width, "max_width must be greater than or equal to min_width" + assert max_height >= min_height, "max_height must be greater than or equal to min_height" + + if optional_context_mask is not None: + assert optional_context_mask.shape[1:] == mask.shape[1:3], f"optional_context_mask dimensions do not match mask dimensions. Expected {mask.shape[1:3]}, got {optional_context_mask.shape[1:]}" + context_mask = optional_context_mask.squeeze(-1) + mask = ((mask > 0) | (context_mask > 0)).float() + + mask_height = mask.shape[1] + mask_width = mask.shape[2] + + scale_w_min = min_width / mask_width + scale_h_min = min_height / mask_height + scale_w_max = max_width / mask_width + scale_h_max = max_height / mask_height + + scale_w = (scale_w_min + scale_w_max) / 2 + scale_h = (scale_h_min + scale_h_max) / 2 + + scale_w = min(scale_w_max, max(scale_w_min, scale_w)) + scale_h = min(scale_h_max, max(scale_h_min, scale_h)) + + new_width = int(mask_width * scale_w) + new_height = int(mask_height * scale_h) + + target_size = { + "target_height": new_height, + "target_width": new_width, + "batch_mode": False + } + return (target_size, ) + + +class CroppedFreeSizeParameters: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mask": ("MASK", ), + "rescale_factor": ("FLOAT", {"default": 1.00, "min": 0.01, "max": 100.0, "step": 0.01}) + }, + "optional": { + "optional_context_mask": ("MASK", ), + } + } + + RETURN_TYPES = ("CROPPEDSIZEDATA",) + RETURN_NAMES = ("target_size",) + FUNCTION = "cropped_free_size_parameters" + CATEGORY = "LevelPixel/Inpaint" + def cropped_free_size_parameters(self, mask, rescale_factor, optional_context_mask=None): + + print("mask shape: ", mask.shape) + if optional_context_mask is not None: + print("optional_context_mask shape: ", optional_context_mask.shape) + assert optional_context_mask.shape[1:] == mask.shape[1:3], f"optional_context_mask dimensions do not match mask dimensions. Expected {mask.shape[1:3]}, got {optional_context_mask.shape[1:]}" + context_mask = optional_context_mask.squeeze(-1) + mask = ((mask > 0) | (context_mask > 0)).float() + + print("new mask shape: ", mask.shape) + height = int(mask.shape[1] * rescale_factor) + width = int(mask.shape[2] * rescale_factor) + + target_size = { + "target_height": height, + "target_width": width, + "batch_mode": False + } + return (target_size, ) + NODE_CLASS_MAPPINGS = { "InpaintCrop|LP": InpaintCrop, "InpaintStitch|LP": InpaintStitch, - "CroppedSizeAspectParameters|LP": CroppedSizeAspectParameters, - "CroppedSizeFixedParameters|LP": CroppedSizeFixedParameters, "ExtendFactorParameters|LP": ExtendFactorParameters, - "PreresizeParameters|LP": PreresizeParameters, - "CalculateTargetSizeByMask|LP": CalculateTargetSizeByMask + "CroppedAspectSizeParameters|LP": CroppedAspectSizeParameters, + "CroppedForsedSizeParameters|LP": CroppedForsedSizeParameters, + "CroppedRangedSizeParameters|LP": CroppedRangedSizeParameters, + "CroppedFreeSizeParameters|LP": CroppedFreeSizeParameters } NODE_DISPLAY_NAME_MAPPINGS = { "InpaintCrop|LP": "Inpaint Crop [LP]", "InpaintStitch|LP": "Inpaint Stitch [LP]", - "CroppedSizeAspectParameters|LP": "Cropped Size Aspect Parameters [LP]", - "CroppedSizeFixedParameters|LP": "Cropped Size Fixed Parameters [LP]", "ExtendFactorParameters|LP": "Extend Factor Parameters [LP]", - "PreresizeParameters|LP": "Preresize Parameters [LP]", - "CalculateTargetSizeByMask|LP": "Calculate Target Size By Mask [LP]" + "CroppedAspectSizeParameters|LP": "Cropped Aspect Size Parameters [LP]", + "CroppedForsedSizeParameters|LP": "Cropped Forsed Size Parameters [LP]", + "CroppedRangedSizeParameters|LP": "Cropped Ranged Size Parameters [LP]", + "CroppedFreeSizeParameters|LP": "Cropped Free Size Parameters [LP]" } \ No newline at end of file diff --git a/web/js/showcontrol.js b/web/js/showcontrol.js new file mode 100644 index 0000000..5aea54c --- /dev/null +++ b/web/js/showcontrol.js @@ -0,0 +1,90 @@ +import { app } from "../../../scripts/app.js"; + +// Some fragments of this code are from https://github.com/LucianoCirino/efficiency-nodes-comfyui + +function inpaintCropStitchHandler(node) { + if (node.comfyClass == "InpaintCrop|LP") { + toggleWidget(node, findWidgetByName(node, "target_size")); + toggleWidget(node, findWidgetByName(node, "aspect_ratio_limit")); + toggleWidget(node, findWidgetByName(node, "force_width")); + toggleWidget(node, findWidgetByName(node, "force_height")); + if (findWidgetByName(node, "mode").value == "none") { + toggleWidget(node, findWidgetByName(node, "target_size"), false); + toggleWidget(node, findWidgetByName(node, "aspect_ratio_limit"), false); + toggleWidget(node, findWidgetByName(node, "force_width"), false); + toggleWidget(node, findWidgetByName(node, "force_height"), false); + } + else if (findWidgetByName(node, "mode").value == "input size parameters") { + toggleWidget(node, findWidgetByName(node, "target_size"), false); + toggleWidget(node, findWidgetByName(node, "aspect_ratio_limit"), false); + toggleWidget(node, findWidgetByName(node, "force_width"), false); + toggleWidget(node, findWidgetByName(node, "force_height"), false); + } + else if (findWidgetByName(node, "mode").value == "aspect size") { + toggleWidget(node, findWidgetByName(node, "target_size"), true); + toggleWidget(node, findWidgetByName(node, "aspect_ratio_limit"), true); + toggleWidget(node, findWidgetByName(node, "force_width"), false); + toggleWidget(node, findWidgetByName(node, "force_height"), false); + } + else if (findWidgetByName(node, "mode").value == "forced size") { + toggleWidget(node, findWidgetByName(node, "target_size"), false); + toggleWidget(node, findWidgetByName(node, "aspect_ratio_limit"), false); + toggleWidget(node, findWidgetByName(node, "force_width"), true); + toggleWidget(node, findWidgetByName(node, "force_height"), true); + } + } + return; +} + +const findWidgetByName = (node, name) => { + return node.widgets ? node.widgets.find((w) => w.name === name) : null; +}; + +// Toggle Widget + change size +function toggleWidget(node, widget, show = false, suffix = "") { + if (!widget) return; + widget.disabled = !show + widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show)); +} + +app.registerExtension({ + name: "levelpixel.showcontrol", + nodeCreated(node) { + if (!node.comfyClass.startsWith("Inpaint")) { + return; + } + + inpaintCropStitchHandler(node); + for (const w of node.widgets || []) { + let widgetValue = w.value; + + // Store the original descriptor if it exists + let originalDescriptor = Object.getOwnPropertyDescriptor(w, 'value') || + Object.getOwnPropertyDescriptor(Object.getPrototypeOf(w), 'value'); + if (!originalDescriptor) { + originalDescriptor = Object.getOwnPropertyDescriptor(w.constructor.prototype, 'value'); + } + + Object.defineProperty(w, 'value', { + get() { + // If there's an original getter, use it. Otherwise, return widgetValue. + let valueToReturn = originalDescriptor && originalDescriptor.get + ? originalDescriptor.get.call(w) + : widgetValue; + + return valueToReturn; + }, + set(newVal) { + // If there's an original setter, use it. Otherwise, set widgetValue. + if (originalDescriptor && originalDescriptor.set) { + originalDescriptor.set.call(w, newVal); + } else { + widgetValue = newVal; + } + + inpaintCropStitchHandler(node); + } + }); + } + } +});