Documentation Menu

Pillow ImageFilter Module

Independent quick-reference. Canonical source: official ImageFilter module documentation.

The ImageFilter module contains definitions for a number of predefined image enhancement and convolution filters, which can be applied with the Image.filter() method.

Basic Usage
from PIL import Image, ImageFilter img = Image.open("photo.jpg") # Apply Gaussian blur blurred = img.filter(ImageFilter.GaussianBlur(radius=2)) # Apply predefined sharpen sharpened = img.filter(ImageFilter.SHARPEN) # Find edges edges = img.filter(ImageFilter.FIND_EDGES)

Blur Filters

FilterParametersDescription
BLUR—Simple box blur kernel. Equivalent to BoxBlur with small radius.
GaussianBlur(radius=2)radius: float or (x,y) tupleGaussian blur using a weighted kernel. Higher radius = more blur. Most realistic blur for photography.
BoxBlur(radius)radius: float or (x,y) tupleEach output pixel is the average of the square neighbourhood. Faster than Gaussian for large radii.
Background Blur Effect
# Blur a background while keeping subject sharp (simple version) background = img.filter(ImageFilter.GaussianBlur(radius=10)) # Blur with asymmetric radius (x=0 horizontal only, y=5 vertical) motion_blur = img.filter(ImageFilter.BoxBlur(radius=(0, 5)))

Edge Detection & Contour Filters

FilterDescription
CONTOURFind image contours. Produces a white image with dark contour lines — ideal for sketching effects.
EDGE_ENHANCEEnhance edges. Subtle — creates a look similar to slight over-sharpening.
EDGE_ENHANCE_MOREMore aggressive edge enhancement.
FIND_EDGESDetects sharp edges. Produces a black image with white edge lines. Good for preprocessing for OCR or CV.
EMBOSSEmboss the image. Creates a raised relief effect.
Contour & Edge Detection Example
from PIL import Image, ImageFilter img = Image.open("portrait.jpg").convert("L") # Extract outline contours (sketch outline) contour_img = img.filter(ImageFilter.CONTOUR) contour_img.save("contours.png") # Extract pure bright edges (ideal for Computer Vision pre-processing) edges = img.filter(ImageFilter.FIND_EDGES) edges.save("detected_edges.png")

Sharpening Filters

FilterParametersDescription
DETAIL—Enhance in-image details.
SHARPEN—Sharpen the image.
SMOOTH—Smooth the image.
SMOOTH_MORE—More aggressive smoothing.
UnsharpMask(radius=2, percent=150, threshold=3)radius, percent, thresholdUnsharp mask (USM). The industry standard sharpening technique. Radius: blur radius for edge detection. Percent: strength of sharpening (0–500%). Threshold: minimum pixel difference to sharpen.
Unsharp Mask Example
# Standard USM for photo sharpening sharpened = img.filter(ImageFilter.UnsharpMask( radius=2, percent=150, threshold=3 ))

Rank Filters

Rank filters pick a specific ranked pixel value from a neighbourhood. Useful for noise reduction and morphological operations.

FilterDescription
MedianFilter(size=3)Picks the median pixel in the neighbourhood. Excellent for removing "salt and pepper" noise without blurring edges.
MinFilter(size=3)Picks the minimum (darkest) pixel. Acts as morphological erosion.
MaxFilter(size=3)Picks the maximum (brightest) pixel. Acts as morphological dilation.
RankFilter(size, rank)General rank filter. rank=0 = Min, rank=size*size//2 = Median, rank=size*size-1 = Max.

Custom Kernel Filter

Create arbitrary convolution kernels with ImageFilter.Kernel(). The kernel is a flat list of values applied over a square neighbourhood.

ParameterTypeDescription
sizetuple[int,int]Kernel size — must be odd, e.g., (3,3) or (5,5).
kernelsequenceA sequence of size[0]*size[1] kernel weights. Listed from top-left, row by row.
scaleintDivisor applied to the output. Usually the sum of the kernel weights.
offsetintAdded to each output value after scaling.
Custom Kernel Examples
# Laplacian edge detection kernel laplacian = ImageFilter.Kernel( size=(3, 3), kernel=[0, -1, 0, -1, 4, -1, 0, -1, 0], scale=1, offset=128 # Offset to make 0-diff appear gray ) edges = img.convert("L").filter(laplacian) # Identity kernel (does nothing) identity = ImageFilter.Kernel((3,3), [0,0,0,0,1,0,0,0,0], scale=1)

All Predefined Filters

Filter ConstantType
BLURPredefined
CONTOURPredefined
DETAILPredefined
EDGE_ENHANCEPredefined
EDGE_ENHANCE_MOREPredefined
EMBOSSPredefined
FIND_EDGESPredefined
SHARPENPredefined
SMOOTHPredefined
SMOOTH_MOREPredefined
GaussianBlur(radius=2)Parameterizable
BoxBlur(radius)Parameterizable
UnsharpMask(radius=2, percent=150, threshold=3)Parameterizable
Kernel(size, kernel, scale, offset=0)Custom convolution
RankFilter(size, rank)Rank
MedianFilter(size=3)Rank
MinFilter(size=3)Rank
MaxFilter(size=3)Rank
ModeFilter(size=3)Rank
Color3DLUT(size, table)3D LUT colour transform
MultibandFilterBase class for multi-band filters
BuiltinFilterBase class for built-in filters
FilterAbstract base class

Tested examples

Everything in this section was run with Pillow 12.3.0; the images and numbers are the real output. Sample photo: NASA/Sam Lott, public domain.

A grid of nine crops of the rocket and tower: original, BLUR, SMOOTH_MORE, SHARPEN, DETAIL, EDGE_ENHANCE_MORE, FIND_EDGES, CONTOUR and EMBOSS
Real output of the script below, on a 240×240 crop of the photo.
builtin_filters.py
from PIL import Image, ImageDraw, ImageFilter

src = Image.open("photo.jpg").crop((560, 380, 960, 780)).resize((240, 240))
filters = ["BLUR", "SMOOTH_MORE", "SHARPEN", "DETAIL", "EDGE_ENHANCE_MORE",
           "FIND_EDGES", "CONTOUR", "EMBOSS"]
cells = [("original", src)] + [(name, src.filter(getattr(ImageFilter, name))) for name in filters]

cols = 3
sheet = Image.new("RGB", (cols * 250, 3 * 275), "white")
draw = ImageDraw.Draw(sheet)
for i, (label, im) in enumerate(cells):
    x, y = (i % cols) * 250, (i // cols) * 275
    sheet.paste(im, (x, y))
    draw.text((x + 120, y + 256), label, fill="black", anchor="mm", font_size=16)
sheet.save("filters.png")
Tested on ✓ Linux ✓ Windows ✓ macOS

GaussianBlur vs BoxBlur: speed

blur_timing.py
import time
from PIL import Image, ImageFilter

img = Image.open("photo.jpg")
for radius in [2, 10, 40]:
    times = {}
    for name, f in [("GaussianBlur", ImageFilter.GaussianBlur(radius)), ("BoxBlur", ImageFilter.BoxBlur(radius))]:
        t = time.perf_counter()
        img.filter(f)
        times[name] = (time.perf_counter() - t) * 1000
    print(f"radius {radius:>2}:  GaussianBlur {times['GaussianBlur']:6.1f} ms   BoxBlur {times['BoxBlur']:6.1f} ms")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
radius  2:  GaussianBlur   76.3 ms   BoxBlur   24.9 ms
radius 10:  GaussianBlur   55.1 ms   BoxBlur   32.0 ms
radius 40:  GaussianBlur   62.6 ms   BoxBlur   26.5 ms

Both run in roughly constant time as the radius grows, because Pillow approximates the Gaussian with repeated box blurs; a single BoxBlur pass was about twice as fast here. BoxBlur looks blockier at large radii; use GaussianBlur unless you have measured that the difference matters.

SHARPEN vs UnsharpMask

unsharp_mask.py
from PIL import Image, ImageFilter

src = Image.open("photo.jpg").crop((980, 360, 1100, 480))          # lattice tower detail
variants = [("original", src),
            ("SHARPEN", src.filter(ImageFilter.SHARPEN)),
            ("Unsharp r=2 150%", src.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))),
            ("Unsharp r=4 300%", src.filter(ImageFilter.UnsharpMask(radius=4, percent=300, threshold=0)))]
sheet = Image.new("RGB", (4 * 250 - 10, 240), "white")
for i, (label, im) in enumerate(variants):
    sheet.paste(im.resize((240, 240), Image.NEAREST), (i * 250, 0))
sheet.save("unsharp.png")
print([label for label, _ in variants])
Tested on ✓ Linux ✓ Windows ✓ macOS
Four enlarged crops of a lattice tower: original, SHARPEN, a moderate unsharp mask and a strong one with visible halos
Real output, enlarged 2×. Left to right: original, SHARPEN, UnsharpMask(2, 150, 3), UnsharpMask(4, 300, 0).

SHARPEN is a fixed 3×3 kernel. UnsharpMask lets you set the size of the detail (radius), the strength (percent) and a threshold below which differences are left alone, which keeps noise and smooth skies from being sharpened. Overdo it and light halos appear along edges, as on the right.

Removing speckle noise: MedianFilter

remove_noise.py
import random
from PIL import Image, ImageFilter

clean = Image.open("photo.jpg").convert("L").resize((400, 267))
noisy = clean.copy()
px = noisy.load()
random.seed(1)
for _ in range(4000):                                              # salt-and-pepper noise
    px[random.randrange(400), random.randrange(267)] = random.choice((0, 255))


def err(a, b):
    return sum(abs(x - y) for x, y in zip(a.get_flattened_data(), b.get_flattened_data())) / (a.width * a.height)


for name, out in [("noisy", noisy), ("GaussianBlur(1)", noisy.filter(ImageFilter.GaussianBlur(1))),
                  ("MedianFilter(3)", noisy.filter(ImageFilter.MedianFilter(3)))]:
    print(f"{name:<16} mean error vs clean: {err(out, clean):5.2f}")
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
noisy            mean error vs clean:  4.66
GaussianBlur(1)  mean error vs clean:  6.36
MedianFilter(3)  mean error vs clean:  2.09

A blur only spreads isolated black and white specks around; a median filter replaces each pixel with the middle value of its neighbourhood, so outliers disappear. The other rank filters, MinFilter and MaxFilter, erode and dilate masks.

Blurring transparent images without dark edges

blur_transparent.py
from PIL import Image, ImageDraw, ImageFilter

disc = Image.new("RGBA", (100, 100), (0, 0, 0, 0))                # transparent pixels store black
ImageDraw.Draw(disc).ellipse((20, 20, 80, 80), fill=(255, 255, 255, 255))

straight = disc.filter(ImageFilter.GaussianBlur(6))
premult = disc.convert("RGBa").filter(ImageFilter.GaussianBlur(6)).convert("RGBA")
print("edge pixel, blurred as RGBA:", straight.getpixel((50, 17)))
print("edge pixel, blurred as RGBa:", premult.getpixel((50, 17)))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
edge pixel, blurred as RGBA: (80, 80, 80, 80)
edge pixel, blurred as RGBa: (255, 255, 255, 80)

Fully transparent pixels still store a colour, usually black. Blurring RGBA mixes that colour into the edges, so a white shape gets a grey fringe. Converting to RGBa (premultiplied alpha) first, and back afterwards, blurs correctly.

Which modes can be filtered

filter_modes.py
from PIL import Image, ImageFilter

photo = Image.open("photo.jpg")
for mode in ["RGB", "L", "RGBA", "P", "1", "I;16", "F"]:
    img = photo.convert("I").convert("I;16") if mode == "I;16" else photo.convert(mode)
    row = []
    for f in [ImageFilter.SHARPEN, ImageFilter.GaussianBlur(2), ImageFilter.MedianFilter(3)]:
        try:
            img.filter(f)
            row.append("ok")
        except ValueError as e:
            row.append(str(e))
    print(f"{mode:<5}", " | ".join(f"{r:<27}" for r in row))
Tested on ✓ Linux ✓ Windows ✓ macOS
Output
RGB   ok                          | ok                          | ok                         
L     ok                          | ok                          | ok                         
RGBA  ok                          | ok                          | ok                         
P     cannot filter palette images | image has wrong mode        | cannot filter palette images
1     ok                          | image has wrong mode        | ok                         
I;16  ok                          | image has wrong mode        | image has wrong mode       
F     image has wrong mode        | image has wrong mode        | ok

Palette images cannot be filtered at all: convert to RGB or RGBA first. GaussianBlur needs 8-bit channels, so it also rejects 1-bit, 16-bit and float images. For 16-bit and float data, NumPy and SciPy (scipy.ndimage) are the better tools.