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

Building Images by Iterating Over Positions


Summary: We consider a procedure that lets us assign colors to pixels in a region of an image based on the position of each pixel.

Introduction

In our most recent explorations of DrFu, we've found ways to create images by setting individual pixels and ways to modify existing images by transforming all the pixels, each with the same technique. Clearly, if we are going to build images, we need a richer set of techniques.

One reasonable strategy is to work with a procedure that allows us to set the color of each pixel in of an image based on the position of the pixel. Why is that useful? As we saw in the initial tasks of drawing smiley faces, if you can write algorithms that set the color at particular positions, then you can draw circles and a host of other shapes. We can simulate such algorithms by looking at each position and checking if it meets certain criteria.

To draw complex and simple shapes, we will often need to check criteria using conditionals. However, we can also draw very interesting images with the strategy of “compute a color for each pixel based on the pixel's location”. As we will see here, it is easy to draw rectangles, to add color washes, and to combine a variety of techniques.

The key procedure: image-compute-pixels!

DrFu provides one basic procedure for computing pixels based on position, image-compute-pixels!. Instead of setting every pixel in the image, image-compute-pixels! works only with pixels in a rectangular region of the image. Why just a region? Efficiency. For a big image in which we only want to set colors in a small area, we would prefer not to have to look at (and do computations for) the rest of the image. By limiting the region, we can also more easily create simple forms. Why a rectangle? It's easier to specify.

This image-compute-pixels! procedure takes a few more parameters than procedures you've used in the past (six, to be specific).

  • image, the image which we will modify;
  • first-col, the first column of the region to modify;
  • first-row, the first row of the region to modify;
  • last-col, the last column of the region to modify;
  • last-row, the last row of the region to modify; and
  • compute-pixel, a function that, given a position, computes a color for the pixel at that position.

For example, the following computes a new color at each position in the rectangular grid from row 5, column 2 to row 10, column 15.

(image-compute-pixels! canvas 5 2 10 15 ___)

The compute-pixel procedure should have the form

(lambda (position) expression-to-compute-color)

What can we fill in for the function? That's the subject of the rest of this reading. However, there's one simple technique we can do with what we know so far: We can ignore the position and simply return a fixed color. Here, we set the preceding rectangular region to red.

(image-compute-pixels! canvas 5 2 10 15 (lambda (pos) (rgb-new 255 0 0)))

Detour: Positions

If we are to write procedures that compute colors from positions, we need to know a bit about how DrFu represents positions. For the purposes of this assignment, there are only two basic procedures you need to work with positions:

  • (position-col pos), which, given a position value, returns the column component of that position-
  • (position-row pos), which, given a position value, returns the row component of that position-

There's also a constructor for positions. As you might expect, (position-new col row) creates a new position value for (col,row). While we won't use position-new in this work, we'll find uses for it in the future.

Color Ranges, Color Blends, and More

At times, we want to make an image that provides a range of colors, such as various kinds of blue or various kinds of grey. For example, we can use image-compute-pixels! to compute a spectrum of blues by building a 17x1 image and setting the blue component of each pixel to 16 times the column number.

(define blues (image-new 17 1))
(image-show blues)
(image-compute-pixels! blues 0 0 16 0 
                        (lambda (pos) (rgb-new 0 0 (* 16 (position-col pos)))))

In a recent assignment, we dealt with an extension of color ranges, which we called blends. We can extend the previous example to express blends more concisely. For example, here is a 128-step blend from red to blue.

(define red-to-blue (image.new 129 1))
(image-show red-to-blue)
(image-compute-pixels! red-to-blue 
                       0 0 
                       128 0 
                       (lambda (pos) (rgb-new (* 2 (- 128 (position-col pos)))
                                               0
                                               (* 2 (position-col pos)))))

Note that we can use this technique to make larger images, too. (Perhaps we can soon stop using the zoom button to figure out what's going on.)

(define big-red-to-blue (image-new 129 16))
(image-show big-red-to-blue)
(image-compute-pixels! big-red-to-blue 
                       0 0 
                       128 15
                       (lambda (pos) (rgb-new (* 2 (- 128 (position-col pos)))
                                              0
				              (* 2 (position-col pos)))))

This blend is somewhat limited, as it goes from pure red to pure blue, and the way in which it works is somewhat concealed in the particular numbers used. You will have an opportunity to generalize this solution in the near future.

Of course, we can do more than just blend colors. We can do almost any computation that creates a color between 0 and 255. Here's an interesting one. Can you predict what it will do?

(define what-am-i (image-new 40 50))
(image-compute-pixels! what-am-i
                       0 0 39 49
                       (lambda (pos)
                         (rgb-new 0
                                  (* 128 (+ 1 (sin (* pi 0.025 (position-col pos)))))
                                  (* 128 (+ 1 (sin (* pi 0.020 (position-row pos))))))))

In the corresponding lab, you'll have an opportunity to experiment with similar computed colors.

Making Shapes (Particularly Circles)

It is also possible to use image-compute-pixels! to generate some simple shapes. You already know how to use it to make rectangular shapes. What about circles and ovals? Well, we can use a formula to make such shapes. Let's start with an easy one: Suppose we want to compute a red circle on a black background, with the circle centered at (40,50) with radius 30.

Let's start by figuring out the left, top, right, and bottom edges of the portion of the image to update. (We don't want to do extra computation, so we use as small a region as possible.) If it's centered at (40,50) the furthest left we can go is the radius away from the center, so that would be 10. Similarly, the highest we can go is the radius above the center, or 20. The right and bottom are similar. Putting it all together, we get a command like

(image-compute-pixels! canvas
                       10 20 70 80
                       _________)

Now, what do we do about the function? Well, a point is within the circle if the Euclidean distance of that point from the center is less than the radius. How do you find the Euclidean distance? We considered that problem earlier, but let's revisit it again. You start by finding the horizontal distance (the absolute value of the difference between the column of the center and the column of the point) and the vertical distance. You square both, add them, and compute the square root of the sum. In Scheme, we might write

;;; Procedure:
;;;   euclidean-distance
;;; Parameters:
;;;   col1, a real number
;;;   row1, a real number
;;;   col2, a real number
;;;   row2, a real number
;;; Purpose:
;;;   Computes the euclidean distance between (col1,row1) and
;;;   (col2,row).
;;; Produces:
;;;   distance, a real number
(define euclidean-distance
  (lambda (col1 row1 col2 row2)
    (sqrt (+ (square (- col2 col1)) (square (- row2 row1))))))

So, for out circle of radius 30 and center (40,50), we want to color a pixel red if it is within the radius and draw the pixel black if it is outside the radius. We can try

(define circle-color
  (lambda (pos)
     (let ((col (position-col pos))
           (row (position-row pos)))
       (if (<= (euclidean-distance 40 50 col row) 30)
           color-red
           color-black))))
(image-compute-pixels! canvas
                       10 20 70 80
                       circle-color)

However, it turns out that computing square roots is relatively expensive. Hence, we might look to other ways to compute the same value. In particular, if the square root of the sum of the squares of the horizontal distance and the vertical distance is less than the radius, then the sum of the squares must be less than the square of the radius. (Say that four times slowly, and it might make sense.) Hence, we might also write

(image-compute-pixels! canvas
                       10 20 70 80
                       (lambda (pos) 
                         (let ((col (position-col pos))
                               (row (position-row pos)))
                           (if (<= (+ (square (- col 40)) (square (- row 50)))
                                  (square 30))
                               color-red
                               color-black))))

So far, so good. But what if we only want to draw the circle, and not the black background? (For example, what if we want to draw circles on top of each other?) DrFu provides a special color, color-transparent, that image-compute-pixels! treats as transparent (that is, that does not replace the previous color).

(image-compute-pixels! canvas
                       10 20 70 80
                       (lambda (pos) 
                         (let ((col (position-col pos))
                               (row (position-row pos)))
                           (if (<= (+ (square (- col 40)) (square (- row 50)))
                                  (square 30))
                               color-red
                               color-transparent))))

But what have we gained from all of this? After all, we already know how to draw circles using GIMP tools. One potential benefit is that we have a bit more understanding of how the GIMP actually goes about drawing circles. But there's an artistic benefit, too. We can now draw circles in color blends that we compute.

(image-compute-pixels! canvas
                       10 20 70 80
                       (lambda (pos) 
                         (let ((col (position-col pos))
                               (row (position-row pos)))
                           (if (<= (+ (square (- col 40)) (square (- row 50)))
                                  (square 30))
                               (rgb-new (* 3 col) 0 (* 3 row))
                               color-transparent))))

You will have a chance to explore this technique a bit more in the corresponding laboratory.

A Disclaimer

Unfortunately, image-compute-pixels! is still a bit of an experimental function. Among other things, this means that it does not work very well with the non-pixel functions. For example, if you draw one part of your image with the GIMP tools and then try to use image-compute-pixels! to put a blend on top of one region, you may find that other regions are also affected. We plan to fix this problem in summer 2008.

Reference

(image-compute-pixels! image first-col first-row last-col last-row function)
DrFu procedure. Create a portion of an image by setting each pixel in the specified region to the result of applying function to the position of the pixel. function must be a function from position to rgb-color. Warning! The current version is buggy, and may affect other pixels.
color-transparent
DrFu Constant. A special color value, used only by image-compute-pixels!. If the color function returns color-transparent for a particular position, the color at that position is left unchanged.
(position-new col row)
DrFu procedure. Build a new position that represents the point at (col,row).
(position-col pos)
DrFu procedure. Extract the column from position pos.
(position-row pos)
DrFu procedure. Extract the row from position pos.

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.