For this assignment you should work with your assigned partner (partner assignments are posted on the course blog post that announces this assignment). Before you start working with your partner, you should read through the assignment yourself and think about the questions. Both partners must fully understand everything you turn in and submit for this. Even if you believe you are finished with the assignment because you have successfully completed all the questions, you are not finished until your partner also understands everything in it.
Remember to follow the pledge you read and signed at the beginning of the semester. For this assignment, you may consult any outside resources, including books, papers, web sites and people, you wish except for materials from previous cs1120, cs150, and cs200 courses. You may consult an outside person (e.g., another friend who is a CS major but is not in this class) who is not a member of the course staff, but that person cannot type anything in for you and all work must remain your own. That is, you can ask general questions such as “can you explain definitions to me?” or “how do lists work in Scheme?”, but outside sources should never give you specific answers to problem set questions. If you use resources other than the class materials, lectures and course staff, explain what you used in your turn-in.
You are strongly encouraged to take advantage of the scheduled office hours for this course. See the course site for the schedule.
A mosaic is a picture made up of lots of smaller pieces. The ancient Babylonians constructed elaborate mosaics using colored tiles. A photomosaic is a mosaic, except instead of using single color tiles for the tiles, it uses photographs. For some example photomosaics, see http://www.photomosaic.com/portfolio.html (and the cover of the course book).
Making a photomosaic is a big task. The way computer scientists (and most other people) solve big problems, is to break them into a few smaller sub-problems whose solutions can be combined to solve the original problem. This approach is sometimes called divide and conquer.
Note that for a big problem like making a photomosaic, doing this once is probably not enough. The sub-problems are still too big to solve easily. So, we need to use divide-and-conquer again to break each sub-problem into several sub-sub-problems. We do this recursively, until we get to sub-sub-sub-…-sub problems that are small enough that we can solve them easily.
We can describe this process with pseudocode (that is, not real Scheme, but made to look like it):
(define (solve-big-problem problem) (if (easy-to-solve? problem) ;;; if the problem is easy to solve, (solve problem) ;;; just solve it! (combine-solutions ;;; otherwise, combine the solutions (map solve-big-problem ;;; of solving each of the sub-problems (divide-problem problem))))) ;;; you get by dividing the original problem
Here, map is a function that applies another function to every item in a list. The (divide-problem problem) evaluates to a list of sub-problems. The value of (map solve-big-problem (divide-problem problem)) is the result of applying solve-big-problem to every sub-problem in that list.
Implementing solve, combine-solutions and divide-problem for a real problem is the difficult part. There is a real art to figuring out how to divide a big problem into a suitable set of smaller problems. Being able to do this well is the main thing that separates good problem solvers from mediocre ones.
Consider the following nine program fragments:
Any complete answer will receive full credit — even if your prediction is wrong. If you and your partner think the expression will evaluate differently, include both of your answers.
A (not too brilliant) kindergartner could follow these directions to make a photomosaic:
We will create a photomosaic using Scheme in almost the same way — except the computer is much dumber than our kindergartner, so we need to break the steps into even smaller problems before it can perform them. (Our computer won’t need to break for milk and cookies, but it will need to collect garbage.)
In step 7.a.i. the kindergartner has to look through the tile pictures to find one that best matches the color on the master picture under that rectangle. Your task for this problem set will be to define a function that does that.
First, we explain how the rest of the photomosaic program works (which is fairly similar to the kindergartner’s description above). We show lots of code here, but don’t worry if you don’t understand all of it. You should attempt to understand how it works, but it is not necessary to completely understand all the code shown here to complete this assignment.
We can divide the photomosaic problem into three big steps:
First, we provide some background on images. Then we solve step 2 first, and then steps 1 and 3. Often, it is easier to solve a problem by considering the sub-problems out of order.
Extracting the ps1.zip will create a ps1 subdirectory containing these files:
You should look at the mosaic.rkt (but don’t worry if you don’t understand much of the code in that file), but you will only need to change and turn in ps1.rkt. If you double-click on ps1.rkt, it should open in DrRacket. Click the Execute or Run button (or use ctrl+T) to interpret the definitions. DrRacket will split into two windows, with the Interactions Window on the bottom. If the language is set correctly, you should see:
Welcome to DrRacket, version 5.1.3 [3m].
Language: Pretty Big; memory limit: 128 megabytes.
in your Interactions Window.
All colors can be created by mixing different amounts of red, green and blue light — if you look closely at a television, you will see the whole image is made up of red, green and blue dots. We can represent a color using three numbers representing how much red, green and blue is in the pixel. For most computer images, there are a limited number of colors. In the images we will use, there are 256 different intensity values for each color. Hence, we can represent a color using three values between 0 and 255 to represent the amount of red, green, and blue in a pixel.
For example, (0 0 0) is black, (255 255 255) is white, (255 0 0) is red, (0 0 255) is blue, and (255 255 0) is
yellow (mixing red and green light makes yellow).
The file mosaic.rkt (included in the ps1.zip file you downloaded) defines some procedures for manipulating colors:
The file mosaic.rkt also defines some standard colors including:
(define white (make-color 255 255 255)) (define black (make-color 0 0 0)) (define red (make-color 255 0 0)) (define green (make-color 0 255 0)) (define blue (make-color 0 0 255)) (define yellow (make-color 255 255 0))
In your Interactions window, try evaluating (show-color red). You should see a small window appear containing a red rectangle:
We can say color 1 is brighter than color 2 if the sum of the red, green and blue values of color 1 is greater than the sum of the red, green and blue values of color 2. This may not correspond exactly to our intuitive perception of colors, but it should be pretty close.
(define (brighter? color1 color2) ;;; Fill in the code here that determines if color1 is brighter than color2 ;;; A good definition will need only a few lines of code. )
Hint: you may want to use functions such as get-red.
Note (added 30 Aug): The actual code in ps1.rkt is slightly different from this. It has a #f in the body of brighter? instead of just the comment. The reason for this is to make the definition grammatically valid. The grammar rule for a procedure body requires one or more expressions, so if there are no expressions there it would not be in the Scheme language and the interpreter would give an error. The #f is there is the template for ps1.rkt so it running the file before you solve this question does not cause an error. You should remove the #f that is there now and replace it with your definition. (Similarly for closer-color? in Question 5.)
Test your procedure by evaluating it with several different parameters. For example, you should get these results:
> (brighter? white red)
> (brighter? red white)
> (brighter? red red) ;; red is as bright as red, but not brighter
> (brighter? red blue)
> (brighter? yellow red)
A computer represents an image by a bitmap. A bitmap is a two-dimensional array of picture elements (pixels). Each pixel is just one dot of color. A bitmap is actually a dotmosaic, where the tiles are individual colored dots.
Instead of storing images as bitmaps, we usually store them using a compressed format such as a GIF or a JPG. Compressed images take up less memory on the disk or to download, but are harder to manipulate. Fortunately, DrRacket provides libraries that include functions for converting them to bitmaps so we don't need to worry about it. We can store our images in compressed formats and use library functions to convert them into bitmaps when we use them in our program.
The pixels in a bitmap are numbered on a grid. The top left corner is (0, 0). If the bitmap is w pixels wide and h pixels high, the bottom right corner is (w - 1, h - 1).
To select tile images for our photomosaic we follow steps 5 through 7 from the kindergartner's description. We don't need to worry about programming step 6 (naptime), but if you are working with a partner do keep remembering to switch places with your partner.
To find a tile image that matches a rectangle of the master image, we need to know the colors of both the tile images and rectangles in the master image.
To find a matching tile image, we need to know the "color" of each tile. One way would be to calculate the average color of each pixel in the tile image by adding the red, green and blue values for each pixel and dividing by the number of points. For example, if we wanted the average color of red (255 0 0) and blue (0 0
255) would sum the colors to get (255 0 255), and divide by two to get (127.5 0 127.5). This looks like purple, which is what you might expect:
|(255 0 0)||+||(0 0 255)||=||(255 0 255)||/ 2 =||(127 0 127)|
We can define a function that produces the sum of two colors by adding the red, green and blue components separately:
(define (add-color color1 color2) (make-color (+ (get-red color1) (get-red color2)) (+ (get-green color1) (get-green color2)) (+ (get-blue color1) (get-blue color2))))
To average many colors, we need to add all the colors together and then divide by the number of colors. One way to add a list of values together is to divide it into two simpler problems: add the first value to the sum of all the other values. That is,
SUM (a, b, c, d, e) = a + SUM (b, c, d, e)
We can do the same thing for the remaining values:
SUM (b, c, d, e) = b + SUM (c, d, e) SUM (c, d, e) = c + SUM (d, e) SUM (d, e) = d + SUM (e) SUM (e) = e + SUM () SUM () = 0
The only tricky part is knowing what to do at the end when there are no colors left. We define the SUM of no values to be 0. Similarly, when there are no more colors left, we evaluate to the 0-color (black).
This is how we define sum-colors:
(define (sum-colors color-list) (if (null? color-list) ;;; If there are no more colors in the list, (make-color 0 0 0) ;;; evaluate to the 0-color (black) (add-color ;;; Otherwise, evaluate to the result of adding (first color-list) ;;; the first color and (sum-colors ;;; the sum (rest color-list))))) ;;; of all the other colors.
To calculate the average color of a bitmap, we just calculate the sum of all the colors of the pixels in the bitmap, and divide by the number of pixels.
For the photomosaic code, we don't want to look at every single pixel though since there are millions of pixels in a large image. Instead, we sample points from the image by selecting points at regular intervals. We do this by defining generate-sample-points a function that selects the points to sample, and applying
average-colors to the result of mapping every sample point to the color of the bitmap point.
We need to divide the master image into tile-sized regions and calculate the average color of each region. This is almost the same as calculating the average color of a bitmap, except here we want the average color of lots of different regions within a single image.
Luckily we've already written a procedure that does almost what we need! We can use the generate-sample-points procedure to do this. To divide the master image, instead of just sampling the color of points, we generate a tile region with the sample point as its bottom left corner with the appropriate width and height.
We now know how to:
Suppose we know how to find the best tile for one region. All we need to do make the whole photomosaic is do that for every region of the master image.
So, we define select-mosaic-tiles to take as inputs (1) a list of lists of color samples for regions in the master image (one list for each row), and (2) a list of the tiles (where each tile is a list of a image name and the average color for that tile) and produce as its output a list of lists of images that describes the photomosaic.
Here's the rough code:
(define (select-mosaic-tiles samples tiles) (map2d find-best-match samples))
The map2d procedure (defined in mosaic.rkt) is like map, except it works on lists of lists. It applies the procedure find-best-match to every inner element in a list of lists.
We need to pass the tiles to find-best-match, though, and we also want to pass a function for matching colors. So, the actual select-mosaic-tiles code is:
(define (select-mosaic-tiles samples tiles color-comparator) (map2d (lambda (sample) (tile-name (find-best-match sample tiles color-comparator))) samples))
The find-best-match function goes through the list of tiles, and selects the tile that best matches the sample color for this rectangle.
The lambda means "make a procedure", so the expression evaluates to a procedure that takes one parameter (sample) which is the average color of the master image for this square. It evaluates (find-best-match sample tiles color-comparator) to find the tile in tiles that best matches the sample color, using the color-comparator procedure to compare two colors (this is the procedure you will need to write for the last question).
Finding the best match is like summing a list of colors — we can break it down into checking if the first one is better than the best one of the rest of the list. That is, if we had a function better-match that can determine which one of two possible tiles is a better match for a particular sample, we can use it like add-colors to find the best match from a list of tiles. As with add-colors, we have to be careful about what happens when there are no colors to add. It doesn't make sense to find the best match if there are no tiles, so we produce an error in that case. If there is only one tile, we know that is the best one. Otherwise, we compare the first tile with the best one we found from the rest of the tiles.
Here's the code:
(define (find-best-match sample tiles color-comparator) (if (null? tiles) ;;; If there are no tiles, (error "No tiles to match!") ;;; we lose. (if (= (length tiles) 1) ;;; If there is just one tile, (first tiles) ;;; that tile is the best match. (pick-better-match ;;; Otherwise, the best match is sample ;;; either the first tile in tiles, (first tiles) ;;; or the best match we would find (find-best-match ;;; from looking at the rest of the sample ;;; tiles. Use pick-better-match (rest tiles) ;;; to determine which one is better. color-comparator) color-comparator)))) (define (pick-better-match sample tile1 tile2 color-comparator) (if (color-comparator sample (tile-color tile1) (tile-color tile2)) tile1 tile2))
We still need to figure out how to create the list of tiles. Suppose we start with a list of filenames of images and want to turn that into a list of bitmaps to use as the tiles. How can we break that into simpler problems?
If we have a function that can load one tile image, then all we need to do is apply that function to every filename in the list of image filenames.
We can do that with (map get-one-image image-names). The function map applies a function (the first parameter) to every element in a list. We use this to define a function that takes a list of image filenames, and produces a list of the corresponding bitmaps:
(define (load-images image-names) (map get-one-image image-names))
Now, all we need are functions for getting a list of image filenames and for getting one image. We could just create the list of images by hand, but for a good photomosaic we need many images. So, we instead put all the tile images in a directory and use a function that gets a list of all files in a directory. DrRacket provides directory-list to do that. So, (load-bitmaps (directory-list)) would evaluate to a list of bitmaps for every file in the current directory. The actual code provided is a bit more complicated since we want to handle directories that contain files that are not images and we want the name of the directory to be a parameter (instead of using the current directory).
One way to display the photomosaic is to create a web page with all the tile images in the right places. The result of select-mosaic-tiles is a two-dimensional list of all the tile images in the photomosaic.
For example, suppose the best tiling for a 3x3 photomosaic (for now, we are just using names of colors for the tiles) is:
red red red white white white blue blue blue
Then, select-mosaic-tiles would evaluate to:
(("red" "red" "red") ("white" "white" "white") ("blue" "blue" "blue"))
We can display this using the divide-and-conquer approach: display the first row of images at the top, and then display the rest of the images below it. Displaying one row of images is similar: put the first image at the left of the screen, and then put the rest of the images to its right.
Our rough code is:
(define (display-tiles output-file tiles) (map display-one-row tiles))
We define rough code for display-one-row similarly:
(define (display-one-row output-file row) (map display-one-tile row))
The actual code is a little more complicated since we need to pass the output-file and to produce good photomosaics we also need some additional parameters (like the size of the tiles).
To produce web pages, we need to put some formatting codes before and after the tiles. (You shouldn't need to look at these codes.) So, we define procedures that print the necessary codes before and after the tiles (print-page-header and print-page-footer). The produce-tiles-page procedure first prints the page header, then displays all the tiles, and finally prints the page footer.
We need to combine all the steps now into one procedure that produces a photomosaic. Here it is:
(define (make-photomosaic master-image ;;; Filename for the "big" picture tiles-directory ;;; Directory containing the tile images tile-width tile-height ;;; Display width and height of the tiles ;;; (doesn't have to match actual size) sample-width sample-height ;;; Sample width and height (each tile covers this size ;;; area in master) output-filename ;;; Name of file to generate (.html) color-comparator) ;;; Function for comparing colors (produce-tiles-page output-filename (choose-tiles (get-one-image master-image) (load-tiles tiles-directory) sample-width sample-height color-comparator) tile-width tile-height))
We take parameters for the master image, directory containing the tiles, tile width and height, sample width and height, the output filename to generate, and the function to compare colors (which you will write).
We then call produce-tiles-page with the output filename, the mosaic tiles (calculated using choose-tiles, and the tile width and height parameters.
We're all done now, except for the procedure that determines which of two colors is a better match for another color. That is left for you to do!
Your function should look like,
(define (closer-color? ;;; The name of your function. sample ;;; The average color of the rectangle you want to match. color1 ;;; The average color of the first tile. color2) ;;; The average color of the second tile. ;;; A Scheme expression that evaluates to #t if color1 is a better ;;; match for sample, and #f otherwise. )
In addition to the standard arithmetic operators (+, *, -, /), comparison operators (<, >, =), provided procedures for manipulating colors (get-red, get-green and get-blue), some other procedures you may find useful include:
You should test out your closer-color? procedure using some simple colors:
> (closer-color? white white black) ;; white is closer to white than black is
> (closer-color? white black white) ;; black is not closer to white than white is
> (closer-color? red magenta red) ;; magenta is not closer to red than red is
> (closer-color? red orange blue) ;; orange is closer to red than blue is
Once your closer-color? procedure works for the simple examples, you can try to create a photomosaic using it by evaluating:
(make-rotundasaic output-filename closer-color?)
The value passed as output-filename should be the full pathname of a file you can create that does not already exist, for example to generate output in the file J:/cs1120/ps1/mosaic.html you would evaluate
(make-rotundasaic "J:/cs1120/ps1/mosaic.html" closer-color?)
(make-rotundasaic "mosaic.html" closer-color?)
Note (updated 1 September): see the comments below on the pathname problems. Just use a plain filename like "mosaic.html" (which needs to be a file that does not already exist), and it will put the result in the directory where you loaded the code from (probably your cs1120/ps1 directory. Sorry for the confusion on this!
If your closer-color? procedure is good, you should be able to recognize the rotunda when you open the mosaic.html file in a web browser. Experiment with different ways of deciding which two colors are closest to improve your photomosaic, but try simple things first. A good closer-color? procedure should produce a photomosaic similar to this one: sample.html. You can view the original Rotunda image here.
You have finished the assignment, but mosaics touch on many interesting and important computer science issues, some of which we will return to later in the course. Next, we highlight some of those connections, but don't worry if you don't understand these yet.
Higher Order Functions. We used map to apply a function to every element of a list. map is an example of a higher order function — that means it is a function that takes other functions as parameters. Higher order functions can be very powerful. In Problem Set 2 (and throughout the course), we will explore higher order functions and recursion.
Lists. Many functions in this assignment operated on lists. In Problem Sets 2 and 3, you will understand lists and implement functions that operate on lists (for example, you will be able to implement map).
Computational Complexity. Our program might use the same tile image over and over again. If there is a large section of the same color in our master image, the same tile image will be repeated for that whole section. This looks pretty bad. A real mosaic would never reuse the same tile. Producing a non-duplicating photomosaic is an extremely hard problem. In fact, for an arbitrary color comparison function, it is such a hard problem that no one knows for sure whether or not there is a fast solution (we'll explain exactly what we mean by fast here later in the course) to finding the
best photomosaic tiling. Computer scientists call this an NP-hard problem. If someone discovers a fast way to produce the best possible non-repeating photomosaic, then it means there are also fast solutions to many other seemingly unrelated and important problems (such as the "traveling salesperson problem" where the goal is to find the best route for a salesperson to visit a set of cities, and the "cure most deadly diseases problem" where the goal is to find a sequence of proteins that folds into a particular shape). We will explore this about halfway through the course.
Searching. Our program takes a long time to run for even small photomosaics. A really smart kindergartner wouldn't find a good match by looking through all the images scattered haphazardly on the floor; instead, she would sort the images into groups based on their color. Then she could easily see all the bluish images together, and pick the one that best matches a particular square. Our program has to consider every image for every rectangle. A better program would be able to quickly find the best image without having to consider every image every time. Computer scientists do a lot of work on finding better ways to search. It is estimated that Google's database contains about 50 Billion webpages. If it searched them the same way we search for images and could consider 10000 pages per second, it would take about 2 months to get a response to your query:
> (exact->inexact ;;; try evaluating it without this to see why it is here (/ (/ 50000000000 10000) (* 60 60 24))) 57.870370370370374
We will look at some better ways of searching in a few weeks. (Note that the possibility of making non-repeating photomosaics faster by sorting the tiles contradicts what we said in the previous paragraph about it being a hard problem! The reason is that the sorting is only possible if our color comparison function has some known structure.)
Concurrency. One thing to note about our problem division solution, is that some of the steps can be done in different orders, but others can only be done after an earlier step is already complete. For example, we can do steps 3, 4 and 5 before step 1, but we cannot do step 7 before step 1. If we have a whole class of kindergartners to help make our photomosaic, we could give each of them a magazine and have them all collect tile pictures at the same time. One way to make computers solve a problem quicker is to divide the problem into pieces that can be done at the same time, and have different computers work on each piece at the same time.
Photomosaic-mosaics. A photomosaic is a mosaic made with photo tiles. We could also make a photomosaic-mosaic, a mosaic made with photomosaic tiles. Of course, why stop there? We could make a photomosaic-mosaic-mosaic, a mosaic where the tiles are photomosaic-mosaics. If we have a lot of computing power, we could make a photomosaic-movie, a movie where the frames are photomosaics, or a moviemosaic-photo, a still image where the tiles are movies instead of still images. Here's a moviemosaic-movie (or perhaps more accurately, moviemosaic-mosaic-mosaic-movie) that has been seen by several billion people: http://www.youtube.com/watch?v=jBcTZWWzJxI.
Software Patents. Robert Silvers claims to have a patent (United States Patent 6,137,498: Digital composition of a mosaic image) on Photomosaics. We're not sure what the ancient Babylonians have to say about this, but software patents raise lots of complex legal issues. One would have to check with a lawyer to determine if you can redistribute your code from this problem set, but we encourage you to do so and would be happy to defend a test case against this patent.