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.
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.
DrFu provides one basic procedure for computing pixels based on position,
image-compute-pixels!. Instead of setting every pixel in
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.
image-compute-pixels! procedure takes a few
more parameters than procedures you've used in the past (six, to be
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 ___)
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)))
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:
(, which, given a position value, returns the column component of that position-
(, which, given a position value, returns the row component of that position-
There's also a constructor for positions. As you might expect,
( creates a new position value for
row). While we won't
position-new in this work, we'll find uses for
it in the future.
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.
It is also possible to use
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,
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.
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.
functionto the position of the pixel.
functionmust be a function from position to rgb-color. Warning! The current version is buggy, and may affect other pixels.
image-compute-pixels!. If the color function returns
color-transparentfor a particular position, the color at that position is left unchanged.
Janet Davis (email@example.com)