Fundamentals of Computer Science I: Media Computing (CS151.01 2008S)

Laboratory: Drawing with Palettes


Summary: In this laboratory, you will explore the use of palettes to more efficiently store images and the effects the approximations such palettes require have on images.

Preparation

a. This laboratory requires a wide variety of procedures, some introduced in this reading, some introduced in previous readings, and some of which you've written. We have reproduced all of those procedures at the end of this reading. Copy them into your definitions pane (or your library).

b. We'll do some work with reading and writing images. Create a 10x10 image called canvas for such experimentation and fill it with random colors.

> (define canvas (image-new 10 10))
> (image-random-fill! canvas)

c. We'll also do some work with existing images. Load a SMALL image of your choice and call it picture.

Exercises

Exercise 1: Experiments with the Simple Encoding

As you may recall from the reading, rgb->approx approximates an RGB color by converting it to a number between 0 and 255 that represents a nearby color. Another function, approx->rgb converts the number to that nearby color.

a. Pick five colors and ensure that rgb->approx converts each to a number between 0 and 255. Try to pick colors that are not at the extremes (that is, that have components whose values are not just 0 and 255). You may find it easiest to do this process with map.

> (map (o rgb->approx cname->rgb) (list "red" ....))

b. Convert those approximations back to RGB colors with approx->rgb. Then, compare those values to the original colors. You might want to try something like the following:

> (define colors (map cname->rgb (list "red" ...))
> (map rgb->string colors)
> (map rgb->cname colors)
> (define encodings (map rgb->approx colors))
> (define approximations (map approx->rgb encodings))
> (map rgb->string approximations)
> (map rgb->cname approximations)

c. What do you notice anything special about the component values of these various colors?

d. Note that we can approximate an RGB color by encoding it and then decoding it. Write a procedure, rgb-approximate, that does just that. (Note that rgb-approximate takes as input an RGB color and returns as output an RGB color.)

e. Using image-variant, apply rgb-approximate to canvas (which should contain random colors).

> (define approximated (image-variant rgb-approximate canvas))
> (image-show approximated)

f. How good is the approximation? What could you do to improve the approximation?

g. Using image-variant, apply rgb-approximate to picture.

h. Does this experiment change your perspective on the success of the approximation?

Exercise 2: Rainbow Palettes

As you may recall, palette-encode-color encodes a color using a color palette (a list of colors) and palette-decode-color decodes a color using a palette.

a. Add the following definition of a simple color palette to your definitions window.

(define rainbow-plus
  (list color-red
        color-orange
        color-yellow
        color-green
        color-blue
        color-indigo
        color-violet
        color-black
        color-white))

b. Using that palette, encode the same colors you encoded in exercise 1. Are these encodings better or worse? Why?

c. Write a procedure, (palette-select palette color), that selects the color in palette used to represent color.

d. See what happens if you apply that procedure to canvas with

(image-variant canvas (lambda (color) (palette-select rainbow-plus color)))

What does that tell you about the usefulness of this palette?

e. See what happens if you apply that procedure to picture. What does that tell you about the usefulness of this palette?

Exercise 3: Palettes from Images

Clearly, using a small palette of fairly restricted colors is not our best approach for approximating an image. What can we do instead? We can choose colors that already appear in an image. Consider the following procedure.

;;; Procedure:
;;;   image-sample-colors
;;; Parameters:
;;;   image, an image
;;; Purpose:
;;;   Grabs a sample of the colors from image.
;;; Produces:
;;;   colors, a list of colors
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   Every color in colors appears somewhere in image.
(define image-sample-colors
  (lambda (image)
    (let ((width (image-width image))
          (height (image-height image)))
      (let kernel ((col 0)
                   (row 0)
                   (colors null))
        (cond
          ; If we've gone beyond the last row, we're done, so return 
          ; the accumulated list of colors
          ((>= row height)
           colors)
          ; If the column is beyond the last column, we need to move on
          ; to a new row. 
          ((>= col width)
           (kernel (- col width) (+ row 1) colors))
          ; Okay, we have a position somewhere in the image.  Grab the color
          ; and go on to the next position.
          (else
           (kernel (+ col 411)
                   row
                   (cons (image-get-pixel image col row) colors))))))))

a. What do you expect the result to look like if you use image-sample-colors to sample colors from portrait?

b. Check your answer experimentally.

c. What do you expect the result to look like if you use image-sample-colors to sample colors from canvas? About how many colors do you expect to be in the list?

d. Check your answer experimentally.

e. As you've probably noticed, the number of colors that image-sample-colors returns depends on the size of the image it is working with. Why? Because it samples every 411th pixel. We could, of course, change the 411 to some smaller number when we were sampling smaller images, but it would be painful to have to change image-sample-colors each time we wanted to work with a different image. Instead, we could add a parameter, n to represent the number of samples to take (i.e, the size of the palette). We could then use n to compute the “distance” between samples. (That is, replace 411 with a formula that involves width, height, and n so that we get approximately n colors selected.)

Make those changes.

Exercise 4: Palettes from Images, Revisited

a. Using your modified version of image-sample-colors, create palettes of size 2, 4, 8, and 16 from picture.

> (define palette-2 (image-sample-colors picture 2))
> (define palette-4 (image-sample-colors picture 4))
> (define palette-8 (image-sample-colors picture 8))
> (define palette-16 (image-sample-colors picture 16))

b. Here's a procedure, (palette-map palette image), which “maps” a palette onto an image by converting each color in the image to the closest color in the palette.

;;; Procedure:
;;;   palette-map
;;; Parameters:
;;;   palette, a color palette
;;;   image, an image
;;; Purpose:
;;;;  Compuate a new image by converting each pixel in image to the
;;;   nearest color in palette.
;;; Produces:
;;;   new-image, an image
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   Each pixel in new-image is in palette.
;;;   new-image is the same width and height as image.
;;;   For each column and row in image, (image-get-pixel new-image row col) is
;;;     relatively close to (image-get-pixel image row col).
;;;     (In particular, it is at least as close as any other color
;;;     in palette).
(define palette-map
  (lambda (palette image)
    (image-variant image (lambda (color) (rgb-closest color palette)))))

Using that procedure, map each of your palettes onto canvas. (Yes, although we created the palettes with picture, you should see what happens with canvas.)

d. As you may recall, image-variant can take a while with larger images. Let's see the effect of palette size on the amount of time it takes to apply a palette to picture First, map palette-2 onto picture and, using a clock or by counting one-Mississippi two-Mississippi, determine approximately how many seconds it takes. Next, map palette-4 onto picture and, using a clock, determine approximately how many seconds it takes. Finally, predict how long it will take to map palettes of size 8 and 16. If you have time, check those results experimentally.

e. As you may have noted, the bigger the palette, the slower it is to compute a variant. At the same time, it is likely that the bigger the palette, the better the approximation. To test this hypothesis, we'll need a relatively small image to work with. However, we want a sensible image, rather than a random image. Pick a 64x64 square from picture, copy it, create a new 64x64 image called selection, and paste into that image.

f. Map palette-16 onto selection. Do you consider that a reasonable approximation?

g. Create a new palette, selection-16, by selecting 16 colors from selection, rather than from picture. Then, map that palette onto selection. Is the approximation better? Worse?

Exercise 5: Storing with Palettes

Although we've been exploring palettes by looking at what happens when we use them interactively with images, most frequently palettes are used when saving and restoring files.

a. Write (palette-save-image palette image filename), which saves an image to a file. The procedure should use the following format for the file:

  • The width of the image.
  • The height of the image.
  • The colors in the image, represented as indexes into the palette.

In writing this procedure, you may want to use image-write-pixmap as a pattern for how to write an image to a file.

;;; Procedure:
;;;   image-write-pixmap
;;; Parameters:
;;;   image, an image
;;;   filename, a string
;;; Purpose:
;;;   Writes the pixmap information on image to the file.
;;; Produces:
;;;   [Nothing; Called for the side effect.]
;;; Preconditions:
;;;   filename is a valid file name.
;;; Postconditions:
;;;   The file named by filename now contains a sequence of integers,
;;;   one for each RGB color in image.
(define image-write-pixmap
  (lambda (image filename)
    (let ((width (image-width image))
          (height (image-height image))
          (port (open-output-file filename)))
      (write width port)
      (newline port)
      (write height port)
      (newline port)
      (let kernel ((col 0)
                   (row 0))
         (cond
           ((>= row height)
            (close-output-port port)
            image)
           ((>= col width)
            (kernel 0 (+ row 1)))
           (else
            (rgb-write (image-get-pixel image col row) port)
            (kernel (+ col 1) row)))))))

b. Write (palette-load-image palette filename), which loads an image from a file, assuming that the image was stored using the same palette. In writing this procedure, you may want to use image-read-pixmap as a pattern for how to read an image from a file.

;;; Procedure:
;;;   image-read-pixmap
;;; Parameters:
;;;   filename, a string
;;; Purpose:
;;;   Read pixmap data from the specified file, returning a new
;;;   image from that data.
;;; Produces:
;;;   image, an image.
;;; Preconditions:
;;;   filename names a file.
;;;   That file was created by image-write-pixmap.
;;; Postconditions:
;;;   image contains teh same colors in the same positions as the image 
;;;   previously written with image-write-pixmap.
(define image-read-pixmap
  (lambda (filename)
    (let* ((port (open-input-file filename))
           (width (read port))
           (height (read port))
           (image (image-new width height)))
      (let kernel ((col 0)
                   (row 0))
         (cond
           ((> (+ col row) (+ (- width 1) (- height 1)))
            (let ((next-color (rgb-read port)))
              (close-input-port port)
              (if (not (eof-object? next-color))
                  (throw "image-read-pixmap!: Data remain in file after image was competely read")))
            image)
           ((eof-object? (peek-char port))
            (close-input-port port)
            (throw "image-read-pixmap!: Premature end of file."))
           ((>= col width)
            (kernel 0 (+ row 1)))
           (else
            (let ((next-color (rgb-read port)))
              (cond 
                ((eof-object? next-color)
                 (close-input-port port)
                 (throw "image-read-pixmap!: Premature end of file."))
                (else
                 (image-set-pixel! image col row next-color)
                 (kernel (+ col 1) row))))))))))

For Those With Extra Time

Extra 1: More Efficient Encoding

One disadvantage of the current version of palette-encode-color is that it ends up scanning the list of colors twice: once to find the closest color and once to find the index of that color. We might instead write a procedure that does the two activities simultaneously.

The following provides one implementation of that technique.

;;; Procedure:
;;;   rgb-index-of-closest
;;; Parameters:
;;;   color, an RGB color
;;;   colors, a list of RGB colors
;;; Purpose:
;;;   Determine the index of the  element of colors that is closest to
;;;   color
;;; Produces:
;;;   index, an integer
;;; Preconditions:
;;;   colors is nonempty.
;;; Postconditions:
;;;   0 <= index < (length colors)
;;;   (rgb-distance color (list-ref colors index)) <= (rgb-distance color c)
;;;     for all c in colors
(define rgb-index-of-closest
  (lambda (color colors)
    (let kernel ((remaining-colors (cdr colors))
                 (closest-so-far (car colors))
                 (index-of-csf 0)
                 (skipped 1))
      (cond
        ((null? remaining-colors)
         index-of-csf)
        ((< (rgb-distance color (car remaining-colors))
            (rgb-distance color closest-so-far))
         (kernel (cdr remaining-colors)
                 (car remaining-colors)
                 skipped
                 (+ skipped 1)))
        (else
         (kernel (cdr remaining-colors)
                 closest-so-far
                 index-of-csf
                 (+ skipped 1)))))))

a. Confirm, using a variety of examples, that it seems to work correctly.

b. Explain, in your own words, how this procedure works.

c. Rewrite palette-encode-color to use rgb-index-of-closest.

d. Rerun one of your timing tests to determine whether this change had any effect.

Extra 2: Storing Palettes

Reproducing the saved image accurately requires that our image file include not just the indices into the palette, but also the palette itself. Rewrite palette-save-image and palette-load-image to use this technique.

You may find the following procedures helpful.

;;; Procedure:
;;;   rgb-list-write
;;; Parameters:
;;;   colors, a list of RGB colors
;;;   outport, an output port
;;; Purpose:
;;;   Writes the elements of colors to outport.
;;; Produces:
;;;   [Nothing; called for the side effect]
;;; Preconditions:
;;;   outport is open for writing
;;; Postconditions:
;;;   The file associated with outport now contains all the colors in
;;;   colors.
(define rgb-list-write
  (lambda (colors outport)
    (foreach! (lambda (color) (rgb-write color outport))
              colors)))

;;; Procedure:
;;;   rgb-list-read
;;; Parameters:
;;;   source, an input port
;;;   n, a non-negative integer
;;; Purpose:
;;;   Reads n RGB colors from source, returning them as a list.
;;; Produces:
;;;   colors, a list of RGB colors
;;; Preconditions:
;;;   source is open for reading
;;; Postconditions:
;;;   colors contains the first n colors in source.
(define rgb-list-read
  (lambda (source n)
     (if (> n 0)
         (cons (rgb-read source)
               (rgb-list-read source (- n 1)))
         null)))

Extra 3: Better Palettes

Some disadvantages of the current mechanism for selecting palettes based on an image are (1) it may result in duplicate colors; (2) it may contain a variety of close colors and, but doing so, approximate some colors better than others. An alternative is to over-sample the image, getting about twice as many colors as you want in the palette, and then to hone the palette, removing or averaging nearby colors. Write a procedure that does just that.

a. Write a procedure, (palette-slightly-improve palette) that improves the palette by replacing one pair of nearby colors with their average.

b. Write a procedure, (palette-improve palette n), that repeatedly calls palette-slightly-improve until there are n colors left.

c. Determine experimentally whether this technique makes better palettes.

Explorations

Exploration 1: Switching Palettes

You can obtain some very interesting effects by encoding an image using one palette and then decoding it using another.

a. Define two palettes, each with sixteen colors. Name them pal and ette.

b. Rather than storing the image to a file and then reloading it using the new palette, we will use image-variant to build a new version of the image. modify the image.

> (image-variant (lambda (color) (palette-decode palette (palette-encode palette color)))
              canvas

Notes

Useful Procedures

; +--------------------------+----------------------------------------
; | Approximating RGB Colors |
; +--------------------------+

;;; Procedure:
;;;   rgb->approx
;;; Parameters:
;;;   color, an rgb color
;;; Purpose:
;;;   Convert color to an encoded approximation.
;;; Produces:
;;;   approx, an approximation
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   0 <= approx < 256
;;;   (approx->rgb approx) is relatively near color.
(define rgb->approx
  (let ((scale (/ 5 255)))
    (lambda (color)
      (inexact->exact (+ (* 36 (round (* scale (rgb-red color))))
                      (* 6 (round (* scale (rgb-green color))))
                      (round (* scale (rgb-blue color))))))))

;;; Procedure:
;;;   approx->rgb
;;; Parameters:
;;;   approx, an integer
;;; Purpose:
;;;   Convert an encoded approximation to a color.
;;; Produces:
;;;   color.
;;; Preconditions:
;;;   0 <= approx < 256
;;;   approx was created by rgb->approx
;;; Postconditions:
;;;   color is close to the original value
;;;   (approx->rgb color) = approx.
(define approx->rgb
  (let* ((scale (/ 255 5))
         (component (lambda (c) (round (* scale c)))))
    (lambda (approx)
      (rgb-new (component 
                (/ (- approx (remainder approx 36)) 36))
               (component 
                (/ (- (remainder approx 36) (remainder approx 6)) 6))
               (component
                (remainder approx 6))))))

; +------------------------+------------------------------------------
; | Palette-Based Encoding |
; +------------------------+

;;; Procedure:
;;;   palette-encode-color
;;; Parameters:
;;;   palette, a nonempty list of RGB colors
;;;   color, an RGB color
;;; Purpose:
;;;   Determine a number that appropriately encodes color using
;;;   the given palette.
;;; Produces:
;;;   encoding, an integer
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   0 <= encoding < (length palette)
;;;   (rgb-distance color (list-ref palette encoding)) <=
;;;     (rgb-distance color (list-ref palette i))
;;;     for all i, 0 <= i < (length palette)
(define palette-encode-color
  (lambda (palette color)
    (list-index-of palette (rgb-closest color palette))))
 ;;; Procedure:
;;;   palette-decode-color
;;; Parameters:
;;;   palette, a nonempty list of RGB colors
;;;   code, an integer
;;; Purpose:
;;;   Converts an encoding (created by palette-encode-color) back
;;;   to a color.
;;; Produces:
;;;   color, an RGB color
;;; Preconditions:
;;;   0 <= code < (length palette)
;;;   code was created with (rgb-encode palette some-color)
;;; Postconditions:
;;;   color is close to some-color
(define palette-decode-color
  (lambda (palette code)
    (list-ref palette code)))

; +---------------------+---------------------------------------------
; | Reading and Writing |
; +---------------------+

;;; Procedure:
;;;   rgb-write
;;; Parameters:
;;;   color, an rgb color
;;;   port, the port to which to write the color
;;; Purpose:
;;;   Write the color to the specified port
;;; Preconditions:
;;;   port is open for writing.
;;; Postconditions:
;;;   When read with rgb-read, the data just appended to the file
;;;   will give color.

(define rgb-write
  (lambda (color port)
    (int-write (rgb-red color) port)
    (int-write (rgb-green color) port)
    (int-write (rgb-blue color) port)))

;;; Procedure:
;;;   rgb-read
;;; Parameters:
;;;   port, the name of an input port
;;; Purpose:
;;;   Reads an RGB color from the file.
;;; Produces:
;;;   color, an RGB color (or <eof>, if the port is at the end of
;;;     the file)
;;; Preconditions:
;;;   port is open for reading.
;;;   port is at the place in a file after which the next data was
;;;     written by rgb-write.
;;; Postconditions:
;;;   color is the color written by the call to rgb-write mentioned
;;;     in the preconditions.

(define rgb-read
  (lambda (port)
    (let* ((red (int-read port))
           (green (int-read port))
           (blue (int-read port)))
       (if (eof-object? blue) 
           blue
           (rgb-new red green blue)))))

(define int-write
  (lambda (int port)
    (write int port)
    (display " " port)))

(define int-read 
  (lambda (port)
    (read port)))

; +-------------------------+-----------------------------------------
; | Miscellaneous Utilities |
; +-------------------------+

;;; Procedure:
;;;   image-random-fill!
;;; Parameters:
;;;   image, an image
;;; Purpose:
;;;   Fills the image with a random set of colors.
;;; Produces:
;;;   [Nothing; called for the side effect]
;;; Preconditions:
;;;   image identifies a valid image.
;;; Postconditions:
;;;   Every pixel in the image has an unpredictable color.
(define image-random-fill!
  (lambda (image)
    (image-compute-pixels! image 0 0 
                           (- (image-width image) 1) (- (image-height image) 1)
                           (lambda (pos)
                             (rgb-new (random 255) (random 255) (random 255))))))

;;; Procedure:
;;;   file-size
;;; Parameters:
;;;   filename, a string
;;; Purpose:
;;;   Determines the number of characters in the file.
;;; Produces:
;;;   s, an integer.
;;; Preconditions:
;;;   filename names a valid file.
;;; Postconditions:
;;;   s is the size of file.  That is,
;;;    s read-char commands from file will succeed.
;;;    The s+1st read from file will fail.
(define file-size
  (lambda (fname)
    (file-size-kernel (open-input-file fname) 0)))

(define file-size-kernel
  (lambda (source size)
    (cond
      ((eof-object? (read-char source))
       (close-input-port source)
       size)
      (else
       (file-size-kernel source (+ size 1))))))

;;; Procedure:
;;;   rgb-distance
;;; Parameters:
;;;   c1, an RGB color
;;;   c2, an RGB color
;;; Purpose:
;;;   Compute a "distance" between c1 and c2 that tells us how
;;;   well c1 estimates c2 (or vice versa).
;;; Produces:
;;;   distance, a real number
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   For any color c,
;;;     (rgb-distance c c) is 0 
;;;   If d is likely to be perceived as closer to c than e is to c, then
;;;     (rgb-distance c d) < (rgb-distance c e)
(define rgb-distance
  (lambda (c1 c2)
    (+ (square (- (rgb-red c1) (rgb-red c2)))
       (square (- (rgb-green c1) (rgb-green c2)))
       (square (- (rgb-blue c1) (rgb-blue c2))))))

;;; Procedure:
;;;   rgb-closer
;;; Parameters:
;;;   color, an RGB color
;;;   estimate1, an RGB color
;;;   estimate2, an RGB color
;;; Purpose:
;;;   Determines which of estimate1 and estimate2 is closer to color.
;;; Produces:
;;;   closer, an RGB color
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   closer is either estimate1 or estimate2
;;;   (rgb-distance color closer) <= (rgb-distance color estimate1)
;;;   (rgb-distance color closer) <= (rgb-distance color estimate2)
(define rgb-closer
  (lambda (color estimate1 estimate2)
    (if (<= (rgb-distance color estimate1) (rgb-distance color estimate2))
        estimate1
        estimate2)))

;;; Procedure:
;;;   rgb-closest
;;; Parameters:
;;;   color, an RGB color
;;;   colors, a list of RGB colors
;;; Purpose:
;;;   Determine the element of colors that is closest to
;;;   color
;;; Produces:
;;;   closest, an RGB color
;;; Preconditions:
;;;   colors is nonempty.
;;; Postconditions:
;;;   closest is an element of colors.
;;;   (rgb-distance color closest) <= (rgb-distance color c)
;;;     for all c in colors
(define rgb-closest
  (lambda (color colors)
    (let kernel ((remaining-colors (cdr colors))
                 (closest-so-far (car colors)))
      (if (null? remaining-colors)
          closest-so-far
          (kernel (cdr remaining-colors)
                  (rgb-closer color closest-so-far (car remaining-colors)))))))

;;; Procedure:
;;;   list-index-of
;;; Parameters:
;;;   lst, a list of values
;;;   val, a value
;;; Purpose:
;;;   Find an index of val in lst.
;;; Produces:
;;;   index, a number (or #f)
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   If there exists a value in lst equal to val, then
;;;     (list-ref lst index) equals val
;;;   If no such value exists, then
;;;     index is #f
(define list-index-of
  (lambda (lst val)
    (cond
      ((null? lst)
       #f)
      ((equal? val (car lst))
       0)
      (else
       (+ 1 (list-index-of (cdr lst) val))))))

Creative Commons License

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright (c) 2007-8 Janet Davis, Matthew Kluber, and Samuel A. Rebelsky. (Selected materials copyright by John David Stone and Henry Walker and used by permission.)

This material is based upon work partially supported by the National Science Foundation under Grant No. CCLI-0633090. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.

This work is licensed under a Creative Commons Attribution-NonCommercial 2.5 License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/2.5/ or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.