INST326 Homework 6

20170718, Image Filters

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:

  • Class Design: You will be asked to design classes around images like we did in class. Your classes adhere to the specifications provided and make sense given what we've learned in class and in the Py3OOP book and should have both attributes and behaviors.
  • Documentation: Your code should be sufficiently documented so as to demonstrate you have an understanding of what your code is doing. This point is especially important if you are citing code from the Internet.
  • Execution: Your code should at least execute. If your code does not run, you will get no points for this criteria.
  • Correctness: Your code should implement the specifications provided correctly.
  • Efficiency: While not a major factor, be prepared to have points counted off if your code is extremely inefficient (e.g., looping through a list without apparent need).

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.

Part 1. Writing Image and Filter Classes

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).
In [2]:
%matplotlib inline
import numpy as np
import pandas as pd
from scipy import misc
import matplotlib.pylab as plt
In [3]:
# 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:

  • An argument called filename that the initializer will use with misc.imread() to read that file into an instance variable, OR
  • An argument called img_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 stored
  • show() that will display the image on the screen
  • save(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.
In [4]:
# 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)

Test Cases

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).

In [5]:
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")
/Users/cbuntain/Development/thirdparty/anaconda3/lib/python3.6/site-packages/ipykernel/__main__.py:12: FutureWarning: comparison to `None` will result in an elementwise object comparison in the future.

Part 2. Interesting Filters

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)

  • A Filter of Your Choice (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.
In [6]:
# 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

Test Cases

In [7]:
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")
/Users/cbuntain/Development/thirdparty/anaconda3/lib/python3.6/site-packages/ipykernel/__main__.py:12: FutureWarning: comparison to `None` will result in an elementwise object comparison in the future.