CSC151.01 2009F Functional Problem Solving : Readings

Building Images by Iterating Over Positions


Summary: We consider a different model of images, one in which the pixels of the image are computed based solely on position.

Introduction

We've now seen a variety of ways for describing images. We can describe an image through a series of GIMP commands, as you did almost immediately after starting Scheme. We can describe an image by composing, scaling, shifting, and recoloring basic shape, as we did with the drawing operations. We can also describe an image by setting each pixel, one by one, although that may get a bit exhausting.

We've also considered how to transform, rather than describe, images. One way to transform an image is to transform each pixel of the image, using the same color transformation for each pixel.

Can we use a similar idea to describe and create images? One reasonable strategy is to work with a procedure that determines the color of each pixel in 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.

We can draw very interesting images using some very basic comptuations to compute the color. We can also draw both complex and simple shapes using conditionals. As we will see, it is easy to draw rectangles and other shapes, to add color blends, and to combine a variety of techniques.

A Simple Model: image-compute

In order to describe an image with a function, we'll need three parameters:

  • a function that takes as input a column and a row and returns a color;
  • the width of the image we want to create; and
  • the height of the image we want to create.

The function will have the form (lambda (col row) cexp), where cexp is an expression that computes an RGB color.

At first, it can be difficult to think of an image in terms of a function from positions to colors. Most of us are more accustomed to thinking of images in terms of the process used to compute them (as we did with the GIMP tools) or in terms of the basic components of the image (as we did with the basic drawing type). Hence, for our original explorations with image-compute, we'll consider a simple 9x5 grid of pixels, which we've scaled below.

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. In this case, we use the color blue.

(image-compute
 (lambda (col row) 
   (rgb-new 0 0 255))
 9 5)

Color Blends

Of course, that's not a very interesting image. So let's make the function a bit more complex. In particular, let's compute a horizontal blend from black at the left to red at the right. In the leftmost column, we'll have all three components set to 0. In the rightmost column, we'll still have the greeen and blue components set to 0, but we'll use 255 for the red component. What about the middle columns? Since we'll want a bit more red each time, and we need to get from 0 to 255 in eight steps, we can increment the red component by 32 each time. (We'll end up with a red component of 256 in the last column, but MediaScript chops that back to 255.) That is, the red component in each column is 32 times that column number.

(image-compute
 (lambda (col row) 
   (rgb-new (* col 32) 0 0))
 9 5)

What about a blend from white to red? In this case, in the left column, all three components will be 255. In the right column, the green and blue components will be 0, but the red component will still be 255. Hence, we keep the red component at 255, but decrease the green and blue by 32 each time. That computation is slightly more difficult, but we can get the effect by subtracting 9 from the column and then multiplying the result by 32.

(image-compute
 (lambda (col row) 
   (rgb-new 255 (* (- 9 col) 32) (* (- 9 col) 32)))
 9 5)

These strategies work equally well for larger, non-scaled images. For example, for a 129x65 horizontal blend from black to red, we'll multiply the column number by 2, rather than 32.

(image-compute
 (lambda (col row) 
   (rgb-new (* col 2) 0 0))
 129 65)

We can also compute vertical blends. Let's do a simple blend from black to blue. Since there are only five rows in our basic image, we multiply the row by 64, rather than 32.

(image-compute
 (lambda (col row) 
   (rgb-new 0 0 (* row 64)))
 9 5)

Once again, we can use the ame technique to make a full-sized image.

(image-compute
 (lambda (col row) 
   (rgb-new 0 0 (* row 4)))
 129 65)

Of course, we need not stick with only horizontal and vertical blends. We can use both techniques together.

(image-compute
 (lambda (col row) 
   (rgb-new (* col 32)  0 (* row 64)))
 9 5)
(image-compute
 (lambda (col row) 
   (rgb-new (* col 2) 0 (* row 4)))
 129 65)

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?

(image-compute
 (lambda (col row)
   (rgb-new 0
            (* 128 (+ 1 (sin (* pi 0.025 col))))
            (* 128 (+ 1 (sin (* pi 0.020 row))))))
  40 50)

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

Computing Lines

What if we want something less abstract? Let's think about how we can compute lines. We'll start with horizontal and vertical lines, and then move on to other kinds of lines.

For a simple horizontal line at a particular row, we make the color computation check if the the row is that row. If so, we choose one color. If not, we use another color. In this example, we'll use black for the line and blue for the background.

(image-compute
 (lambda (col row) 
   (if (= row 3)
       (rgb-new 0 0 0)
       (rgb-new 0 0 255)))
 9 5)

Of course, we can compute both horizontal and vertical lines in the same image. This time, we'll overlay white lines on a blue background.

(image-compute 
 (lambda (col row)
  (if (or (= col 6) (= row 1))
      (rgb-new 255 255 255)
      (rgb-new 0 0 255)))
 9 5)

We need not be restricted to monocolor lines. The lines, like the images, can have blends. The backgrounds, too, can have blends. Here, we combine a white-red blend for the ilne and a black-blue blend for the background.

(image-compute 
 (lambda (col row)
   (if (= row 2)
       (rgb-new 255 (* 32 (- 9 col)) (* 32 (- 9 col)))
       (rgb-new 0 0 (* 32 col))))
   9 5)

Some diagonal lines are fairly easy. In the following, we simply choose one color if the row and column are equal and another otherwise.

(image-compute 
 (lambda (col row)
  (if (= col row)
      (rgb-new 255 255 255)
      (rgb-new 0 0 0)))
 9 5)

We can also shift the line a bit by adding to or subtracting from the column. Here, we add a grey diagonal line to the previous image.

(image-compute 
 (lambda (col row)
   (cond ((= col row)       (rgb-new 255 255 255))
         ((= col (+ row 3)) (rgb-new 128 128 128))
         (else              (rgb-new 0 0 0))))
 9 5)

Of course, these diagonal lines are a bit jagged. They look a bit better (but not perfect) at a normal scale.

(image-compute 
 (lambda (col row)
   (cond ((= col row)       (rgb-new 255 255 255))
         ((= col (+ row 3)) (rgb-new 128 128 128))
         (else              (rgb-new 0 0 0))))
 129 65)

More importantly, angled lines that don't have a slope of 1 or -1 are even more difficult to do well. We will consider some strategies later in the course.

Computing Some Simple Shapes

For rectangles, we can use a similar technique to the one we used for horizontal and vertical lines. Instead of just checking whether the column or row equals a particular number, we can check whether the column and row are in a particular range.

(image-compute 
 (lambda (col row)
    (if (and (<= 1 row 3) (<= 2 col 5))
        (rgb-new 255 0 0)
        (rgb-new 0 0 255)))
 9 5)

We can use a combination of those techniques to draw triangles. In this case, instead of checking whether column and row are equal (or off by a constant amount), we compare the column to the row (plus or minus an offset).

(image-compute 
 (lambda (col row)
   (if (and (<= 3 col (+ row 2)) (<= row 3))
       (rgb-new 0 0 255)
       (rgb-new 255 0 0)))
 9 5)

Of course, these triangles look a bit better when rendered at a normal resolution with appropriately larger values.

(image-compute 
 (lambda (col row)
   (if (and (<= 16 col (+ row 2)) (<= row 64))
       (rgb-new 0 0 255)
       (rgb-new 255 0 0)))
 145 81)

Computing Circles

It is also possible to use image-compute to generate some simple shapes. You already know how to use it to make rectangular shapes. Rectangles and simple triangles were fairly easy to compute. What about more complex shapes, such as circles and ovals? In order to compute such shapes, we'll need to figure out a mathematical formula for the shape.

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.

What should the function look like? If you think back to your geometry class, 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

(image-compute
 (lambda (col row)
   (if (<= (euclidean-distance 40 50 col row) 30)
       (rgb-new 255 0 0)
       (rgb-new 0 0 0)))
   145 91)

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 
 (lambda (col row)
   (if (<= (+ (square (- col 40)) (square (- row 50)))
              (square 30))
       (rgb-new 255 0 0)
       (rgb-new 0 0 0)))
   145 91)

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
 (lambda (col row) 
   (if (<= (+ (square (- col 40)) (square (- row 50)))
          (square 30))
       (rgb-new (* 3 col) 0 (* 3 row))
       (rgb-new 0 0 0)))
 145 91)

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

Updating Images

While we prefer to use this technique to create new images, there are also some benefits to computing some pixels in an image. MediaScript provides three procedures that compute pixels in an already existing image.

  • (image-compute-pixels! image pos2color), which behaves much like image-compute, except that it modifies an existing image.
  • (region-compute-pixels! image left top width height pos2color), which behaves much like image-compute-pixels!, except that it modifies only a rectangular region of the image.
  • (selection-compute-pixels! image pos2color), which behaves much like image-compute-pixels!, except that it modifies only the pixels in a selection.

Consider, for example, this public domain image of a kitten.

http://public-photo.net/displayimage-2485.html

We can put a small color blend near one of the kitten's ears. (We admit that adding such a color blend is somewhat silly, but our purpose at this point is to introduce techniques that we want to challenge you to use creatively.)

(region-compute-pixels!
 kitten
 70 35 33 17
 (lambda (col row)
   (rgb-new (* (- col 70) 8) 0 (* (- row 35) 16))))

Because these procedures effectively overlay a computed image on an existing image, we provide a special value, rgb-transparent, which color functions can return when they underlying pixel should not be changed. For example, here's a more useful modification to the image: We can get an interesting effect by overlaying a mask of black pixels in every other location.

(image-compute-pixels!
 kitten
 (lambda (col row)
   (if (odd? (+ col row)) (rgb-new 0 0 0) rgb-transparent)))
(image-compute-pixels!
 kitten
 (lambda (col row)
   (if (odd? (+ col row)) (rgb-new 0 0 255) rgb-transparent)))

Reference

(image-compute pos2color width height)
MediaScript GIMP Procedure. Create a new width-by-height image by using pos2color (a function of the form (lambda (col row) color)) to compute the color at each position in the image. compute
(image-compute-pixels! image pos2color)
MediaScheme GIMP Procedure. Set each pixel in the image to the result of applying function to the position of the pixel. function must have the form (lambda (col row) expression-to-compute-color).
(region-compute-pixels! image left top width height pos2color)
MediaScheme GIMP 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 of the form (lambda (col row) expression-to-compute-color).
(selection-compute-pixels! image pos2color)
MediaScheme GIMP Procedure. Set each pixel in the selected area of the image to the result of applying function to the position of the pixel. function must have the form (lambda (col row) expression-to-compute-color).
rgb-transparent
MediaScheme Color Constant. A special color value, used by image-compute-pixels! (and variants thereof). If the color function returns rgb-transparent for a particular position, the color at that position is left unchanged.

Creative Commons License

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright (c) 2007-9 Janet Davis, Matthew Kluber, Samuel A. Rebelsky, and Jerod Weinman. (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.