From 7e8378891ad4a7e013ee7634803eb11fa4ba2a2f Mon Sep 17 00:00:00 2001 From: tandede <1090179959@qq.com> Date: Wed, 19 Aug 2026 09:57:44 +0800 Subject: [PATCH] Fix transparent Porter-Duff blend modes --- comfy_extras/nodes_compositing.py | 13 ++-- .../nodes_compositing_test.py | 59 +++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 tests-unit/comfy_extras_test/nodes_compositing_test.py diff --git a/comfy_extras/nodes_compositing.py b/comfy_extras/nodes_compositing.py index 8fcbe720e73..8cb674c434b 100644 --- a/comfy_extras/nodes_compositing.py +++ b/comfy_extras/nodes_compositing.py @@ -46,7 +46,7 @@ def porter_duff_composite(src_image: torch.Tensor, src_alpha: torch.Tensor, dst_ out_image = torch.zeros_like(dst_image) elif mode == PorterDuffMode.DARKEN: out_alpha = src_alpha + dst_alpha - src_alpha * dst_alpha - out_image = (1 - dst_alpha) * src_image + (1 - src_alpha) * dst_image + torch.min(src_image, dst_image) + out_image = (1 - dst_alpha) * src_image + (1 - src_alpha) * dst_image + torch.min(src_image * dst_alpha, dst_image * src_alpha) elif mode == PorterDuffMode.DST: out_alpha = dst_alpha out_image = dst_image @@ -64,14 +64,15 @@ def porter_duff_composite(src_image: torch.Tensor, src_alpha: torch.Tensor, dst_ out_image = dst_image + (1 - dst_alpha) * src_image elif mode == PorterDuffMode.LIGHTEN: out_alpha = src_alpha + dst_alpha - src_alpha * dst_alpha - out_image = (1 - dst_alpha) * src_image + (1 - src_alpha) * dst_image + torch.max(src_image, dst_image) + out_image = (1 - dst_alpha) * src_image + (1 - src_alpha) * dst_image + torch.max(src_image * dst_alpha, dst_image * src_alpha) elif mode == PorterDuffMode.MULTIPLY: - out_alpha = src_alpha * dst_alpha - out_image = src_image * dst_image + out_alpha = src_alpha + dst_alpha - src_alpha * dst_alpha + out_image = (1 - dst_alpha) * src_image + (1 - src_alpha) * dst_image + src_image * dst_image elif mode == PorterDuffMode.OVERLAY: out_alpha = src_alpha + dst_alpha - src_alpha * dst_alpha - out_image = torch.where(2 * dst_image < dst_alpha, 2 * src_image * dst_image, - src_alpha * dst_alpha - 2 * (dst_alpha - src_image) * (src_alpha - dst_image)) + overlap = torch.where(2 * dst_image < dst_alpha, 2 * src_image * dst_image, + src_alpha * dst_alpha - 2 * (src_alpha - src_image) * (dst_alpha - dst_image)) + out_image = (1 - dst_alpha) * src_image + (1 - src_alpha) * dst_image + overlap elif mode == PorterDuffMode.SCREEN: out_alpha = src_alpha + dst_alpha - src_alpha * dst_alpha out_image = src_image + dst_image - src_image * dst_image diff --git a/tests-unit/comfy_extras_test/nodes_compositing_test.py b/tests-unit/comfy_extras_test/nodes_compositing_test.py new file mode 100644 index 00000000000..927ade807af --- /dev/null +++ b/tests-unit/comfy_extras_test/nodes_compositing_test.py @@ -0,0 +1,59 @@ +import pytest +import torch + +from comfy_extras.nodes_compositing import PorterDuffMode, porter_duff_composite + + +SOURCE = torch.tensor([0.8, 0.2, 0.6]) +BACKDROP = torch.tensor([0.3, 0.7, 0.4]) + + +def reference_source_over(mode, source_alpha, backdrop_alpha): + if mode == PorterDuffMode.DARKEN: + mixed = torch.minimum(BACKDROP, SOURCE) + elif mode == PorterDuffMode.LIGHTEN: + mixed = torch.maximum(BACKDROP, SOURCE) + elif mode == PorterDuffMode.MULTIPLY: + mixed = BACKDROP * SOURCE + elif mode == PorterDuffMode.OVERLAY: + mixed = torch.where( + 2 * BACKDROP <= 1, + 2 * BACKDROP * SOURCE, + 1 - 2 * (1 - BACKDROP) * (1 - SOURCE), + ) + + output_alpha = source_alpha + backdrop_alpha * (1 - source_alpha) + premultiplied = ( + source_alpha * (1 - backdrop_alpha) * SOURCE + + source_alpha * backdrop_alpha * mixed + + (1 - source_alpha) * backdrop_alpha * BACKDROP + ) + output = premultiplied / output_alpha if output_alpha else torch.zeros_like(SOURCE) + return output, 1 - output_alpha + + +@pytest.mark.parametrize( + "mode", + [ + PorterDuffMode.DARKEN, + PorterDuffMode.LIGHTEN, + PorterDuffMode.MULTIPLY, + PorterDuffMode.OVERLAY, + ], +) +@pytest.mark.parametrize( + ("source_alpha", "backdrop_alpha"), + [(1.0, 0.0), (0.0, 1.0), (0.35, 0.65), (1.0, 1.0)], +) +def test_blend_modes_use_source_over_alpha(mode, source_alpha, backdrop_alpha): + output, output_mask = porter_duff_composite( + SOURCE.reshape(1, 1, 3), + torch.tensor(1 - source_alpha).reshape(1, 1, 1), + BACKDROP.reshape(1, 1, 3), + torch.tensor(1 - backdrop_alpha).reshape(1, 1, 1), + mode, + ) + expected, expected_mask = reference_source_over(mode, source_alpha, backdrop_alpha) + + torch.testing.assert_close(output.flatten(), expected) + torch.testing.assert_close(output_mask.flatten(), torch.tensor([expected_mask]))