From f48917ca96640cce9640b198db79ec08d00422a6 Mon Sep 17 00:00:00 2001 From: Neosettler Date: Sun, 17 Aug 2025 20:55:58 -0400 Subject: [PATCH 1/3] Update image.py fix(nodes): paste_image, paste mask operation doesn't want a black backdrop, black should translate to transparent, the hack here is to used the reference image instead, thus, acting as transparent but it's not. + img_scale, add "multiple of" for convenience --- invokeai/app/invocations/image.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index 10aafb3c52c..a6d7c6a395f 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -147,7 +147,7 @@ def invoke(self, context: InvocationContext) -> ImageOutput: title="Paste Image", tags=["image", "paste"], category="image", - version="1.2.2", + version="1.3.0", ) class ImagePasteInvocation(BaseInvocation, WithMetadata, WithBoard): """Pastes an image into another image.""" @@ -161,6 +161,7 @@ class ImagePasteInvocation(BaseInvocation, WithMetadata, WithBoard): x: int = InputField(default=0, description="The left x coordinate at which to paste the image") y: int = InputField(default=0, description="The top y coordinate at which to paste the image") crop: bool = InputField(default=False, description="Crop to base image dimensions") + invert_mask: bool = InputField(default=False, description="Invert mask color") def invoke(self, context: InvocationContext) -> ImageOutput: base_image = context.images.get_pil(self.base_image.image_name, mode="RGBA") @@ -168,21 +169,18 @@ def invoke(self, context: InvocationContext) -> ImageOutput: mask = None if self.mask is not None: mask = context.images.get_pil(self.mask.image_name, mode="L") - mask = ImageOps.invert(mask) - # TODO: probably shouldn't invert mask here... should user be required to do it? + if self.invert_mask: + mask = ImageOps.invert(mask) min_x = min(0, self.x) min_y = min(0, self.y) max_x = max(base_image.width, image.width + self.x) max_y = max(base_image.height, image.height + self.y) - new_image = Image.new(mode="RGBA", size=(max_x - min_x, max_y - min_y), color=(0, 0, 0, 0)) - new_image.paste(base_image, (abs(min_x), abs(min_y))) - # Create a temporary image to paste the image with transparency - temp_image = Image.new("RGBA", new_image.size) + temp_image = base_image temp_image.paste(image, (max(0, self.x), max(0, self.y)), mask=mask) - new_image = Image.alpha_composite(new_image, temp_image) + new_image = Image.alpha_composite(base_image, temp_image) if self.crop: base_w, base_h = base_image.size @@ -452,7 +450,7 @@ def invoke(self, context: InvocationContext) -> ImageOutput: title="Scale Image", tags=["image", "scale"], category="image", - version="1.2.2", + version="1.3.0", ) class ImageScaleInvocation(BaseInvocation, WithMetadata, WithBoard): """Scales an image by a factor""" @@ -464,16 +462,25 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata, WithBoard): description="The factor by which to scale the image", ) resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") + multiple_of: int = InputField( + default=8, + description="If > 1, the output image will be resized to the nearest multiple in both dimensions.", + ) def invoke(self, context: InvocationContext) -> ImageOutput: image = context.images.get_pil(self.image.image_name) resample_mode = PIL_RESAMPLING_MAP[self.resample_mode] - width = int(image.width * self.scale_factor) - height = int(image.height * self.scale_factor) + target_width = int(image.width * self.scale_factor) + target_height = int(image.height * self.scale_factor) + + # We may need to resize the image to a multiple of 8. Use floor division to ensure we don't scale the image up in the final resize + if self.multiple_of > 1: + target_width = int(target_width // self.multiple_of * self.multiple_of) + target_height = int(target_height // self.multiple_of * self.multiple_of) resize_image = image.resize( - (width, height), + (target_width, target_height), resample=resample_mode, ) From 333c3adf4530b17b696b12530cec292add1e0658 Mon Sep 17 00:00:00 2001 From: Neosettler Date: Mon, 18 Aug 2025 12:16:30 -0400 Subject: [PATCH 2/3] make default values backward compatible --- invokeai/app/invocations/image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index a6d7c6a395f..f8a4ed1e7b4 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -161,7 +161,7 @@ class ImagePasteInvocation(BaseInvocation, WithMetadata, WithBoard): x: int = InputField(default=0, description="The left x coordinate at which to paste the image") y: int = InputField(default=0, description="The top y coordinate at which to paste the image") crop: bool = InputField(default=False, description="Crop to base image dimensions") - invert_mask: bool = InputField(default=False, description="Invert mask color") + invert_mask: bool = InputField(default=True, description="Invert mask color") def invoke(self, context: InvocationContext) -> ImageOutput: base_image = context.images.get_pil(self.base_image.image_name, mode="RGBA") @@ -463,7 +463,7 @@ class ImageScaleInvocation(BaseInvocation, WithMetadata, WithBoard): ) resample_mode: PIL_RESAMPLING_MODES = InputField(default="bicubic", description="The resampling mode") multiple_of: int = InputField( - default=8, + default=1, description="If > 1, the output image will be resized to the nearest multiple in both dimensions.", ) From fd14142c489e7d6814909ee2357e25ccac5a76c8 Mon Sep 17 00:00:00 2001 From: Neosettler Date: Mon, 18 Aug 2025 15:25:48 -0400 Subject: [PATCH 3/3] restore paste behavior --- invokeai/app/invocations/image.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index f8a4ed1e7b4..057ff03d38c 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -177,10 +177,13 @@ def invoke(self, context: InvocationContext) -> ImageOutput: max_x = max(base_image.width, image.width + self.x) max_y = max(base_image.height, image.height + self.y) + new_image = Image.new(mode="RGBA", size=(max_x - min_x, max_y - min_y), color=(0, 0, 0, 0)) + new_image.paste(base_image, (abs(min_x), abs(min_y))) + # Create a temporary image to paste the image with transparency - temp_image = base_image + temp_image = new_image temp_image.paste(image, (max(0, self.x), max(0, self.y)), mask=mask) - new_image = Image.alpha_composite(base_image, temp_image) + new_image = Image.alpha_composite(new_image, temp_image) if self.crop: base_w, base_h = base_image.size