Skip to content

Commit

Permalink
fix(app): mutating an image also changes the in-memory cached image
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
psychedelicious committed Oct 24, 2024
1 parent 7002359 commit f696541
Showing 1 changed file with 5 additions and 1 deletion.
6 changes: 5 additions & 1 deletion invokeai/app/services/shared/invocation_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]:
Expand Down

0 comments on commit f696541

Please sign in to comment.