PIL Image Filters
Image filters transform a picture by changing its pixels. Pillow provides ready-made ImageFilter effects, whole-image ImageEnhance controls, and everything you need to design your own pixel-by-pixel filters.
Learning Objectives
- 12.6.2.4 Create filters for image processing
Conceptual Anchor
A Filter Is a Transformation
Think about a photo-editing app. A filter takes an input image, applies a rule, and produces a new output image. Some rules look at neighbouring pixels (blur, sharpen, edges); others modify each pixel directly (negative, grayscale, sepia, warm tone).
Image.open()
Load the source image
filter / enhance
Apply a transformation
compare
Inspect the result
save()
Write a new image file
Built-in Filters — Visual Reference
The easiest way to understand an image filter is to compare the same source image after different transformations. All examples below use the same generated scene.
Original
No filter applied.
img
Gaussian Blur
Smooths local detail.
ImageFilter.GaussianBlur(4)
Sharpen
Emphasises local edges.
ImageFilter.SHARPEN
Find Edges
Highlights strong intensity changes.
ImageFilter.FIND_EDGES
Emboss
Produces a raised relief-like effect.
ImageFilter.EMBOSS
Smooth
Reduces small variations more gently.
ImageFilter.SMOOTH
| Filter | Typical purpose | Python |
|---|---|---|
BLUR |
General blur / smoothing | img.filter(ImageFilter.BLUR) |
GaussianBlur(radius) |
Adjustable blur strength | img.filter(ImageFilter.GaussianBlur(5)) |
SHARPEN |
Make edges more pronounced | img.filter(ImageFilter.SHARPEN) |
CONTOUR |
Extract contour-like lines | img.filter(ImageFilter.CONTOUR) |
DETAIL |
Enhance fine detail | img.filter(ImageFilter.DETAIL) |
EDGE_ENHANCE |
Strengthen edges | img.filter(ImageFilter.EDGE_ENHANCE) |
FIND_EDGES |
Detect edge-like intensity changes | img.filter(ImageFilter.FIND_EDGES) |
EMBOSS |
Create a relief / emboss effect | img.filter(ImageFilter.EMBOSS) |
SMOOTH |
Light smoothing | img.filter(ImageFilter.SMOOTH) |
Interactive Filter Lab
Choose a filter to see the same image transformed immediately. The browser preview approximates the visual idea; the code shown below is the Pillow command you would use in Python.
Original image
Python
img = Image.open("photo.jpg")
Using ImageFilter
1 Apply and Save a Built-in Filter
from PIL import Image, ImageFilter
img = Image.open("photo.jpg")
blurred = img.filter(
ImageFilter.GaussianBlur(4)
)
blurred.save("photo_blurred.png")2 Chain Filters
from PIL import Image, ImageFilter
img = Image.open("photo.jpg")
result = img.filter(ImageFilter.SHARPEN)
result = result.filter(ImageFilter.FIND_EDGES)
result.save("sketch_effect.png")Order Matters
A filter changes the input for the next filter.
SHARPEN → FIND_EDGES does not necessarily produce the same
result as FIND_EDGES → SHARPEN.
Image Enhancement
ImageEnhance adjusts the appearance of the whole image.
A factor of 1.0 keeps the original. Lower and higher factors
decrease or increase the selected property.
Try the factors
1.00
1.00
1.00
1.00
| Enhancer | 0.0 | 1.0 | > 1.0 |
|---|---|---|---|
Brightness |
Black | Original | Brighter |
Contrast |
Flat gray-like result | Original | More contrast |
Color |
Grayscale | Original | More colour saturation |
Sharpness |
Blurred | Original | Sharper |
3 Increase Brightness
from PIL import Image, ImageEnhance
img = Image.open("photo.jpg")
enhancer = ImageEnhance.Brightness(img)
bright = enhancer.enhance(1.5)
bright.save("bright.png")Why Do Blur and Edge Filters Look at Neighbours?
Many built-in filters do not change a pixel independently. They examine a small neighbourhood around the pixel and combine those values. This small matrix is commonly called a kernel.
3 × 3 Neighbourhood
The new value of centre pixel X can depend on the surrounding pixels P.
Conceptual difference
| Blur | makes nearby values more similar |
| Sharpen | emphasises differences |
| Edge detection | looks for strong local changes |
Custom Filters — Pixel Mathematics
A custom filter can read a pixel (r, g, b), calculate new channel
values and write the result back. Use the controls below to see exactly what
happens to one pixel.
Input Pixel
(142, 87, 201)
(113, 168, 54)
Formula
Worked Custom Filters
4 Manual Grayscale
from PIL import Image
img = Image.open("photo.jpg").convert("RGB")
w, h = img.size
for x in range(w):
for y in range(h):
r, g, b = img.getpixel((x, y))
gray = int(
0.299 * r +
0.587 * g +
0.114 * b
)
img.putpixel(
(x, y),
(gray, gray, gray)
)
img.save("manual_gray.png")5 Negative
from PIL import Image
img = Image.open("photo.jpg").convert("RGB")
w, h = img.size
for x in range(w):
for y in range(h):
r, g, b = img.getpixel((x, y))
img.putpixel(
(x, y),
(255 - r, 255 - g, 255 - b)
)
img.save("negative.png")6 Sepia
from PIL import Image
img = Image.open("photo.jpg").convert("RGB")
w, h = img.size
for x in range(w):
for y in range(h):
r, g, b = img.getpixel((x, y))
tr = min(255, int(0.393*r + 0.769*g + 0.189*b))
tg = min(255, int(0.349*r + 0.686*g + 0.168*b))
tb = min(255, int(0.272*r + 0.534*g + 0.131*b))
img.putpixel((x, y), (tr, tg, tb))
img.save("sepia.png")7 Threshold
Convert the pixel to an intensity. If that intensity is above the threshold, make it white; otherwise make it black.
65 < 128 → BLACK
threshold = 128
205 > 128 → WHITE
from PIL import Image
img = Image.open("photo.jpg").convert("RGB")
w, h = img.size
threshold = 128
for x in range(w):
for y in range(h):
r, g, b = img.getpixel((x, y))
average = (r + g + b) // 3
if average > threshold:
new_color = (255, 255, 255)
else:
new_color = (0, 0, 0)
img.putpixel((x, y), new_color)
img.save("threshold.png")8 Warm Filter
from PIL import Image
img = Image.open("photo.jpg").convert("RGB")
w, h = img.size
for x in range(w):
for y in range(h):
r, g, b = img.getpixel((x, y))
new_r = min(255, r + 20)
new_g = g
new_b = max(0, b - 20)
img.putpixel(
(x, y),
(new_r, new_g, new_b)
)
img.save("warm.png")9 Keep Only the Red Channel
from PIL import Image
img = Image.open("photo.jpg").convert("RGB")
w, h = img.size
for x in range(w):
for y in range(h):
r, g, b = img.getpixel((x, y))
img.putpixel(
(x, y),
(r, 0, 0)
)
img.save("red_only.png")Keep RGB Values in Range
Standard 8-bit RGB channels use values from 0 to 255.
Custom formulas may produce values outside this range, so clamp values before
writing them back.
10 Clamp a Calculated Value
# Keep a number between 0 and 255
new_r = min(255, max(0, calculated_r))
new_g = min(255, max(0, calculated_g))
new_b = min(255, max(0, calculated_b))A Small Helper Function
For larger projects you can write
def clamp(v): return min(255, max(0, v))
and reuse it for every channel.
Further Viewing
Blurring, Sharpening and Smoothing with Pillow
A short visual walkthrough from Real Python showing built-in ImageFilter operations and comparisons.
Common Pitfalls
1. filter() Returns a New Image
img.filter(...) does not replace img.
Store the result:
blurred = img.filter(ImageFilter.BLUR).
2. Unexpected Image Mode
If your custom filter expects r, g, b, first use
img = img.convert("RGB").
3. Values Outside 0–255
A custom formula can produce negative values or values above 255.
Clamp channel values before calling putpixel().
4. Slow Pixel-by-Pixel Code
Nested for loops are excellent for learning how filters work,
but large images may process slowly. Resize a test image while developing.
5. Overwriting Your Original File
During practice, save results under a new filename such as
photo_blur.png instead of replacing the source file.
Practice Tasks
Which Pillow module contains BLUR,
SHARPEN and FIND_EDGES?
Explain the difference between a neighbour-based filter such as blur and a custom negative filter that changes each pixel independently.
Load an image, apply GaussianBlur(5),
and save the result as blurred.png.
Use ImageEnhance.Brightness to create one version at
factor 0.6 and another at 1.6.
A pixel is (220, 80, 30). Calculate the result of the
negative filter (255-r, 255-g, 255-b).
A warm filter adds 50 to red. Explain why
min(255, r + 50) is safer than simply using
r + 50.
Design your own custom filter using a clear mathematical rule. Explain what happens to R, G and B, then apply it to every pixel.
Build a mini filter pipeline that produces three files from one source: a blurred image, an edge image and your own custom colour effect.
Self-Check Quiz
Q1: Which module provides BLUR, SHARPEN and
FIND_EDGES?
ImageFilter from Pillow.
Q2: What does enhancement factor 1.0 mean?
Q3: What does ImageEnhance.Color(...).enhance(0) produce?
Q4: Why might a custom filter begin with convert("RGB")?
r, g, b.
Q5: What does a threshold filter do?
Q6: What is the negative of (100, 150, 200)?
(155, 105, 55).
Q7: Why is clamping necessary in custom colour filters?
0 and 255.