Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 2 additions & 50 deletions Tests/test_imageops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -624,61 +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_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))

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))

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)
Expand Down
89 changes: 10 additions & 79 deletions src/PIL/ImageOps.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,79 +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.
:param mask: single-channel mask.
: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


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)
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),
),
)

return out


def neon_effect(
image: Image.Image, color: tuple[int, int, int] = (255, 0, 255), alpha: float = 0.2
) -> Image.Image:
Expand All @@ -775,15 +702,19 @@ 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))

glow = _glow_mask(edges)
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)
return Image.blend(image, neon, alpha)


def invert(image: Image.Image) -> Image.Image:
Expand Down