Filters and enhancement

Blur, sharpen and detect edges with ImageFilter, tune brightness and contrast with ImageEnhance, and understand what each operation costs.

ImageFilter

from PIL import Image, ImageFilter

im = Image.open("photo.jpg").convert("RGB")

soft = im.filter(ImageFilter.GaussianBlur(radius=2))
sharp = im.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))
edges = im.filter(ImageFilter.FIND_EDGES)
clean = im.filter(ImageFilter.MedianFilter(size=5))          # salt-and-pepper noise

# a custom 3x3 kernel: a simple emboss
emboss = ImageFilter.Kernel((3, 3), (-2, -1, 0, -1, 1, 1, 0, 1, 2), scale=1, offset=128)
out = im.filter(emboss)
FilterEffectCost
GaussianBlurSmooth, natural lookingLinear in radius per pixel
BoxBlurCheap approximation of blurLow
UnsharpMaskSharpen with halo controlModerate, three parameters to tune
SHARPEN / SMOOTHFixed 3x3 convolutionVery low
FIND_EDGES / CONTOUREdge emphasisLow
MedianFilterRemoves impulse noise, keeps edgesHigher, grows with size
RankFilter / ModeFilterOrder statistics per windowHigher for large windows

ImageEnhance

from PIL import ImageEnhance

im = Image.open("photo.jpg").convert("RGB")

im = ImageEnhance.Brightness(im).enhance(1.15)      # 1.0 is unchanged
im = ImageEnhance.Contrast(im).enhance(1.2)
im = ImageEnhance.Color(im).enhance(0.9)            # saturation
im = ImageEnhance.Sharpness(im).enhance(1.5)

# chaining reads top to bottom because each call returns a new image
im.save("enhanced.jpg", quality=88)
  • Every enhancer uses the same scale: below 1 reduces the property, 1 leaves it alone, above 1 increases it.
  • Each step returns a new image; the original is untouched unless you rebind the name.
  • Order matters. Brightening after increasing contrast gives a different result from the reverse, and neither is wrong.

Cost and where to filter

# filter the small version when the output is small
thumb = im.copy()
thumb.thumbnail((800, 800), Image.Resampling.LANCZOS)
thumb = thumb.filter(ImageFilter.GaussianBlur(radius=1.5))

# or draft a JPEG down before doing anything expensive
import io
small = Image.open("huge.jpg")
small.draft("RGB", (1600, 1600))          # decodes at a reduced scale
small = small.filter(ImageFilter.UnsharpMask(radius=1, percent=120))
💡
Filter objects are stateless and reusable, so build them once outside a loop. The cost that matters is per pixel and per radius: blurring a 4000 px image at radius 20 is far more expensive than blurring the 800 px version you are going to publish.

FAQ

My image looks over-sharpened and has glowing edges. Why?
Unsharp mask adds a scaled copy of the high-frequency content. A percent above about 200 or a radius above 3 on a small image produces visible halos. Raise threshold so flat areas are left alone.
Does enhance() work on palette images?
ImageEnhance needs a mode it can compute with. Convert a P image to RGB or L first, otherwise the results are unpredictable.

Drawing and converting formats ImageOps: autocontrast, pad, fit and montage

Last refreshed 2026-09-18.