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.
Mental Model
generic_filter is like convolution but with a custom function instead of a fixed kernel: at each position, it collects the neighborhood values into a flat array and passes them to your callback. This unlimited flexibility comes at a cost -- the callback runs in Python, not C -- so use it only when no built-in filter can express your operation.
Where convolution is restricted to linear operators (weighted sums) and
morphology to set-based operators (min, max, connectivity),
generic_filter handles nonlinear operators — median, percentile,
variance, entropy, or any arbitrary computation. It completes the trio:
linear (convolution) → geometric (morphology) → fully general (generic filter).
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:
python
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 ```python 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 ```python 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:
```python 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:
```python 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:
```python 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:
```python 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] \ -2neighborhood[3] + 2neighborhood[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(sx2 + sy2) print("\nStandard Sobel magnitude:") print(magnitude_standard) ```
Side-Effect Programming Pattern¶
Generic filters can accumulate state during execution, useful for building data structures:
```python 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:
```python 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:
```python 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
Exercises¶
Exercise 1. Write a short code example that demonstrates the main concept covered on this page. Include comments explaining each step.
Solution to Exercise 1
Refer to the code examples in the page content above. A complete solution would recreate the key pattern with clear comments explaining the NumPy operations involved.
Exercise 2. Predict the output of a code snippet that uses the features described on this page. Explain why the output is what it is.
Solution to Exercise 2
The output depends on how NumPy handles the specific operation. Key factors include array shapes, dtypes, and broadcasting rules. Trace through the computation step by step.
Exercise 3. Write a practical function that applies the concepts from this page to solve a real data processing task. Test it with sample data.
Solution to Exercise 3
```python import numpy as np
Example: apply the page's concept to process sample data¶
data = np.random.default_rng(42).random((5, 3))
Apply the relevant operation¶
result = data # replace with actual operation print(result) ```
Exercise 4. Identify a common mistake when using the features described on this page. Write code that demonstrates the mistake and then show the corrected version.
Solution to Exercise 4
A common mistake is misunderstanding array shapes or dtypes. Always check .shape and .dtype when debugging unexpected results.