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.ThreadPoolExecutor
to process image tiles in parallel. - Streaming for Large Images: Use
ImageFile.LOAD_TRUNCATED_IMAGES= True
and 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.Kernel
for 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.