diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index ee73424df73..4ac8a13aa78 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -230,7 +230,7 @@ def invoke(self, context: InvocationContext) -> ImageOutput: title="Mask from Alpha", tags=["image", "mask"], category="mask", - version="1.2.2", + version="1.3.0", ) class MaskFromAlphaInvocation(BaseInvocation, WithMetadata, WithBoard): """Extracts the alpha channel of an image as a mask.""" @@ -239,9 +239,11 @@ class MaskFromAlphaInvocation(BaseInvocation, WithMetadata, WithBoard): invert: bool = InputField(default=False, description="Whether or not to invert the mask") def invoke(self, context: InvocationContext) -> ImageOutput: - image = context.images.get_pil(self.image.image_name) + # Converting gives an image without an alpha channel a fully opaque one. Its last band is a color or + # palette channel, not alpha. + image = context.images.get_pil(self.image.image_name, mode="RGBA") - image_mask = image.split()[-1] + image_mask = image.getchannel("A") if self.invert: image_mask = ImageOps.invert(image_mask) diff --git a/invokeai/frontend/api/openapi.json b/invokeai/frontend/api/openapi.json index ba39225a438..bee014d3eee 100644 --- a/invokeai/frontend/api/openapi.json +++ b/invokeai/frontend/api/openapi.json @@ -85692,7 +85692,7 @@ "tags": ["image", "mask"], "title": "Mask from Alpha", "type": "object", - "version": "1.2.2", + "version": "1.3.0", "output": { "$ref": "#/components/schemas/ImageOutput" } diff --git a/tests/app/invocations/test_image.py b/tests/app/invocations/test_image.py index 08c5eb660d0..67fa27c75fb 100644 --- a/tests/app/invocations/test_image.py +++ b/tests/app/invocations/test_image.py @@ -7,7 +7,12 @@ import torch from PIL import Image, ImageFilter -from invokeai.app.invocations.image import ImageField, OklabUnsharpMaskInvocation, OklchImageHueAdjustmentInvocation +from invokeai.app.invocations.image import ( + ImageField, + MaskFromAlphaInvocation, + OklabUnsharpMaskInvocation, + OklchImageHueAdjustmentInvocation, +) from invokeai.app.invocations.primitives import ImageCollectionInvocation from invokeai.backend.image_util.color_conversion import ( linear_srgb_from_oklab, @@ -49,6 +54,51 @@ def _max_abs_diff_uint8(left: Image.Image, right: Image.Image) -> int: return int(numpy.abs(left_arr - right_arr).max()) +def _invoke_mask_from_alpha(input_image: Image.Image, invert: bool = False) -> list[int]: + context = MagicMock() + # Like the real context, convert to the requested mode. + context.images.get_pil.side_effect = lambda image_name, mode=None: ( + input_image.convert(mode) if mode else input_image.copy() + ) + context.images.save.side_effect = lambda image, image_category: SimpleNamespace( + image_name="out", width=image.width, height=image.height + ) + + MaskFromAlphaInvocation(image=ImageField(image_name="in"), invert=invert).invoke(context) + saved_image = context.images.save.call_args.kwargs["image"] + + assert saved_image.mode == "L" + return numpy.asarray(saved_image).reshape(-1).tolist() + + +def test_mask_from_alpha_uses_the_alpha_channel() -> None: + input_image = Image.new("RGBA", (3, 1)) + input_image.putdata([(10, 20, 30, 0), (10, 20, 30, 128), (10, 20, 30, 255)]) + + assert _invoke_mask_from_alpha(input_image) == [0, 128, 255] + assert _invoke_mask_from_alpha(input_image, invert=True) == [255, 127, 0] + + +def test_mask_from_alpha_treats_an_image_without_alpha_as_opaque() -> None: + rgb_image = Image.new("RGB", (3, 1)) + rgb_image.putdata([(255, 0, 0), (0, 255, 0), (0, 0, 64)]) + grayscale_image = Image.new("L", (3, 1)) + grayscale_image.putdata([0, 64, 200]) + + assert _invoke_mask_from_alpha(rgb_image) == [255, 255, 255] + assert _invoke_mask_from_alpha(rgb_image, invert=True) == [0, 0, 0] + assert _invoke_mask_from_alpha(grayscale_image) == [255, 255, 255] + + +def test_mask_from_alpha_uses_palette_transparency() -> None: + palette_image = Image.new("P", (3, 1)) + palette_image.putpalette([0, 0, 0, 255, 255, 255]) + palette_image.putdata([0, 1, 0]) + palette_image.info["transparency"] = 0 + + assert _invoke_mask_from_alpha(palette_image) == [0, 255, 0] + + def test_image_collection_invocation_preserves_existing_collection_values() -> None: images = [ImageField(image_name="first"), ImageField(image_name="second")]