Skip to content

Conversation

@matheusmpff
Copy link

Description:

This PR introduces two new image processing functions to the PIL.ImageOps module:

sepia

Applies a classic sepia tone effect to RGB images.

Converts the image to RGB if necessary.

Computes new pixel values using the standard sepia formula and clamps results to [0, 255].

Preserves the original image size and mode.

Practical use: adds a warm, vintage look to images with minimal processing overhead.

neon_effect

Applies a neon/glow effect to an image.

Internally uses a Sobel edge-detection filter , Gaussian blur, and colorization.

Combines the neon layer with the original image using alpha blending.

Supports RGB images and customizable neon color.

Practical use: highlights edges and details in a visually striking, glowing style.

Benefits:

Expands Pillow’s “ready-made” filters with two highly requested effects.

Provides developers with visually appealing filters without third-party dependencies.

Results

Image after sepia effect:
sepia

Image after neon-glow effect:
neon

Notes

There are tests for all functions created in this PR covering the main basic behaviors

@aclark4life
Copy link
Member

Very cool, thank you!

@radarhere radarhere changed the title Feat neon and sepia filters Neon and sepia filters Dec 14, 2025
@radarhere
Copy link
Member

I've created matheusmpff#1 with some suggestions.

image = image.convert("L")

Kx = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]
Ky = [[1, 2, 1], [0, 0, 0], [-1, -2, -1]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to confirm - you're sure the Ky values are correct? Looking at https://en.wikipedia.org/wiki/Sobel_operator#Formulation, one might expect that Ky should actually be [[-1, -2, -1], [0, 0, 0], [1, 2, 1]]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had used an inverted kernel for the Sobel operator in the y direction. The right one is the [[-1, -2, -1], [0, 0, 0], [1, 2, 1]]. But I think in the implementation we use the absolute values of gy so I think the results will be the same. It should be great to change for the correct one

:param image: Image to create the effect
:param color: RGB color used for neon effect
:alpha: controls the intensity of the neon effect
Copy link
Member

@radarhere radarhere Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:alpha: controls the intensity of the neon effect
:param alpha: Controls the intensity of the neon effect. If alpha is 0.0, a copy of
the image is returned unaltered.

This function computes the Sobel gradient magnitude using the
horizontal (Gx) and vertical (Gy) Sobel kernels.
:param image: the image to apply the filter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:param image: the image to apply the filter
:param image: The image to be filtered

Comment on lines +672 to +673
for y in range(1, image.height - 1):
for x in range(1, image.width - 1):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for y in range(1, image.height - 1):
for x in range(1, image.width - 1):
for x in range(1, image.width - 1):
for y in range(1, image.height - 1):

Nitpick: I think it would be nice if the order of the loops matched the order in sepia()

Comment on lines +609 to +610
def test_sepia_preserves_size_and_mode() -> None:
img = Image.new("RGB", (10, 10), (100, 150, 200))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_sepia_preserves_size_and_mode() -> None:
img = Image.new("RGB", (10, 10), (100, 150, 200))
@pytest.mark.parametrize("mode", ("L", "RGB"))
def test_sepia_size_and_mode(mode: str) -> None:
img = Image.new(mode, (10, 10))

Tests sepia() with a non-RGB image.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants