CS71 Lab 5: Photobooth Filters

As far as the customer is concerned, the interface is the product --Jef Raskin


Due Monday, March 18, before midnight


The goal for this lab assignment are to


Getting started

By this point in the semester, everyone should have a github account. Most people are in teams of two for this assignment, so your repository should have the form

$ cd cs71
$ cd 03-photobooth-USER1-USER2.git
$ git pull

If you are working in a team, be professional. As part of your homework submission, you will be asked to fill out a performance evaluation of your teammate.


Photobooth Filters

In this assignment, you will implement a feature that allows the user to apply a post-processing filter to the composite images they build with Photobooth.

Your implementation should be based on the design documentation of the following sections. Please skim through the document before starting.


User interface overview

You will add a new menu to the menu bar, called "Filters". This menu should have options for


User stories

Each user story represents a feature that the application should support. Implementing a user story may require modifying several classes. You should test each user story before implementing a new one.

1. Applying a filter

The user can apply a filter to the current image by clicking a filter type from the menu's Filter menu. This will modify the current compoiste image based on the filter. If the user edits layers after applying a filter, the filter will be removed and the user will have to reapply it again.


Architecture overview

The filter architecture uses a Factory pattern to create filters of different types. Furthermore, our architecture will use static creator methods to allow new filters to be added to application without changing existing code!

You will implement the following new classes:

Containership Diagram

Containership Diagram

Specifications

You should extend the API of CompositeModel to encapsulate the FilterFactory API. In other words, the MainWindow does not need to know that CompositeModel uses FilterFactory to manage its filters. MainWindow should only use the methods in CompositeModel to implement the filter menu.

// New private data members
FilterFactory _filters = new FilterFactory();

// property: get only
// Return the names of the filters contained in the filter factory
public List<string> FilterNames

// requires: nothing
// effects: Applies the filter corresponding to name to the composite image 
//   and invokes callbacks to notify that the composite image has changed
//   Does nothing if name does not correspond to a valid filter
public void RunFilter(string name)

// Wrapper function for FilterFactory.RegisterFilter
public void RegisterFilter(string name, Filter.CreateFn fn)

// Wrapper function for FilterFactory.DeregisterFilter
public void DeregisterFilter(string name)

The API for FilterFactory should look like

// private variables
Dictionary<string, Filter.CreateFn> _filters;

// default constructor
public FilterFactory()

// effects: associates the filter creator function with name 
//   replaces the existing creator if one exists
public void RegisterFilter(string name, Filter.CreateFn fn)

// effects: removes the CreatorFn corresponding to name, if one exists; 
// otherwise, does nothing
public void DeregisterFilter(string name)

// effects: returns a new filter corresponding to name or null if none exists
public Filter Create(string name)


// effects: Returns the list of names corresponding to each filter
public List<string> GetFilterNames()

The API for Filter is below. See Filter.cs for the implementation as well as an example child class called NoneFilter.

public abstract class Filter
{
     // A struct representing the color of a pixel
     // Pixels consist of red, green, blue color components
     protected struct RGB

     // static creator method for creating filters
     // subclasses should define their own CreateFn which returns a 
     // new instance of the Filter
     public delegate Filter CreateFn();

     // requires: valid buffer
     // effects: creates a new Pixbuf modified according to the filter
     public abstract Gdk.Pixbuf Run(Gdk.Pixbuf buffer);

     // requires: 0 <= row < Width; 0 <= col < Height
     // effects: returns RGB with the color of the pixel
     protected static RGB GetPixel(Gdk.Pixbuf buffer, int row, int col)

     // requires: 0 <= row < Width; 0 <= col < Height
     // effects: sets the RGB value of buffer to the color of the given pixel
     protected static void SetPixel(Gdk.Pixbuf buffer, int row, int col, RGB color)
}

// Subclass of Filter; implements a NoOp
public class NoneFilter : Filter
{
     public static Filter Create()
     {
         return new NoneFilter();
     }

     public override Gdk.Pixbuf Run(Gdk.Pixbuf buffer)
     {
         Gdk.Pixbuf result = buffer.Copy();
         return result;
     }
}

When you initialize the CompositeModel, you should call CompositeModel.RegisterFilter to register a filter with the application. For example, to register the NoneFilter, you would call

_compositeModel.RegisterFilter("None", NoneFilter.Create);

The above call should register the filter's creator with composite model's filter factory. When you initialize the menu in MainWindow, you should use CompositeModel.FilterNames to create options for all the registered filters. This type of architecture is an example of a plugin API and can be extended to load new types of filters without recompiling the Photobooth application!

Operational Specifications

You are asked to implement four types of filters for this assignment

This section describes the algorithms needed to compute each filter

Grayscale Filter

for each row from 0 to Height
    for each col from 0 to Width
        get the pixel (row, col) # Call base class function
        gray = (pixel.red + pixel.green + pixel.blue)/3
        set the pixel (row, col) to RGB(gray,gray,gray) # Call base class function

Lighten Filter

for each row from 0 to Height
    for each col from 0 to Width
        get the pixel (row, col) # Call base class function
        red = Clamp(pixel.red + 100, 0, 255)
        green = Clamp(pixel.green + 100, 0, 255)
        blue = Clamp(pixel.blue + 100, 0, 255)
        set the pixel (row, col) to RGB(red,green,blue) # Call base class function

Darken Filter

for each row from 0 to Height
    for each col from 0 to Width
        get the pixel (row, col) # Call base class function
        red = Clamp(pixel.red - 100, 0, 255)
        green = Clamp(pixel.green - 100, 0, 255)
        blue = Clamp(pixel.blue - 100, 0, 255)
        set the pixel (row, col) to RGB(red,green,blue) # Call base class function

Jitter Filter

for each row from 0 to Height
    for each col from 0 to Width
        get the pixel (row, col) # Call base class function
        red = Clamp(pixel.red + random number between -100 and 100, 0, 255)
        green = Clamp(pixel.green + random number between - 100 and 100, 0, 255)
        blue = Clamp(pixel.blue + random number between - 100 and 100, 0, 255)
        set the pixel (row, col) to RGB(red,green,blue) # Call base class function

Setup and event flows

You should register the filter creators with CompositeModel after you create it. For most of you, this will be in the constructor of MainWindow. You should register the creators before you set up the menu because you will need to registered names to set the menu options.

For this design, all callbacks should be in your class MainWindow.

Generic event chain for Menubar events


Tips and tricks

1. How can I can I figure out how to use the widgets from GTK#?

The API documentation is here. The class examples repository has several example programs under /gui to get you started with small programs.

Below are links to some widgets you'll likely need for your assignment.

2. I'm getting unexpected colors. What might be wrong?

Byte only fits values between 0 and 255, so if have the potential overflow if you have values out of that range. When you add and subtract byte types, it is converted to an integer. You should Clamp the results between 0 and 255 before converting back to byte again.

3. How do I implement the factory pattern I need for this assignment?

The simplest version of a factory hard-codes the constructors needed to istatiate a type within a function (AnimalFactory1.cs in the course examples).

For this assignment, you should implement a factory that manages static creator methods. This technique allows a user to implement new filters without changing the implementation of
photobooth. In your class examples repository, this approach is implemented in AnimalFactory2.cs).


BONUS: Try implementing more filters of your choice!


Grading Rubric

Your implementation of CompositeModel and FilterFactory will be tested against a new subclass of Filter.


(5 points) Turning in your labs

Update the README.md file with any feedback on the assignment, how long it took you to do, and what you found most challenging.

Please fill out a performance evaluation of your teammate.

Use git to hand-in your assignment.

$ git status
$ git add .
$ git status
$ git commit -m "description of my awesome work"
$ git push

NOTES