From f6965418194b873f046c86e2bdd0bb2b46540281 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 25 Oct 2024 09:12:29 +1000 Subject: [PATCH] fix(app): mutating an image also changes the in-memory cached image We use an in-memory cache for PIL images to reduce I/O. If a node mutates the image in any way, the cached image object is also updated (but the on-disk image file is not). We've lucked out that this hasn't caused major issues in the past (well, maybe it has but we didn't understand them?) mainly because of a happy accident. When you call `context.images.get_pil` in a node, if you provide an image mode (e.g. `mode="RGB"`), we call `convert` on the image. This returns a copy. The node can do whatever it wants to that copy and nothing breaks. However, when mode is not specified, we return the image directly. This is where we get in trouble - nodes that load the image like this, and then mutate the image, update the cache. Other nodes that reference that same image will now get the mutated version of it. The fix is super simple - we make sure to return only copies from `get_pil`. --- invokeai/app/services/shared/invocation_context.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/shared/invocation_context.py b/invokeai/app/services/shared/invocation_context.py index 60ae978c5ee..bf7f9892359 100644 --- a/invokeai/app/services/shared/invocation_context.py +++ b/invokeai/app/services/shared/invocation_context.py @@ -221,7 +221,7 @@ def save( ) def get_pil(self, image_name: str, mode: IMAGE_MODES | None = None) -> Image: - """Gets an image as a PIL Image object. + """Gets an image as a PIL Image object. This method returns a copy of the image. Args: image_name: The name of the image to get. @@ -233,11 +233,15 @@ def get_pil(self, image_name: str, mode: IMAGE_MODES | None = None) -> Image: image = self._services.images.get_pil_image(image_name) if mode and mode != image.mode: try: + # convert makes a copy! image = image.convert(mode) except ValueError: self._services.logger.warning( f"Could not convert image from {image.mode} to {mode}. Using original mode instead." ) + else: + # copy the image to prevent the user from modifying the original + image = image.copy() return image def get_metadata(self, image_name: str) -> Optional[MetadataField]: