Extending Pillow’s ImageFilter module with custom filters unlocks powerful image processing capabilities—ranging from edge detection to color grading via 3D lookup tables. This guide covers building convolution kernels, 3D LUT transforms, and integrating NumPy for per-pixel operations, with performance tips like Pillow-SIMD and multi-threading.
1. Creating a Convolution Kernel Filter
Convolution applies a matrix kernel over each pixel neighborhood. Extend ImageFilter.Kernel to implement custom effects (e.g., emboss, sharpen, edge detection).
from PIL import Image, ImageFilter
# Define a 3x3 edge-detection kernel
kernel = [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
]
edge_filter = ImageFilter.Kernel(
size=(3, 3),
kernel=kernel,
scale=None, # sum of kernel; None means sum
offset=0
)
image = Image.open("input.jpg")
result = image.filter(edge_filter)
result.save("output_edge.jpg")
Tip: For large kernels (5×5, 7×7), pre-normalize kernel values to avoid integer overflow.
2. Implementing a 3D Lookup Table (LUT) Color Grade
3D LUTs map input RGB values to new RGB output. Use a cube of size N³ as the LUT and trilinear interpolation for smooth transitions.
from PIL import Image
import numpy as np
def apply_3d_lut(image: Image.Image, lut: np.ndarray) -> Image.Image:
"""
image: PIL Image RGB
lut: NumPy array shape (N,N,N,3) with values [0,255]
"""
arr = np.array(image).astype(np.float32) / 255.0
N = lut.shape[0]
# Scale coords
coords = arr * (N - 1)
x, y, z = coords[...,0], coords[...,1], coords[...,2]
x0, y0, z0 = np.floor(x).astype(int), np.floor(y).astype(int), np.floor(z).astype(int)
x1, y1, z1 = np.clip(x0+1, 0, N-1), np.clip(y0+1, 0, N-1), np.clip(z0+1, 0, N-1)
# Trilinear interpolation weights
xd, yd, zd = x - x0, y - y0, z - z0
# Fetch 8 corners
c000 = lut[x0, y0, z0]
c100 = lut[x1, y0, z0]
c010 = lut[x0, y1, z0]
c001 = lut[x0, y0, z1]
c101 = lut[x1, y0, z1]
c011 = lut[x0, y1, z1]
c110 = lut[x1, y1, z0]
c111 = lut[x1, y1, z1]
c00 = c000*(1-xd)[...,None] + c100*xd[...,None]
c01 = c001*(1-xd)[...,None] + c101*xd[...,None]
c10 = c010*(1-xd)[...,None] + c110*xd[...,None]
c11 = c011*(1-xd)[...,None] + c111*xd[...,None]
c0 = c00*(1-yd)[...,None] + c10*yd[...,None]
c1 = c01*(1-yd)[...,None] + c11*yd[...,None]
c = c0*(1-zd)[...,None] + c1*zd[...,None]
c = np.clip(c * 255.0, 0, 255).astype(np.uint8)
return Image.fromarray(c)
# Usage example
lut = np.load("my_lut.npy") # Precomputed LUT (N×N×N×3)
img = Image.open("input.jpg").convert("RGB")
out = apply_3d_lut(img, lut)
out.save("output_lut.jpg")
.npy for fast loading.
3. Integrating NumPy for Pixel-Level Algorithms
For complex algorithms—like bilateral filter or custom morphological operations—convert image to NumPy, process, then back to PIL.
from PIL import Image
import numpy as np
from scipy.ndimage import gaussian_filter
def bilateral_filter_pil(image: Image.Image, sigma=2.0) -> Image.Image:
arr = np.array(image).astype(np.float32)
# Apply Gaussian blur on each channel
blurred = np.zeros_like(arr)
for c in range(3):
blurred[...,c] = gaussian_filter(arr[...,c], sigma=sigma)
# Combine original and blurred via weighted average
result = (0.5 * arr + 0.5 * blurred).astype(np.uint8)
return Image.fromarray(result)
# Usage
img = Image.open("input.jpg")
filtered = bilateral_filter_pil(img, sigma=1.5)
filtered.save("output_bilateral.jpg")
Tip: Use Pillow-SIMD (pip install pillow-simd) for ~15× speedups on core operations.
4. Performance Optimization & Memory Management
- Vectorize Operations: Leverage NumPy’s vectorized math instead of Python loops.
- Multi-threading: Use
concurrent.futures.ThreadPoolExecutorto process image tiles in parallel. - Streaming for Large Images: Use
ImageFile.LOAD_TRUNCATED_IMAGES= Trueand process in chunks to limit RAM.
5. Packaging Custom Filters
Bundle filters as a Python package:
# setup.py
from setuptools import setup, find_packages
setup(
name="advanced_pillow_filters",
version="0.1.0",
packages=find_packages(),
install_requires=["Pillow", "numpy", "scipy"],
entry_points={
"PIL.ImageFilter": [
"EdgeDetect=advanced_pillow_filters:edge_filter",
"MyLUT=advanced_pillow_filters:apply_3d_lut",
],
}
)
Install via pip install . and import filters by name.
6. Summary Checklist
- Use
ImageFilter.Kernelfor convolution effects. - Implement 3D LUTs with trilinear interpolation in NumPy.
- Convert to/from NumPy arrays for advanced per-pixel algorithms.
- Optimize performance via Pillow-SIMD and multi-threading.
- Package filters for easy reuse and distribution.
