Generic Filter Operations with scipy.ndimage¶
While convolution applies a fixed kernel operation, generic_filter() allows you to apply arbitrary Python functions over neighborhoods. This enables complex, stateful, and custom operations on array data.
Introduction to Generic Filters¶
ndi.generic_filter() slides a neighborhood window over an array and applies a user-defined function to each neighborhood. The signature is:
result = ndi.generic_filter(input, function, size=None, footprint=None,
mode='reflect', cval=0.0, extra_arguments=())
The function receives a flattened array of all values in the neighborhood and should return a scalar (or array of scalars for multiple outputs).
When to Use Generic Filters
Use generic_filter() when:
- Your operation cannot be expressed as a linear convolution
- You need to apply statistics (median, percentile, min, max)
- You want to implement custom neighborhood logic
- You need to track state during filtering
Footprint vs Size Parameters¶
You can define neighborhoods in two ways:
Size parameter: Creates a rectangular neighborhood
import numpy as np
from scipy import ndimage as ndi
image = np.arange(25).reshape(5, 5)
# Size parameter: 3x3 neighborhood
result = ndi.generic_filter(image, np.min, size=3)
print("Using size=3 (3x3 neighborhood):")
print(result)
Footprint parameter: Custom neighborhood shape
import numpy as np
from scipy import ndimage as ndi
image = np.arange(25).reshape(5, 5)
# Cross-shaped footprint (diamond)
footprint = np.array([[0, 1, 0],
[1, 1, 1],
[0, 1, 0]], dtype=bool)
result = ndi.generic_filter(image, np.sum, footprint=footprint)
print("Using cross-shaped footprint:")
print(result)
Footprints give fine control over which neighbors participate in the operation. This is essential for defining connectivity in segmentation and morphological operations.
Footprint Choice
- Size: Simple, rectangular, efficient
- Footprint: Custom shapes, connectivity control, more flexible
Percentile-Based Neighborhood Operations¶
A practical application is computing percentiles within neighborhoods, useful for adaptive filtering:
import numpy as np
from scipy import ndimage as ndi
import matplotlib.pyplot as plt
# Create a noisy image
np.random.seed(42)
image = np.zeros((100, 100))
image[25:75, 25:75] = 100
image += 30 * np.random.randn(100, 100)
# Function to compute 75th percentile
def percentile_75(neighborhood):
return np.percentile(neighborhood, 75)
# Apply generic filter
result = ndi.generic_filter(image, percentile_75, size=5)
# Visualize
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].imshow(image, cmap='gray')
axes[0].set_title('Noisy Image')
axes[1].imshow(result, cmap='gray')
axes[1].set_title('75th Percentile Filter')
plt.tight_layout()
plt.show()
# Compare with standard filters
median_result = ndi.median_filter(image, size=5)
print("Percentile filter range:", result.min(), "-", result.max())
print("Median filter range:", median_result.min(), "-", median_result.max())
Why Percentile Filters?
- Median (50th percentile): Removes outliers, preserves edges
- Higher percentiles: Remove dark noise, preserve bright features
- Lower percentiles: Remove bright noise
- Adaptive to local image statistics
Conway's Game of Life¶
A classic example of custom neighborhood logic is Conway's Game of Life. Each cell's next state depends on its current state and its 8 neighbors:
import numpy as np
from scipy import ndimage as ndi
# Conway's Game of Life rules:
# 1. A live cell with 2-3 neighbors survives
# 2. A dead cell with exactly 3 neighbors becomes alive
# 3. All other cells die or stay dead
def conway_step(neighborhood):
"""
Compute next generation of Conway's Game of Life.
neighborhood: flattened 3x3 array with center cell as neighborhood[4]
Returns: 1 if cell is alive, 0 if dead
"""
center = neighborhood[4]
neighbors_alive = np.sum(neighborhood) - center # Don't count center
if center == 1: # Cell is alive
return 1 if 2 <= neighbors_alive <= 3 else 0
else: # Cell is dead
return 1 if neighbors_alive == 3 else 0
# Initialize a simple pattern (blinker - period 2)
board = np.zeros((10, 10), dtype=int)
board[5, 4:7] = 1 # Three cells in a row
print("Generation 0:")
print(board)
# Define cross-shaped footprint (8 neighbors + center)
footprint = np.ones((3, 3), dtype=bool)
# Simulate generations
for gen in range(1, 5):
# Use mode='wrap' for toroidal topology (wraparound edges)
board = ndi.generic_filter(board, conway_step, footprint=footprint,
mode='wrap', cval=0)
print(f"\nGeneration {gen}:")
print(board)
This example demonstrates several key features:
- Custom logic: Pure Python function defining complex rules
- Neighborhood access: Function receives all neighbors at once
- Boundary handling:
mode='wrap'creates a toroidal board (edges wrap around) - State persistence: Simple state management during filtering
Why Game of Life?
This demonstrates that generic_filter() can:
- Express complex conditional logic
- Handle non-linear operations
- Work with custom neighborhood topologies
- Model cellular automata and pattern propagation
Efficient Multi-Output Generic Filter¶
You can return multiple values per neighborhood using a structured array:
import numpy as np
from scipy import ndimage as ndi
image = np.arange(25).reshape(5, 5).astype(float)
# Function returning multiple statistics
def neighborhood_stats(neighborhood):
"""Return min, max, and median"""
# Note: generic_filter expects scalar output
# For multiple outputs, use structured approach
return np.median(neighborhood)
# Better approach: compute separately or use map_array with custom logic
min_filter = ndi.minimum_filter(image, size=3)
max_filter = ndi.maximum_filter(image, size=3)
median_filter = ndi.median_filter(image, size=3)
print("Min values:", min_filter.sum())
print("Max values:", max_filter.sum())
print("Median values:", median_filter.sum())
Advanced: Sobel Magnitude in Single Pass¶
While convolution-based approaches are typical, generic filters can compute edge magnitude directly:
import numpy as np
from scipy import ndimage as ndi
# Create simple test image
image = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]], dtype=float)
def sobel_magnitude(neighborhood):
"""
Compute Sobel magnitude from 3x3 neighborhood.
neighborhood layout:
[0] [1] [2]
[3] [4] [5]
[6] [7] [8]
"""
# Sobel kernels
sx = -neighborhood[0] + neighborhood[2] \
-2*neighborhood[3] + 2*neighborhood[5] \
-neighborhood[6] + neighborhood[8]
sy = -neighborhood[0] - 2*neighborhood[1] - neighborhood[2] \
+neighborhood[6] + 2*neighborhood[7] + neighborhood[8]
return np.sqrt(sx**2 + sy**2)
result = ndi.generic_filter(image, sobel_magnitude, size=3)
print("Sobel magnitude (single pass):")
print(result)
# Compare with standard approach
sx = ndi.sobel(image, axis=0)
sy = ndi.sobel(image, axis=1)
magnitude_standard = np.sqrt(sx**2 + sy**2)
print("\nStandard Sobel magnitude:")
print(magnitude_standard)
Side-Effect Programming Pattern¶
Generic filters can accumulate state during execution, useful for building data structures:
import numpy as np
from scipy import ndimage as ndi
image = np.array([[1, 0, 2],
[0, 3, 0],
[4, 0, 5]], dtype=int)
# Global accumulator (use with caution!)
detected_neighbors = {}
def find_local_max(neighborhood):
"""Find local maxima and track their neighbors"""
center_val = neighborhood[4]
max_neighbor = np.max(neighborhood)
# Simple detection: center equals neighborhood max
if center_val == max_neighbor and center_val > 0:
detected_neighbors[len(detected_neighbors)] = {
'value': center_val,
'neighbors': neighborhood.tolist()
}
return 1.0
return 0.0
result = ndi.generic_filter(image, find_local_max, size=3)
print("Local maxima found:")
print(result)
print("\nDetected structure:")
for idx, data in detected_neighbors.items():
print(f" Maxima {idx}: value={data['value']}")
State Accumulation Caution
Global state during filtering is useful for analysis but: - Can produce unexpected results with boundary modes - Makes debugging harder - Doesn't parallelize well - Use only when truly necessary
Performance Comparison¶
Generic filters are more flexible but slower than specialized operations:
import numpy as np
from scipy import ndimage as ndi
import time
image = np.random.rand(500, 500)
# Method 1: Generic filter (flexible, slower)
def custom_median(neighborhood):
return np.median(neighborhood)
start = time.time()
result1 = ndi.generic_filter(image, custom_median, size=5)
t1 = time.time() - start
# Method 2: Optimized median filter
start = time.time()
result2 = ndi.median_filter(image, size=5)
t2 = time.time() - start
print(f"Generic filter: {t1:.4f}s")
print(f"Optimized median: {t2:.4f}s")
print(f"Slowdown: {t1/t2:.1f}x")
print(f"Results match: {np.allclose(result1, result2)}")
Practical Example: Adaptive Local Contrast¶
Here's a practical application that enhances local contrast:
import numpy as np
from scipy import ndimage as ndi
import matplotlib.pyplot as plt
# Create synthetic image
np.random.seed(42)
image = np.zeros((100, 100))
image[20:80, 20:80] = 0.7
image[40:60, 40:60] = 0.3
image += 0.05 * np.random.randn(100, 100)
def local_contrast(neighborhood):
"""
Normalize neighborhood values to enhance local contrast.
Returns normalized center value.
"""
center = neighborhood[4]
local_mean = np.mean(neighborhood)
local_std = np.std(neighborhood)
if local_std < 1e-6: # Avoid division by zero
return 0.5
# Normalize to [0, 1]
normalized = (center - local_mean) / (3 * local_std) + 0.5
return np.clip(normalized, 0, 1)
# Apply adaptive contrast enhancement
enhanced = ndi.generic_filter(image, local_contrast, size=7)
# Visualize
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].imshow(image, cmap='gray')
axes[0].set_title('Original Image')
axes[1].imshow(enhanced, cmap='gray')
axes[1].set_title('Adaptive Contrast Enhancement')
plt.tight_layout()
plt.show()
Key Takeaways¶
generic_filter() is a powerful tool for custom neighborhood operations:
- Applies arbitrary Python functions to neighborhoods
- Supports custom footprints for flexible connectivity
- Enables complex logic like cellular automata
- Slower than specialized filters but more flexible
- Useful for percentiles, statistics, and adaptive operations
See also: - Convolution and Filtering - Linear filtering operations - Binary Structures and Morphology - Connectivity and labeling