Last week, we developed some code to apply the mean and median filter using 3
by 3
grids and ignoring the left/right/top/bottom edges of the image:
import matplotlib.pyplot as plt
import numpy as np
We read in a sample image and converted to grayscale by averaging the RGB values:
hamburg = plt.imread('hamburg.png')
hamburg_gray = hamburg.mean(axis=2)
plt.imshow(hamburg_gray,cmap='gray',vmin=0,vmax=1)
We copied the hamburg_gray
array before adding noise. We used Boolean arrays to identify which pixels will be hit with salt noise and which will be hit with pepper noise.
noisy_hamburg = hamburg_gray.copy()
num_rows, num_cols = hamburg_gray.shape
random_array = np.random.random((num_rows, num_cols))
# Let's generate an Boolean array with True in the locations that we want
# to add salt noise and False everywhere else
salt_mask = (random_array > .9)
# Then, we can use `salt_mask` as a slice on the `noisy_hamburg` array:
noisy_hamburg[salt_mask] = 1
# Do the same thing for pepper noise:
pepper_mask = (random_array < .1)
noisy_hamburg[pepper_mask] = 0
Note: The cell above is a useful skeleton for writing the sp_noise
function described in the project page. It requires some modification to take inputs img
and noise
, where img
is the image array that will have noise added to it, and noise
measures the total proportion of pixels that will be hit by salt or pepper noise.
plt.imshow(noisy_hamburg, cmap='gray',vmin=0,vmax=1)
We then applied the mean filter using 3
by 3
grids, ignoring the left/right/top/bottom edges of the image:
filtered_hamburg = noisy_hamburg.copy()
# For now, we need to skip the first row and last row
for i in range(1,num_rows-1):
# We also need to skip the first column and last column
for j in range(1, num_cols-1):
grid = noisy_hamburg[i-1:i+2, j-1:j+2]
mean = np.mean(grid)
filtered_hamburg[i,j] = mean
plt.imshow(filtered_hamburg,cmap='gray',vmin=0,vmax=1)
plt.title('Mean filter')
We also applied the median filter:
filtered_hamburg = noisy_hamburg.copy()
# For now, we need to skip the first row and last row
for i in range(1,num_rows-1):
# We also need to skip the first column and last column
for j in range(1, num_cols-1):
grid = noisy_hamburg[i-1:i+2, j-1:j+2]
median = np.median(grid)
filtered_hamburg[i,j] = median
plt.imshow(filtered_hamburg,cmap='gray',vmin=0,vmax=1)
plt.title('Median filter')
Let's write a mean_filter
function based on the mean filter code above:
def mean_filter(noisy_img):
filtered_img = noisy_img.copy()
num_rows, num_cols = noisy_img.shape
# For now, we need to skip the first row and last row
for i in range(1,num_rows-1):
# We also need to skip the first column and last column
for j in range(1, num_cols-1):
grid = noisy_img[i-1:i+2, j-1:j+2]
mean = np.mean(grid)
filtered_img[i,j] = mean
return filtered_img
Can we write a median_filter
function as well?
def median_filter(noisy_img):
filtered_img = noisy_img.copy()
num_rows, num_cols = noisy_img.shape
# For now, we need to skip the first row and last row
for i in range(1,num_rows-1):
# We also need to skip the first column and last column
for j in range(1, num_cols-1):
grid = noisy_img[i-1:i+2, j-1:j+2]
median = np.median(grid)
filtered_img[i,j] = median
return filtered_img
filtered_hamburg = median_filter(noisy_hamburg)
plt.imshow(filtered_hamburg, cmap='gray', vmin=0, vmax=1)
Note: We could combine these two functions into a singler filter
function that takes in a noisy image array (noisy_img
) along 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.
def img_filter(noisy_img, filter_func=np.mean):
filtered_img = noisy_img.copy()
num_rows, num_cols = noisy_img.shape
# For now, we need to skip the first row and last row
for i in range(1,num_rows-1):
# We also need to skip the first column and last column
for j in range(1, num_cols-1):
grid = noisy_img[i-1:i+2, j-1:j+2]
filtered_img[i,j] = filter_func(grid)
return filtered_img
mean_filtered_hamburg = img_filter(noisy_hamburg, np.mean)
median_filtered_hamburg = img_filter(noisy_hamburg, np.median)
plt.figure(figsize=(10,10))
plt.subplot(2,2,1)
plt.imshow(hamburg_gray,cmap='gray',vmin=0,vmax=1)
plt.title('Original image')
plt.axis('off')
plt.subplot(2,2,2)
plt.imshow(noisy_hamburg,cmap='gray',vmin=0,vmax=1)
plt.title('Noisy image')
plt.axis('off')
plt.subplot(2,2,3)
plt.imshow(mean_filtered_hamburg,cmap='gray',vmin=0,vmax=1)
plt.title('Mean filtered image')
plt.axis('off')
plt.subplot(2,2,4)
plt.imshow(median_filtered_hamburg,cmap='gray',vmin=0,vmax=1)
plt.title('Median filtered image')
plt.axis('off')
plt.tight_layout()
Until now, we've simply ignored filtering of edge pixels. 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.
num_rows, num_cols = noisy_hamburg.shape
# Define a larger array with an extra on top/bottom
# and an extra column on left/right
padded_img = np.zeros((num_rows+2, num_cols+2))
# Place the noisy array in the center of the padded array
padded_img[1:-1, 1:-1] = noisy_hamburg
Now, we want to apply our mean filter to this padded_img
array. We need to be careful about our indexing:
filtered_img = np.zeros((num_rows, num_cols))
for i in range(num_rows):
for j in range(num_cols):
grid = padded_img[i:i+3, j:j+3]
filtered_img[i,j] = np.mean(grid)
plt.figure(figsize=(10,10))
plt.imshow(filtered_img, cmap='gray',vmin=0,vmax=1)
plt.axis('off')
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 no better off. We could split the difference by using np.ones(...)/2
to use a gray border.
Note: To implement this strategy of dealing with edge pixels, we will need update our mean_filter
/median_filter
/img_filter
functions. It will helpful to first a get_padded_img
function that takes in an array and returns a padded version.
What needs to change if we want to use grids of different sizes, say s=3
, s=5
, s=7
, ...?
Let's try 5
by 5
grids:
filtered_hamburg = noisy_hamburg.copy()
# For now, we need to skip the first row and last row
for i in range(2,num_rows-2):
# We also need to skip the first column and last column
for j in range(2, num_cols-2):
grid = noisy_hamburg[i-2:i+3, j-2:j+3]
mean = np.mean(grid)
filtered_hamburg[i,j] = mean
plt.figure(figsize=(10,10))
plt.imshow(filtered_hamburg, cmap='gray',vmin=0,vmax=1)
plt.axis('off')
What about 7
by 7
grids?
filtered_hamburg = noisy_hamburg.copy()
# For now, we need to skip the first row and last row
for i in range(3,num_rows-3):
# We also need to skip the first column and last column
for j in range(3, num_cols-3):
grid = noisy_hamburg[i-3:i+4, j-3:j+4]
mean = np.mean(grid)
filtered_hamburg[i,j] = mean
plt.figure(figsize=(10,10))
plt.imshow(filtered_hamburg, cmap='gray',vmin=0,vmax=1)
plt.axis('off')
Exercise: Modify the examples above to take in a paramter s
, which is an odd integer, and apply the mean filter using an s
by s
grid. You can skip any rows/columns along the edge as necessary.
Exercise: Once you've done this, combine it with the earlier discussion on filtered edge pixels.
import matplotlib.pyplot as plt
import numpy as np
Let's look at some other considerations when dealing with image denoising (not things that are necessary for the project).
color_img = plt.imread('Pilotwings.jpg') / 255 # Convert the integer RGB data to floating point
plt.imshow(color_img)
Let's add some salt and pepper noise:
noisy_img = color_img.copy()
num_rows, num_cols = noisy_img.shape[:2]
random_array = np.random.random((num_rows, num_cols))
salt_mask = random_array > .9
pepper_mask = random_array < .1
noisy_img[salt_mask] = 1
noisy_img[pepper_mask] = 0
plt.imshow(noisy_img)
Let's try to apply our median filter:
filtered_img = noisy_img.copy()
# For now, we need to skip the first row and last row
for i in range(1,num_rows-1):
# We also need to skip the first column and last column
for j in range(1, num_cols-1):
grid = noisy_img[i-1:i+2, j-1:j+2]
median = np.median(grid)
filtered_img[i,j] = median
plt.imshow(filtered_img)
With our old code: for each pixel to be filtered, we've constructed a grid
consisting of 3
rows, 3
columns, and 3
color channels (RGB). We then the np.median
of this 3
by 3
by 3
slice, and set the RGB value for the filtered pixel to this median.
It might be better to separately compute the median red value, median green value, and median blue value.
#help(np.median)
We can use the keyword argument axis=[0,1]
inside np.median
to only compute medians throught the rows and columns, but the colors:
filtered_img = noisy_img.copy()
# For now, we need to skip the first row and last row
for i in range(1,num_rows-1):
# We also need to skip the first column and last column
for j in range(1, num_cols-1):
grid = noisy_img[i-1:i+2, j-1:j+2]
median = np.median(grid, axis=[0,1])
filtered_img[i,j] = median
plt.imshow(filtered_img)
In the code above, we've hit all color channels together with salt/pepper noise (if they get hit at all). It could also be the case that the color channels are independently affected by "salt"/"pepper" noise.
noisy_img = color_img.copy()
num_rows, num_cols = noisy_img.shape[:2]
random_array = np.random.random((num_rows, num_cols,3))
salt_mask = random_array > .9
pepper_mask = random_array < .1
noisy_img[salt_mask] = 1
noisy_img[pepper_mask] = 0
plt.imshow(noisy_img)
filtered_img = noisy_img.copy()
# For now, we need to skip the first row and last row
for i in range(1,num_rows-1):
# We also need to skip the first column and last column
for j in range(1, num_cols-1):
grid = noisy_img[i-1:i+2, j-1:j+2]
median = np.median(grid, axis=[0,1])
filtered_img[i,j] = median
plt.imshow(filtered_img)
For large images, the median filter code above is somewhat slow. It would be nice if we could use NumPy to avoid the for
loop iteration through every row and column.
For simplicity, let's return to working with grayscale images:
img = (plt.imread('Pilotwings.jpg') / 255).mean(axis=2)
noisy_img = img.copy()
num_rows, num_cols = noisy_img.shape
random_array = np.random.random((num_rows, num_cols))
salt_mask = random_array > .9
pepper_mask = random_array < .1
noisy_img[salt_mask] = 1
noisy_img[pepper_mask] = 0
plt.imshow(noisy_img, cmap='gray', vmin=0, vmax=1)
from time import time
t0 = time()
filtered_img = noisy_img.copy()
# For now, we need to skip the first row and last row
for i in range(1,num_rows-1):
# We also need to skip the first column and last column
for j in range(1, num_cols-1):
grid = noisy_img[i-1:i+2, j-1:j+2]
median = np.median(grid)
filtered_img[i,j] = median
t1 = time()
print('Execution time:', t1-t0)
Idea: Let's construct nine versions of our array, in the following manner:
Then if we look at the [i,j]
th entries of each of these shifted versions, we have all the values that makeup our 3
by 3
grid to compute a median.
num_rows, num_cols = noisy_img.shape
shift1 = noisy_img.copy()
# Shift down by one
shift2 = np.zeros((num_rows, num_cols))
shift2[1:, :] = noisy_img[:-1, :]
# Shift up by one
shift3 = np.zeros((num_rows, num_cols))
shift3[:-1, :] = noisy_img[1:, :]
# Shift left by one
shift4 = np.zeros((num_rows, num_cols))
shift4[:, :-1] = noisy_img[:, 1:]
# Shift right by one
shift5 = np.zeros((num_rows, num_cols))
shift4[:, 1:] = noisy_img[:, :-1]
# Shift up by one, left by one
shift6 = np.zeros((num_rows, num_cols))
shift6[:-1,:-1] = noisy_img[1:, 1:]
# Shift up by one, right by one
shift7 = np.zeros((num_rows, num_cols))
shift7[:-1,1:] = noisy_img[1:, :-1]
# Shift down by one, left by one
shift8 = np.zeros((num_rows, num_cols))
shift8[1:,:-1] = noisy_img[:-1, 1:]
# Shift down by one, right by one
shift9 = np.zeros((num_rows, num_cols))
shift9[1:,1:] = noisy_img[:-1, :-1]
The np.stack
function can stack several arrays together into a new dimension:
stacked_img = np.stack([shift1, shift2, shift3,
shift4, shift5, shift6,
shift7, shift8, shift9])
print(stacked_img.shape)
Now compute median across axis 0
:
filtered_img = np.median(stacked_img, axis=0)
plt.imshow(filtered_img, cmap='gray',vmin=0, vmax=1)
Let's go back and time this new code.
t0 = time()
num_rows, num_cols = noisy_img.shape
shift1 = np.zeros((num_rows, num_cols))
shift1[:,:] = noisy_img[:,:]
# Shift down by one
shift2 = np.zeros((num_rows, num_cols))
shift2[1:, :] = noisy_img[:-1, :]
# Shift up by one
shift3 = np.zeros((num_rows, num_cols))
shift3[:-1, :] = noisy_img[1:, :]
# Shift left by one
shift4 = np.zeros((num_rows, num_cols))
shift4[:, :-1] = noisy_img[:, 1:]
# Shift right by one
shift5 = np.zeros((num_rows, num_cols))
shift5[:, 1:] = noisy_img[:, :-1]
# Shift up by one, left by one
shift6 = np.zeros((num_rows, num_cols))
shift6[:-1,:-1] = noisy_img[1:, 1:]
# Shift up by one, right by one
shift7 = np.zeros((num_rows, num_cols))
shift7[:-1,1:] = noisy_img[1:, :-1]
# Shift down by one, left by one
shift8 = np.zeros((num_rows, num_cols))
shift8[1:,:-1] = noisy_img[:-1, 1:]
# Shift down by one, right by one
shift9 = np.zeros((num_rows, num_cols))
shift9[1:,1:] = noisy_img[:-1, :-1]
stacked_img = np.stack([shift1, shift2, shift3,
shift4, shift5, shift6,
shift7, shift8, shift9])
filtered_img2 = np.median(stacked_img, axis=0)
t1 = time()
print('Execution time:', t1 - t0)
This new version is around ~100 times faster.
Can we clean up our code a little bit?
num_rows, num_cols = noisy_img.shape
for row_shift in [-1,0,1]:
for column_shift in [-1,0,1]:
shift = np.zeros((num_rows,num_cols))
shift[row_shift:, :] = noisy_img[:-row_shift]
# Needs some thought after class...