Wednesday, November 4th, 2026¶
Last class, we discussed using the mean and/or median filters to remove salt and pepper noise.
import numpy as np
import matplotlib.pyplot as plt
We worked with some sample images from the course webpage, and converted them to grayscale.
circuit = plt.imread('circuit.png')
gray_circuit = np.mean(circuit, axis=2) # Convert the RGB circuit array to 2D grayscale
We then added salt and pepper noise to these images.
noisy_circuit = gray_circuit.copy() # Initialize noisy_img as a 2D grayscale array
nrows, ncols = noisy_circuit.shape
r = np.random.random((nrows, ncols)) # Array of the same shape as noisy_img full of random floats
# between 0 and 1.
salt_mask = r > .9 # Identify 10% of entries that will be hit with salt noise
pepper_mask = r < .1 # Identify 10% of entries that will be hit with pepper noise
noisy_circuit[salt_mask] = 1 # Add salt noise
noisy_circuit[pepper_mask] = 0 # Add pepper noise
We then applied the mean/median filter using 3 by 3 grids and ignoring any edge pixels.
median_filtered_circuit = noisy_circuit.copy()
mean_filtered_circuit = noisy_circuit.copy()
nrows, ncols = noisy_circuit.shape
for i in range(1,nrows-1): # For every row other than the first and last
for j in range(1,ncols-1): # For every column other than the first and last
grid = noisy_circuit[i-1:i+2, j-1:j+2] # Build a 3x3 grid centered at pixel i,j
grid_median = np.median(grid)
grid_mean = np.mean(grid)
mean_filtered_circuit[i,j] = grid_mean # Set i,j-th filtered pixel to mean of the 3x3 grid
median_filtered_circuit[i,j] = grid_median # Set i,j-th filtered pixel to median of the 3x3 grid
plt.figure(figsize=(10,8))
plt.subplot(2,2,1)
plt.imshow(gray_circuit, cmap='gray', vmin=0, vmax=1)
plt.title('Original image')
plt.subplot(2,2,2)
plt.imshow(noisy_circuit, cmap='gray', vmin=0, vmax=1)
plt.title('Noisy image')
plt.subplot(2,2,3)
plt.imshow(mean_filtered_circuit, cmap='gray', vmin=0, vmax=1)
plt.title('Mean (3x3) filtered image')
plt.subplot(2,2,4)
plt.imshow(median_filtered_circuit, cmap='gray', vmin=0, vmax=1)
plt.title('Median (3x3) filtered image')
plt.tight_layout()
Exercise: Use the code above to write a function simple_mean_filter that applies the mean filter to non-edge pixels using a 3 by 3 grid.
Exercise: Modify the simple_mean_filter function to write a simple_median_filter that instead applies the median filter.
Note: We could combine these two functions into a single simple_filter function that takes in a noisy image array (noisy_img) along with some function (filter_func) that will be applied to each grid.
The filter_func input should be a function that takes in an 2D array and returns a float (e.g. np.mean or np.median).
def simple_filter(noisy_img, filter_func=np.median):
...
...
for ...
for ...
grid = ...
filtered_img[i,j] = filter_func(grid)
return filtered_img
Dealing with edge pixels¶
With the simple filters written above, we have ignored filtering pixels along the edge of the image. Let's try to deal with them now.
The project page discusses adding extra rows/columns to our array so that we are able to construct a 3 by 3 grid centered at all pixels of our original image.
If we are using 3 by 3 grids, we need to add one extra row/column on all sides. Our strategy then is to:
- Create an array
padded_imgthat has two additional rows and two additional columns than thenoisy_imgarray; - Place the contents of the
noisy_imgarray into a slice of thepadded_imgarray that skips the first and last rows and first and last columns; - Apply the the mean/median filter to each of the non-edge pixels of the
padded_img(which corresponds to applying the filter to every pixel of thenoisy_imgarray).
Let's take a closer look at one corner of the padded_img array:
Now, we want to use the padded_img array to build our 3 by 3 grids for computing means/medians. Let's copy in our median filter code and make any necessary changes.
Note: We have added black border to the padded_img (since we used np.zeros), which propogates into the filtered_img when we take any mean that includes padded pixels. We could instead add a white border (by using np.ones), but that's not any better. We could split the difference by using np.ones(...)/2 to use a gray border. For the project, it might be a good idea to think of other ways that one could deal with filtering pixels along the edges.
Let's try to turn the sample code above into some functions that we can use more broadly.
Exercise: Write a function get_padded_img(img, pad) that takes in a 2D array img and returns a padded array that adds pad rows at the top, bottom, left, and right of the array. That is, the padded_img array should have 2*pad extra rows and 2*pad extra columns.
Exercise: Write a function median_filter_with_edges that takes in a noisy image array and applies the mean/median filter to every pixel (including edges) using a 3 by 3 grid.
Handling different grid sizes¶
So far, we've been applying the mean/median filters using specifically a 3 by 3 grid centered on each pixel. What if we wan to use grids of different sizes, say s=3, s=5, s=7,...?
Let's think through how we would change our code to treat 5 by 5 grids. For now, let's again ignore the filtering of edge pixels.
What about 7 by 7 grids?
Note: The grid size s and the pad size pad are related by pad = (s-1)//2 or s = 2*pad + 1.
Exercise: Modify the examples above to take in a parameter s, which is an odd integer, and apply the mean/median filter using an s by s grid. You can skip filtering any rows/columns along the edge as necessary.
Putting things together¶
We've discussed how to deal with edge pixels (for the special case of 3 by 3 grids) and how to deal with varying grid sizes. We want to be able to handle both at the same time.
Exercise: Combine the two discussions (dealing with different grid sizes and dealing with edge pixels) to write functions mean_filter(img, s) and median_filter(img,s) that apply the mean/median filters to an input 2D array img using s by s grids, including all edge pixels.
Remaining tasks to be addressed for Project 4¶
The exercises over the last week have asked you to create functions that accomplish the tasks that we've worked on in class. Namely, we want to have the following functions available:
- The
sp.noisefunction should take in a grayscale image array and anoiseparameter and create a copy of the image array with salt and pepper noise added (with approximately $100\cdot$noise% of the pixels being hit with salt/pepper noise). - The
get_padded_imgfunction should take in a grayscale array and a desiredpadsize and create a padded array withpadadditional rows at the top/bottom andpadadditional columns at the left/right of the image array. - The
mean_filterfunction should take in a grayscale image array and apply the mean filter, accounting for edge pixels (using theget_padded_imgfunction). - The
median_filterfunction should take in a grayscale image array and apply the median filter, accounting for edge pixels (using theget_padded_imgfunction). - You may choose to combine the
mean_filterandmedian_filterfunctions into a single function, which takes in an additional argumentfilter_functhat controls the type of filter (e.g.filter_func = np.meanorfilter_func = np.median).
Once you have working mean/median filters that deal with edge pixels using any odd-sized grid, you can explore how well they work on images with varying levels of noise. Does the noise level affect which filter works better? What about the grid size? Does the "best" grid size change when the amount of noise changes?
Additionally, try to think of other ways the images could be filtered. You can start from the mean/median filters and think of ways to modify or improve them, or come up with something new altogether.
Speed - "NumPy-ifying" the process¶
For large images, the median filter code above is somewhat slow. It would be nice if we could use NumPy to avoid the nested for loop iteration through every row and column.
For reference, let's time how long it takes to apply the median filter to an image. I will use the sample code at the top of the notebook, but you should use your median_filter function instead.
Idea: Let's construct nine versions of our array. In particular, we want the following variations of the noisy_img array:
- The
noisy_imgarray, - The
noisy_imgarray shifted down by one pixel, - The
noisy_imgarray shifted up by one pixel, - The
noisy_imgarray shifted left by one pixel, - The
noisy_imgarray shifted right by one pixel, - The
noisy_imgarray shifted up by one pixel and left by one pixel, - The
noisy_imgarray shifted up by one pixel and right by one pixel, - The
noisy_imgarray shifted down by one pixel and left by one pixel, and - The
noisy_imgarray shifted down by one pixel and right by one pixel.
Then if we collect the [i,j]th entry of each of these shifted versions, we have all the values that makeup our 3 by 3 grid to compute a median.
How can we construct each of these shifted versions? We will again want to use a padded version of our array to make room for each of these shifts. We can then just take various slices of the padded_img array to construct these shifted versions.
Now that we have all of these shifted versions, we need some way to compute medians element-wise through these nine shifts. Let's create a new $4$-dimesional array, shifts, with the following structure:
- Axis
0corresponds to the pixel row in the original image. - Axis
1corresponds to the pixel column in the original image. - Axis
2corresponds to the row within the3by3grid of neighboring pixels in the original image. - Axis
3corresponds to the column within the3by3grid of neighboring pixels in the original image.
In other words, shifts will have shape (nrows, ncols, 3, 3) and shifts[i,j,m,n] will store the pixel in the [m,n] position of grid, where grid is the 3 by 3 grid of pixels centered on pixel [i,j] of the original image.
With this new structure, we now want to compute the mean or median of each 3 by 3 grid of neighboring pixels, separately for each pixel in the original image. That is, we want to compute np.mean(shifts[i,j]) separately for each pixel in position [i,j] of the original image.
Recall: The np.mean and np.median functions take in an optional argument axis that specify through which axes the mean/median should be computed. In our case, we want to compute the mean/median through axes 2 and 3 (i.e. the two axes that correspond to the row/column position in each 3 by 3 grid of neighboring pixels).
Let's time it to see how this compares.
We see that this is way faster than our previous approach!
Note: In this implementation, we manually wrote out each of the nine shifted arrays that were used. Can we do this in a more algorithmic way? That is, can we have Python automatically generate these shifts? One idea is to again use a pair of nested for loops, but this time we iterate through lists of horizontal and vertical shifts (instead of rows and columns).
Note: This implementation is hard-coded to use 3 by 3 grid sizes. Can we modify it to work with any odd-sized grid?