Homework 6 uses class design and inheritance to construct a set of interchangable image filters.
At a high level, you will create an image class like in the in-class exercise on image processing and a set of filter classes that can be passed to the image class and applied to the image.
You will be graded on the following rubric:
NOTE While you may work in groups to discuss strategies for solving these problems, you are expected to do your own work. Excessive similarity among answers may result in punitive action.
Write a class called Filter
. This class will provide the base class for the filters you will write in the next part and represents an identify filter (i.e., this filter simply makes a copy of the image). This class should have no initializer arguments and the following method:
apply(img_np_array)
that simply takes a NumPy image array as an argument and returns this array unchanged (this is the identity filter).%matplotlib inline
import numpy as np
import pandas as pd
from scipy import misc
import matplotlib.pylab as plt
# Implement the Filter class here
class Filter:
def apply(self, img_np_array):
return img_np_array
Write a class called Image
. This class represents an image created from a file or created from a filter. You should create an initializer that takes one of two arguments:
filename
that the initializer will use with misc.imread()
to read that file into an instance variable, ORimg_data
that is a NumPy array representing an image that will get stored in the same instance variable in which you would save the result of misc.imread()
.Your Image
class should also have the following methods:
get_image_data()
that returns the image data you storedshow()
that will display the image on the screensave(filename)
that will save your image to a file using misc.imsave()
apply_filter(filter)
that will take an instance of the Filter
class you wrote above, apply it to a copy of this instance's image data, create a new Image
instance using the returned data, and return this new Image
object. That is, create a copy of this instance's image data using np.copy()
, call filter.apply()
on this copy, and create a new Image
from the result using the img_data
initializer argument.# Implement the Image class here
class Image:
def __init__(self, filename=None, img_data=None):
self.img_data = None
if ( filename ):
self.img_data = misc.imread(filename)
else:
self.img_data = img_data
assert(self.img_data != None)
def get_image_data(self):
return self.img_data
def show(self):
plt.imshow(self.img_data)
plt.show()
def apply_filter(self, filter_obj):
copied_img = np.copy(self.img_data)
copied_img = filter_obj.apply(copied_img)
return Image(img_data=copied_img)
def save(self, filename):
misc.imsave(filename, self.img_data)
The following code will test your Image
and Filter
classes. You should be able to read in a file using your Image
class, display it, apply a filter to it, and save the result (which should look identical to the original image).
identity_filter = Filter()
test_img = Image(filename="watchmen.jpg")
test_img.show()
filtered_img = test_img.apply_filter(identity_filter)
filtered_img.show()
filtered_img.save("watchmen-filter.jpg")
For this part, you will implement four subclasses of the Filter
class. For these subclasses, you will modify the apply(img_np_array)
function to create the following four filters:
Black and White Filter (BWFilter
) - This filter will convert an image into black and while by taking the mean of the red, green, and blue channels, and assigning each channel in the filtered image to this mean.
Sepia Filter (SepiaFilter
) - This filter will apply a sepia (yellowish) tone to the image. To apply this filter, use the following code to update each channel, where red, green, and blue are the original channel values in each pixel:
new_red = min(255, (red * 0.393 + green * 0.769 + blue * 0.189))
new_green = min(255, (red * 0.349 + green * 0.686 + blue * 0.168))
new_blue = min(255, (red * 0.272 + green * 0.534 + blue * 0.131))
Vignette (VigFilter
) - This filter will black out the corners of the image and create a circular black frame around the center of the image. To create this vignette, use the following code:
This code will create the following effect:
# get the height and width of the image
lx, ly = img_data.shape[:2]
# Create a w x h grid
X, Y = np.ogrid[0:lx, 0:ly]
# Find all pixels further than some radius
# away from the center of the image
mask = (X - lx / 2) ** 2 + (Y - ly / 2) ** 2 > lx * ly / 4
# Make all pixels in this mask black
img_data[mask] = np.array([0]*3, dtype=np.uint8)
CustomFilter
) - Implement a final filter of your own choice. Examples include zeroing the red and green channels or boosting the blue channel. NOTE: Pixel channels max out at 255, so be sure and account for these boundaries if you're doubling a channel.# Implement here
class BWFilter(Filter):
def apply(self, img_np_array):
# Go through each pixel and set each channel to the average across
# all channels
for i in range(img_np_array.shape[0]):
for j in range(img_np_array.shape[1]):
pixel = img_np_array[i, j, :] # Get this pixel
average = np.mean(pixel) # Calculate its average
# set the pixel to a numpy array containing this average
# and using the data type 8-bit unsigned int
img_np_array[i, j] = np.array([average] * 3, dtype=np.uint8)
return img_np_array
class SepiaFilter(Filter):
def apply(self, img_np_array):
# Go through each pixel and set each channel to the average across
# all channels
for i in range(img_np_array.shape[0]):
for j in range(img_np_array.shape[1]):
pixel = img_np_array[i, j, :] # Get this pixel
red = pixel[0]
green = pixel[1]
blue = pixel[2]
new_red = min(255, (red * 0.393 + green * 0.769 + blue * 0.189))
new_green = min(255, (red * 0.349 + green * 0.686 + blue * 0.168))
new_blue = min(255, (red * 0.272 + green * 0.534 + blue * 0.131))
img_np_array[i, j] = np.array([new_red, new_green, new_blue], dtype=np.uint8)
return img_np_array
class VigFilter(Filter):
def apply(self, img_data):
# get the height and width of the image
lx, ly = img_data.shape[:2]
# Create a w x h grid
X, Y = np.ogrid[0:lx, 0:ly]
# Find all pixels further than some radius
# away from the center of the image
mask = (X - lx / 2) ** 2 + (Y - ly / 2) ** 2 > lx * ly / 4
# Make all pixels in this mask black
img_data[mask] = np.array([0]*3, dtype=np.uint8)
return img_data
class CustomFilter(Filter):
def apply(self, img_np_array):
# Go through each pixel and set each channel to the average across
# all channels
for i in range(img_np_array.shape[0]):
for j in range(img_np_array.shape[1]):
pixel = img_np_array[i, j, :] # Get this pixel
red = pixel[0]
green = pixel[1]
blue = pixel[2]
new_red = red
new_green = green
new_blue = min(255, (blue * 1.5))
img_np_array[i, j] = np.array([new_red, new_green, new_blue], dtype=np.uint8)
return img_np_array
bw_filter = BWFilter()
sepia_filter = SepiaFilter()
vig_filter = VigFilter()
custom_filter = CustomFilter()
# Show image
test_img = Image(filename="cinematic.jpg")
test_img.show()
# Apply black and white filter
filtered_img = test_img.apply_filter(bw_filter)
filtered_img.show()
filtered_img.save("cinematic-bw.jpg")
# Apply sepia filter
filtered_img = test_img.apply_filter(sepia_filter)
filtered_img.show()
filtered_img.save("cinematic-sepia.jpg")
# Apply vignette filter
filtered_img = test_img.apply_filter(vig_filter)
filtered_img.show()
filtered_img.save("cinematic-vig.jpg")
# Apply custom filter
filtered_img = test_img.apply_filter(custom_filter)
filtered_img.show()
filtered_img.save("cinematic-custom.jpg")