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)| Filter | Effect | Cost |
|---|---|---|
GaussianBlur | Smooth, natural looking | Linear in radius per pixel |
BoxBlur | Cheap approximation of blur | Low |
UnsharpMask | Sharpen with halo control | Moderate, three parameters to tune |
SHARPEN / SMOOTH | Fixed 3x3 convolution | Very low |
FIND_EDGES / CONTOUR | Edge emphasis | Low |
MedianFilter | Removes impulse noise, keeps edges | Higher, grows with size |
RankFilter / ModeFilter | Order statistics per window | Higher 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.Related
Drawing and converting formats ImageOps: autocontrast, pad, fit and montage
Last refreshed 2026-09-18.