Functional Problem Solving (CSC 151 2014F) : Readings
Primary: [Front Door] [Schedule] - [Academic Honesty] [Disabilities] [Email] - [FAQ] [Teaching & Learning] [Grading] [Rubric] - [Calendar]
Current: [Assignment] [EBoard] [Lab] [Outline] [Reading]
Sections: [Assignments] [EBoards] [Examples] [Handouts] [Labs] [Outlines] [Readings]
Reference: [Setup] [VM] [Errors] - [Functions A-Z] [Functions By Topic] - [Racket] [Scheme Report (R5RS)] [R6RS] [TSPL4]
Related Courses: [Davis (2013F)] [Rebelsky (2014S)] [Weinman (2014F)]
Misc: [Submit Questions] - [SamR] [Glimmer Labs] [CS@Grinnell] [Grinnell] - [Issue Tracker (Course)]
Summary: We consider a different model of images, one in which the pixels of the image are computed based solely on position.
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.
image-compute
In order to describe an image with a function, we'll need three parameters:
function
that takes as input
a column and a row and returns a color;width
of the image we want
to create; andheight
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) (irgb 0 0 255)) 9 5) |
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) (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.
![]() |
(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.
![]() |
(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.
![]() |
(image-compute (lambda (col row) (irgb 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) (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.
![]() |
(image-compute (lambda (col row) (irgb (* col 32) 0 (* row 64))) 9 5) |
![]() |
(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.
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) (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.
![]() |
(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.
![]() |
(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.
![]() |
(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.
![]() |
(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.
![]() |
(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.
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)) (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).
![]() |
(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.
![]() |
(image-compute (lambda (col row) (if (and (<= 16 col (+ row 2)) (<= row 64)) (irgb 0 0 255) (irgb 255 0 0))) 145 81) |
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
![]() |
(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.
![]() |
(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.
(image-compute
pos2color
width
height
)
pos2color
(a function of the form (lambda (col row) color)
)
to compute the color at each position in the image.
As you learned in the reading, one of the simplest ways to use
is to create an image
of a uniform color. For example, we might make a small pink image with
image-compute
(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
with a negative
width or height?
image-compute
e. Check your answer experimentally.
f. What do you think will happen if you provide
with something other
than a function for the first parameter?
image-compute
g. Check your answer experimentally.
h. What do you think will happen if you provide
with a function
that returns a non-color?
image-compute
i. Check your answer experimentally. (Remember that integers are interpreted as colors.)
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.