diff --git a/README.md b/README.md index 0f81255..fc23205 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Check ComfyUI here: https://github.com/comfyanonymous/ComfyUI "✂️ Inpaint Stitch" is a node that stitches the inpainted image back into the original image without altering unmasked areas. +"✂️ Extend Image for Outpainting" is a node that extends an image and masks in order to use the power of Inpaint Crop and Stich (rescaling, blur, blend, restitching) for outpainting. + The main advantages of inpainting only in a masked area with these nodes are: - It's much faster than sampling the whole image. - It enables setting the right amount of context from the image for the prompt to be more accurately represented in the generated picture. @@ -60,6 +62,8 @@ If you want to inpaint with SDXL, use forced size = 1024. # Changelog ## 2024-06-07 +- Added the "Extend Image for Outpainting" node that allows leveraging the power of Inpaint Crop and Stitch (rescaling, blur, blend, restitching) for Outpainting. +## 2024-06-07 - Added a blending radius for seamless inpainting. - Added a blur mask setting that grows and blurs the mask, providing better support ## 2024-06-01 diff --git a/__init__.py b/__init__.py index b141280..1fdd580 100755 --- a/__init__.py +++ b/__init__.py @@ -1,16 +1,19 @@ from .inpaint_cropandstitch import InpaintCrop from .inpaint_cropandstitch import InpaintStitch +from .inpaint_cropandstitch import InpaintExtendOutpaint WEB_DIRECTORY = "js" NODE_CLASS_MAPPINGS = { "InpaintCrop": InpaintCrop, - "InpaintStitch": InpaintStitch + "InpaintStitch": InpaintStitch, + "InpaintExtendOutpaint": InpaintExtendOutpaint, } NODE_DISPLAY_NAME_MAPPINGS = { "InpaintCrop": "✂️ Inpaint Crop", - "InpaintStitch": "✂️ Inpaint Stitch" + "InpaintStitch": "✂️ Inpaint Stitch", + "InpaintExtendOutpaint": "✂️ Extend Image for Outpainting", } __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] diff --git a/inpaint_cropandstitch.py b/inpaint_cropandstitch.py index adf739e..8fa12ab 100644 --- a/inpaint_cropandstitch.py +++ b/inpaint_cropandstitch.py @@ -434,6 +434,7 @@ class InpaintCrop: return (stitch, cropped_image, cropped_mask) + class InpaintStitch: """ ComfyUI-InpaintCropAndStitch @@ -550,3 +551,112 @@ class InpaintStitch: cropped_output = output[:, start_y:start_y + initial_height, start_x:start_x + initial_width, :] output = cropped_output return (output,) + + +class InpaintExtendOutpaint: + """ + ComfyUI-InpaintCropAndStitch + https://github.com/lquesada/ComfyUI-InpaintCropAndStitch + + This node extends an image for inpainting with Inpaint Crop and Stitch. + """ + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "mask": ("MASK",), + "mode": (["factors", "pixels"], {"default": "factors"}), + "expand_up_pixels": ("INT", {"default": 0, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "expand_up_factor": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 100.0, "step": 0.01}), + "expand_down_pixels": ("INT", {"default": 0, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "expand_down_factor": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 100.0, "step": 0.01}), + "expand_left_pixels": ("INT", {"default": 0, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "expand_left_factor": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 100.0, "step": 0.01}), + "expand_right_pixels": ("INT", {"default": 0, "min": 0, "max": nodes.MAX_RESOLUTION, "step": 1}), + "expand_right_factor": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 100.0, "step": 0.01}), + }, + "optional": { + "optional_context_mask": ("MASK",), + } + } + + CATEGORY = "inpaint" + + RETURN_TYPES = ("IMAGE", "MASK", "MASK") + RETURN_NAMES = ("image", "mask", "context_mask") + + FUNCTION = "inpaint_extend" + + def inpaint_extend(self, image, mask, mode, expand_up_pixels, expand_up_factor, expand_down_pixels, expand_down_factor, expand_left_pixels, expand_left_factor, expand_right_pixels, expand_right_factor, optional_context_mask=None): + assert image.shape[0] == mask.shape[0], "Batch size of images and masks must be the same" + if optional_context_mask is not None: + assert optional_context_mask.shape[0] == image.shape[0], "Batch size of optional_context_masks must be the same as images or None" + + results_image = [] + results_mask = [] + results_context_mask = [] + + batch_size = image.shape[0] + for b in range(batch_size): + one_image = image[b].unsqueeze(0) # Adding batch dimension + one_mask = mask[b].unsqueeze(0) # Adding batch dimension + one_context_mask = optional_context_mask[b].unsqueeze(0) if optional_context_mask is not None else None + + + if one_mask.shape[1] != one_image.shape[1] or one_mask.shape[2] != one_image.shape[2]: + assert False, "mask size must match image size" + if one_context_mask is not None and (one_context_mask.shape[1] != one_image.shape[1] or one_context_mask.shape[2] != one_image.shape[2]): + assert False, "context_mask size must match image size" + + # Get original dimensions + orig_height, orig_width = one_image.shape[1], one_image.shape[2] + + if mode == "factors": + # Calculate new dimensions based on factors + new_height = int(orig_height * (expand_up_factor + expand_down_factor - 1)) + new_width = int(orig_width * (expand_left_factor + expand_right_factor - 1)) + + up_padding = int(orig_height * (expand_up_factor - 1)) + down_padding = new_height - orig_height - up_padding + left_padding = int(orig_width * (expand_left_factor - 1)) + right_padding = new_width - orig_width - left_padding + elif mode == "pixels": + # Calculate new dimensions based on pixel expansion + new_height = orig_height + expand_up_pixels + expand_down_pixels + new_width = orig_width + expand_left_pixels + expand_right_pixels + + up_padding = expand_up_pixels + down_padding = expand_down_pixels + left_padding = expand_left_pixels + right_padding = expand_right_pixels + else: + raise ValueError("Mode must be either 'factors' or 'pixels'") + + # Expand image + new_image = torch.zeros((one_image.shape[0], new_height, new_width, one_image.shape[3]), dtype=one_image.dtype) + new_image[:, up_padding:up_padding + orig_height, left_padding:left_padding + orig_width, :] = one_image.squeeze(0) + + # Expand mask + new_mask = torch.ones((one_mask.shape[0], new_height, new_width), dtype=one_mask.dtype) + new_mask[:, up_padding:up_padding + orig_height, left_padding:left_padding + orig_width] = one_mask.squeeze(0) + + # Expand context mask if present + if one_context_mask is not None: + new_context_mask = torch.ones((one_context_mask.shape[0], new_height, new_width), dtype=one_context_mask.dtype) + new_context_mask[:, up_padding:up_padding + orig_height, left_padding:left_padding + orig_width] = one_context_mask.squeeze(0) + + # Append results + results_image.append(new_image.squeeze(0)) + results_mask.append(new_mask.squeeze(0)) + if one_context_mask is not None: + results_context_mask.append(new_context_mask.squeeze(0)) + + # Stack the results to form batches + output_image = torch.stack(results_image, dim=0) + output_mask = torch.stack(results_mask, dim=0) + output_context_mask = None + if optional_context_mask is not None: + output_context_mask = torch.stack(results_context_mask, dim=0) + + return (output_image, output_mask, output_context_mask) diff --git a/js/showcontrol.js b/js/showcontrol.js index 5d24714..13db108 100644 --- a/js/showcontrol.js +++ b/js/showcontrol.js @@ -3,31 +3,51 @@ import { app } from "../../scripts/app.js"; // Some fragments of this code are from https://github.com/LucianoCirino/efficiency-nodes-comfyui function inpaintCropHandler(node) { - if (node.comfyClass != "InpaintCrop") { - return; - } - toggleWidget(node, findWidgetByName(node, "force_width")); - toggleWidget(node, findWidgetByName(node, "force_height")); - toggleWidget(node, findWidgetByName(node, "rescale_factor")); - toggleWidget(node, findWidgetByName(node, "min_width")); - toggleWidget(node, findWidgetByName(node, "min_height")); - toggleWidget(node, findWidgetByName(node, "max_width")); - toggleWidget(node, findWidgetByName(node, "max_height")); - toggleWidget(node, findWidgetByName(node, "padding")); - if (findWidgetByName(node, "mode").value == "free size") { - toggleWidget(node, findWidgetByName(node, "rescale_factor"), true); - toggleWidget(node, findWidgetByName(node, "padding"), true); - } - else if (findWidgetByName(node, "mode").value == "ranged size") { - toggleWidget(node, findWidgetByName(node, "min_width"), true); - toggleWidget(node, findWidgetByName(node, "min_height"), true); - toggleWidget(node, findWidgetByName(node, "max_width"), true); - toggleWidget(node, findWidgetByName(node, "max_height"), true); - toggleWidget(node, findWidgetByName(node, "padding"), true); - } - else if (findWidgetByName(node, "mode").value == "forced size") { - toggleWidget(node, findWidgetByName(node, "force_width"), true); - toggleWidget(node, findWidgetByName(node, "force_height"), true); + if (node.comfyClass == "InpaintCrop") { + toggleWidget(node, findWidgetByName(node, "force_width")); + toggleWidget(node, findWidgetByName(node, "force_height")); + toggleWidget(node, findWidgetByName(node, "rescale_factor")); + toggleWidget(node, findWidgetByName(node, "min_width")); + toggleWidget(node, findWidgetByName(node, "min_height")); + toggleWidget(node, findWidgetByName(node, "max_width")); + toggleWidget(node, findWidgetByName(node, "max_height")); + toggleWidget(node, findWidgetByName(node, "padding")); + if (findWidgetByName(node, "mode").value == "free size") { + toggleWidget(node, findWidgetByName(node, "rescale_factor"), true); + toggleWidget(node, findWidgetByName(node, "padding"), true); + } + else if (findWidgetByName(node, "mode").value == "ranged size") { + toggleWidget(node, findWidgetByName(node, "min_width"), true); + toggleWidget(node, findWidgetByName(node, "min_height"), true); + toggleWidget(node, findWidgetByName(node, "max_width"), true); + toggleWidget(node, findWidgetByName(node, "max_height"), true); + toggleWidget(node, findWidgetByName(node, "padding"), true); + } + else if (findWidgetByName(node, "mode").value == "forced size") { + toggleWidget(node, findWidgetByName(node, "force_width"), true); + toggleWidget(node, findWidgetByName(node, "force_height"), true); + } + } else if (node.comfyClass == "InpaintExtendOutpaint") { + toggleWidget(node, findWidgetByName(node, "expand_up_pixels")); + toggleWidget(node, findWidgetByName(node, "expand_up_factor")); + toggleWidget(node, findWidgetByName(node, "expand_down_pixels")); + toggleWidget(node, findWidgetByName(node, "expand_down_factor")); + toggleWidget(node, findWidgetByName(node, "expand_left_pixels")); + toggleWidget(node, findWidgetByName(node, "expand_left_factor")); + toggleWidget(node, findWidgetByName(node, "expand_right_pixels")); + toggleWidget(node, findWidgetByName(node, "expand_right_factor")); + if (findWidgetByName(node, "mode").value == "factors") { + toggleWidget(node, findWidgetByName(node, "expand_up_factor"), true); + toggleWidget(node, findWidgetByName(node, "expand_down_factor"), true); + toggleWidget(node, findWidgetByName(node, "expand_left_factor"), true); + toggleWidget(node, findWidgetByName(node, "expand_right_factor"), true); + } + if (findWidgetByName(node, "mode").value == "pixels") { + toggleWidget(node, findWidgetByName(node, "expand_up_pixels"), true); + toggleWidget(node, findWidgetByName(node, "expand_down_pixels"), true); + toggleWidget(node, findWidgetByName(node, "expand_left_pixels"), true); + toggleWidget(node, findWidgetByName(node, "expand_right_pixels"), true); + } } return; }