CS 467 - Assignment three

Due: 2020-03-12 1:30p

Goals

  • Up your random game by adding a statistical model to the mix
  • Visualize some of the implications of data structure and algorithm choice

Prerequisites

  1. Accept the assignment on our Github Classroom.
  2. Download the git repository to your local computer.

Assignment

In this assignment, you are going to continue the thread of the last assignment and again rip an image apart and then put it back together again. However, this time you are going to do it at a pixel level. In fact, you are going to construct a Markov chain that provides a statistical model of the relationship between colors in your image. More concretely, given a color from the original image, your model will know the probability of some other color being its immediate neighbor.

Markov chains, as you may know, are a state-based, stochastic model of possible sequences of events in which the probability of a particular event happening (and the accompanying state transition) is a function only of the current state. You will use this to paint your picture one pixel at a time. The state of the model tells you the current color to paint. As you walk across the canvas, advancing to the next pixel, you will pick the next color based on the probability of it being a neighbor of the current color.

As an example, given this input image (which is a basic visualization of hue and saturation in HSB):

Practical 3 raw image

This is an example of what you might get:

Practical 3 goal

Algorithm

There are two parts to this assignment: building the model, and painting the image.

Building the model

After loading the image, the algorithm for building the model is fairly basic. Under the hood, you need some way to store a collection of keys (states/colors) and associated values (the list of colors that have been observed in neighbors). While you could use a simple JavaScript object, I suggest you actually use the Map data structure.

To store colors, I suggest converting to a string representation of the form "rgb(0,0,0)". This can be passed to any of the p5.js color functions and is in a convenient form for making comparisons.

There is a bit of cleverness we can use to record statistical likelihood. Imagine you are on a pixel that is red (R), and its four neighbors are green (G), blue (B), green (G), and white (W). So, given R, there is a 50% chance that the next color will be G, a 25% chance that it will be B and a 25% chance that it will be W. You could record these percentages and update them as you found new instances of R. However, a simpler approach is to just put the colors in a list allowing for duplicates ([G, B, G, W]). Now, if you randomly choose an element from this list, the probability of choosing a particular color matches the observed probability. This does require significantly increased space, but provided you have the memory, it makes the math easier.

The algorithm is then fairly simple:

  • For each pixel in the image

    • Figure out the color of the pixel
    • Visit each of the neighbors of the pixel and get their colors
    • Add each neighboring color to the list associated with the current color

Painting the image

Having built the model, there is a variety of ways that you could make use of it. A naive approach would be to start in one corner and fill in the image row by row. However, this would result in a fairly boring image and there would only be correspondence along the row, but not between rows. Another approach would be to do a random walk around the canvas. This actually looks pretty interesting, but is not a terribly efficient way to fill the space. The most interesting seems to be to do a stack-based flood fill.

To implement this, you will need a stack (JavaScript arrays already have push and pop functions) and a way to keep track of when a pixel has been "visited" (like a 2D array of Booleans). The algorithm then looks like this:

  1. Make sure the stack is empty and set off of the pixels to "unvisited".
  2. Pick a random location on the canvas and a random color from your Markov chain.
  3. Mark the pixel as visited and push a record including position and color onto the stack (a JS object works great for this)
  4. While the stack is not empty

    1. Pop the top record off the stack
    2. Color the pixel
    3. For each unvisited neighboring pixel

      1. Pick a color using the Markov chain and the current color
      2. Mark the pixel as visited
      3. Add a record with position and color data to an array
    4. After all of the neighbors have been processed, shuffle the array
    5. Add all of the neighbors from the shuffled array to the stack

The shuffling of the neighbors is an important step (and yes, p5.js has a shuffle function). If you add them in the same order every time you will basically get a depth first search of the space and while it will be slightly more coherent than the totally naive "lawnmower" approach, the result will bear a strong resemblance.

Interestingly, if you use a queue instead of a stack, you will get a completely different (though still compelling) image.

Implementation details

You will find that in the template code, I've filled in a little bit more than the bare bones. Some of what I have given you is the image loading code that you are already familiar with. I have also sketched in the analysis function for you.

As you might imagine, visiting every pixel and building a map of color co-occurrence is a costly function. To help alleviate that I added a line that resizing the image down to 400 pixels wide (height is set automatically). If your computer is very slow, you may want to make this even smaller. While we are throwing out a lot of information, the dominate colors will still be similarly related.

Requirements

  • The Markov Chain should be built as described with information for at least four neighbors (top, bottom, left, and right).
  • The drawing algorithm using a stack should operate as described: every pixel should be colored based on the Markov model, and the order of the neighbors should be pushed on the stack in random order.
  • The draw function should not loop more often than needed to draw the image.
  • Sophisticated When the user types 'r', the image should randomly redraw (but the analysis shouldn't be run again).
  • Sophisticated When the user types 's', the image should be saved.
  • Sophisticated The drawing should be done dynamically so we can watch the progress (i.e., only color 100-200 pixels per run on the draw loop).
  • Sophisticated Add a selector to allow the user to switch between a stack or a queue for the image generation.

*A Sophisticated mark will be achieved by meeting all Sophisticated requirements*

You are, of course, welcome to innovate beyond these requirements if inspiration strikes.

Finishing up

Commit your changes to git and push them back up to GitHub. I will find them there.