From 88830186b2add95afc26222c4704ae711b7114f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Dec 2025 11:47:38 +1100 Subject: [PATCH 1/4] Inlined _glow_mask --- Tests/test_imageops.py | 17 ----------------- src/PIL/ImageOps.py | 19 +++---------------- 2 files changed, 3 insertions(+), 33 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index f39a839624d..2f930a445e5 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -631,23 +631,6 @@ def test_sobel_output_mode_and_size() -> None: assert out.size == img.size -def test_glow_mask_preserves_mode_and_size() -> None: - img = Image.new("L", (10, 10), 128) - out = ImageOps._glow_mask(img) - - assert out.mode == "L" - assert out.size == img.size - - -def test_glow_mask_increases_intensity() -> None: - img = Image.new("L", (1, 1), 128) - out = ImageOps._glow_mask(img) - - value = out.getpixel((0, 0)) - assert isinstance(value, (int, float)) - assert value > 128 - - def test_neon_colorize_output_mode() -> None: mask = Image.new("L", (5, 5), 128) out = ImageOps._neon_colorize(mask, (255, 0, 0)) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 9f4b76e7531..e05c35c9f99 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -689,20 +689,6 @@ def sobel(image: Image.Image) -> Image.Image: return out -def _glow_mask(edge_img: Image.Image) -> Image.Image: - """ - Apply a glow-enhancing mask transformation to an edge image. - - :param edge_img: A grayscale image containing edge intensities. - :return: An image. - """ - - def screen_point(value: int) -> int: - return 255 - ((255 - value) * (255 - value) // 255) - - return edge_img.point(screen_point) - - def _neon_colorize(mask: Image.Image, color: tuple[int, int, int]) -> Image.Image: """ Apply a color tint to an intensity mask for neon/glow effects. @@ -780,9 +766,10 @@ def neon_effect( edges = sobel(image) edges = edges.filter(ImageFilter.GaussianBlur(2)) - glow = _glow_mask(edges) - neon = _neon_colorize(glow, color) + # Apply a glow-enhancing mask transformation + glow = edges.point(lambda value: 255 - ((255 - value) ** 2 // 255)) + neon = _neon_colorize(glow, color) return _neon_blend(image, neon, alpha) From 0e2b57ac0a4193e5018ba7ec3c889a60e64d9bbc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Dec 2025 12:01:19 +1100 Subject: [PATCH 2/4] Colorize image band by band --- src/PIL/ImageOps.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index e05c35c9f99..195e6ad8401 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -696,17 +696,9 @@ def _neon_colorize(mask: Image.Image, color: tuple[int, int, int]) -> Image.Imag :param color: color to be applied :return: An image """ - r, g, b = color - out = Image.new("RGB", mask.size) - - for y in range(mask.height): - for x in range(mask.width): - v = mask.getpixel((x, y)) - assert isinstance(v, (int, float)) - - out.putpixel((x, y), tuple(min(255, int(v * c / 255)) for c in (r, g, b))) - - return out + return Image.merge( + "RGB", tuple(mask.point(lambda v: min(255, int(v * c / 255))) for c in color) + ) def _neon_blend( From d241df1ba9448e582881d633cf784872b3b71d37 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Dec 2025 12:12:38 +1100 Subject: [PATCH 3/4] Inlined _neon_colorize --- Tests/test_imageops.py | 19 ++----------------- src/PIL/ImageOps.py | 31 +++++++++---------------------- 2 files changed, 11 insertions(+), 39 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 2f930a445e5..670f3ffa0d1 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -615,7 +615,7 @@ def test_sepia_preserves_size_and_mode() -> None: def test_sobel_detects_edge() -> None: - img = Image.new("L", (5, 5), 0) + img = Image.new("L", (5, 5)) for x in range(3, 5): img.putpixel((x, 2), 255) @@ -624,28 +624,13 @@ def test_sobel_detects_edge() -> None: def test_sobel_output_mode_and_size() -> None: - img = Image.new("RGB", (10, 10), "black") + img = Image.new("RGB", (10, 10)) out = ImageOps.sobel(img) assert out.mode == "L" assert out.size == img.size -def test_neon_colorize_output_mode() -> None: - mask = Image.new("L", (5, 5), 128) - out = ImageOps._neon_colorize(mask, (255, 0, 0)) - - assert out.mode == "RGB" - assert out.size == mask.size - - -def test_neon_colorize_red_channel_only() -> None: - mask = Image.new("L", (1, 1), 255) - out = ImageOps._neon_colorize(mask, (255, 0, 0)) - - assert out.getpixel((0, 0)) == (255, 0, 0) - - def test_neon_blend_alpha_zero() -> None: base = Image.new("RGB", (1, 1), (10, 20, 30)) neon = Image.new("RGB", (1, 1), (200, 200, 200)) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 195e6ad8401..d80861604ce 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -689,18 +689,6 @@ def sobel(image: Image.Image) -> Image.Image: return out -def _neon_colorize(mask: Image.Image, color: tuple[int, int, int]) -> Image.Image: - """ - Apply a color tint to an intensity mask for neon/glow effects. - :param mask: single-channel mask. - :param color: color to be applied - :return: An image - """ - return Image.merge( - "RGB", tuple(mask.point(lambda v: min(255, int(v * c / 255))) for c in color) - ) - - def _neon_blend( original: Image.Image, neon: Image.Image, alpha: float = 0.55 ) -> Image.Image: @@ -725,15 +713,11 @@ def _neon_blend( value2 = neon.getpixel((x, y)) assert isinstance(value1, tuple) assert isinstance(value2, tuple) - r1, g1, b1 = value1 - r2, g2, b2 = value2 out.putpixel( (x, y), - ( - int((1 - alpha) * r1 + alpha * r2), - int((1 - alpha) * g1 + alpha * g2), - int((1 - alpha) * b1 + alpha * b2), + tuple( + int((1 - alpha) * value1[i] + alpha * value2[i]) for i in range(3) ), ) @@ -753,15 +737,18 @@ def neon_effect( :param color: RGB color used for neon effect :alpha: controls the intensity of the neon effect :return: An image - """ - edges = sobel(image) - edges = edges.filter(ImageFilter.GaussianBlur(2)) + edges = sobel(image).filter(ImageFilter.GaussianBlur(2)) # Apply a glow-enhancing mask transformation glow = edges.point(lambda value: 255 - ((255 - value) ** 2 // 255)) - neon = _neon_colorize(glow, color) + # Apply a color tint to the intensity mask + neon = Image.merge( + "RGB", + tuple(glow.point(lambda value: min(255, int(value * c / 255))) for c in color), + ) + return _neon_blend(image, neon, alpha) From 20b3ccb51cc77ccc1bf7191386ebe1c7e021d540 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 Dec 2025 12:15:17 +1100 Subject: [PATCH 4/4] Replaced _neon_blend with Image.blend --- Tests/test_imageops.py | 16 ---------------- src/PIL/ImageOps.py | 37 +------------------------------------ 2 files changed, 1 insertion(+), 52 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 670f3ffa0d1..09568f0bf95 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -631,22 +631,6 @@ def test_sobel_output_mode_and_size() -> None: assert out.size == img.size -def test_neon_blend_alpha_zero() -> None: - base = Image.new("RGB", (1, 1), (10, 20, 30)) - neon = Image.new("RGB", (1, 1), (200, 200, 200)) - - out = ImageOps._neon_blend(base, neon, alpha=0) - assert out.getpixel((0, 0)) == (10, 20, 30) - - -def test_neon_blend_alpha_one() -> None: - base = Image.new("RGB", (1, 1), (10, 20, 30)) - neon = Image.new("RGB", (1, 1), (200, 200, 200)) - - out = ImageOps._neon_blend(base, neon, alpha=1) - assert out.getpixel((0, 0)) == (200, 200, 200) - - def test_neon_effect_mode_and_size() -> None: img = Image.new("RGB", (20, 20)) out = ImageOps.neon_effect(img) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index d80861604ce..d0ebf0e4340 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -689,41 +689,6 @@ def sobel(image: Image.Image) -> Image.Image: return out -def _neon_blend( - original: Image.Image, neon: Image.Image, alpha: float = 0.55 -) -> Image.Image: - """ - Blend the original image with its neon/glow layer - - :param original: Image to blend whith neon layer - :param neon: neon Layer - :param alpha: controls intensity of neon effect - :return: An image - """ - if alpha < 0: - alpha = 0 - if alpha > 1: - alpha = 1 - - out = Image.new("RGB", original.size) - - for y in range(original.height): - for x in range(original.width): - value1 = original.getpixel((x, y)) - value2 = neon.getpixel((x, y)) - assert isinstance(value1, tuple) - assert isinstance(value2, tuple) - - out.putpixel( - (x, y), - tuple( - int((1 - alpha) * value1[i] + alpha * value2[i]) for i in range(3) - ), - ) - - return out - - def neon_effect( image: Image.Image, color: tuple[int, int, int] = (255, 0, 255), alpha: float = 0.2 ) -> Image.Image: @@ -749,7 +714,7 @@ def neon_effect( tuple(glow.point(lambda value: min(255, int(value * c / 255))) for c in color), ) - return _neon_blend(image, neon, alpha) + return Image.blend(image, neon, alpha) def invert(image: Image.Image) -> Image.Image: