Skip to content
Open
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
2 changes: 1 addition & 1 deletion Tests/test_image_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_sanity_error(mode: str) -> None:

def test_noop_on_small_images() -> None:
# If image is smaller than the kernel size, return it as-is.
kernel_size: tuple[int, int] = ImageFilter.SMOOTH_MORE.filterargs[0]
kernel_size: tuple[int, int] = ImageFilter.SMOOTH_MORE.filterargs.size
kernel_w, kernel_h = kernel_size
for w in range(1, kernel_w):
for h in range(1, kernel_h):
Expand Down
54 changes: 31 additions & 23 deletions src/PIL/ImageFilter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import abc
import math
from typing import cast
from typing import NamedTuple, cast

TYPE_CHECKING = False
if TYPE_CHECKING:
Expand All @@ -40,9 +40,17 @@ class MultibandFilter(Filter):
pass


# Arguments passed to the core filter method
class _FilterArgs(NamedTuple):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd kind of want to note that these are unpacked directly to ImagingCore.filter().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ok, I've added a comment.

size: tuple[int, int]
scale: float

@akx akx Sep 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'd personally like to call this divisor, because that's what it practically is (and in fact, that's the "argument name" in _filter() in _imaging.c).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Kernel calls it scale.

def __init__(
self,
size: tuple[int, int],
kernel: Sequence[float],
scale: float | None = None,
offset: float = 0,
) -> None:

I would rather be consistent with the existing Python API.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sure thing.

offset: float
kernel: Sequence[float]


class BuiltinFilter(MultibandFilter):
name: str
filterargs: tuple[Any, ...]
filterargs: _FilterArgs

def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:
if image.mode == "P":
Expand Down Expand Up @@ -80,7 +88,7 @@ def __init__(
if size[0] * size[1] != len(kernel):
msg = "not enough coefficients in kernel"
raise ValueError(msg)
self.filterargs = size, scale, offset, kernel
self.filterargs = _FilterArgs(size, scale, offset, kernel)


class RankFilter(Filter):
Expand Down Expand Up @@ -265,96 +273,96 @@ def filter(self, image: _imaging.ImagingCore) -> _imaging.ImagingCore:

class BLUR(BuiltinFilter):
name = "Blur"
filterargs = (5, 5), 16, 0, (
filterargs = _FilterArgs(size=(5, 5), scale=16, offset=0, kernel=(
1, 1, 1, 1, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1,
) # fmt: skip
)) # fmt: skip


class CONTOUR(BuiltinFilter):
name = "Contour"
filterargs = (3, 3), 1, 255, (
filterargs = _FilterArgs(size=(3, 3), scale=1, offset=255, kernel=(
-1, -1, -1,
-1, 8, -1,
-1, -1, -1,
) # fmt: skip
)) # fmt: skip


class DETAIL(BuiltinFilter):
name = "Detail"
filterargs = (3, 3), 6, 0, (
filterargs = _FilterArgs(size=(3, 3), scale=6, offset=0, kernel=(
0, -1, 0,
-1, 10, -1,
0, -1, 0,
) # fmt: skip
)) # fmt: skip


class EDGE_ENHANCE(BuiltinFilter):
name = "Edge-enhance"
filterargs = (3, 3), 2, 0, (
filterargs = _FilterArgs(size=(3, 3), scale=2, offset=0, kernel=(
-1, -1, -1,
-1, 10, -1,
-1, -1, -1,
) # fmt: skip
)) # fmt: skip


class EDGE_ENHANCE_MORE(BuiltinFilter):
name = "Edge-enhance More"
filterargs = (3, 3), 1, 0, (
filterargs = _FilterArgs(size=(3, 3), scale=1, offset=0, kernel=(
-1, -1, -1,
-1, 9, -1,
-1, -1, -1,
) # fmt: skip
)) # fmt: skip


class EMBOSS(BuiltinFilter):
name = "Emboss"
filterargs = (3, 3), 1, 128, (
filterargs = _FilterArgs(size=(3, 3), scale=1, offset=128, kernel=(
-1, 0, 0,
0, 1, 0,
0, 0, 0,
) # fmt: skip
)) # fmt: skip


class FIND_EDGES(BuiltinFilter):
name = "Find Edges"
filterargs = (3, 3), 1, 0, (
filterargs = _FilterArgs(size=(3, 3), scale=1, offset=0, kernel=(
-1, -1, -1,
-1, 8, -1,
-1, -1, -1,
) # fmt: skip
)) # fmt: skip


class SHARPEN(BuiltinFilter):
name = "Sharpen"
filterargs = (3, 3), 16, 0, (
filterargs = _FilterArgs(size=(3, 3), scale=16, offset=0, kernel=(
-2, -2, -2,
-2, 32, -2,
-2, -2, -2,
) # fmt: skip
)) # fmt: skip


class SMOOTH(BuiltinFilter):
name = "Smooth"
filterargs = (3, 3), 13, 0, (
filterargs = _FilterArgs(size=(3, 3), scale=13, offset=0, kernel=(
1, 1, 1,
1, 5, 1,
1, 1, 1,
) # fmt: skip
)) # fmt: skip


class SMOOTH_MORE(BuiltinFilter):
name = "Smooth More"
filterargs = (5, 5), 100, 0, (
filterargs = _FilterArgs(size=(5, 5), scale=100, offset=0, kernel=(
1, 1, 1, 1, 1,
1, 5, 5, 5, 1,
1, 5, 44, 5, 1,
1, 5, 5, 5, 1,
1, 1, 1, 1, 1,
) # fmt: skip
)) # fmt: skip


class Color3DLUT(MultibandFilter):
Expand Down