From b5fadba6737924e938b150f61cd42009ea2f573b Mon Sep 17 00:00:00 2001 From: Mohammad Hijjawi Date: Wed, 4 Nov 2026 22:31:06 +0000 Subject: [PATCH 1/2] fix(nodes): read the real alpha channel in Mask from Alpha Mask from Alpha took the image's last band, which for an image without alpha is the blue channel (RGB), the gray channel (L) or palette indices (P). Convert to RGBA first, so opaque images give a solid mask and palette transparency is honored. --- invokeai/app/invocations/image.py | 6 ++-- tests/app/invocations/test_image.py | 52 ++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index ee73424df73..a84dbc4c7a2 100644 --- a/invokeai/app/invocations/image.py +++ b/invokeai/app/invocations/image.py @@ -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/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")] From 061f8ffb415f45a94e6a1fb2a3f5d7c59e2d1fec Mon Sep 17 00:00:00 2001 From: Mohammad Hijjawi Date: Wed, 11 Nov 2026 16:43:23 +0000 Subject: [PATCH 2/2] fix(nodes): bump Mask from Alpha to 1.3.0 The node now returns an opaque mask for images without an alpha channel instead of their last band, so saved workflows should be flagged as using an older version. --- invokeai/app/invocations/image.py | 2 +- invokeai/frontend/api/openapi.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/app/invocations/image.py b/invokeai/app/invocations/image.py index a84dbc4c7a2..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.""" 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" }