Skip to main content

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 shapes, 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? With some careful math, you can draw a variety of shapes. For example, if we determine the distance of each pixel from some point, and base the color on that distance, we can probably draw a circle.

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.

Blue pixels

(image-compute
 (lambda (col row) 
   (irgb 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.

left-to-right blend from black to red

(image-compute
 (lambda (col row) 
   (irgb (* 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.

left-to-right blend from white to red

(image-compute
 (lambda (col row) 
   (irgb 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.

unzoomed left-to-right blend from black to red

(image-compute
 (lambda (col row) 
   (irgb (* 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.

zoomed in top-to-bottom black to blue blend

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

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

unzoomed top-to-bottom blend from black to blue

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

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

A mix of a black to blue and black to red blend with purple in the upper right corner

(image-compute
 (lambda (col row) 
   (irgb (* col 32)  0 (* row 64)))
 9 5)

Unzoomed mixture of red and blue blends

(image-compute
 (lambda (col row) 
   (irgb (* 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)
   (irgb 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.

Zoomed in image of mostly blue pixels with a black line

(image-compute
 (lambda (col row) 
   (if (= row 3)
       (irgb 0 0 0)
       (irgb 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.

Blue image with white crossed lines

(image-compute 
 (lambda (col row)
  (if (or (= col 6) (= row 1))
      (irgb 255 255 255)
      (irgb 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 line and a black-blue blend for the background.

zoomed in horizontal blend with a white to red blended line

(image-compute 
 (lambda (col row)
   (if (= row 2)
       (irgb 255 (* 32 (- 9 col)) (* 32 (- 9 col)))
       (irgb 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.

Zoomed in diagonal white line on a black background

(image-compute 
 (lambda (col row)
  (if (= col row)
      (irgb 255 255 255)
      (irgb 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.

A diagonal white line by a diagonal gray line

(image-compute 
 (lambda (col row)
   (cond [(= col row)       (irgb 255 255 255)]
         [(= col (+ row 3)) (irgb 128 128 128)]
         [else              (irgb 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.

Unzoomed diagonal lines

(image-compute 
 (lambda (col row)
   (cond [(= col row)       (irgb 255 255 255)]
         [(= col (+ row 3)) (irgb 128 128 128)]
         [else              (irgb 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.

Zoomed in red square on a blue background

(image-compute 
 (lambda (col row)
    (if (and (<= 1 row 3) (<= 2 col 5))
        (irgb 255 0 0)
        (irgb 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).

Zoomed in blue triangle on a red blackground

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

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

Unzoomed blue triangle on red background

(image-compute 
 (lambda (col row)
   (if (and (<= 16 col (+ row 2)) (<= row 64))
       (irgb 0 0 255)
       (irgb 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 our 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

Red circle on black background

(image-compute
 (lambda (col row)
   (if (<= (euclidean-distance 40 50 col row) 30)
       (irgb 255 0 0)
       (irgb 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))
       (irgb 255 0 0)
       (irgb 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.

Blue to red diagonal blended circle on a black background

(image-compute
 (lambda (col row) 
   (if (<= (+ (square (- col 40)) (square (- row 50)))
          (square 30))
       (irgb (* 3 col) 0 (* 3 row))
       (irgb 0 0 0)))
 145 91)

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

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.

Self Checks

Check 1: Simple Images

As you learned in the reading, one of the simplest ways to use image-compute is to create an image of a uniform color. For example, we might make a small pink image with

(define image-color (color-name->rgb "hotpink"))
(image-show (image-compute (lambda (col row) image-color)
                           30 10))

a. Confirm that these instructions work as advertised.

b. Using a similar instruction, make a larger pink image.

c. Using a similar instruction, make a larger black image.

d. What do you think will happen if you provide image-compute with a negative width or height?

e. Check your answer experimentally.

f. What do you think will happen if you provide image-compute with something other than a function for the first parameter?

g. Check your answer experimentally.

h. What do you think will happen if you provide image-compute with a function that returns a non-color?

i. Check your answer experimentally. (Remember that integers are interpreted as colors.)

Check 2: Color Blends

a. The reading claims that the following instructions will create an image the provides a blend from black to red. Confirm that the claim is correct.

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

b. What do you expect the image to look like if we use a larger width (say 257) and height (say 129)?

c. Check your answer experimentally.

d. What do you expect the image to look like if we use a smaller width (say 65) and height (say 33)?

e. Check your answer experimentally.

As you may have noted, when we used the 129x65 formula for a bigger image, the rightmost pixels were all red. You will rectify this shortcoming in the lab.