Monday, October 27th, 2025¶
import numpy as np
import matplotlib.pyplot as plt
Project 4 - Image denoising (continued)¶
Last week, we discussed adding salt and pepper noise to a grayscale image. For now, I will load in the face.png file and the noisy version from the course webpage (saved as noisy_img.png). For your report, you will want to choose one of the images from the project page (or another image of your choosing) and add your own noise (using your sp.noise function) before filtering.
img = np.mean(plt.imread('face.png')[:,:,:3], axis=2)
noisy_img = np.mean(plt.imread('noisy_img.png')[:,:,:3], axis=2)
We finished our discussion last class with an implementation of the mean/median filters using a 3 by 3 grid centered each pixel, where we ignored any pixels in the first/last rows or first/last columns.
num_rows, num_cols = noisy_img.shape
mean_filtered_img = noisy_img.copy()
for i in range(1, num_rows-1): # For now, let's skip the first and last rows
for j in range(1, num_cols-1): # and skip the first and last columns
grid = noisy_img[i-1:i+2, j-1:j+2]
mean = np.mean(grid)
mean_filtered_img[i,j] = mean
Exercise: Use code above to write a function simple_mean_filter that applies the mean filter to non-edge pixels using a 3 by 3 grid.
The median filter looks nearly identical, only changing the np.mean function to the np.median function (and changing the mean variable to median for readability).
num_rows, num_cols = noisy_img.shape
median_filtered_img = noisy_img.copy()
for i in range(1, num_rows-1): # For now, let's skip the first and last rows
for j in range(1, num_cols-1): # and skip the first and last columns
grid = noisy_img[i-1:i+2, j-1:j+2]
median = np.median(grid)
median_filtered_img[i,j] = median
Exercise: Modify the simple_mean_filter function to write a simple_median_filter that instead applies the median filter.
Exercise: 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):
...
Let's generate a plot that compares the original image, the noisy version, the mean filtered image, and the median filtered image.
plt.figure(figsize=(10,6))
plt.subplot(2,2,1)
plt.imshow(img, cmap='gray', vmin=0, vmax=1)
plt.title('Original image')
plt.axis('off')
plt.subplot(2,2,2)
plt.imshow(noisy_img, cmap='gray', vmin=0, vmax=1)
plt.title('Noisy image')
plt.axis('off')
plt.subplot(2,2,3)
plt.imshow(mean_filtered_img, cmap='gray', vmin=0, vmax=1)
plt.title('Mean filtered image')
plt.axis('off')
plt.subplot(2,2,4)
plt.imshow(median_filtered_img, cmap='gray', vmin=0, vmax=1)
plt.title('Median filtered image')
plt.axis('off')
plt.tight_layout()
Note: It might be useful to write a function that produces figures like the one above. This way, we can use this plotting on several different examples of a noised and filtered image for comparison.
Dealing with edge pixels¶
With the simple filters written above, we 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).
nrows, ncols = noisy_img.shape
padded_img = np.ones((nrows + 2, ncols + 2))/2 # Create an array of 0.5 that will store our padded image
padded_img[1:-1, 1:-1] = noisy_img
Let's take a closer look at one corner of the padded_img array:
plt.imshow(padded_img[:50, :50], cmap='gray', vmin=0, vmax=1)
<matplotlib.image.AxesImage at 0x1df999bf750>
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.
num_rows, num_cols = noisy_img.shape
median_filtered_img = noisy_img.copy()
for i in range(num_rows):
for j in range(num_cols):
grid = padded_img[i:i+3, j:j+3] # Generate `grid` from `padded_img` so that we
# can always look left/right/up/down
median = np.median(grid)
median_filtered_img[i,j] = median
plt.imshow(median_filtered_img, cmap='gray', vmin=0, vmax=1)
plt.axis('off')
(np.float64(-0.5), np.float64(399.5), np.float64(265.5), np.float64(-0.5))
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 edge_filter that takes in a noisy image array and applies the mean/median filter to every pixel (including edges) using a 3 by 3 grid.
Grid size¶
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.
num_rows, num_cols = noisy_img.shape
median_filtered_img = noisy_img.copy()
for i in range(2, num_rows-2): # For now, let's skip the first and last rows
for j in range(2, num_cols-2): # and skip the first and last columns
grid = noisy_img[i-2:i+3, j-2:j+3]
median = np.median(grid)
median_filtered_img[i,j] = median
plt.imshow(median_filtered_img, cmap='gray', vmin=0, vmax=1)
<matplotlib.image.AxesImage at 0x1df9a3a0690>
What about 7 by 7 grids?
num_rows, num_cols = noisy_img.shape
median_filtered_img = noisy_img.copy()
for i in range(3, num_rows-3): # For now, let's skip the first and last rows
for j in range(3, num_cols-3): # and skip the first and last columns
grid = noisy_img[i-3:i+4, j-3:j+4]
median = np.median(grid)
median_filtered_img[i,j] = median
plt.imshow(median_filtered_img, cmap='gray', vmin=0, vmax=1)
<matplotlib.image.AxesImage at 0x1df9a30ca50>
Note: We say (on the board) that 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.
s = 9
pad = (s-1)//2
num_rows, num_cols = noisy_img.shape
median_filtered_img = noisy_img.copy()
for i in range(..., num_rows-...): # For now, let's skip the first and last rows
for j in range(..., num_cols-...): # and skip the first and last columns
grid = noisy_img[..., ...]
median = np.median(grid)
median_filtered_img[i,j] = median
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.
def get_widths_RGBs(tartan_recipe):
def get_vertical_stripes(widths, RGBs):
def get_horizontal_stripes(vertical_stripes):
def get_checkerboard_tartan(tartan_recipe, size=500):
return checkerboard_tartan
def get_authentic_tartan(tartan_recipe, color_dict, size=500):